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,643 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import re
8
+ from dataclasses import dataclass
9
+ from datetime import UTC, datetime
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from urllib.parse import quote
13
+ from urllib.request import Request, urlopen
14
+
15
+ from pydantic import BaseModel
16
+
17
+ from ..devflow_state import publish_devflow_state
18
+ from ..stores.execution_store import ExecutionStore
19
+ from ..vendor.datalumina_genai.core.nodes.base import Node
20
+ from ..vendor.datalumina_genai.core.schema import NodeConfig, WorkflowSchema
21
+ from ..vendor.datalumina_genai.core.task import TaskContext
22
+ from ..vendor.datalumina_genai.core.workflow import Workflow
23
+ from .paths import get_idea_paths
24
+
25
+ DAG_ID = "idea_creation_dag"
26
+
27
+ _CURRENT_STORE: ExecutionStore | None = None
28
+ _CURRENT_RUN_ID: str | None = None
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class IdeaCreationDagResult:
33
+ exit_code: int
34
+ run_id: str
35
+ pipeline_dir: Path
36
+ message: str
37
+ outcome: dict[str, Any]
38
+
39
+
40
+ class IdeaCreationDagEvent(BaseModel):
41
+ repo_root: str
42
+ idea_id: str
43
+ project_id: str | None = None
44
+ text: str | None = None
45
+ source_path: str | None = None
46
+ pipeline_key: str
47
+
48
+
49
+ REQUIRED_CONTRACT_FIELDS = (
50
+ "summary",
51
+ "problem_statement",
52
+ "target_users",
53
+ "desired_outcomes",
54
+ "initial_scope",
55
+ "constraints",
56
+ "acceptance_criteria",
57
+ )
58
+
59
+
60
+ STOPWORDS = {
61
+ "the", "and", "for", "with", "that", "this", "from", "into", "your", "their", "them", "then", "when", "have", "will", "would",
62
+ "should", "could", "build", "app", "tool", "platform", "system", "users", "user", "team", "workflow", "workflows", "core",
63
+ }
64
+
65
+
66
+ def _store_run() -> tuple[ExecutionStore, str]:
67
+ if _CURRENT_STORE is None or _CURRENT_RUN_ID is None:
68
+ raise RuntimeError("idea creation dag missing runtime store/run_id")
69
+ return _CURRENT_STORE, _CURRENT_RUN_ID
70
+
71
+
72
+ def _stable_hash(payload: Any) -> str:
73
+ return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
74
+
75
+
76
+ def _stable_id(prefix: str, payload: Any, *, size: int = 12) -> str:
77
+ return f"{prefix}{_stable_hash(payload)[:size]}"
78
+
79
+
80
+ def _pipeline_root(repo_root: Path, *, idea_id: str, pipeline_key: str) -> Path:
81
+ return get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "pipelines" / DAG_ID / pipeline_key
82
+
83
+
84
+ def _write_json(path: Path, payload: dict[str, Any]) -> None:
85
+ path.parent.mkdir(parents=True, exist_ok=True)
86
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
87
+
88
+
89
+ def _load_json(path: Path) -> dict[str, Any]:
90
+ if not path.exists():
91
+ return {}
92
+ payload = json.loads(path.read_text(encoding="utf-8"))
93
+ return payload if isinstance(payload, dict) else {}
94
+
95
+
96
+ def _keychain_get(service: str, account: str) -> str | None:
97
+ try:
98
+ import subprocess
99
+ proc = subprocess.run(
100
+ ["security", "find-generic-password", "-s", service, "-a", account, "-w"],
101
+ capture_output=True,
102
+ text=True,
103
+ check=False,
104
+ timeout=10,
105
+ )
106
+ except Exception:
107
+ return None
108
+ if proc.returncode != 0:
109
+ return None
110
+ value = proc.stdout.strip()
111
+ return value or None
112
+
113
+
114
+ def _resolve_supabase_rest_config() -> tuple[str, str] | None:
115
+ if os.environ.get("PYTEST_CURRENT_TEST"):
116
+ return None
117
+ url = (
118
+ os.environ.get("DEVFLOW_SUPABASE_URL")
119
+ or os.environ.get("SUPABASE_URL")
120
+ or _keychain_get("Supabase URL", "Clarity")
121
+ )
122
+ key = (
123
+ os.environ.get("DEVFLOW_SUPABASE_SERVICE_KEY")
124
+ or os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
125
+ or os.environ.get("SUPABASE_SERVICE_KEY")
126
+ or _keychain_get("Supabase Service Key", "Clarity")
127
+ )
128
+ if not url or not key:
129
+ return None
130
+ return url.rstrip("/"), key
131
+
132
+
133
+ def _postgrest_request(*, method: str, url: str, key: str, body: Any | None = None, prefer: str | None = None) -> Any:
134
+ payload = None if body is None else json.dumps(body).encode("utf-8")
135
+ req = Request(url, data=payload, method=method)
136
+ req.add_header("apikey", key)
137
+ req.add_header("Authorization", f"Bearer {key}")
138
+ if body is not None:
139
+ req.add_header("Content-Type", "application/json")
140
+ if prefer:
141
+ req.add_header("Prefer", prefer)
142
+ with urlopen(req, timeout=30) as resp:
143
+ raw = resp.read().decode("utf-8")
144
+ return json.loads(raw) if raw else None
145
+
146
+
147
+ def _tokenize(text: str) -> set[str]:
148
+ return {
149
+ token for token in re.findall(r"[a-z0-9]{3,}", (text or "").lower())
150
+ if token not in STOPWORDS
151
+ }
152
+
153
+
154
+ def _jaccard(a: set[str], b: set[str]) -> float:
155
+ if not a or not b:
156
+ return 0.0
157
+ return len(a & b) / max(1, len(a | b))
158
+
159
+
160
+ def _normalize_contract(*, idea_id: str, project_id: str | None, payload: dict[str, Any], raw_text: str | None) -> dict[str, Any]:
161
+ readiness = payload.get("readiness_contract") if isinstance(payload.get("readiness_contract"), dict) else payload
162
+ sufficient = payload.get("sufficient_idea") if isinstance(payload.get("sufficient_idea"), dict) else {}
163
+ initial_scope = readiness.get("initial_scope") if isinstance(readiness.get("initial_scope"), dict) else {}
164
+ normalized = {
165
+ "idea_id": idea_id,
166
+ "project_id": str(project_id or payload.get("project_id") or readiness.get("project_id") or "").strip() or None,
167
+ "summary": str(readiness.get("summary") or payload.get("summary") or sufficient.get("summary") or raw_text or idea_id).strip(),
168
+ "problem_statement": str(readiness.get("problem_statement") or sufficient.get("problem") or payload.get("problem_statement") or "").strip(),
169
+ "target_users": [str(item).strip() for item in (readiness.get("target_users") or sufficient.get("target_users") or []) if str(item).strip()],
170
+ "desired_outcomes": [str(item).strip() for item in (readiness.get("desired_outcomes") or sufficient.get("user_outcomes") or []) if str(item).strip()],
171
+ "initial_scope": {
172
+ "in_scope": [str(item).strip() for item in (initial_scope.get("in_scope") or sufficient.get("scope") or []) if str(item).strip()],
173
+ "out_of_scope": [str(item).strip() for item in (initial_scope.get("out_of_scope") or []) if str(item).strip()],
174
+ },
175
+ "constraints": [str(item).strip() for item in (readiness.get("constraints") or sufficient.get("constraints") or []) if str(item).strip()],
176
+ "acceptance_criteria": [str(item).strip() for item in (readiness.get("acceptance_criteria") or sufficient.get("acceptance_criteria") or []) if str(item).strip()],
177
+ "assumptions": list(readiness.get("assumptions") or payload.get("assumptions") or []),
178
+ "confidence": float(readiness.get("confidence") or payload.get("confidence") or 0.0),
179
+ "raw_text": raw_text or payload.get("latest_message") or "",
180
+ "source_contract": readiness,
181
+ }
182
+ return normalized
183
+
184
+
185
+ def _missing_fields(contract: dict[str, Any]) -> list[str]:
186
+ missing: list[str] = []
187
+ for key in REQUIRED_CONTRACT_FIELDS:
188
+ value = contract.get(key)
189
+ if key == "initial_scope":
190
+ if not isinstance(value, dict) or not list(value.get("in_scope") or []):
191
+ missing.append(key)
192
+ elif value in (None, "", [], {}):
193
+ missing.append(key)
194
+ return missing
195
+
196
+
197
+ def _scope_fragments(contract: dict[str, Any]) -> list[str]:
198
+ items = [
199
+ *[str(item).strip() for item in contract.get("desired_outcomes") or []],
200
+ *[str(item).strip() for item in (contract.get("initial_scope") or {}).get("in_scope") or []],
201
+ ]
202
+ deduped: list[str] = []
203
+ seen: set[str] = set()
204
+ for item in items:
205
+ if item and item not in seen:
206
+ deduped.append(item)
207
+ seen.add(item)
208
+ return deduped
209
+
210
+
211
+ def evaluate_contract_sufficiency(contract: dict[str, Any]) -> dict[str, Any]:
212
+ missing = _missing_fields(contract)
213
+ assumptions = list(contract.get("assumptions") or [])
214
+ fragments = _scope_fragments(contract)
215
+ summary_tokens = _tokenize(str(contract.get("summary") or ""))
216
+ problem_tokens = _tokenize(str(contract.get("problem_statement") or ""))
217
+ fragment_tokens = [_tokenize(item) for item in fragments if item]
218
+
219
+ split_recommended = False
220
+ split_candidates: list[dict[str, Any]] = []
221
+ if len(fragment_tokens) >= 2:
222
+ anchor = fragment_tokens[0]
223
+ low_overlap = sum(1 for tokens in fragment_tokens[1:] if _jaccard(anchor, tokens) < 0.12)
224
+ if len(fragments) >= 3 and low_overlap >= 2:
225
+ split_recommended = True
226
+ for index, fragment in enumerate(fragments[:3], start=1):
227
+ split_candidates.append({
228
+ "slot": index,
229
+ "summary": fragment,
230
+ "problem_statement": str(contract.get("problem_statement") or contract.get("summary") or "").strip(),
231
+ "target_users": list(contract.get("target_users") or []),
232
+ "desired_outcomes": [fragment],
233
+ "initial_scope": {"in_scope": [fragment], "out_of_scope": []},
234
+ "constraints": list(contract.get("constraints") or []),
235
+ "acceptance_criteria": [fragment, *list(contract.get("acceptance_criteria") or [])[:1]],
236
+ "assumptions": list(assumptions),
237
+ })
238
+
239
+ thin_signals: list[str] = []
240
+ if missing:
241
+ thin_signals.append("missing_required_fields")
242
+ if len(summary_tokens | problem_tokens) < 8:
243
+ thin_signals.append("very_low_specificity")
244
+ if len(fragments) <= 1:
245
+ thin_signals.append("single_fragment_scope")
246
+ if float(contract.get("confidence") or 0.0) < 0.55:
247
+ thin_signals.append("low_contract_confidence")
248
+ if len(assumptions) >= 5:
249
+ thin_signals.append("assumption_heavy")
250
+
251
+ if split_recommended:
252
+ decision = "split"
253
+ passed = True
254
+ rationale = "The contract bundles multiple low-overlap outcome/scope fragments that read like separate ideas."
255
+ elif thin_signals:
256
+ decision = "too_thin"
257
+ passed = False
258
+ rationale = "The contract is still too thin to persist as a coherent idea without pretending confidence."
259
+ else:
260
+ decision = "sufficient"
261
+ passed = True
262
+ rationale = "The contract is specific enough to stand as one coherent idea without splitting."
263
+
264
+ return {
265
+ "kind": "idea_contract_validation.v1",
266
+ "decision": decision,
267
+ "passed": passed,
268
+ "rationale": rationale,
269
+ "missing_fields": missing,
270
+ "thin_signals": thin_signals,
271
+ "split_candidates": split_candidates,
272
+ "validated_contract": contract,
273
+ }
274
+
275
+
276
+ def build_creation_preview(*, idea_payload: dict[str, Any]) -> dict[str, Any]:
277
+ contract = _normalize_contract(
278
+ idea_id=str(idea_payload.get("idea_id") or "idea"),
279
+ project_id=str(idea_payload.get("project_id") or "") or None,
280
+ payload=idea_payload,
281
+ raw_text=str(idea_payload.get("latest_message") or idea_payload.get("summary") or ""),
282
+ )
283
+ validation = evaluate_contract_sufficiency(contract)
284
+ follow_up = None
285
+ if validation["decision"] == "too_thin":
286
+ missing = list(validation.get("missing_fields") or [])
287
+ biggest_gap = missing[0] if missing else "specificity"
288
+ follow_up = f"This is still too thin to build cleanly. Biggest gap: {biggest_gap}."
289
+ elif validation["decision"] == "split":
290
+ follow_up = "This likely wants to split into multiple ideas; don’t treat it as one by default."
291
+ else:
292
+ follow_up = "This reads as one coherent idea and can move into idea creation."
293
+ return {
294
+ "decision": validation["decision"],
295
+ "passed": bool(validation["passed"]),
296
+ "rationale": validation["rationale"],
297
+ "missing_fields": list(validation.get("missing_fields") or []),
298
+ "follow_up": follow_up,
299
+ }
300
+
301
+
302
+ def build_creation_queue_items(*, idea_payload: dict[str, Any]) -> list[dict[str, Any]]:
303
+ base_idea_id = str(idea_payload.get("idea_id") or "idea").strip() or "idea"
304
+ project_id = str(idea_payload.get("project_id") or "").strip() or None
305
+ contract = _normalize_contract(
306
+ idea_id=base_idea_id,
307
+ project_id=project_id,
308
+ payload=idea_payload,
309
+ raw_text=str(idea_payload.get("latest_message") or idea_payload.get("summary") or ""),
310
+ )
311
+ validation = evaluate_contract_sufficiency(contract)
312
+ contracts = list(validation.get("split_candidates") or []) if validation.get("decision") == "split" else [dict(validation.get("validated_contract") or contract)]
313
+ items: list[dict[str, Any]] = []
314
+ for index, contract_payload in enumerate(contracts, start=1):
315
+ slot = int(contract_payload.get("slot") or index)
316
+ queued_idea_id = base_idea_id if validation.get("decision") != "split" else _stable_id(
317
+ "idea_",
318
+ {"parent": base_idea_id, "slot": slot, "summary": contract_payload.get("summary")},
319
+ )
320
+ payload = dict(idea_payload)
321
+ payload["idea_id"] = queued_idea_id
322
+ payload["project_id"] = project_id
323
+ payload["title"] = str(contract_payload.get("summary") or queued_idea_id)
324
+ payload["summary"] = str(contract_payload.get("summary") or queued_idea_id)
325
+ payload["readiness_contract"] = dict(contract_payload)
326
+ payload["sufficient_idea"] = {
327
+ "summary": contract_payload.get("summary"),
328
+ "problem": contract_payload.get("problem_statement"),
329
+ "target_users": list(contract_payload.get("target_users") or []),
330
+ "user_outcomes": list(contract_payload.get("desired_outcomes") or []),
331
+ "scope": list((contract_payload.get("initial_scope") or {}).get("in_scope") or []),
332
+ "constraints": list(contract_payload.get("constraints") or []),
333
+ "acceptance_criteria": list(contract_payload.get("acceptance_criteria") or []),
334
+ }
335
+ if validation.get("decision") == "split":
336
+ payload["parent_idea_id"] = base_idea_id
337
+ payload["idea_creation_queue"] = {
338
+ "decision": validation.get("decision"),
339
+ "rationale": validation.get("rationale"),
340
+ "slot": slot,
341
+ "parent_idea_id": base_idea_id if validation.get("decision") == "split" else None,
342
+ }
343
+ items.append({
344
+ "idea_id": queued_idea_id,
345
+ "title": str(contract_payload.get("summary") or queued_idea_id),
346
+ "payload": payload,
347
+ "validation_decision": str(validation.get("decision") or "sufficient"),
348
+ "slot": slot,
349
+ })
350
+ return items
351
+
352
+
353
+ def _collect_relevant_paths(*, root: Path, query_tokens: list[str], allowed_suffixes: tuple[str, ...]) -> list[str]:
354
+ if not root.exists():
355
+ return []
356
+ matches: list[tuple[int, str]] = []
357
+ for path in root.rglob("*"):
358
+ if not path.is_file() or path.suffix.lower() not in allowed_suffixes:
359
+ continue
360
+ rel = str(path)
361
+ hay = rel.lower()
362
+ score = sum(3 for token in query_tokens if token and token in hay)
363
+ if score == 0:
364
+ try:
365
+ text = path.read_text(encoding="utf-8", errors="ignore")[:4000].lower()
366
+ except Exception:
367
+ text = ""
368
+ score = sum(1 for token in query_tokens if token and token in text)
369
+ if score > 0:
370
+ matches.append((score, rel))
371
+ matches.sort(key=lambda item: (-item[0], item[1]))
372
+ return [path for _score, path in matches[:12]]
373
+
374
+
375
+ def _persist_local_idea(*, repo_root: Path, idea_id: str, payload: dict[str, Any]) -> str:
376
+ idea_path = get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "idea.json"
377
+ idea_path.parent.mkdir(parents=True, exist_ok=True)
378
+ _write_json(idea_path, payload)
379
+ return str(idea_path)
380
+
381
+
382
+ def _persist_supabase_ideas(*, rows: list[dict[str, Any]]) -> None:
383
+ config = _resolve_supabase_rest_config()
384
+ if config is None or not rows:
385
+ return
386
+ url, key = config
387
+ try:
388
+ _postgrest_request(
389
+ method="POST",
390
+ url=f"{url}/rest/v1/devflow_project_ideas?on_conflict=idea_id",
391
+ key=key,
392
+ body=rows,
393
+ prefer="resolution=merge-duplicates",
394
+ )
395
+ except Exception:
396
+ return
397
+
398
+
399
+ def _dfs_running(*, project_id: str | None, run_id: str, idea_id: str, summary: str) -> None:
400
+ if not project_id:
401
+ return
402
+ publish_devflow_state(
403
+ project_id=project_id,
404
+ run_id=run_id,
405
+ current_state="running",
406
+ current_status="processing",
407
+ run_summary=summary,
408
+ display="project",
409
+ display_path=f"idea:{idea_id}",
410
+ )
411
+
412
+
413
+ def _dfs_terminal(*, project_id: str | None, run_id: str, idea_id: str, summary: str, current_state: str, current_status: str, error_message: str | None = None) -> None:
414
+ if not project_id:
415
+ return
416
+ publish_devflow_state(
417
+ project_id=project_id,
418
+ run_id=run_id,
419
+ current_state=current_state,
420
+ current_status=current_status,
421
+ run_summary=summary,
422
+ error_message=error_message,
423
+ display="project",
424
+ display_path=f"idea:{idea_id}",
425
+ )
426
+
427
+
428
+ class ValidateIdeaContractNode(Node):
429
+ async def process(self, task_context: TaskContext) -> TaskContext:
430
+ event = task_context.event
431
+ repo_root = Path(event.repo_root)
432
+ store, run_id = _store_run()
433
+ node_exec_id = store.create_node_attempt(run_id=run_id, node_id="validate_contract", node_name="ValidateIdeaContract", attempt=1)
434
+ payload = _load_json(Path(str(event.source_path))) if event.source_path else _load_json(get_idea_paths(repo_root, idea_id=event.idea_id).idea_dir / "idea.json")
435
+ contract = _normalize_contract(idea_id=event.idea_id, project_id=event.project_id, payload=payload, raw_text=event.text)
436
+ validation = evaluate_contract_sufficiency(contract)
437
+ stage_path = _pipeline_root(repo_root, idea_id=event.idea_id, pipeline_key=event.pipeline_key) / "validate_contract.json"
438
+ _write_json(stage_path, validation)
439
+ store.add_artifact(run_id=run_id, node_exec_id=node_exec_id, kind="idea_contract_validation", uri=str(stage_path), metadata=validation)
440
+ task_context.metadata["contract_validation"] = validation
441
+ if not validation["passed"]:
442
+ store.mark_node_finished(node_exec_id=node_exec_id, status="failed", output=validation, error={"message": validation["rationale"]})
443
+ task_context.metadata["outcome"] = {
444
+ "status": "contract_insufficient",
445
+ "idea_id": event.idea_id,
446
+ "validation": validation,
447
+ }
448
+ task_context.metadata["message"] = json.dumps(task_context.metadata["outcome"], indent=2, sort_keys=True) + "\n"
449
+ task_context.metadata["exit_code"] = 2
450
+ task_context.stop_workflow()
451
+ return task_context
452
+ store.mark_node_finished(node_exec_id=node_exec_id, status="succeeded", output=validation)
453
+ return task_context
454
+
455
+
456
+ class GatherIdeaContextNode(Node):
457
+ async def process(self, task_context: TaskContext) -> TaskContext:
458
+ event = task_context.event
459
+ repo_root = Path(event.repo_root)
460
+ store, run_id = _store_run()
461
+ node_exec_id = store.create_node_attempt(run_id=run_id, node_id="gather_context", node_name="GatherIdeaContext", attempt=1)
462
+ validation = dict(task_context.metadata.get("contract_validation") or {})
463
+ contracts = list(validation.get("split_candidates") or []) if validation.get("decision") == "split" else [dict(validation.get("validated_contract") or {})]
464
+ bundles: list[dict[str, Any]] = []
465
+ for index, contract in enumerate(contracts, start=1):
466
+ tokens = sorted({* _tokenize(str(contract.get("summary") or "")), * _tokenize(str(contract.get("problem_statement") or "")), * _tokenize(" ".join(contract.get("target_users") or [])), * _tokenize(" ".join(contract.get("desired_outcomes") or [])), * _tokenize(" ".join((contract.get("initial_scope") or {}).get("in_scope") or []))})
467
+ bundles.append({
468
+ "slot": index,
469
+ "contract": contract,
470
+ "retrieval_basis": tokens[:16],
471
+ "code_is_source_of_truth": True,
472
+ "code_paths": _collect_relevant_paths(root=repo_root / "src", query_tokens=tokens, allowed_suffixes=(".py", ".ts", ".tsx", ".js", ".jsx")),
473
+ "source_doc_paths": _collect_relevant_paths(root=repo_root / "ai_docs" / "context" / "source_docs", query_tokens=tokens, allowed_suffixes=(".json", ".md")),
474
+ "project_doc_paths": _collect_relevant_paths(root=repo_root / "ai_docs" / "context" / "v2" / "project_docs", query_tokens=tokens, allowed_suffixes=(".json", ".md")),
475
+ })
476
+ payload = {
477
+ "kind": "idea_context_bundle.v1",
478
+ "idea_id": event.idea_id,
479
+ "decision": validation.get("decision"),
480
+ "bundles": bundles,
481
+ "retrieval_policy": "contract-driven; code wins when code and docs diverge",
482
+ }
483
+ stage_path = _pipeline_root(repo_root, idea_id=event.idea_id, pipeline_key=event.pipeline_key) / "gather_context.json"
484
+ _write_json(stage_path, payload)
485
+ store.add_artifact(run_id=run_id, node_exec_id=node_exec_id, kind="idea_context_bundle", uri=str(stage_path), metadata={"bundle_count": len(bundles)})
486
+ store.mark_node_finished(node_exec_id=node_exec_id, status="succeeded", output=payload)
487
+ task_context.metadata["context_bundle"] = payload
488
+ return task_context
489
+
490
+
491
+ class PersistIdeaNode(Node):
492
+ async def process(self, task_context: TaskContext) -> TaskContext:
493
+ event = task_context.event
494
+ repo_root = Path(event.repo_root)
495
+ store, run_id = _store_run()
496
+ node_exec_id = store.create_node_attempt(run_id=run_id, node_id="persist", node_name="PersistIdea", attempt=1)
497
+ validation = dict(task_context.metadata.get("contract_validation") or {})
498
+ context_bundle = dict(task_context.metadata.get("context_bundle") or {})
499
+ project_id = str(event.project_id or (validation.get("validated_contract") or {}).get("project_id") or "").strip() or None
500
+ persisted_rows: list[dict[str, Any]] = []
501
+ persisted_local: list[dict[str, Any]] = []
502
+ for bundle in list(context_bundle.get("bundles") or []):
503
+ contract = dict(bundle.get("contract") or {})
504
+ slot = int(bundle.get("slot") or 1)
505
+ persisted_idea_id = event.idea_id if validation.get("decision") != "split" else _stable_id("idea_", {"parent": event.idea_id, "slot": slot, "summary": contract.get("summary")})
506
+ local_payload = {
507
+ "idea_id": persisted_idea_id,
508
+ "parent_idea_id": event.idea_id if validation.get("decision") == "split" else None,
509
+ "project_id": project_id,
510
+ "origin": "Devin",
511
+ "status": "ready_for_registration",
512
+ "shape": "sufficient",
513
+ "title": str(contract.get("summary") or persisted_idea_id),
514
+ "summary": str(contract.get("summary") or persisted_idea_id),
515
+ "sufficient_idea": {
516
+ "summary": contract.get("summary"),
517
+ "problem": contract.get("problem_statement"),
518
+ "target_users": contract.get("target_users") or [],
519
+ "user_outcomes": contract.get("desired_outcomes") or [],
520
+ "scope": list((contract.get("initial_scope") or {}).get("in_scope") or []),
521
+ "constraints": contract.get("constraints") or [],
522
+ "acceptance_criteria": contract.get("acceptance_criteria") or [],
523
+ },
524
+ "readiness_contract": contract,
525
+ "idea_creation": {
526
+ "validation_decision": validation.get("decision"),
527
+ "validation_rationale": validation.get("rationale"),
528
+ "context_bundle": bundle,
529
+ "pipeline_dir": str(_pipeline_root(repo_root, idea_id=event.idea_id, pipeline_key=event.pipeline_key)),
530
+ "run_id": run_id,
531
+ },
532
+ }
533
+ artifact_path = _persist_local_idea(repo_root=repo_root, idea_id=persisted_idea_id, payload=local_payload)
534
+ persisted_local.append({"idea_id": persisted_idea_id, "artifact_path": artifact_path})
535
+ persisted_rows.append({
536
+ "idea_id": persisted_idea_id,
537
+ "project_id": project_id,
538
+ "run_id": run_id,
539
+ "title": local_payload["title"],
540
+ "summary": local_payload["summary"],
541
+ "status": local_payload["status"],
542
+ "shape": local_payload["shape"],
543
+ "origin": "Devin",
544
+ "artifact_path": artifact_path,
545
+ "idea_payload": local_payload,
546
+ "updated_at": datetime.now(UTC).isoformat(),
547
+ })
548
+ _persist_supabase_ideas(rows=persisted_rows)
549
+ payload = {
550
+ "kind": "idea_persist_result.v1",
551
+ "idea_id": event.idea_id,
552
+ "project_id": project_id,
553
+ "decision": validation.get("decision"),
554
+ "persisted_ideas": persisted_local,
555
+ "supabase_rows_attempted": len(persisted_rows),
556
+ "local_state_written": True,
557
+ }
558
+ stage_path = _pipeline_root(repo_root, idea_id=event.idea_id, pipeline_key=event.pipeline_key) / "persist.json"
559
+ _write_json(stage_path, payload)
560
+ store.add_artifact(run_id=run_id, node_exec_id=node_exec_id, kind="idea_persist_result", uri=str(stage_path), metadata=payload)
561
+ store.mark_node_finished(node_exec_id=node_exec_id, status="succeeded", output=payload)
562
+ task_context.metadata["outcome"] = {
563
+ "status": "completed",
564
+ "idea_id": event.idea_id,
565
+ "decision": validation.get("decision"),
566
+ "persisted_ideas": persisted_local,
567
+ "idea_creation_run_id": run_id,
568
+ "idea_creation_pipeline_dir": str(_pipeline_root(repo_root, idea_id=event.idea_id, pipeline_key=event.pipeline_key)),
569
+ }
570
+ task_context.metadata["message"] = json.dumps(task_context.metadata["outcome"], indent=2, sort_keys=True) + "\n"
571
+ task_context.metadata["exit_code"] = 0
572
+ return task_context
573
+
574
+
575
+ class IdeaCreationWorkflow(Workflow):
576
+ workflow_schema = WorkflowSchema(
577
+ description="Idea creation DAG (validate contract -> gather context -> persist)",
578
+ event_schema=IdeaCreationDagEvent,
579
+ start=ValidateIdeaContractNode,
580
+ nodes=[
581
+ NodeConfig(node=ValidateIdeaContractNode, connections=[GatherIdeaContextNode]),
582
+ NodeConfig(node=GatherIdeaContextNode, connections=[PersistIdeaNode]),
583
+ NodeConfig(node=PersistIdeaNode, connections=[]),
584
+ ],
585
+ )
586
+
587
+
588
+ def build_pipeline_key(*, repo_root: Path, idea_id: str, text: str | None, source_path: Path | None) -> str:
589
+ return _stable_id("run_", {"repo_root": str(repo_root), "idea_id": idea_id, "text": text or "", "source_path": str(source_path) if source_path else None})
590
+
591
+
592
+ async def run_idea_creation_dag_async(*, repo_root: Path, store: ExecutionStore, idea_id: str, project_id: str | None, text: str | None, source_path: Path | None) -> IdeaCreationDagResult:
593
+ pipeline_key = build_pipeline_key(repo_root=repo_root, idea_id=idea_id, text=text, source_path=source_path)
594
+ pipeline_dir = _pipeline_root(repo_root, idea_id=idea_id, pipeline_key=pipeline_key)
595
+ pipeline_dir.mkdir(parents=True, exist_ok=True)
596
+ run_id = store.create_run(
597
+ dag_id=DAG_ID,
598
+ dag_version="v1",
599
+ root_correlation_id=f"corr_{pipeline_key}",
600
+ config={"idea_id": idea_id, "pipeline_key": pipeline_key, "project_id": project_id},
601
+ )
602
+ store.mark_run_started(run_id=run_id)
603
+ _dfs_running(project_id=project_id, run_id=run_id, idea_id=idea_id, summary="Creating idea from contract")
604
+ wf = IdeaCreationWorkflow()
605
+ global _CURRENT_STORE, _CURRENT_RUN_ID
606
+ _CURRENT_STORE = store
607
+ _CURRENT_RUN_ID = run_id
608
+ try:
609
+ ctx = await asyncio.to_thread(
610
+ wf.run,
611
+ {
612
+ "repo_root": str(repo_root),
613
+ "idea_id": idea_id,
614
+ "project_id": project_id,
615
+ "text": text,
616
+ "source_path": str(source_path) if source_path else None,
617
+ "pipeline_key": pipeline_key,
618
+ },
619
+ )
620
+ except Exception as exc:
621
+ store.mark_run_finished(run_id=run_id, status="failed")
622
+ _dfs_terminal(project_id=project_id, run_id=run_id, idea_id=idea_id, summary="Idea creation failed", current_state="failed", current_status="failed", error_message=str(exc))
623
+ raise
624
+ finally:
625
+ _CURRENT_STORE = None
626
+ _CURRENT_RUN_ID = None
627
+ exit_code = int(ctx.metadata.get("exit_code") or 0)
628
+ store.mark_run_finished(run_id=run_id, status="succeeded" if exit_code == 0 else "failed")
629
+ if exit_code == 0:
630
+ _dfs_terminal(project_id=project_id, run_id=run_id, idea_id=idea_id, summary="Idea creation complete", current_state="completed", current_status="succeeded")
631
+ else:
632
+ _dfs_terminal(project_id=project_id, run_id=run_id, idea_id=idea_id, summary="Idea creation blocked", current_state="failed", current_status="failed", error_message=str(ctx.metadata.get("message") or "contract insufficient"))
633
+ return IdeaCreationDagResult(
634
+ exit_code=exit_code,
635
+ run_id=run_id,
636
+ pipeline_dir=pipeline_dir,
637
+ message=str(ctx.metadata.get("message") or ""),
638
+ outcome=dict(ctx.metadata.get("outcome") or {}),
639
+ )
640
+
641
+
642
+ def run_idea_creation_dag(*, repo_root: Path, store: ExecutionStore, idea_id: str, project_id: str | None, text: str | None, source_path: Path | None) -> IdeaCreationDagResult:
643
+ return asyncio.run(run_idea_creation_dag_async(repo_root=repo_root, store=store, idea_id=idea_id, project_id=project_id, text=text, source_path=source_path))