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,1069 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import subprocess
7
+ from dataclasses import dataclass
8
+ from datetime import UTC, datetime
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from urllib.parse import quote
12
+ from urllib.request import Request, urlopen
13
+
14
+ from pydantic import BaseModel
15
+
16
+ from ..devflow_state import publish_devflow_state
17
+ from ..idea.paths import get_idea_paths
18
+ from ..stores.execution_store import ExecutionStore
19
+ from ..vendor.datalumina_genai.core.nodes.agent import AgentConfig, AgentNode
20
+ from ..vendor.datalumina_genai.core.nodes.base import Node
21
+ from ..vendor.datalumina_genai.core.nodes.router import BaseRouter, RouterNode
22
+ from ..vendor.datalumina_genai.core.schema import NodeConfig, WorkflowSchema
23
+ from ..vendor.datalumina_genai.core.task import TaskContext
24
+ from ..vendor.datalumina_genai.core.workflow import Workflow
25
+ from . import agentic as scope_idea_agentic
26
+ from .models import (
27
+ ArtifactLineage,
28
+ GoldilocksAssessmentArtifact,
29
+ GoldilocksDecisionArtifact,
30
+ IdeaCandidateArtifact,
31
+ IdeaCandidateSetArtifact,
32
+ IdeaRegistryRecordArtifact,
33
+ IdeaResolutionPackageArtifact,
34
+ IdeaSplitPlanArtifact,
35
+ NarrowScopeReviewArtifact,
36
+ RegisteredIdeaArtifact,
37
+ ScopeContextArtifact,
38
+ ScopeIdeaDagSummary,
39
+ SourceEvidenceRef,
40
+ )
41
+
42
+ DAG_ID = "scope_to_idea_dag"
43
+
44
+ _CURRENT_STORE: ExecutionStore | None = None
45
+ _CURRENT_RUN_ID: str | None = None
46
+
47
+
48
+ def _dfs_running(*, project_id: str, run_id: str, scope_id: str, summary: str = "Generating ideas from scopes") -> None:
49
+ publish_devflow_state(
50
+ project_id=project_id,
51
+ run_id=run_id,
52
+ current_state="running",
53
+ current_status="processing",
54
+ run_summary=summary,
55
+ display="project",
56
+ display_path=f"scope:{scope_id}",
57
+ )
58
+
59
+
60
+ def _dfs_terminal(
61
+ *,
62
+ project_id: str,
63
+ run_id: str,
64
+ scope_id: str,
65
+ current_state: str,
66
+ current_status: str,
67
+ summary: str,
68
+ error_message: str | None = None,
69
+ ) -> None:
70
+ publish_devflow_state(
71
+ project_id=project_id,
72
+ run_id=run_id,
73
+ current_state=current_state,
74
+ current_status=current_status,
75
+ run_summary=summary,
76
+ error_message=error_message,
77
+ display="project",
78
+ display_path=f"scope:{scope_id}",
79
+ )
80
+
81
+
82
+ def _keychain_get(service: str, account: str) -> str | None:
83
+ try:
84
+ proc = subprocess.run(
85
+ ["security", "find-generic-password", "-s", service, "-a", account, "-w"],
86
+ capture_output=True,
87
+ text=True,
88
+ check=False,
89
+ timeout=10,
90
+ )
91
+ except Exception:
92
+ return None
93
+ if proc.returncode != 0:
94
+ return None
95
+ value = proc.stdout.strip()
96
+ return value or None
97
+
98
+
99
+ def _resolve_supabase_rest_config() -> tuple[str, str] | None:
100
+ url = (
101
+ os.environ.get("DEVFLOW_SUPABASE_URL")
102
+ or os.environ.get("SUPABASE_URL")
103
+ or _keychain_get("Supabase URL", "Clarity")
104
+ )
105
+ key = (
106
+ os.environ.get("DEVFLOW_SUPABASE_SERVICE_KEY")
107
+ or os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
108
+ or os.environ.get("SUPABASE_SERVICE_KEY")
109
+ or _keychain_get("Supabase Service Key", "Clarity")
110
+ )
111
+ if not url or not key:
112
+ return None
113
+ return url.rstrip("/"), key
114
+
115
+
116
+ def _postgrest_request(*, method: str, url: str, key: str, body: Any | None = None, prefer: str | None = None) -> Any:
117
+ payload = None if body is None else json.dumps(body).encode("utf-8")
118
+ req = Request(url, data=payload, method=method)
119
+ req.add_header("apikey", key)
120
+ req.add_header("Authorization", f"Bearer {key}")
121
+ if body is not None:
122
+ req.add_header("Content-Type", "application/json")
123
+ if prefer:
124
+ req.add_header("Prefer", prefer)
125
+ with urlopen(req, timeout=30) as resp:
126
+ raw = resp.read().decode("utf-8")
127
+ return json.loads(raw) if raw else None
128
+
129
+
130
+ def _sync_registered_ideas_to_supabase(*, project_id: str, scope_id: str, run_id: str, pipeline_dir: Path) -> None:
131
+ config = _resolve_supabase_rest_config()
132
+ if config is None:
133
+ return
134
+ url, key = config
135
+ registry_path = pipeline_dir / "idea_registry_record.json"
136
+ if not registry_path.exists():
137
+ return
138
+ registry = json.loads(registry_path.read_text(encoding="utf-8"))
139
+ ideas = registry.get("registered_ideas") or []
140
+ rows: list[dict[str, Any]] = []
141
+ for idea in ideas:
142
+ if not isinstance(idea, dict) or not idea.get("idea_id"):
143
+ continue
144
+ persisted: dict[str, Any] = {}
145
+ idea_ref = idea.get("idea_ref")
146
+ if isinstance(idea_ref, str) and Path(idea_ref).exists():
147
+ persisted = json.loads(Path(idea_ref).read_text(encoding="utf-8"))
148
+ payload = persisted.get("idea") if isinstance(persisted.get("idea"), dict) else {}
149
+ rows.append(
150
+ {
151
+ "idea_id": str(idea.get("idea_id")),
152
+ "project_id": project_id,
153
+ "scope_set_id": registry.get("scope_set_id") or persisted.get("scope_set_id"),
154
+ "scope_id": scope_id,
155
+ "run_id": run_id,
156
+ "title": str(
157
+ idea.get("title")
158
+ or persisted.get("title")
159
+ or payload.get("title")
160
+ or payload.get("goal")
161
+ or payload.get("problem")
162
+ or idea.get("idea_id")
163
+ ),
164
+ "summary": (
165
+ idea.get("summary")
166
+ or persisted.get("summary")
167
+ or payload.get("summary")
168
+ or payload.get("goal")
169
+ or payload.get("problem")
170
+ ),
171
+ "status": idea.get("status") or persisted.get("status"),
172
+ "shape": idea.get("shape") or registry.get("scope_shape"),
173
+ "resolution_status": registry.get("resolution_status"),
174
+ "origin": "scope_to_idea",
175
+ "artifact_path": str(idea_ref or registry_path),
176
+ "updated_at": datetime.now(UTC).isoformat(),
177
+ }
178
+ )
179
+ delete_url = f"{url}/rest/v1/devflow_project_ideas?project_id=eq.{quote(project_id)}&scope_id=eq.{quote(scope_id)}"
180
+ _postgrest_request(method="DELETE", url=delete_url, key=key)
181
+ if rows:
182
+ upsert_url = f"{url}/rest/v1/devflow_project_ideas?on_conflict=idea_id"
183
+ _postgrest_request(method="POST", url=upsert_url, key=key, body=rows, prefer="resolution=merge-duplicates")
184
+
185
+
186
+ @dataclass(frozen=True)
187
+ class ScopeToIdeaDagResult:
188
+ exit_code: int
189
+ run_id: str
190
+ pipeline_dir: Path
191
+ message: str
192
+ outcome: dict[str, Any]
193
+
194
+
195
+ class ScopeToIdeaDagEvent(BaseModel):
196
+ repo_root: str
197
+ project_id: str
198
+ scope_set_id: str
199
+ scope_id: str
200
+ scope_payload_path: str | None = None
201
+ scope_payload_inline: dict[str, Any] | None = None
202
+ pipeline_key: str
203
+
204
+
205
+ def _store_run() -> tuple[ExecutionStore, str]:
206
+ if _CURRENT_STORE is None or _CURRENT_RUN_ID is None:
207
+ raise RuntimeError("scope->idea dag missing runtime store/run_id")
208
+ return _CURRENT_STORE, _CURRENT_RUN_ID
209
+
210
+
211
+ def _stable_hash(payload: Any) -> str:
212
+ return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
213
+
214
+
215
+ def _stable_id(prefix: str, payload: Any, *, size: int = 12) -> str:
216
+ return f"{prefix}{_stable_hash(payload)[:size]}"
217
+
218
+
219
+ def _write_json(path: Path, payload: dict[str, Any]) -> None:
220
+ path.parent.mkdir(parents=True, exist_ok=True)
221
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
222
+
223
+
224
+ def _append_node_trace(*, pipeline_root: Path, node: str, event: str, detail: dict[str, Any] | None = None) -> None:
225
+ path = pipeline_root / "node_trace.jsonl"
226
+ record = {
227
+ "ts": datetime.now(UTC).isoformat(),
228
+ "node": node,
229
+ "event": event,
230
+ }
231
+ if detail:
232
+ record["detail"] = detail
233
+ path.parent.mkdir(parents=True, exist_ok=True)
234
+ with path.open("a", encoding="utf-8") as fh:
235
+ fh.write(json.dumps(record, sort_keys=True) + "\n")
236
+
237
+
238
+ def _agent_timeout_seconds() -> int:
239
+ raw = os.environ.get("DEVFLOW_SCOPE_IDEA_AGENT_TIMEOUT")
240
+ if not raw:
241
+ return 300
242
+ try:
243
+ value = int(raw)
244
+ except ValueError:
245
+ return 300
246
+ return value if value > 0 else 300
247
+
248
+
249
+ def _relative_to_repo(repo_root: Path, path: Path) -> str:
250
+ try:
251
+ return str(path.relative_to(repo_root))
252
+ except ValueError:
253
+ return str(path)
254
+
255
+
256
+ def _lineage_entry(
257
+ *,
258
+ stage: str,
259
+ origin: str,
260
+ mode: str,
261
+ repo_root: Path,
262
+ artifact_path: Path | None = None,
263
+ agent_run_path: Path | None = None,
264
+ generated_from: list[str] | None = None,
265
+ notes: list[str] | None = None,
266
+ metadata: dict[str, Any] | None = None,
267
+ ) -> ArtifactLineage:
268
+ return ArtifactLineage(
269
+ stage=stage,
270
+ origin=origin, # type: ignore[arg-type]
271
+ mode=mode,
272
+ artifact_path=_relative_to_repo(repo_root, artifact_path) if artifact_path else None,
273
+ agent_run_ref=_relative_to_repo(repo_root, agent_run_path) if agent_run_path else None,
274
+ generated_from=generated_from or [],
275
+ notes=notes or [],
276
+ metadata=metadata or {},
277
+ )
278
+
279
+
280
+ def _run_scope_idea_agent(
281
+ *,
282
+ repo_root: Path,
283
+ pipeline_root: Path,
284
+ stage_name: str,
285
+ output_model: type[BaseModel],
286
+ context_payload: dict[str, Any],
287
+ guidance: list[str],
288
+ ) -> tuple[BaseModel, ArtifactLineage]:
289
+ artifact, envelope = scope_idea_agentic.run_scope_idea_agent_step(
290
+ repo_root=repo_root,
291
+ stage_name=stage_name,
292
+ output_model=output_model,
293
+ context_payload=context_payload,
294
+ guidance=guidance,
295
+ timeout_seconds=_agent_timeout_seconds(),
296
+ )
297
+ agent_run_path = scope_idea_agentic.persist_agent_run(pipeline_root=pipeline_root, node_id=stage_name, envelope=envelope)
298
+ return artifact, _lineage_entry(
299
+ stage=stage_name,
300
+ origin="model",
301
+ mode="agent",
302
+ repo_root=repo_root,
303
+ agent_run_path=agent_run_path,
304
+ notes=["Artifact was produced from the model-backed execution path."],
305
+ metadata={"model_backed": True},
306
+ )
307
+
308
+
309
+ def _pipeline_root(repo_root: Path, *, scope_id: str, pipeline_key: str) -> Path:
310
+ return repo_root / ".devflow" / "scopes" / scope_id / "pipelines" / DAG_ID / pipeline_key
311
+
312
+
313
+ def build_pipeline_key(*, repo_root: Path, project_id: str, scope_set_id: str, scope_id: str, scope_payload: dict[str, Any]) -> str:
314
+ return _stable_id(
315
+ "run_",
316
+ {
317
+ "repo_root": str(repo_root),
318
+ "project_id": project_id,
319
+ "scope_set_id": scope_set_id,
320
+ "scope_id": scope_id,
321
+ "scope_payload": scope_payload,
322
+ },
323
+ )
324
+
325
+
326
+ def _load_scope_payload(*, inline_payload: dict[str, Any] | None, payload_path: Path | None) -> dict[str, Any]:
327
+ if inline_payload and payload_path:
328
+ raise ValueError("Provide exactly one of scope_payload_inline or scope_payload_path")
329
+ if payload_path:
330
+ return json.loads(payload_path.read_text(encoding="utf-8"))
331
+ if inline_payload:
332
+ return dict(inline_payload)
333
+ raise ValueError("Missing scope payload for scope->idea DAG")
334
+
335
+
336
+ def _scope_payload_item(payload: dict[str, Any]) -> dict[str, Any]:
337
+ scope_item = payload.get("scope_item")
338
+ if isinstance(scope_item, dict):
339
+ return scope_item
340
+ return payload
341
+
342
+
343
+ def _coerce_source_support(items: Any) -> list[SourceEvidenceRef]:
344
+ if not isinstance(items, list):
345
+ return []
346
+ refs: list[SourceEvidenceRef] = []
347
+ for item in items:
348
+ if isinstance(item, dict):
349
+ refs.append(SourceEvidenceRef.model_validate(item))
350
+ elif isinstance(item, str) and item.strip():
351
+ refs.append(SourceEvidenceRef(ref=item.strip()))
352
+ return refs
353
+
354
+
355
+ def _scope_context_from_payload(*, payload: dict[str, Any], project_id: str, scope_set_id: str, scope_id: str) -> ScopeContextArtifact:
356
+ scope_item = _scope_payload_item(payload)
357
+ scope_title = str(scope_item.get("title") or payload.get("scope_title") or payload.get("title") or scope_id).strip()
358
+ scope_description = str(scope_item.get("description") or payload.get("scope_description") or payload.get("description") or "").strip()
359
+ assumptions = scope_item.get("assumptions")
360
+ if not isinstance(assumptions, list):
361
+ assumptions = payload.get("assumptions")
362
+ constraints = scope_item.get("cross_cutting_constraints")
363
+ if not isinstance(constraints, list):
364
+ constraints = payload.get("cross_cutting_constraints")
365
+ neighbor_scope_refs = scope_item.get("neighbor_scope_refs")
366
+ if not isinstance(neighbor_scope_refs, list):
367
+ neighbor_scope_refs = payload.get("neighbor_scope_refs")
368
+ source_support = scope_item.get("source_support")
369
+ if not isinstance(source_support, list):
370
+ source_support = payload.get("source_support")
371
+
372
+ artifact = ScopeContextArtifact(
373
+ project_id=project_id,
374
+ scope_set_id=scope_set_id,
375
+ scope_id=scope_id,
376
+ scope_title=scope_title,
377
+ scope_description=scope_description,
378
+ source_support=_coerce_source_support(source_support),
379
+ assumptions=[str(item) for item in assumptions or []],
380
+ cross_cutting_constraints=[str(item) for item in constraints or []],
381
+ neighbor_scope_refs=[str(item) for item in neighbor_scope_refs or []],
382
+ approval_status=str(scope_item.get("approval_status") or payload.get("approval_status") or payload.get("status") or "approved"),
383
+ )
384
+ if not artifact.scope_title:
385
+ raise ValueError(f"Scope payload for {scope_id} is missing scope_item.title/title")
386
+ if not artifact.scope_description:
387
+ raise ValueError(f"Scope payload for {scope_id} is missing scope_item.description/description")
388
+ return artifact
389
+
390
+
391
+ def _build_constraints(scope: ScopeContextArtifact, *, default_constraint: str) -> list[str]:
392
+ constraints = [item for item in scope.cross_cutting_constraints if item]
393
+ if default_constraint not in constraints:
394
+ constraints.append(default_constraint)
395
+ return constraints
396
+
397
+
398
+ def _build_traceability(scope: ScopeContextArtifact, *, extras: list[str] | None = None) -> list[str]:
399
+ traceability = [
400
+ f"project:{scope.project_id}",
401
+ f"scope_set:{scope.scope_set_id}",
402
+ f"scope:{scope.scope_id}",
403
+ ]
404
+ for source_ref in scope.source_support:
405
+ traceability.append(f"source:{source_ref.ref}")
406
+ if extras:
407
+ traceability.extend(extras)
408
+ return traceability
409
+
410
+
411
+ class LoadApprovedScopeItemNode(Node):
412
+ async def process(self, task_context: TaskContext) -> TaskContext:
413
+ event = task_context.event
414
+ repo_root = Path(event.repo_root)
415
+ _dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Loading approved scope")
416
+ payload = _load_scope_payload(
417
+ inline_payload=event.scope_payload_inline,
418
+ payload_path=Path(event.scope_payload_path) if event.scope_payload_path else None,
419
+ )
420
+ artifact = _scope_context_from_payload(
421
+ payload=payload,
422
+ project_id=event.project_id,
423
+ scope_set_id=event.scope_set_id,
424
+ scope_id=event.scope_id,
425
+ )
426
+ stage_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "scope_context.json"
427
+ _write_json(stage_path, artifact.model_dump())
428
+ task_context.metadata["scope_context"] = artifact
429
+ self.save_output(artifact)
430
+ return task_context
431
+
432
+
433
+ class AssessScopeGoldilocksNode(AgentNode):
434
+ def get_agent_config(self) -> AgentConfig:
435
+ return AgentConfig(
436
+ instructions="Assess whether the approved scope is too broad, just right, or too narrow.",
437
+ output_type=GoldilocksAssessmentArtifact,
438
+ )
439
+
440
+ async def process(self, task_context: TaskContext) -> TaskContext:
441
+ event = task_context.event
442
+ repo_root = Path(event.repo_root)
443
+ _dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Assessing scope goldilocks")
444
+ pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
445
+ scope = task_context.metadata["scope_context"]
446
+ artifact, execution_lineage = _run_scope_idea_agent(
447
+ repo_root=repo_root,
448
+ pipeline_root=pipeline_root,
449
+ stage_name="assess_scope_goldilocks",
450
+ output_model=GoldilocksAssessmentArtifact,
451
+ context_payload={
452
+ "project_id": scope.project_id,
453
+ "scope_set_id": scope.scope_set_id,
454
+ "scope_id": scope.scope_id,
455
+ "approved_scope": scope.model_dump(),
456
+ },
457
+ guidance=[
458
+ "Assess whether the approved scope is too broad, just right, or too narrow for one idea candidate.",
459
+ "Treat the approved scope as the anchor. Do not rewrite or broaden its intent.",
460
+ "Set split_recommended true only when the approved scope clearly bundles multiple distinct business outcomes that a human would recognize as separate planning units.",
461
+ "Do not split merely because the scope contains multiple sub-workflows, metrics, linked entities, lifecycle steps, or supporting artifact types.",
462
+ "Should-not-split examples: customer management, reporting, documents and photos, and job management when they still describe one shared business slice.",
463
+ "Should-split example: quotes when internal composition and customer delivery/approval are clearly separate outcome surfaces.",
464
+ "Keep canonical idea sufficiency downstream; this node judges boundary size only.",
465
+ ],
466
+ )
467
+ artifact_data = artifact.model_dump()
468
+ notes = list(artifact_data.get("notes") or [])
469
+ notes.extend(execution_lineage.notes)
470
+ artifact_data["notes"] = list(dict.fromkeys(notes))
471
+ artifact = GoldilocksAssessmentArtifact.model_validate(artifact_data)
472
+ stage_path = pipeline_root / "goldilocks_assessment.json"
473
+ artifact.execution_lineage = execution_lineage.model_copy(
474
+ update={
475
+ "artifact_path": _relative_to_repo(repo_root, stage_path),
476
+ "generated_from": [_relative_to_repo(repo_root, pipeline_root / "scope_context.json")],
477
+ }
478
+ )
479
+ _write_json(stage_path, artifact.model_dump())
480
+ task_context.metadata["goldilocks_assessment"] = artifact
481
+ self.save_output(artifact)
482
+ return task_context
483
+
484
+
485
+ class GoldilocksDecisionNode(Node):
486
+ async def process(self, task_context: TaskContext) -> TaskContext:
487
+ event = task_context.event
488
+ repo_root = Path(event.repo_root)
489
+ _dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Routing scope shape")
490
+ assessment = task_context.metadata["goldilocks_assessment"]
491
+ next_node = {
492
+ "too_broad": "SplitScopeIntoIdeaCandidatesNode",
493
+ "just_right": "DraftIdeaFromScopeNode",
494
+ "too_narrow": "NarrowScopeReviewPackageNode",
495
+ }[assessment.scope_shape]
496
+ artifact = GoldilocksDecisionArtifact(
497
+ decision=assessment.scope_shape,
498
+ reason=assessment.reasoning,
499
+ next_node=next_node,
500
+ )
501
+ stage_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "goldilocks_decision.json"
502
+ _write_json(stage_path, artifact.model_dump())
503
+ task_context.metadata["goldilocks_decision"] = artifact
504
+ self.save_output(artifact)
505
+ return task_context
506
+
507
+
508
+ class _RouteTooBroad(RouterNode):
509
+ def determine_next_node(self, task_context: TaskContext) -> Node | None:
510
+ decision = task_context.metadata["goldilocks_decision"]
511
+ if decision.decision == "too_broad":
512
+ return SplitScopeIntoIdeaCandidatesNode(task_context=task_context)
513
+ return None
514
+
515
+
516
+ class _RouteJustRight(RouterNode):
517
+ def determine_next_node(self, task_context: TaskContext) -> Node | None:
518
+ decision = task_context.metadata["goldilocks_decision"]
519
+ if decision.decision == "just_right":
520
+ return DraftIdeaFromScopeNode(task_context=task_context)
521
+ return None
522
+
523
+
524
+ class _RouteTooNarrow(RouterNode):
525
+ def determine_next_node(self, task_context: TaskContext) -> Node | None:
526
+ decision = task_context.metadata["goldilocks_decision"]
527
+ if decision.decision == "too_narrow":
528
+ return NarrowScopeReviewPackageNode(task_context=task_context)
529
+ return None
530
+
531
+
532
+ class GoldilocksDecisionRouter(BaseRouter):
533
+ def __init__(self) -> None:
534
+ self.routes = [_RouteTooBroad(), _RouteJustRight(), _RouteTooNarrow()]
535
+ self.fallback = NarrowScopeReviewPackageNode()
536
+
537
+
538
+ class SplitScopeIntoIdeaCandidatesNode(AgentNode):
539
+ def get_agent_config(self) -> AgentConfig:
540
+ return AgentConfig(
541
+ instructions="Split broad approved scope into child idea candidates while preserving parent traceability.",
542
+ output_type=IdeaSplitPlanArtifact,
543
+ )
544
+
545
+ async def process(self, task_context: TaskContext) -> TaskContext:
546
+ event = task_context.event
547
+ repo_root = Path(event.repo_root)
548
+ _dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Splitting broad scope into ideas")
549
+ pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
550
+ scope = task_context.metadata["scope_context"]
551
+ assessment = task_context.metadata["goldilocks_assessment"]
552
+ artifact, execution_lineage = _run_scope_idea_agent(
553
+ repo_root=repo_root,
554
+ pipeline_root=pipeline_root,
555
+ stage_name="split_scope_into_idea_candidates",
556
+ output_model=IdeaSplitPlanArtifact,
557
+ context_payload={
558
+ "project_id": scope.project_id,
559
+ "scope_set_id": scope.scope_set_id,
560
+ "scope_id": scope.scope_id,
561
+ "approved_scope": scope.model_dump(),
562
+ "goldilocks_assessment": assessment.model_dump(),
563
+ },
564
+ guidance=[
565
+ "Split the approved parent scope into 2-3 coherent idea candidates only when the goldilocks assessment already says the scope is too broad.",
566
+ "Each child idea must stay fully inside the approved parent scope and preserve parent_scope_id plus traceability.",
567
+ "Do not silently create story-ready slices; produce idea candidates suitable for downstream idea sufficiency review.",
568
+ "Coverage_of_parent_scope should account for the parent scope bullets without drifting beyond them.",
569
+ "Preserve the assessment split rationale unless stronger evidence from the approved scope justifies a clearer phrasing of the same reason.",
570
+ "Do not split into foundation-vs-linkage ideas, metric-family ideas, or artifact-type ideas unless the approved scope itself clearly frames those as separate business outcomes.",
571
+ "If one child idea mostly exists to support another child idea, keep them together instead of splitting.",
572
+ ],
573
+ )
574
+ artifact_data = artifact.model_dump()
575
+ risks = list(artifact_data.get("remaining_risks") or [])
576
+ risks.extend(execution_lineage.notes)
577
+ artifact_data["remaining_risks"] = list(dict.fromkeys(risks))
578
+ artifact = IdeaSplitPlanArtifact.model_validate(artifact_data)
579
+ stage_path = pipeline_root / "idea_split_plan.json"
580
+ artifact.execution_lineage = execution_lineage.model_copy(
581
+ update={
582
+ "artifact_path": _relative_to_repo(repo_root, stage_path),
583
+ "generated_from": [
584
+ _relative_to_repo(repo_root, pipeline_root / "scope_context.json"),
585
+ _relative_to_repo(repo_root, pipeline_root / "goldilocks_assessment.json"),
586
+ ],
587
+ }
588
+ )
589
+ for child in artifact.child_ideas:
590
+ child.execution_lineage = artifact.execution_lineage
591
+ _write_json(stage_path, artifact.model_dump())
592
+ task_context.metadata["idea_split_plan"] = artifact
593
+ self.save_output(artifact)
594
+ return task_context
595
+
596
+
597
+ class DraftIdeaFromScopeNode(AgentNode):
598
+ def get_agent_config(self) -> AgentConfig:
599
+ return AgentConfig(
600
+ instructions="Draft one idea from a Goldilocks-sized approved scope.",
601
+ output_type=IdeaCandidateArtifact,
602
+ )
603
+
604
+ async def process(self, task_context: TaskContext) -> TaskContext:
605
+ event = task_context.event
606
+ repo_root = Path(event.repo_root)
607
+ _dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Drafting idea from scope")
608
+ pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
609
+ _append_node_trace(pipeline_root=pipeline_root, node="DraftIdeaFromScopeNode", event="enter")
610
+ scope = task_context.metadata["scope_context"]
611
+ assessment = task_context.metadata["goldilocks_assessment"]
612
+ try:
613
+ artifact, execution_lineage = _run_scope_idea_agent(
614
+ repo_root=repo_root,
615
+ pipeline_root=pipeline_root,
616
+ stage_name="draft_idea_from_scope",
617
+ output_model=IdeaCandidateArtifact,
618
+ context_payload={
619
+ "project_id": scope.project_id,
620
+ "scope_set_id": scope.scope_set_id,
621
+ "scope_id": scope.scope_id,
622
+ "approved_scope": scope.model_dump(),
623
+ "goldilocks_assessment": assessment.model_dump(),
624
+ },
625
+ guidance=[
626
+ "Draft exactly one idea candidate from the approved scope when the goldilocks assessment says the scope is just_right.",
627
+ "Keep the approved scope as the anchor; do not broaden or compress it into a different intent.",
628
+ "Return a real idea candidate artifact with explicit problem, users, goal, scope bullets, constraints, acceptance criteria, assumptions, and traceability.",
629
+ "Do not treat the result as canonically sufficient for stories; downstream idea sufficiency still decides that.",
630
+ "Preserve refs and traceability back to the approved scope and source evidence.",
631
+ ],
632
+ )
633
+ except Exception as exc:
634
+ _append_node_trace(pipeline_root=pipeline_root, node="DraftIdeaFromScopeNode", event="failure", detail={"error": str(exc)})
635
+ raise
636
+ artifact = IdeaCandidateArtifact.model_validate(artifact.model_dump())
637
+ stage_path = pipeline_root / "idea_candidate.json"
638
+ artifact.execution_lineage = execution_lineage.model_copy(
639
+ update={
640
+ "artifact_path": _relative_to_repo(repo_root, stage_path),
641
+ "generated_from": [
642
+ _relative_to_repo(repo_root, pipeline_root / "scope_context.json"),
643
+ _relative_to_repo(repo_root, pipeline_root / "goldilocks_assessment.json"),
644
+ ],
645
+ }
646
+ )
647
+ _write_json(stage_path, artifact.model_dump())
648
+ _append_node_trace(pipeline_root=pipeline_root, node="DraftIdeaFromScopeNode", event="success", detail={"artifact": _relative_to_repo(repo_root, stage_path)})
649
+ task_context.metadata["idea_candidate"] = artifact
650
+ self.save_output(artifact)
651
+ return task_context
652
+
653
+
654
+ class NarrowScopeReviewPackageNode(Node):
655
+ async def process(self, task_context: TaskContext) -> TaskContext:
656
+ event = task_context.event
657
+ repo_root = Path(event.repo_root)
658
+ _dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Packaging narrow-scope review")
659
+ scope = task_context.metadata["scope_context"]
660
+ assessment = task_context.metadata["goldilocks_assessment"]
661
+ stage_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "narrow_scope_review.json"
662
+ artifact = NarrowScopeReviewArtifact(
663
+ project_id=scope.project_id,
664
+ scope_set_id=scope.scope_set_id,
665
+ scope_id=scope.scope_id,
666
+ parent_scope_id=scope.scope_id,
667
+ refs=_build_traceability(scope),
668
+ reason_too_narrow=assessment.reasoning,
669
+ suggested_merge_targets=scope.neighbor_scope_refs,
670
+ recommended_next_action="Request human review or explicit merge recommendation; do not auto-merge.",
671
+ execution_lineage=_lineage_entry(
672
+ stage="narrow_scope_review",
673
+ origin="deterministic",
674
+ mode="deterministic",
675
+ repo_root=repo_root,
676
+ artifact_path=stage_path,
677
+ generated_from=[
678
+ _relative_to_repo(repo_root, _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "scope_context.json"),
679
+ _relative_to_repo(repo_root, _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "goldilocks_assessment.json"),
680
+ ],
681
+ notes=["Review package is deterministic packaging of a too-narrow boundary decision."],
682
+ metadata={"model_backed": False},
683
+ ),
684
+ )
685
+ _write_json(stage_path, artifact.model_dump())
686
+ task_context.metadata["narrow_scope_review"] = artifact
687
+ self.save_output(artifact)
688
+ return task_context
689
+
690
+
691
+ class NormalizeIdeaCandidateSetNode(Node):
692
+ async def process(self, task_context: TaskContext) -> TaskContext:
693
+ event = task_context.event
694
+ repo_root = Path(event.repo_root)
695
+ _dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Normalizing idea candidate set")
696
+ pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
697
+ _append_node_trace(pipeline_root=pipeline_root, node="NormalizeIdeaCandidateSetNode", event="enter")
698
+ scope = task_context.metadata["scope_context"]
699
+ split_plan = task_context.metadata.get("idea_split_plan")
700
+ direct_candidate = task_context.metadata.get("idea_candidate")
701
+ pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
702
+ if split_plan is not None:
703
+ artifact = IdeaCandidateSetArtifact(
704
+ project_id=scope.project_id,
705
+ scope_set_id=scope.scope_set_id,
706
+ scope_id=scope.scope_id,
707
+ parent_scope_id=scope.scope_id,
708
+ refs=_build_traceability(scope),
709
+ idea_candidates=split_plan.child_ideas,
710
+ candidate_count=len(split_plan.child_ideas),
711
+ split_applied=True,
712
+ split_rationale=split_plan.split_rationale,
713
+ lineage=[split_plan.execution_lineage] if split_plan.execution_lineage else [],
714
+ )
715
+ else:
716
+ candidates = [direct_candidate] if direct_candidate is not None else []
717
+ artifact = IdeaCandidateSetArtifact(
718
+ project_id=scope.project_id,
719
+ scope_set_id=scope.scope_set_id,
720
+ scope_id=scope.scope_id,
721
+ parent_scope_id=scope.scope_id,
722
+ refs=_build_traceability(scope),
723
+ idea_candidates=candidates,
724
+ candidate_count=len(candidates),
725
+ split_applied=False,
726
+ split_rationale=None,
727
+ lineage=[direct_candidate.execution_lineage] if direct_candidate is not None and direct_candidate.execution_lineage else [],
728
+ )
729
+ stage_path = pipeline_root / "idea_candidate_set.json"
730
+ artifact.lineage.append(
731
+ _lineage_entry(
732
+ stage="normalize_idea_candidate_set",
733
+ origin="deterministic",
734
+ mode="deterministic",
735
+ repo_root=repo_root,
736
+ artifact_path=stage_path,
737
+ generated_from=[line.artifact_path for line in artifact.lineage if line and line.artifact_path],
738
+ notes=["Candidate set normalizes one-vs-many shaping results without changing their origin."],
739
+ metadata={"model_backed": False},
740
+ )
741
+ )
742
+ _write_json(stage_path, artifact.model_dump())
743
+ _append_node_trace(pipeline_root=pipeline_root, node="NormalizeIdeaCandidateSetNode", event="success", detail={"artifact": _relative_to_repo(repo_root, stage_path), "candidate_count": artifact.candidate_count})
744
+ task_context.metadata["idea_candidate_set"] = artifact
745
+ self.save_output(artifact)
746
+ return task_context
747
+
748
+
749
+ class RegisterIdeasNode(Node):
750
+ async def process(self, task_context: TaskContext) -> TaskContext:
751
+ event = task_context.event
752
+ repo_root = Path(event.repo_root)
753
+ _dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Registering ideas")
754
+ pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
755
+ _append_node_trace(pipeline_root=pipeline_root, node="RegisterIdeasNode", event="enter")
756
+ scope = task_context.metadata["scope_context"]
757
+ candidate_set = task_context.metadata["idea_candidate_set"]
758
+ registry_root = repo_root / ".devflow" / "ideas"
759
+ registry_root.mkdir(parents=True, exist_ok=True)
760
+ registered: list[RegisteredIdeaArtifact] = []
761
+ assessment = task_context.metadata["goldilocks_assessment"]
762
+ for candidate in candidate_set.idea_candidates:
763
+ idea_id = _stable_id("idea_", {"scope_id": scope.scope_id, "candidate": candidate.model_dump()})
764
+ idea_paths = get_idea_paths(repo_root, idea_id=idea_id)
765
+ idea_paths.idea_dir.mkdir(parents=True, exist_ok=True)
766
+ registry_path = idea_paths.idea_dir / "idea.json"
767
+ candidate_lineage = candidate.execution_lineage
768
+ if candidate_lineage is None:
769
+ raise ValueError(f"Idea candidate {candidate.idea_candidate_id} is missing execution lineage")
770
+ summary = candidate.goal.strip() or candidate.problem.strip() or candidate.title.strip() or idea_id
771
+ persisted = {
772
+ "idea_id": idea_id,
773
+ "project_id": scope.project_id,
774
+ "scope_set_id": scope.scope_set_id,
775
+ "source_scope_id": scope.scope_id,
776
+ "parent_scope_id": candidate.parent_scope_id,
777
+ "status": "ready_for_story_sufficiency",
778
+ "title": candidate.title,
779
+ "summary": summary,
780
+ "idea": candidate.model_dump(),
781
+ "traceability": {
782
+ "scope_id": scope.scope_id,
783
+ "scope_artifact": str((_pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "scope_context.json").relative_to(repo_root)),
784
+ "candidate_artifact": str((_pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "idea_candidate_set.json").relative_to(repo_root)),
785
+ },
786
+ "lineage": {
787
+ "scope_to_idea_origin": candidate_lineage.origin,
788
+ "scope_to_idea_mode": candidate_lineage.mode,
789
+ "scope_to_idea_stage": candidate_lineage.stage,
790
+ "scope_to_idea_artifact": candidate_lineage.artifact_path,
791
+ "scope_to_idea_agent_run_ref": candidate_lineage.agent_run_ref,
792
+ "model_backed": bool(candidate_lineage.metadata.get("model_backed")),
793
+ },
794
+ }
795
+ _write_json(registry_path, persisted)
796
+ registered.append(
797
+ RegisteredIdeaArtifact(
798
+ idea_id=idea_id,
799
+ source_scope_id=scope.scope_id,
800
+ parent_scope_id=candidate.parent_scope_id,
801
+ idea_candidate_id=candidate.idea_candidate_id,
802
+ title=candidate.title,
803
+ summary=summary,
804
+ registry_ref=str(registry_path),
805
+ idea_ref=str(registry_path),
806
+ status="ready_for_story_sufficiency",
807
+ execution_lineage=candidate_lineage,
808
+ )
809
+ )
810
+ stage_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "idea_registry_record.json"
811
+ artifact = IdeaRegistryRecordArtifact(
812
+ project_id=scope.project_id,
813
+ scope_set_id=scope.scope_set_id,
814
+ scope_id=scope.scope_id,
815
+ parent_scope_id=scope.scope_id,
816
+ refs=_build_traceability(scope),
817
+ scope_shape=assessment.scope_shape,
818
+ registered_ideas=registered,
819
+ registration_timestamp=datetime.now(UTC).isoformat(),
820
+ registry_root=str(registry_root),
821
+ lineage=list(candidate_set.lineage) + [
822
+ _lineage_entry(
823
+ stage="register_ideas",
824
+ origin="deterministic",
825
+ mode="deterministic",
826
+ repo_root=repo_root,
827
+ artifact_path=stage_path,
828
+ generated_from=[_relative_to_repo(repo_root, _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "idea_candidate_set.json")],
829
+ notes=["Registry write preserves prior shaping lineage and exposes persisted idea records."],
830
+ metadata={"model_backed": False},
831
+ )
832
+ ],
833
+ )
834
+ _write_json(stage_path, artifact.model_dump())
835
+ _append_node_trace(pipeline_root=pipeline_root, node="RegisterIdeasNode", event="success", detail={"artifact": _relative_to_repo(repo_root, stage_path), "registered_idea_count": len(artifact.registered_ideas)})
836
+ task_context.metadata["idea_registry_record"] = artifact
837
+ self.save_output(artifact)
838
+ return task_context
839
+
840
+
841
+ class IdeaResolutionPackageNode(Node):
842
+ async def process(self, task_context: TaskContext) -> TaskContext:
843
+ event = task_context.event
844
+ repo_root = Path(event.repo_root)
845
+ _dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Packaging scope-to-idea resolution")
846
+ pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
847
+ _append_node_trace(pipeline_root=pipeline_root, node="IdeaResolutionPackageNode", event="enter")
848
+ scope = task_context.metadata["scope_context"]
849
+ assessment = task_context.metadata["goldilocks_assessment"]
850
+ registry = task_context.metadata.get("idea_registry_record")
851
+ narrow = task_context.metadata.get("narrow_scope_review")
852
+
853
+ if registry is not None:
854
+ artifact = IdeaResolutionPackageArtifact(
855
+ project_id=scope.project_id,
856
+ scope_set_id=scope.scope_set_id,
857
+ scope_id=scope.scope_id,
858
+ parent_scope_id=scope.scope_id,
859
+ refs=_build_traceability(scope),
860
+ scope_shape=assessment.scope_shape,
861
+ split_applied=bool(task_context.metadata.get("idea_split_plan")),
862
+ resolution_status="ideas_registered",
863
+ registered_idea_count=len(registry.registered_ideas),
864
+ registered_ideas=registry.registered_ideas,
865
+ assumptions_added=scope.assumptions,
866
+ remaining_risks=["Idea sufficiency is intentionally deferred to idea->DevFlow stories DAG."],
867
+ recommended_next_action="Submit registered idea(s) to the idea->story sufficiency DAG.",
868
+ lineage=list(registry.lineage),
869
+ )
870
+ else:
871
+ review_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "narrow_scope_review.json"
872
+ artifact = IdeaResolutionPackageArtifact(
873
+ project_id=scope.project_id,
874
+ scope_set_id=scope.scope_set_id,
875
+ scope_id=scope.scope_id,
876
+ parent_scope_id=scope.scope_id,
877
+ refs=_build_traceability(scope),
878
+ scope_shape=assessment.scope_shape,
879
+ split_applied=False,
880
+ resolution_status="narrow_scope_review_required",
881
+ registered_idea_count=0,
882
+ assumptions_added=scope.assumptions,
883
+ remaining_risks=[narrow.reason_too_narrow] if narrow is not None else [],
884
+ recommended_next_action="Human review required before any merge or rescoping.",
885
+ review_required=True,
886
+ review_package_ref=str(review_path) if review_path.exists() else None,
887
+ lineage=[narrow.execution_lineage] if narrow is not None and narrow.execution_lineage else [],
888
+ )
889
+ stage_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "idea_resolution_package.json"
890
+ artifact.lineage.append(
891
+ _lineage_entry(
892
+ stage="idea_resolution_package",
893
+ origin="deterministic",
894
+ mode="deterministic",
895
+ repo_root=repo_root,
896
+ artifact_path=stage_path,
897
+ generated_from=[line.artifact_path for line in artifact.lineage if line and line.artifact_path],
898
+ notes=["Resolution package summarizes the run without changing the shaped idea lineage."],
899
+ metadata={"model_backed": False},
900
+ )
901
+ )
902
+ _write_json(stage_path, artifact.model_dump())
903
+ _append_node_trace(pipeline_root=pipeline_root, node="IdeaResolutionPackageNode", event="success", detail={"artifact": _relative_to_repo(repo_root, stage_path), "resolution_status": artifact.resolution_status})
904
+ summary = ScopeIdeaDagSummary(
905
+ exit_code=0,
906
+ run_id=_store_run()[1],
907
+ pipeline_dir=str(_pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)),
908
+ message="scope->idea run complete",
909
+ outcome={
910
+ "project_id": scope.project_id,
911
+ "scope_id": scope.scope_id,
912
+ "scope_shape": assessment.scope_shape,
913
+ "registered_idea_count": artifact.registered_idea_count,
914
+ "resolution_status": artifact.resolution_status,
915
+ },
916
+ )
917
+ _write_json(_pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "summary.json", summary.model_dump())
918
+ task_context.metadata["outcome"] = dict(summary.outcome)
919
+ task_context.metadata["message"] = json.dumps({**summary.outcome, "run_id": summary.run_id, "pipeline_dir": summary.pipeline_dir}, sort_keys=True) + "\n"
920
+ task_context.metadata["exit_code"] = 0
921
+ self.save_output(artifact)
922
+ return task_context
923
+
924
+
925
+ class ScopeToIdeaWorkflow(Workflow):
926
+ workflow_schema = WorkflowSchema(
927
+ description="Approved scope -> idea DAG (load -> goldilocks -> route -> shape/register -> resolution)",
928
+ event_schema=ScopeToIdeaDagEvent,
929
+ start=LoadApprovedScopeItemNode,
930
+ nodes=[
931
+ NodeConfig(node=LoadApprovedScopeItemNode, connections=[AssessScopeGoldilocksNode]),
932
+ NodeConfig(node=AssessScopeGoldilocksNode, connections=[GoldilocksDecisionNode]),
933
+ NodeConfig(node=GoldilocksDecisionNode, connections=[GoldilocksDecisionRouter]),
934
+ NodeConfig(node=GoldilocksDecisionRouter, connections=[SplitScopeIntoIdeaCandidatesNode, DraftIdeaFromScopeNode, NarrowScopeReviewPackageNode], is_router=True),
935
+ NodeConfig(node=SplitScopeIntoIdeaCandidatesNode, connections=[NormalizeIdeaCandidateSetNode]),
936
+ NodeConfig(node=DraftIdeaFromScopeNode, connections=[NormalizeIdeaCandidateSetNode]),
937
+ NodeConfig(node=NarrowScopeReviewPackageNode, connections=[IdeaResolutionPackageNode]),
938
+ NodeConfig(node=NormalizeIdeaCandidateSetNode, connections=[RegisterIdeasNode]),
939
+ NodeConfig(node=RegisterIdeasNode, connections=[IdeaResolutionPackageNode]),
940
+ NodeConfig(node=IdeaResolutionPackageNode, connections=[]),
941
+ ],
942
+ )
943
+
944
+
945
+ def run_scope_to_idea_dag(
946
+ *,
947
+ repo_root: Path,
948
+ store: ExecutionStore,
949
+ project_id: str,
950
+ scope_set_id: str,
951
+ scope_id: str,
952
+ scope_payload_inline: dict[str, Any] | None = None,
953
+ scope_payload_path: Path | None = None,
954
+ ) -> ScopeToIdeaDagResult:
955
+ payload = _load_scope_payload(inline_payload=scope_payload_inline, payload_path=scope_payload_path)
956
+ _scope_context_from_payload(
957
+ payload=payload,
958
+ project_id=project_id,
959
+ scope_set_id=scope_set_id,
960
+ scope_id=scope_id,
961
+ )
962
+ pipeline_key = build_pipeline_key(
963
+ repo_root=repo_root,
964
+ project_id=project_id,
965
+ scope_set_id=scope_set_id,
966
+ scope_id=scope_id,
967
+ scope_payload=payload,
968
+ )
969
+ pipeline_dir = _pipeline_root(repo_root, scope_id=scope_id, pipeline_key=pipeline_key)
970
+ pipeline_dir.mkdir(parents=True, exist_ok=True)
971
+
972
+ run_id = store.create_run(
973
+ dag_id=DAG_ID,
974
+ dag_version="v1_scaffold",
975
+ root_correlation_id=f"corr_{pipeline_key}",
976
+ config={
977
+ "project_id": project_id,
978
+ "scope_set_id": scope_set_id,
979
+ "scope_id": scope_id,
980
+ "pipeline_key": pipeline_key,
981
+ },
982
+ )
983
+ store.mark_run_started(run_id=run_id)
984
+ _dfs_running(project_id=project_id, run_id=run_id, scope_id=scope_id)
985
+
986
+ wf = ScopeToIdeaWorkflow()
987
+ global _CURRENT_STORE, _CURRENT_RUN_ID
988
+ _CURRENT_STORE = store
989
+ _CURRENT_RUN_ID = run_id
990
+ try:
991
+ ctx = wf.run(
992
+ {
993
+ "repo_root": str(repo_root),
994
+ "project_id": project_id,
995
+ "scope_set_id": scope_set_id,
996
+ "scope_id": scope_id,
997
+ "scope_payload_inline": None if scope_payload_path else payload,
998
+ "scope_payload_path": str(scope_payload_path) if scope_payload_path else None,
999
+ "pipeline_key": pipeline_key,
1000
+ }
1001
+ )
1002
+ except Exception as exc:
1003
+ store.mark_run_finished(run_id=run_id, status="failed")
1004
+ _dfs_terminal(
1005
+ project_id=project_id,
1006
+ run_id=run_id,
1007
+ scope_id=scope_id,
1008
+ current_state="error",
1009
+ current_status="failed",
1010
+ summary="Scope to idea failed",
1011
+ error_message=str(exc),
1012
+ )
1013
+ raise
1014
+ finally:
1015
+ _CURRENT_STORE = None
1016
+ _CURRENT_RUN_ID = None
1017
+
1018
+ exit_code = int(ctx.metadata.get("exit_code") or 0)
1019
+ store.mark_run_finished(run_id=run_id, status="succeeded" if exit_code == 0 else "failed")
1020
+ if exit_code == 0:
1021
+ _dfs_terminal(
1022
+ project_id=project_id,
1023
+ run_id=run_id,
1024
+ scope_id=scope_id,
1025
+ current_state="idle",
1026
+ current_status="completed",
1027
+ summary="Ideas generated from scope",
1028
+ )
1029
+ _sync_registered_ideas_to_supabase(
1030
+ project_id=project_id,
1031
+ scope_id=scope_id,
1032
+ run_id=run_id,
1033
+ pipeline_dir=pipeline_dir,
1034
+ )
1035
+ registry_path = pipeline_dir / "idea_registry_record.json"
1036
+ if registry_path.exists():
1037
+ registry = json.loads(registry_path.read_text(encoding="utf-8"))
1038
+ for idea in registry.get("registered_ideas") or []:
1039
+ if not isinstance(idea, dict):
1040
+ continue
1041
+ idea_id = str(idea.get("idea_id") or "").strip()
1042
+ idea_ref = str(idea.get("idea_ref") or "").strip()
1043
+ if not idea_id or not idea_ref:
1044
+ continue
1045
+ store.enqueue_idea_task(
1046
+ project_id=project_id,
1047
+ enqueue_run_id=run_id,
1048
+ idea_id=idea_id,
1049
+ title=str(idea.get("title") or idea_id or "idea"),
1050
+ idea_payload_path=idea_ref,
1051
+ candidate_planes=[],
1052
+ )
1053
+ else:
1054
+ _dfs_terminal(
1055
+ project_id=project_id,
1056
+ run_id=run_id,
1057
+ scope_id=scope_id,
1058
+ current_state="error",
1059
+ current_status="failed",
1060
+ summary="Scope to idea failed",
1061
+ error_message=str(ctx.metadata.get("message") or "scope_to_idea failed"),
1062
+ )
1063
+ return ScopeToIdeaDagResult(
1064
+ exit_code=exit_code,
1065
+ run_id=run_id,
1066
+ pipeline_dir=pipeline_dir,
1067
+ message=str(ctx.metadata.get("message") or ""),
1068
+ outcome=dict(ctx.metadata.get("outcome") or {}),
1069
+ )