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,1086 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from devflow_engine.errors.runtime_observability import build_runtime_observability
10
+ from devflow_engine.idea.idea_creation_dag import run_idea_creation_dag
11
+ from devflow_engine.idea.story_pipeline import run_idea_to_devflow_stories_dag
12
+
13
+ from .devflow_state import publish_devflow_state
14
+ from .playwright_workflow import run_post_integration_playwright_dag
15
+ from .process import dag as process_dag
16
+ from .recovery.dag import run_failure_recovery_dag
17
+ from .scope_idea.dag import run_scope_to_idea_dag
18
+ from .source_doc_mutation_dag import run_source_doc_mutation_dag
19
+ from .source_docs_updater import process_source_doc_update_once
20
+ from .stores.execution_store import ExecutionStore
21
+ from .worker_guard import ensure_worker_start_allowed
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class WorkerStartResult:
26
+ project_id: str
27
+ worker_id: str
28
+ processed: dict[str, Any] | None
29
+ report: dict[str, Any]
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class WorkerDrainResult:
34
+ project_id: str
35
+ processed_count: int
36
+ processed: list[dict[str, Any]]
37
+ stopped_reason: str
38
+ final_report: dict[str, Any]
39
+
40
+
41
+ def _queue_label(queue_type: str | None) -> str:
42
+ return {
43
+ 'error': 'Errors',
44
+ 'recovery': 'Recovery',
45
+ 'scope': 'Scopes',
46
+ 'idea_creation': 'Idea creation',
47
+ 'idea': 'Ideas',
48
+ 'story': 'Stories',
49
+ 'source_doc_mutation': 'Source doc mutations',
50
+ 'integration': 'Integration',
51
+ 'source_docs': 'Source docs',
52
+ None: 'Queue',
53
+ }.get(queue_type, 'Queue')
54
+
55
+
56
+ def _publish_queue_boundary_state(*, project_id: str, run_id: str | None, queue_type: str | None, next_queue_type: str | None) -> None:
57
+ if next_queue_type is None:
58
+ summary = f"{_queue_label(queue_type)} queue completed"
59
+ else:
60
+ summary = f"{_queue_label(queue_type)} queue completed · next: {_queue_label(next_queue_type)}"
61
+ publish_devflow_state(
62
+ project_id=project_id,
63
+ run_id=run_id,
64
+ current_state='idle',
65
+ current_status='completed',
66
+ run_summary=summary,
67
+ display='project',
68
+ display_path=f"queue:{next_queue_type or queue_type or 'none'}",
69
+ )
70
+
71
+
72
+ def _has_processed_successful_integration(processed: list[dict[str, Any]]) -> bool:
73
+ return any(item.get("queue_type") == "integration" and "error" not in item for item in processed)
74
+
75
+
76
+ def _integration_drained_without_recovery(*, project_id: str, store: ExecutionStore) -> bool:
77
+ with store._connect() as conn:
78
+ integration_row = conn.execute(
79
+ (
80
+ "SELECT integration_queue_id FROM integration_queue "
81
+ "WHERE project_id=? AND status IN ('queued','claimed','in_progress','failed') LIMIT 1"
82
+ ),
83
+ (project_id,),
84
+ ).fetchone()
85
+ if integration_row is not None:
86
+ return False
87
+ recovery_row = store._select_active_recovery_queue_row(
88
+ conn,
89
+ project_id=project_id,
90
+ statuses=("queued", "claimed", "in_progress", "blocked"),
91
+ source_queue_type="integration",
92
+ columns="rq.recovery_queue_id",
93
+ )
94
+ return recovery_row is None
95
+
96
+
97
+ def _maybe_run_post_integration_playwright_workflow(
98
+ *,
99
+ project_id: str,
100
+ repo_root: Path,
101
+ store: ExecutionStore,
102
+ processed: list[dict[str, Any]],
103
+ last_run_id: str | None,
104
+ ) -> str | None:
105
+ if any(item.get("queue_type") == "post_integration_playwright" for item in processed):
106
+ return last_run_id
107
+ if not _has_processed_successful_integration(processed):
108
+ return last_run_id
109
+ if not _integration_drained_without_recovery(project_id=project_id, store=store):
110
+ return last_run_id
111
+
112
+ result = run_post_integration_playwright_dag(
113
+ repo_root=repo_root,
114
+ store=store,
115
+ project_id=project_id,
116
+ trigger_run_id=last_run_id,
117
+ )
118
+ processed.append(
119
+ {
120
+ "queue_type": "post_integration_playwright",
121
+ "item_id": result.run_id,
122
+ "status": "completed" if result.exit_code == 0 else "failed",
123
+ "pipeline_dir": str(result.pipeline_dir),
124
+ }
125
+ )
126
+ return result.run_id
127
+
128
+
129
+ def _load_story_payload(*, store: ExecutionStore, story_queue_id: str) -> tuple[dict[str, Any], dict[str, Any]]:
130
+ item = store.get_story_queue_item(story_queue_id=story_queue_id)
131
+ if item is None:
132
+ raise ValueError(f"story_queue_id not found: {story_queue_id}")
133
+ artifact = store.get_artifact(artifact_id=str(item["story_artifact_id"]))
134
+ if artifact is None:
135
+ raise ValueError(f"artifact not found: {item['story_artifact_id']}")
136
+
137
+ payload = dict(artifact.get("metadata") or {})
138
+ artifact_uri = str(artifact.get("uri") or "")
139
+ if artifact_uri:
140
+ path = Path(artifact_uri)
141
+ if path.exists():
142
+ payload = json.loads(path.read_text(encoding="utf-8"))
143
+
144
+ story = {
145
+ "story_id": payload.get("story_id"),
146
+ "story_uuid": payload.get("story_uuid"),
147
+ "title": payload.get("title"),
148
+ "project_id": item.get("project_id"),
149
+ "contract": payload,
150
+ }
151
+ failure_context = item.get("failure_context") if isinstance(item.get("failure_context"), dict) else {}
152
+ replay = failure_context.get("replay") if isinstance(failure_context.get("replay"), dict) else None
153
+ if replay:
154
+ story["replay"] = replay
155
+ return item, story
156
+
157
+
158
+ def _load_scope_payload(*, store: ExecutionStore, scope_queue_id: str) -> dict[str, Any]:
159
+ item = store.get_scope_queue_item(scope_queue_id=scope_queue_id)
160
+ if item is None:
161
+ raise ValueError(f"scope_queue_id not found: {scope_queue_id}")
162
+ return item
163
+
164
+
165
+ def _load_idea_creation_payload(*, store: ExecutionStore, idea_creation_queue_id: str) -> dict[str, Any]:
166
+ item = store.get_idea_creation_queue_item(idea_creation_queue_id=idea_creation_queue_id)
167
+ if item is None:
168
+ raise ValueError(f"idea_creation_queue_id not found: {idea_creation_queue_id}")
169
+ return item
170
+
171
+
172
+ def _load_idea_payload(*, store: ExecutionStore, idea_queue_id: str) -> dict[str, Any]:
173
+ item = store.get_idea_queue_item(idea_queue_id=idea_queue_id)
174
+ if item is None:
175
+ raise ValueError(f"idea_queue_id not found: {idea_queue_id}")
176
+ return item
177
+
178
+
179
+ def _load_source_doc_mutation_payload(*, store: ExecutionStore, source_doc_mutation_queue_id: str) -> dict[str, Any]:
180
+ item = store.get_source_doc_mutation_queue_item(source_doc_mutation_queue_id=source_doc_mutation_queue_id)
181
+ if item is None:
182
+ raise ValueError(f"source_doc_mutation_queue_id not found: {source_doc_mutation_queue_id}")
183
+ return item
184
+
185
+
186
+ def _load_integration_payload(*, store: ExecutionStore, integration_queue_id: str) -> dict[str, Any]:
187
+ item = store.get_integration_queue_item(integration_queue_id=integration_queue_id)
188
+ if item is None:
189
+ raise ValueError(f'integration_queue_id not found: {integration_queue_id}')
190
+ return item
191
+
192
+
193
+ def _resolve_integration_payload_path(*, repo_root: Path, item: dict[str, Any]) -> tuple[Path | None, dict[str, Any] | None]:
194
+ raw_path = str(item.get("integration_payload_path") or "").strip()
195
+ if raw_path:
196
+ candidate = Path(raw_path).expanduser()
197
+ if not candidate.is_absolute():
198
+ candidate = repo_root / candidate
199
+ try:
200
+ resolved = candidate.resolve()
201
+ except Exception:
202
+ resolved = candidate
203
+ if resolved.is_file():
204
+ try:
205
+ payload = json.loads(resolved.read_text(encoding="utf-8"))
206
+ except Exception:
207
+ payload = None
208
+ if isinstance(payload, dict):
209
+ return resolved, payload
210
+ return None, None
211
+
212
+
213
+ def _load_recovery_payload(*, store: ExecutionStore, recovery_queue_id: str) -> dict[str, Any]:
214
+ item = store.get_recovery_queue_item(recovery_queue_id=recovery_queue_id)
215
+ if item is None:
216
+ raise ValueError(f"recovery_queue_id not found: {recovery_queue_id}")
217
+ return item
218
+
219
+
220
+ def _enqueue_recovery_request(
221
+ *,
222
+ project_id: str,
223
+ repo_root: Path,
224
+ store: ExecutionStore,
225
+ source_queue_type: str,
226
+ source_item_id: str,
227
+ enqueue_run_id: str,
228
+ title: str,
229
+ failure_message: str | None,
230
+ failure_context: dict[str, Any] | None,
231
+ ) -> str:
232
+ recovery_root = repo_root / ".devflow" / "recovery" / "requests"
233
+ recovery_root.mkdir(parents=True, exist_ok=True)
234
+ request_path = recovery_root / f"{source_queue_type}_{source_item_id}_recovery_request.json"
235
+ request_payload = {
236
+ "contract": "recovery_queue_request.v1",
237
+ "project_id": project_id,
238
+ "source_queue_type": source_queue_type,
239
+ "source_item_id": source_item_id,
240
+ "enqueue_run_id": enqueue_run_id,
241
+ "title": title,
242
+ "failure_message": failure_message,
243
+ "failure_context": failure_context or {},
244
+ }
245
+ request_path.write_text(json.dumps(request_payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
246
+ return store.enqueue_recovery_task(
247
+ project_id=project_id,
248
+ enqueue_run_id=enqueue_run_id,
249
+ source_queue_type=source_queue_type,
250
+ source_item_id=source_item_id,
251
+ title=f"Recovery: {title}",
252
+ recovery_request_path=str(request_path),
253
+ failure_context=failure_context,
254
+ )
255
+
256
+
257
+ def _implementation_failure_routes_to_error_solving(failure_context: dict[str, Any] | None) -> bool:
258
+ ctx = failure_context if isinstance(failure_context, dict) else {}
259
+ return (
260
+ str(ctx.get("kind") or "") == "implementation_failure.v1"
261
+ and str(ctx.get("classification_hint") or "") == "project_code"
262
+ )
263
+
264
+
265
+ def _create_error_task_for_implementation_failure(
266
+ *,
267
+ store: ExecutionStore,
268
+ project_id: str,
269
+ run_id: str,
270
+ story_queue_id: str,
271
+ story_id: str,
272
+ failure_message: str | None,
273
+ failure_context: dict[str, Any],
274
+ ) -> str:
275
+ failure_kind = str(failure_context.get("failure_kind") or "implementation_failure")
276
+ failed_stage = str(failure_context.get("failed_stage") or failure_context.get("stage") or "implementation")
277
+ return store.create_error_task_from_failure(
278
+ project_id=project_id,
279
+ run_id=run_id,
280
+ plane="implementation",
281
+ title=f"Implementation failure: {story_id or story_queue_id}",
282
+ severity="high",
283
+ source_kind="implementation_failure.v1",
284
+ source_ref=story_id or story_queue_id,
285
+ error_type=failure_kind,
286
+ message=failure_message or f"{failed_stage} failed",
287
+ stacktrace="",
288
+ next_steps=[
289
+ "Run devflow doctor for triage.",
290
+ "Run devflow error solve for project-code repair.",
291
+ ],
292
+ fingerprint_inputs={
293
+ "kind": "implementation_failure.v1",
294
+ "project_id": project_id,
295
+ "story_id": story_id,
296
+ "story_queue_id": story_queue_id,
297
+ "failed_stage": failed_stage,
298
+ "actual_failed_node": failure_context.get("actual_failed_node"),
299
+ "failure_kind": failure_kind,
300
+ },
301
+ )
302
+
303
+
304
+ _STORY_CHURN_THRESHOLD = 3
305
+
306
+
307
+ def _normalize_story_failure_node(node_id: str | None) -> str | None:
308
+ raw = str(node_id or "").strip()
309
+ if not raw:
310
+ return None
311
+ if "." in raw:
312
+ raw = raw.split(".", 1)[0]
313
+ return raw
314
+
315
+
316
+ def _story_actual_failed_node(*, failure_context: dict[str, Any] | None, failed_stage: str | None = None) -> str | None:
317
+ ctx = failure_context if isinstance(failure_context, dict) else {}
318
+ return _normalize_story_failure_node(
319
+ str(ctx.get("actual_failed_node") or "").strip()
320
+ or str(failed_stage or "").strip()
321
+ or str(ctx.get("failed_stage") or "").strip()
322
+ )
323
+
324
+
325
+ def _build_story_churn_state(
326
+ *,
327
+ story_id: str,
328
+ prior_failure_context: dict[str, Any] | None,
329
+ failure_context: dict[str, Any] | None,
330
+ failed_stage: str | None,
331
+ ) -> dict[str, Any]:
332
+ current_node = _story_actual_failed_node(failure_context=failure_context, failed_stage=failed_stage) or "unknown"
333
+ prior_ctx = prior_failure_context if isinstance(prior_failure_context, dict) else {}
334
+ prior_state = prior_ctx.get("churn_state") if isinstance(prior_ctx.get("churn_state"), dict) else {}
335
+ prior_story_id = str(prior_state.get("story_id") or story_id).strip() or story_id
336
+ prior_node = _normalize_story_failure_node(
337
+ str(prior_state.get("failing_node") or "").strip()
338
+ or str(prior_ctx.get("actual_failed_node") or "").strip()
339
+ or str(prior_ctx.get("failed_stage") or "").strip()
340
+ )
341
+
342
+ count = 1
343
+ if prior_story_id == story_id and prior_node == current_node:
344
+ count = int(prior_state.get("same_story_same_node_failures") or 0) + 1
345
+
346
+ return {
347
+ "story_id": story_id,
348
+ "failing_node": current_node,
349
+ "same_story_same_node_failures": count,
350
+ "threshold": _STORY_CHURN_THRESHOLD,
351
+ "blocked_for_churn": count >= _STORY_CHURN_THRESHOLD,
352
+ "requires_human_intervention": count >= _STORY_CHURN_THRESHOLD,
353
+ }
354
+
355
+
356
+ def _peek_next_queue_type(*, project_id: str, store: ExecutionStore) -> str | None:
357
+ with store._connect() as conn:
358
+ recovery_row = store._select_active_recovery_queue_row(
359
+ conn,
360
+ project_id=project_id,
361
+ statuses=("queued", "claimed", "in_progress", "blocked"),
362
+ columns="rq.recovery_queue_id",
363
+ )
364
+ if recovery_row is not None:
365
+ return 'recovery'
366
+ error_row = conn.execute(
367
+ "SELECT error_task_id FROM error_tasks WHERE project_id=? AND status IN ('open','triaged') ORDER BY created_at ASC LIMIT 1",
368
+ (project_id,),
369
+ ).fetchone()
370
+ if error_row is not None:
371
+ return 'error'
372
+ scope_row = conn.execute(
373
+ "SELECT scope_queue_id FROM scope_queue WHERE project_id=? AND status='queued' ORDER BY created_at ASC LIMIT 1",
374
+ (project_id,),
375
+ ).fetchone()
376
+ if scope_row is not None:
377
+ return 'scope'
378
+ idea_creation_row = conn.execute(
379
+ "SELECT idea_creation_queue_id FROM idea_creation_queue WHERE project_id=? AND status='queued' ORDER BY created_at ASC LIMIT 1",
380
+ (project_id,),
381
+ ).fetchone()
382
+ if idea_creation_row is not None:
383
+ return 'idea_creation'
384
+ idea_row = conn.execute(
385
+ "SELECT idea_queue_id FROM idea_queue WHERE project_id=? AND status='queued' ORDER BY created_at ASC LIMIT 1",
386
+ (project_id,),
387
+ ).fetchone()
388
+ if idea_row is not None:
389
+ return 'idea'
390
+ source_doc_mutation_row = conn.execute(
391
+ "SELECT source_doc_mutation_queue_id FROM source_doc_mutation_queue WHERE project_id=? AND status='queued' ORDER BY created_at ASC LIMIT 1",
392
+ (project_id,),
393
+ ).fetchone()
394
+ if source_doc_mutation_row is not None:
395
+ return 'source_doc_mutation'
396
+ story_row = conn.execute(
397
+ "SELECT story_queue_id FROM story_queue WHERE project_id=? AND status='queued' ORDER BY created_at ASC LIMIT 1",
398
+ (project_id,),
399
+ ).fetchone()
400
+ if story_row is not None:
401
+ return 'story'
402
+ integration_row = conn.execute(
403
+ "SELECT integration_queue_id FROM integration_queue WHERE project_id=? AND status='queued' ORDER BY created_at ASC LIMIT 1",
404
+ (project_id,),
405
+ ).fetchone()
406
+ if integration_row is not None:
407
+ return 'integration'
408
+ return None
409
+
410
+
411
+ def _cleanup_failed_claim(
412
+ *,
413
+ project_id: str,
414
+ repo_root: Path,
415
+ store: ExecutionStore,
416
+ worker_id: str,
417
+ claimed: dict[str, Any] | None,
418
+ active_run_id: str | None,
419
+ exc: BaseException,
420
+ ) -> None:
421
+ if active_run_id is not None:
422
+ store.mark_run_finished(run_id=active_run_id, status="failed")
423
+ if claimed is not None:
424
+ message = str(exc) or repr(exc)
425
+ if isinstance(exc, (SystemExit, KeyboardInterrupt)):
426
+ message = exc.__class__.__name__ if not str(exc) else f"{exc.__class__.__name__}: {exc}"
427
+ failure_context: dict[str, Any] = {"error_type": exc.__class__.__name__, "error": message}
428
+ if isinstance(getattr(exc, "failure_context", None), dict):
429
+ failure_context.update(getattr(exc, "failure_context"))
430
+ if claimed["queue_type"] == "idea_creation":
431
+ try:
432
+ item = _load_idea_creation_payload(store=store, idea_creation_queue_id=str(claimed["item_id"]))
433
+ failure_context.update({"idea_id": str(item.get("idea_id") or ""), "idea_creation_queue_id": str(item.get("idea_creation_queue_id") or "")})
434
+ except Exception:
435
+ pass
436
+ if claimed["queue_type"] == "idea":
437
+ try:
438
+ item = _load_idea_payload(store=store, idea_queue_id=str(claimed["item_id"]))
439
+ failure_context.update({"idea_id": str(item.get("idea_id") or ""), "idea_queue_id": str(item.get("idea_queue_id") or "")})
440
+ idea_payload = Path(str(item.get("idea_payload_path") or ""))
441
+ if idea_payload.exists():
442
+ idea_json = json.loads(idea_payload.read_text(encoding="utf-8"))
443
+ ideation_output = idea_json.get("ideation_output") if isinstance(idea_json.get("ideation_output"), dict) else {}
444
+ story_generation = ideation_output.get("story_generation") if isinstance(ideation_output.get("story_generation"), dict) else {}
445
+ resume_cursor = story_generation.get("resume_cursor")
446
+ if isinstance(resume_cursor, dict):
447
+ failure_context["resume_cursor"] = resume_cursor
448
+ except Exception:
449
+ pass
450
+ if claimed["queue_type"] == "integration":
451
+ try:
452
+ item = _load_integration_payload(store=store, integration_queue_id=str(claimed["item_id"]))
453
+ failure_context.update({"idea_id": str(item.get("idea_id") or ""), "integration_queue_id": str(item.get("integration_queue_id") or "")})
454
+ except Exception:
455
+ pass
456
+ store.complete_project_queue_item(
457
+ project_id=project_id,
458
+ worker_id=worker_id,
459
+ queue_type=str(claimed["queue_type"]),
460
+ item_id=str(claimed["item_id"]),
461
+ status="failed",
462
+ current_run_id=active_run_id,
463
+ failure_message=message,
464
+ failure_context=failure_context,
465
+ )
466
+ if str(claimed["queue_type"]) in {"scope", "idea_creation", "idea", "story", "integration"}:
467
+ item_title = str(claimed["item_id"])
468
+ try:
469
+ if str(claimed["queue_type"]) == "scope":
470
+ source_item = _load_scope_payload(store=store, scope_queue_id=str(claimed["item_id"]))
471
+ item_title = str(source_item.get("title") or source_item.get("scope_id") or claimed["item_id"])
472
+ elif str(claimed["queue_type"]) == "idea_creation":
473
+ source_item = _load_idea_creation_payload(store=store, idea_creation_queue_id=str(claimed["item_id"]))
474
+ item_title = str(source_item.get("title") or source_item.get("idea_id") or claimed["item_id"])
475
+ elif str(claimed["queue_type"]) == "idea":
476
+ source_item = _load_idea_payload(store=store, idea_queue_id=str(claimed["item_id"]))
477
+ item_title = str(source_item.get("title") or source_item.get("idea_id") or claimed["item_id"])
478
+ elif str(claimed["queue_type"]) == "integration":
479
+ source_item = _load_integration_payload(store=store, integration_queue_id=str(claimed["item_id"]))
480
+ item_title = str(source_item.get("title") or source_item.get("idea_id") or claimed["item_id"])
481
+ else:
482
+ source_item, _story = _load_story_payload(store=store, story_queue_id=str(claimed["item_id"]))
483
+ item_title = str(source_item.get("title") or source_item.get("story_id") or claimed["item_id"])
484
+ except Exception:
485
+ pass
486
+ _enqueue_recovery_request(
487
+ project_id=project_id,
488
+ repo_root=repo_root,
489
+ store=store,
490
+ source_queue_type=str(claimed["queue_type"]),
491
+ source_item_id=str(claimed["item_id"]),
492
+ enqueue_run_id=str(active_run_id or claimed.get("run_id") or ""),
493
+ title=item_title,
494
+ failure_message=message,
495
+ failure_context=failure_context,
496
+ )
497
+ store.stop_project_worker(project_id=project_id)
498
+
499
+
500
+
501
+
502
+ def reconcile_stale_worker(*, project_id: str, repo_root: Path, store: ExecutionStore, reason: str = "worker process died before cleanup") -> dict[str, Any] | None:
503
+ report = store.get_project_worker_report(project_id=project_id)
504
+ worker_id = str(report.get("worker_id") or "").strip()
505
+ queue_type = str(report.get("active_queue_type") or "").strip() or None
506
+ item_id = str(report.get("active_item_id") or "").strip() or None
507
+ run_id = str(report.get("current_run_id") or "").strip() or None
508
+ status = str(report.get("status") or "").strip()
509
+ if not worker_id or not queue_type or not item_id or status not in {"running", "starting", "idle", "stopping"}:
510
+ return None
511
+
512
+ failure_context: dict[str, Any] = {
513
+ "error_type": "WorkerAbandonedError",
514
+ "error": reason,
515
+ "project_id": project_id,
516
+ "repo_root": str(repo_root),
517
+ "reconciled": True,
518
+ }
519
+ if run_id:
520
+ store.mark_run_finished(run_id=run_id, status="failed")
521
+ if queue_type == "idea_creation":
522
+ try:
523
+ item = _load_idea_creation_payload(store=store, idea_creation_queue_id=item_id)
524
+ failure_context.update({"idea_id": str(item.get("idea_id") or ""), "idea_creation_queue_id": str(item.get("idea_creation_queue_id") or "")})
525
+ except Exception:
526
+ pass
527
+ if queue_type == "idea":
528
+ try:
529
+ item = _load_idea_payload(store=store, idea_queue_id=item_id)
530
+ failure_context.update({"idea_id": str(item.get("idea_id") or ""), "idea_queue_id": str(item.get("idea_queue_id") or "")})
531
+ idea_payload = Path(str(item.get("idea_payload_path") or ""))
532
+ if idea_payload.exists():
533
+ idea_json = json.loads(idea_payload.read_text(encoding="utf-8"))
534
+ ideation_output = idea_json.get("ideation_output") if isinstance(idea_json.get("ideation_output"), dict) else {}
535
+ story_generation = ideation_output.get("story_generation") if isinstance(ideation_output.get("story_generation"), dict) else {}
536
+ failed_stage = story_generation.get("failed_stage")
537
+ if isinstance(failed_stage, str) and failed_stage.strip():
538
+ failure_context["failed_stage"] = failed_stage
539
+ resume_cursor = story_generation.get("resume_cursor")
540
+ if isinstance(resume_cursor, dict):
541
+ failure_context["resume_cursor"] = resume_cursor
542
+ except Exception:
543
+ pass
544
+ if queue_type == "integration":
545
+ try:
546
+ item = _load_integration_payload(store=store, integration_queue_id=item_id)
547
+ failure_context.update({"idea_id": str(item.get("idea_id") or ""), "integration_queue_id": str(item.get("integration_queue_id") or "")})
548
+ except Exception:
549
+ pass
550
+ store.complete_project_queue_item(
551
+ project_id=project_id,
552
+ worker_id=worker_id,
553
+ queue_type=queue_type,
554
+ item_id=item_id,
555
+ status="failed",
556
+ current_run_id=run_id,
557
+ failure_message=reason,
558
+ failure_context=failure_context,
559
+ )
560
+ if queue_type in {"scope", "idea_creation", "idea", "story", "integration"}:
561
+ item_title = item_id
562
+ try:
563
+ if queue_type == "scope":
564
+ source_item = _load_scope_payload(store=store, scope_queue_id=item_id)
565
+ item_title = str(source_item.get("title") or source_item.get("scope_id") or item_id)
566
+ elif queue_type == "idea_creation":
567
+ source_item = _load_idea_creation_payload(store=store, idea_creation_queue_id=item_id)
568
+ item_title = str(source_item.get("title") or source_item.get("idea_id") or item_id)
569
+ elif queue_type == "idea":
570
+ source_item = _load_idea_payload(store=store, idea_queue_id=item_id)
571
+ item_title = str(source_item.get("title") or source_item.get("idea_id") or item_id)
572
+ elif queue_type == "integration":
573
+ source_item = _load_integration_payload(store=store, integration_queue_id=item_id)
574
+ item_title = str(source_item.get("title") or source_item.get("idea_id") or item_id)
575
+ else:
576
+ source_item, _story = _load_story_payload(store=store, story_queue_id=item_id)
577
+ item_title = str(source_item.get("title") or source_item.get("story_id") or item_id)
578
+ except Exception:
579
+ pass
580
+ _enqueue_recovery_request(
581
+ project_id=project_id,
582
+ repo_root=repo_root,
583
+ store=store,
584
+ source_queue_type=queue_type,
585
+ source_item_id=item_id,
586
+ enqueue_run_id=str(run_id or worker_id),
587
+ title=item_title,
588
+ failure_message=reason,
589
+ failure_context=failure_context,
590
+ )
591
+ final_report = store.stop_project_worker(project_id=project_id)
592
+ publish_devflow_state(
593
+ project_id=project_id,
594
+ run_id=run_id,
595
+ current_state="failed",
596
+ current_status="failed",
597
+ run_summary=reason,
598
+ display="project",
599
+ display_path=f"queue:{queue_type}",
600
+ )
601
+ return {"reconciled": True, "queue_type": queue_type, "item_id": item_id, "run_id": run_id, "worker_id": worker_id, "report": final_report}
602
+
603
+ def process_once(*, project_id: str, repo_root: Path, store: ExecutionStore) -> WorkerStartResult:
604
+ ensure_worker_start_allowed(repo_root=repo_root)
605
+ source_doc_tick = process_source_doc_update_once(store=store, repo_root=repo_root)
606
+ if source_doc_tick.task is not None:
607
+ return WorkerStartResult(
608
+ project_id=project_id,
609
+ worker_id=source_doc_tick.worker_id,
610
+ processed={"queue_type": "source_docs", "item_id": source_doc_tick.task["task_id"]},
611
+ report=store.get_project_worker_report(project_id=project_id),
612
+ )
613
+
614
+ worker_id = store.start_project_worker(project_id=project_id, repo_root=repo_root)
615
+ processed: dict[str, Any] | None = None
616
+ claimed: dict[str, Any] | None = None
617
+ active_run_id: str | None = None
618
+
619
+ try:
620
+ claimed = store.claim_next_project_queue_item(project_id=project_id, worker_id=worker_id)
621
+ if claimed is None:
622
+ report = store.stop_project_worker(project_id=project_id)
623
+ return WorkerStartResult(project_id=project_id, worker_id=worker_id, processed=None, report=report)
624
+
625
+ processed = {"queue_type": claimed["queue_type"], "item_id": claimed["item_id"]}
626
+ if claimed["queue_type"] == "recovery":
627
+ item = _load_recovery_payload(store=store, recovery_queue_id=str(claimed["item_id"]))
628
+ run = store.start_run(
629
+ kind="worker.recovery",
630
+ repo_root=repo_root,
631
+ args={
632
+ "project_id": project_id,
633
+ "recovery_queue_id": item["recovery_queue_id"],
634
+ "source_queue_type": item["source_queue_type"],
635
+ "source_item_id": item["source_item_id"],
636
+ },
637
+ )
638
+ active_run_id = run.run_id
639
+ store.update_project_worker_context(worker_id=worker_id, current_run_id=run.run_id, current_node_exec_id=None)
640
+ with store._connect() as conn:
641
+ conn.execute("UPDATE recovery_queue SET status='in_progress', started_run_id=?, updated_at=? WHERE recovery_queue_id=?", (run.run_id, int(time.time()), item["recovery_queue_id"]))
642
+ result = run_failure_recovery_dag(
643
+ repo_root=repo_root,
644
+ store=store,
645
+ project_id=project_id,
646
+ queue_type=str(item["source_queue_type"]),
647
+ item_id=str(item["source_item_id"]),
648
+ run_id=run.run_id,
649
+ )
650
+ final_status = "completed" if result.exit_code == 0 else "failed"
651
+ store.mark_run_finished(run_id=run.run_id, status="succeeded" if result.exit_code == 0 else "failed")
652
+ store.complete_project_queue_item(
653
+ project_id=project_id,
654
+ worker_id=worker_id,
655
+ queue_type="recovery",
656
+ item_id=str(claimed["item_id"]),
657
+ status=final_status,
658
+ current_run_id=run.run_id,
659
+ failure_message=None if result.exit_code == 0 else result.message,
660
+ )
661
+ elif claimed["queue_type"] == "error":
662
+ result = store.run_error_solver_dag(
663
+ error_task_id=str(claimed["item_id"]),
664
+ repo_root=repo_root,
665
+ build_observability_fn=lambda ctx: build_runtime_observability(
666
+ repo_root=repo_root,
667
+ runtime_context=ctx.get("runtimeContext") if isinstance(ctx.get("runtimeContext"), dict) else None,
668
+ repro_command=None,
669
+ project_id=project_id,
670
+ store=store,
671
+ ),
672
+ first_pass_fix_fn=lambda _ctx: {"fixed": False},
673
+ iterative_fix_fn=lambda _ctx: {"fixed": False},
674
+ needs_user_input_fn=lambda _ctx: True,
675
+ runtime_context=None,
676
+ )
677
+ outcome = str(result.get("outcome") or "blocked")
678
+ store.complete_project_queue_item(project_id=project_id, worker_id=worker_id, queue_type="error", item_id=str(claimed["item_id"]), status=outcome, current_run_id=str(claimed.get("run_id") or "") or None)
679
+ elif claimed["queue_type"] == "scope":
680
+ item = _load_scope_payload(store=store, scope_queue_id=str(claimed["item_id"]))
681
+ run = store.start_run(kind="worker.scope_to_idea", repo_root=repo_root, args={"project_id": project_id, "scope_queue_id": item["scope_queue_id"], "scope_id": item["scope_id"]})
682
+ active_run_id = run.run_id
683
+ store.update_project_worker_context(worker_id=worker_id, current_run_id=run.run_id, current_node_exec_id=None)
684
+ with store._connect() as conn:
685
+ conn.execute("UPDATE scope_queue SET status='in_progress', started_run_id=?, updated_at=? WHERE scope_queue_id=?", (run.run_id, int(time.time()), item["scope_queue_id"]))
686
+ result = run_scope_to_idea_dag(repo_root=repo_root, store=store, project_id=project_id, scope_set_id=str(item.get("scope_set_id") or ""), scope_id=str(item["scope_id"]), scope_payload_path=Path(str(item["scope_payload_path"])))
687
+ final_status = "completed" if result.exit_code == 0 else "failed"
688
+ store.mark_run_finished(run_id=run.run_id, status="succeeded" if result.exit_code == 0 else "failed")
689
+ store.complete_project_queue_item(project_id=project_id, worker_id=worker_id, queue_type="scope", item_id=str(claimed["item_id"]), status=final_status, current_run_id=run.run_id, failure_message=None if result.exit_code == 0 else result.message)
690
+ if final_status == "failed":
691
+ _enqueue_recovery_request(
692
+ project_id=project_id,
693
+ repo_root=repo_root,
694
+ store=store,
695
+ source_queue_type="scope",
696
+ source_item_id=str(claimed["item_id"]),
697
+ enqueue_run_id=run.run_id,
698
+ title=str(item.get("title") or item.get("scope_id") or claimed["item_id"]),
699
+ failure_message=result.message,
700
+ failure_context=None,
701
+ )
702
+ elif claimed["queue_type"] == "idea_creation":
703
+ item = _load_idea_creation_payload(store=store, idea_creation_queue_id=str(claimed["item_id"]))
704
+ run = store.start_run(kind="worker.idea_creation", repo_root=repo_root, args={"project_id": project_id, "idea_creation_queue_id": item["idea_creation_queue_id"], "idea_id": item["idea_id"]})
705
+ active_run_id = run.run_id
706
+ store.update_project_worker_context(worker_id=worker_id, current_run_id=run.run_id, current_node_exec_id=None)
707
+ with store._connect() as conn:
708
+ conn.execute("UPDATE idea_creation_queue SET status='in_progress', started_run_id=?, updated_at=? WHERE idea_creation_queue_id=?", (run.run_id, int(time.time()), item["idea_creation_queue_id"]))
709
+ result = run_idea_creation_dag(repo_root=repo_root, store=store, idea_id=str(item["idea_id"]), project_id=project_id, text=None, source_path=Path(str(item["idea_payload_path"])))
710
+ final_status = "completed" if result.exit_code == 0 else "failed"
711
+ store.mark_run_finished(run_id=run.run_id, status="succeeded" if result.exit_code == 0 else "failed")
712
+ store.complete_project_queue_item(project_id=project_id, worker_id=worker_id, queue_type="idea_creation", item_id=str(claimed["item_id"]), status=final_status, current_run_id=run.run_id, failure_message=None if result.exit_code == 0 else result.message)
713
+ if final_status == "failed":
714
+ _enqueue_recovery_request(
715
+ project_id=project_id,
716
+ repo_root=repo_root,
717
+ store=store,
718
+ source_queue_type="idea_creation",
719
+ source_item_id=str(claimed["item_id"]),
720
+ enqueue_run_id=run.run_id,
721
+ title=str(item.get("title") or item.get("idea_id") or claimed["item_id"]),
722
+ failure_message=result.message,
723
+ failure_context=None,
724
+ )
725
+ elif claimed["queue_type"] == "idea":
726
+ item = _load_idea_payload(store=store, idea_queue_id=str(claimed["item_id"]))
727
+ run = store.start_run(kind="worker.idea_to_story", repo_root=repo_root, args={"project_id": project_id, "idea_queue_id": item["idea_queue_id"], "idea_id": item["idea_id"]})
728
+ active_run_id = run.run_id
729
+ store.update_project_worker_context(worker_id=worker_id, current_run_id=run.run_id, current_node_exec_id=None)
730
+ with store._connect() as conn:
731
+ conn.execute("UPDATE idea_queue SET status='in_progress', started_run_id=?, updated_at=? WHERE idea_queue_id=?", (run.run_id, int(time.time()), item["idea_queue_id"]))
732
+ result = run_idea_to_devflow_stories_dag(
733
+ repo_root=repo_root,
734
+ store=store,
735
+ idea_id=str(item["idea_id"]),
736
+ text=None,
737
+ source_path=Path(str(item["idea_payload_path"])),
738
+ max_stories=0,
739
+ planes=list(item.get("candidate_planes") or []),
740
+ )
741
+ final_status = "completed" if result.exit_code == 0 else "failed"
742
+ store.mark_run_finished(run_id=run.run_id, status="succeeded" if result.exit_code == 0 else "failed")
743
+ store.complete_project_queue_item(project_id=project_id, worker_id=worker_id, queue_type="idea", item_id=str(claimed["item_id"]), status=final_status, current_run_id=run.run_id, failure_message=None if result.exit_code == 0 else result.message)
744
+ if final_status == "failed":
745
+ _enqueue_recovery_request(
746
+ project_id=project_id,
747
+ repo_root=repo_root,
748
+ store=store,
749
+ source_queue_type="idea",
750
+ source_item_id=str(claimed["item_id"]),
751
+ enqueue_run_id=run.run_id,
752
+ title=str(item.get("title") or item.get("idea_id") or claimed["item_id"]),
753
+ failure_message=result.message,
754
+ failure_context=None,
755
+ )
756
+ elif claimed["queue_type"] == "source_doc_mutation":
757
+ item = _load_source_doc_mutation_payload(store=store, source_doc_mutation_queue_id=str(claimed["item_id"]))
758
+ run = store.start_run(kind="worker.source_doc_mutation", repo_root=repo_root, args={"project_id": project_id, "source_doc_mutation_queue_id": item["source_doc_mutation_queue_id"], "idea_id": item.get("idea_id")})
759
+ active_run_id = run.run_id
760
+ store.update_project_worker_context(worker_id=worker_id, current_run_id=run.run_id, current_node_exec_id=None)
761
+ with store._connect() as conn:
762
+ conn.execute("UPDATE source_doc_mutation_queue SET status='in_progress', started_run_id=?, updated_at=? WHERE source_doc_mutation_queue_id=?", (run.run_id, int(time.time()), item["source_doc_mutation_queue_id"]))
763
+ result = run_source_doc_mutation_dag(request=dict(item.get("mutation_request") or {}))
764
+ final_status = "completed"
765
+ store.mark_run_finished(run_id=run.run_id, status="succeeded")
766
+ store.complete_project_queue_item(project_id=project_id, worker_id=worker_id, queue_type="source_doc_mutation", item_id=str(claimed["item_id"]), status=final_status, current_run_id=run.run_id)
767
+ elif claimed["queue_type"] == "integration":
768
+ item = _load_integration_payload(store=store, integration_queue_id=str(claimed["item_id"]))
769
+ from .integration.dag import prepare_integration_payload
770
+ run = store.start_run(
771
+ kind="worker.integration",
772
+ repo_root=repo_root,
773
+ args={
774
+ "project_id": project_id,
775
+ "integration_queue_id": item["integration_queue_id"],
776
+ "idea_id": item["idea_id"],
777
+ },
778
+ )
779
+ active_run_id = run.run_id
780
+ store.update_project_worker_context(worker_id=worker_id, current_run_id=run.run_id, current_node_exec_id=None)
781
+ with store._connect() as conn:
782
+ conn.execute(
783
+ "UPDATE integration_queue SET status='in_progress', started_run_id=?, updated_at=? WHERE integration_queue_id=?",
784
+ (run.run_id, int(time.time()), item["integration_queue_id"]),
785
+ )
786
+ publish_devflow_state(
787
+ project_id=project_id,
788
+ run_id=run.run_id,
789
+ current_state="running",
790
+ current_status="processing",
791
+ run_summary="Integrating",
792
+ display="project",
793
+ display_path=f'idea:{item["idea_id"]}',
794
+ )
795
+ from .integration.dag import _sync_integration_to_supabase, run_integration_dag
796
+ _sync_integration_to_supabase(
797
+ idea_id=str(item["idea_id"]),
798
+ project_id=project_id,
799
+ run_id=run.run_id,
800
+ repo_root=repo_root,
801
+ result=None,
802
+ status="running",
803
+ )
804
+ payload_path, payload = _resolve_integration_payload_path(repo_root=repo_root, item=item)
805
+ if payload_path is None or payload is None:
806
+ payload_path = prepare_integration_payload(repo_root=repo_root, idea_id=str(item["idea_id"]))
807
+ payload = json.loads(payload_path.read_text(encoding="utf-8"))
808
+ result = run_integration_dag(
809
+ repo_root=repo_root,
810
+ idea_id=str(item["idea_id"]),
811
+ implemented_idea=payload.get("implemented_idea", {}),
812
+ implemented_stories=payload.get("implemented_stories", []),
813
+ code_evidence=payload.get("code_evidence"),
814
+ source_docs=payload.get("source_docs"),
815
+ project_id=project_id,
816
+ dag_run_id=run.run_id,
817
+ )
818
+ final_status = "completed" if result.exit_code == 0 else "failed"
819
+ store.mark_run_finished(run_id=run.run_id, status="succeeded" if result.exit_code == 0 else "failed")
820
+ publish_devflow_state(
821
+ project_id=project_id,
822
+ run_id=run.run_id,
823
+ current_state="idle",
824
+ current_status="completed" if result.exit_code == 0 else "failed",
825
+ run_summary="Integration complete" if result.exit_code == 0 else "Integration failed",
826
+ error_message=result.message if result.exit_code != 0 else None,
827
+ display="project",
828
+ display_path=f'idea:{item["idea_id"]}',
829
+ )
830
+ _sync_integration_to_supabase(
831
+ idea_id=str(item["idea_id"]),
832
+ project_id=project_id,
833
+ run_id=run.run_id,
834
+ repo_root=repo_root,
835
+ result=result,
836
+ status=final_status,
837
+ )
838
+ store.complete_project_queue_item(
839
+ project_id=project_id,
840
+ worker_id=worker_id,
841
+ queue_type="integration",
842
+ item_id=str(claimed["item_id"]),
843
+ status=final_status,
844
+ current_run_id=run.run_id,
845
+ failure_message=None if result.exit_code == 0 else result.message,
846
+ )
847
+ if final_status == "failed":
848
+ _enqueue_recovery_request(
849
+ project_id=project_id,
850
+ repo_root=repo_root,
851
+ store=store,
852
+ source_queue_type="integration",
853
+ source_item_id=str(claimed["item_id"]),
854
+ enqueue_run_id=run.run_id,
855
+ title=str(item.get("title") or item.get("idea_id") or claimed["item_id"]),
856
+ failure_message=result.message,
857
+ failure_context={"idea_id": str(item.get("idea_id") or ""), "integration_queue_id": str(item.get("integration_queue_id") or "")},
858
+ )
859
+ else:
860
+ item, story = _load_story_payload(store=store, story_queue_id=str(claimed["item_id"]))
861
+ run = store.start_run(
862
+ kind="worker.story.execute",
863
+ repo_root=repo_root,
864
+ args={"project_id": project_id, "story_queue_id": item["story_queue_id"], "story_id": item["story_id"]},
865
+ )
866
+ active_run_id = run.run_id
867
+ store.update_project_worker_context(worker_id=worker_id, current_run_id=run.run_id, current_node_exec_id=None)
868
+ with store._connect() as conn:
869
+ conn.execute(
870
+ "UPDATE story_queue SET status='in_progress', started_run_id=?, updated_at=? WHERE story_queue_id=?",
871
+ (run.run_id, int(time.time()), item["story_queue_id"]),
872
+ )
873
+ publish_devflow_state(
874
+ project_id=project_id,
875
+ run_id=run.run_id,
876
+ current_state="running",
877
+ current_status="processing",
878
+ run_summary="Implementing",
879
+ display="project",
880
+ display_path=f'story:{item["story_id"]}',
881
+ )
882
+
883
+ result = process_dag.run_process_dag(repo_root=repo_root, store=store, story=story)
884
+ final_status = "completed" if result.exit_code == 0 else "failed"
885
+ failure_context = None
886
+ if result.exit_code == 0:
887
+ store.update_story_status(story_id=str(item["story_id"]), status="implemented")
888
+ from devflow_engine.devflow_state import update_story_status_in_supabase
889
+ update_story_status_in_supabase(story_id=str(item["story_id"]), status="implemented")
890
+ else:
891
+ result_failure_context = getattr(result, "failure_context", None)
892
+ failure_context = dict(result_failure_context) if isinstance(result_failure_context, dict) else {}
893
+ prior_failure_context = item.get("failure_context") if isinstance(item.get("failure_context"), dict) else {}
894
+ failed_stage = getattr(result, "failed_stage", None)
895
+ implementation_run_id = getattr(result, "implementation_run_id", None)
896
+ if isinstance(failed_stage, str) and failed_stage.strip():
897
+ failure_context.setdefault("failed_stage", failed_stage)
898
+ if isinstance(implementation_run_id, str) and implementation_run_id.strip():
899
+ failure_context.setdefault("implementation_run_id", implementation_run_id)
900
+ actual_failed_node = _story_actual_failed_node(failure_context=failure_context, failed_stage=failed_stage)
901
+ if actual_failed_node:
902
+ failure_context["actual_failed_node"] = actual_failed_node
903
+ story_id = str(item.get("story_id") or story.get("story_id") or "").strip()
904
+ if story_id:
905
+ failure_context["churn_state"] = _build_story_churn_state(
906
+ story_id=story_id,
907
+ prior_failure_context=prior_failure_context,
908
+ failure_context=failure_context,
909
+ failed_stage=failed_stage,
910
+ )
911
+ if isinstance(story.get("replay"), dict):
912
+ failure_context["prior_replay"] = dict(story["replay"])
913
+ churn_state = failure_context.get("churn_state") if isinstance(failure_context.get("churn_state"), dict) else {}
914
+ if bool(churn_state.get("blocked_for_churn")):
915
+ failure_context["queue_status"] = "blocked"
916
+ failure_context["recovery_disposition"] = "blocked_for_churn"
917
+ final_status = "blocked"
918
+ elif str(failure_context.get("recovery_disposition") or "") == "blocked_at_redreview":
919
+ final_status = "blocked"
920
+ elif _implementation_failure_routes_to_error_solving(failure_context):
921
+ error_task_id = _create_error_task_for_implementation_failure(
922
+ store=store,
923
+ project_id=project_id,
924
+ run_id=run.run_id,
925
+ story_queue_id=str(claimed["item_id"]),
926
+ story_id=str(item.get("story_id") or story.get("story_id") or ""),
927
+ failure_message=result.message,
928
+ failure_context=failure_context,
929
+ )
930
+ failure_context["error_task_id"] = error_task_id
931
+ failure_context["handoff_lane"] = "error_solving"
932
+ failure_context["recovery_disposition"] = "delegated_to_error_solving"
933
+ store.mark_run_finished(run_id=run.run_id, status="succeeded" if result.exit_code == 0 else "failed")
934
+ publish_devflow_state(
935
+ project_id=project_id,
936
+ run_id=run.run_id,
937
+ current_state="idle",
938
+ current_status="completed" if result.exit_code == 0 else ("blocked" if final_status == "blocked" else "failed"),
939
+ run_summary="Implementation complete" if result.exit_code == 0 else ("Implementation blocked" if final_status == "blocked" else "Implementation failed"),
940
+ error_message=result.message if result.exit_code != 0 else None,
941
+ display="project",
942
+ display_path=f'story:{item["story_id"]}',
943
+ )
944
+ store.complete_project_queue_item(
945
+ project_id=project_id,
946
+ worker_id=worker_id,
947
+ queue_type="story",
948
+ item_id=str(claimed["item_id"]),
949
+ status=final_status,
950
+ current_run_id=run.run_id,
951
+ failure_message=None if result.exit_code == 0 else result.message,
952
+ failure_context=failure_context,
953
+ )
954
+ if final_status == "failed" and not _implementation_failure_routes_to_error_solving(failure_context):
955
+ _enqueue_recovery_request(
956
+ project_id=project_id,
957
+ repo_root=repo_root,
958
+ store=store,
959
+ source_queue_type="story",
960
+ source_item_id=str(claimed["item_id"]),
961
+ enqueue_run_id=run.run_id,
962
+ title=str(item.get("title") or item.get("story_id") or claimed["item_id"]),
963
+ failure_message=result.message,
964
+ failure_context=failure_context,
965
+ )
966
+
967
+ report = store.stop_project_worker(project_id=project_id)
968
+ return WorkerStartResult(project_id=project_id, worker_id=worker_id, processed=processed, report=report)
969
+ except BaseException as exc:
970
+ _cleanup_failed_claim(
971
+ project_id=project_id,
972
+ repo_root=repo_root,
973
+ store=store,
974
+ worker_id=worker_id,
975
+ claimed=claimed,
976
+ active_run_id=active_run_id,
977
+ exc=exc,
978
+ )
979
+ raise
980
+
981
+
982
+ def drain_project_queue(
983
+ *,
984
+ project_id: str,
985
+ repo_root: Path,
986
+ store: ExecutionStore,
987
+ queue_type: str | None = None,
988
+ max_items: int | None = None,
989
+ ) -> WorkerDrainResult:
990
+ ensure_worker_start_allowed(repo_root=repo_root)
991
+ processed: list[dict[str, Any]] = []
992
+ final_report = store.get_project_worker_report(project_id=project_id)
993
+ last_run_id: str | None = None
994
+
995
+ while True:
996
+ next_queue_type = _peek_next_queue_type(project_id=project_id, store=store)
997
+ if next_queue_type is None:
998
+ last_run_id = _maybe_run_post_integration_playwright_workflow(
999
+ project_id=project_id,
1000
+ repo_root=repo_root,
1001
+ store=store,
1002
+ processed=processed,
1003
+ last_run_id=last_run_id,
1004
+ )
1005
+ _publish_queue_boundary_state(project_id=project_id, run_id=last_run_id, queue_type=queue_type, next_queue_type=None)
1006
+ return WorkerDrainResult(
1007
+ project_id=project_id,
1008
+ processed_count=len(processed),
1009
+ processed=processed,
1010
+ stopped_reason='empty',
1011
+ final_report=final_report,
1012
+ )
1013
+ if queue_type is not None and next_queue_type != queue_type:
1014
+ _publish_queue_boundary_state(project_id=project_id, run_id=last_run_id, queue_type=queue_type, next_queue_type=next_queue_type)
1015
+ return WorkerDrainResult(
1016
+ project_id=project_id,
1017
+ processed_count=len(processed),
1018
+ processed=processed,
1019
+ stopped_reason=f'next_queue_type:{next_queue_type}',
1020
+ final_report=final_report,
1021
+ )
1022
+ if max_items is not None and len(processed) >= max_items:
1023
+ return WorkerDrainResult(
1024
+ project_id=project_id,
1025
+ processed_count=len(processed),
1026
+ processed=processed,
1027
+ stopped_reason='max_items',
1028
+ final_report=final_report,
1029
+ )
1030
+
1031
+ # Clear any stale idle/stuck worker before claiming so process_once can
1032
+ # register a fresh worker. This handles the case where a previous iteration
1033
+ # left the worker in 'idle' state (claim returned None without stopping).
1034
+ store.stop_project_worker(project_id=project_id)
1035
+
1036
+ try:
1037
+ result = process_once(project_id=project_id, repo_root=repo_root, store=store)
1038
+ final_report = result.report
1039
+ last_run_id = str(final_report.get('current_run_id') or last_run_id or '') or last_run_id
1040
+ if result.processed is None:
1041
+ return WorkerDrainResult(
1042
+ project_id=project_id,
1043
+ processed_count=len(processed),
1044
+ processed=processed,
1045
+ stopped_reason='no_claim',
1046
+ final_report=final_report,
1047
+ )
1048
+ processed.append(result.processed)
1049
+ except Exception as exc:
1050
+ # Stop any stale worker so the next iteration can register cleanly.
1051
+ store.stop_project_worker(project_id=project_id)
1052
+ final_report = store.get_project_worker_report(project_id=project_id)
1053
+ last_run_id = str(final_report.get('current_run_id') or last_run_id or '') or last_run_id
1054
+ next_queue_type_after_failure = _peek_next_queue_type(project_id=project_id, store=store)
1055
+ failure_record: dict[str, Any] = {
1056
+ 'queue_type': queue_type or str(final_report.get('active_queue_type') or 'unknown'),
1057
+ 'error': str(exc),
1058
+ }
1059
+ processed.append(failure_record)
1060
+ if next_queue_type_after_failure is None:
1061
+ last_run_id = _maybe_run_post_integration_playwright_workflow(
1062
+ project_id=project_id,
1063
+ repo_root=repo_root,
1064
+ store=store,
1065
+ processed=processed,
1066
+ last_run_id=last_run_id,
1067
+ )
1068
+ return WorkerDrainResult(
1069
+ project_id=project_id,
1070
+ processed_count=len(processed),
1071
+ processed=processed,
1072
+ stopped_reason='empty_after_failure',
1073
+ final_report=final_report,
1074
+ )
1075
+ if queue_type is not None and next_queue_type_after_failure != queue_type:
1076
+ _publish_queue_boundary_state(project_id=project_id, run_id=last_run_id, queue_type=queue_type, next_queue_type=next_queue_type_after_failure)
1077
+ return WorkerDrainResult(
1078
+ project_id=project_id,
1079
+ processed_count=len(processed),
1080
+ processed=processed,
1081
+ stopped_reason=f'next_queue_type:{next_queue_type_after_failure}',
1082
+ final_report=final_report,
1083
+ )
1084
+ import time as _time
1085
+ _time.sleep(1) # brief backoff before retrying to avoid tight CPU loops
1086
+ continue