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,446 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ import uuid
6
+ from dataclasses import dataclass
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+ from ..planning.render_drafts import render_draft_story_markdown
11
+ from .paths import get_idea_paths
12
+ from .sufficiency import extract_sufficient_idea
13
+
14
+
15
+ def _iso_now() -> str:
16
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class DraftSet:
21
+ draft_set_id: str
22
+ root: Path
23
+
24
+
25
+ def _read_analysis(repo_root: Path, *, idea_id: str, analysis_id: str) -> dict:
26
+ paths = get_idea_paths(repo_root, idea_id=idea_id)
27
+ p = paths.analysis_root / analysis_id / "analysis.json"
28
+ if not p.exists():
29
+ return {
30
+ "analysis_id": analysis_id,
31
+ "idea_id": idea_id,
32
+ "repo_root": str(repo_root),
33
+ "created_at": _iso_now(),
34
+ "evidence": [],
35
+ }
36
+ return json.loads(p.read_text(encoding="utf-8"))
37
+
38
+
39
+ def _read_idea_payload(repo_root: Path, *, idea_id: str) -> dict:
40
+ idea_json = get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "idea.json"
41
+ if not idea_json.exists():
42
+ return {"idea_id": idea_id}
43
+ return json.loads(idea_json.read_text(encoding="utf-8"))
44
+
45
+
46
+ def _read_raw_idea_text(repo_root: Path, *, idea_id: str) -> str:
47
+ candidate = repo_root / ".devflow" / f"{idea_id}.txt"
48
+ if not candidate.exists():
49
+ return ""
50
+ return candidate.read_text(encoding="utf-8").strip()
51
+
52
+
53
+ def _safe_plane_list(planes: str | None) -> list[str]:
54
+ if not planes:
55
+ return ["api"]
56
+ items = [p.strip() for p in planes.split(",") if p.strip()]
57
+ return sorted(set(items)) or ["api"]
58
+
59
+
60
+ def _dedupe(items: list[str]) -> list[str]:
61
+ seen: set[str] = set()
62
+ ordered: list[str] = []
63
+ for item in items:
64
+ text = item.strip()
65
+ if not text or text in seen:
66
+ continue
67
+ seen.add(text)
68
+ ordered.append(text)
69
+ return ordered
70
+
71
+
72
+ def _split_bullet_prefix(text: str) -> str:
73
+ return re.sub(r"^\s*(?:[-*]|\d+\.)\s*", "", text).strip()
74
+
75
+
76
+ def _parse_idea_sections(raw_text: str) -> dict[str, list[str]]:
77
+ sections: dict[str, list[str]] = {}
78
+ current: str | None = None
79
+
80
+ def normalize_heading(text: str) -> str:
81
+ lowered = re.sub(r"[^a-z0-9+]+", " ", text.lower()).strip()
82
+ if "primary users" in lowered:
83
+ return "target_users"
84
+ if lowered == "problem":
85
+ return "problem"
86
+ if lowered == "goal":
87
+ return "goal"
88
+ if lowered.startswith("scope"):
89
+ return "scope"
90
+ if "out of scope" in lowered:
91
+ return "out_of_scope"
92
+ if "constraint" in lowered or "non goals" in lowered:
93
+ return "constraints"
94
+ if "acceptance criteria" in lowered:
95
+ return "acceptance_criteria"
96
+ if "definition of done" in lowered or "good enough" in lowered:
97
+ return "definition_of_done"
98
+ return lowered.replace(" ", "_")
99
+
100
+ for raw_line in raw_text.splitlines():
101
+ line = raw_line.strip()
102
+ if not line:
103
+ continue
104
+ if line.endswith(":") and not line.startswith(("-", "*")):
105
+ current = normalize_heading(line[:-1])
106
+ sections.setdefault(current, [])
107
+ continue
108
+ if current is None:
109
+ continue
110
+ sections.setdefault(current, []).append(_split_bullet_prefix(line))
111
+
112
+ return sections
113
+
114
+
115
+ def _coerce_list(value: object) -> list[str]:
116
+ if isinstance(value, str):
117
+ text = value.strip()
118
+ return [text] if text else []
119
+ if isinstance(value, list):
120
+ return [str(item).strip() for item in value if str(item).strip()]
121
+ return []
122
+
123
+
124
+ def _idea_context(repo_root: Path, *, idea_id: str) -> dict[str, object]:
125
+ payload = _read_idea_payload(repo_root, idea_id=idea_id)
126
+ raw_text = _read_raw_idea_text(repo_root, idea_id=idea_id)
127
+ sections = _parse_idea_sections(raw_text)
128
+ extracted = extract_sufficient_idea(raw_text) if raw_text else {}
129
+ sufficient = payload.get("sufficient_idea") if isinstance(payload.get("sufficient_idea"), dict) else {}
130
+ combined = {**extracted, **sufficient}
131
+
132
+ target_users = _dedupe(_coerce_list(combined.get("target_users")) + sections.get("target_users", []))
133
+ scope = _dedupe(_coerce_list(combined.get("scope")) + sections.get("scope", []))
134
+ constraints = _dedupe(_coerce_list(combined.get("constraints")) + sections.get("constraints", []))
135
+ acceptance = _dedupe(_coerce_list(combined.get("acceptance_criteria")) + sections.get("acceptance_criteria", []))
136
+ out_of_scope = _dedupe(sections.get("out_of_scope", []))
137
+ done = _dedupe(sections.get("definition_of_done", []))
138
+ problem_parts = _coerce_list(combined.get("problem")) + sections.get("problem", [])
139
+ goal_parts = _coerce_list(combined.get("user_outcomes")) + sections.get("goal", [])
140
+
141
+ return {
142
+ "title": str(payload.get("title") or idea_id).strip(),
143
+ "raw_text": raw_text,
144
+ "target_users": target_users,
145
+ "problem": " ".join(problem_parts).strip(),
146
+ "goal": " ".join(goal_parts).strip(),
147
+ "scope": scope,
148
+ "constraints": constraints,
149
+ "acceptance_criteria": acceptance,
150
+ "out_of_scope": out_of_scope,
151
+ "definition_of_done": done,
152
+ }
153
+
154
+
155
+ def _keyword_terms(context: dict[str, object]) -> list[str]:
156
+ source = " ".join(
157
+ [
158
+ str(context.get("title") or ""),
159
+ str(context.get("problem") or ""),
160
+ str(context.get("goal") or ""),
161
+ " ".join(_coerce_list(context.get("scope"))),
162
+ " ".join(_coerce_list(context.get("acceptance_criteria"))),
163
+ ]
164
+ ).lower()
165
+ if any(term in source for term in ("age verification", "18+", "adult", "age gate")):
166
+ return [
167
+ "age",
168
+ "verified",
169
+ "verification",
170
+ "adult",
171
+ "attestation",
172
+ "agegatepage",
173
+ "ageverifiedroute",
174
+ "age_gate",
175
+ "age-gate",
176
+ "ai gateway",
177
+ "content",
178
+ "profile",
179
+ ]
180
+ words = re.findall(r"[a-z0-9_]{4,}", source)
181
+ return _dedupe(words[:12])
182
+
183
+
184
+ def _find_repo_signal_anchors(repo_root: Path, *, keywords: list[str], limit: int = 12) -> list[str]:
185
+ candidate_roots = [
186
+ repo_root / "src",
187
+ repo_root / "supabase",
188
+ repo_root / "ai_docs" / "context" / "project_docs",
189
+ repo_root / "README.md",
190
+ ]
191
+ allowed_suffixes = {".md", ".tsx", ".ts", ".js", ".jsx", ".py", ".sql", ".json", ".toml", ".yml", ".yaml"}
192
+ scored: list[tuple[int, str]] = []
193
+ repo_name = repo_root.name
194
+
195
+ for root in candidate_roots:
196
+ if not root.exists():
197
+ continue
198
+ paths = [root] if root.is_file() else sorted(root.rglob("*"))
199
+ for path in paths:
200
+ if not path.is_file() or path.suffix.lower() not in allowed_suffixes:
201
+ continue
202
+ rel = path.relative_to(repo_root).as_posix()
203
+ lower_rel = rel.lower()
204
+ score = sum(4 for keyword in keywords if keyword in lower_rel)
205
+ if score == 0:
206
+ try:
207
+ sample = path.read_text(encoding="utf-8", errors="ignore")[:20000].lower()
208
+ except OSError:
209
+ continue
210
+ score += sum(1 for keyword in keywords if keyword in sample)
211
+ if score <= 0:
212
+ continue
213
+ scored.append((score, f"fs:{repo_name}/{rel}"))
214
+
215
+ scored.sort(key=lambda item: (-item[0], item[1]))
216
+ return _dedupe([anchor for _, anchor in scored])[:limit]
217
+
218
+
219
+ def _plane_anchor(plane: str, repo_anchors: list[str], analysis_anchors: list[str]) -> str:
220
+ plane_keywords = {
221
+ "auth": ["ageverifiedroute", "router", "route", "auth", "verified"],
222
+ "api": ["ai-gateway", "unlock-content", "select-content", "gateway", "supabase", "function", "sql"],
223
+ "ui": ["agegatepage", "age-gate", "page", "user_stories", "story"],
224
+ "integrations": ["outbox", "queue", "provider", "webhook"],
225
+ "ops": ["health", "doctor", "migration", "workflow"],
226
+ }.get(plane, [])
227
+ if plane_keywords:
228
+ scored = sorted(
229
+ repo_anchors,
230
+ key=lambda anchor: (
231
+ -sum(3 if keyword in anchor.lower() else 0 for keyword in plane_keywords),
232
+ anchor,
233
+ ),
234
+ )
235
+ if scored and any(keyword in scored[0].lower() for keyword in plane_keywords):
236
+ return scored[0]
237
+ if repo_anchors:
238
+ return repo_anchors[0]
239
+ return analysis_anchors[0] if analysis_anchors else ""
240
+
241
+
242
+ def _plane_oracle(plane: str, title: str) -> str:
243
+ if "age verification" in title.lower():
244
+ if plane == "auth":
245
+ return "Authenticated but unverified users are redirected to /age-gate until the profile records completed age verification."
246
+ if plane == "api":
247
+ return "Restricted AI/content requests are denied until the profile stores age_verified, age_verified_at, and age_verification_method for the signed-in user."
248
+ if plane == "ui":
249
+ return "The signed-in user can complete a single 18+ self-attestation flow and then reach the previously blocked adult feature."
250
+ return f"{title}: the user-observable contract remains satisfied for the {plane} plane."
251
+
252
+
253
+ def _build_draft_content(*, plane: str, context: dict[str, object]) -> dict[str, object]:
254
+ title = str(context.get("title") or "")
255
+ target_users = _coerce_list(context.get("target_users"))
256
+ role = target_users[1] if len(target_users) > 1 else (target_users[0] if target_users else "authenticated user")
257
+ problem = str(context.get("problem") or "the restricted experience is unsafe or incoherent without clear gating").strip()
258
+ goal = str(context.get("goal") or "complete the required flow and unlock the intended experience").strip()
259
+ acceptance = _coerce_list(context.get("acceptance_criteria"))
260
+ scope = _coerce_list(context.get("scope"))
261
+ out_of_scope = _coerce_list(context.get("out_of_scope"))
262
+ constraints = _coerce_list(context.get("constraints"))
263
+ done = _coerce_list(context.get("definition_of_done"))
264
+
265
+ if "age verification" in title.lower():
266
+ story_statement = (
267
+ f"As an {role}, I want to complete one 18+ self-attestation flow before entering restricted adult features, "
268
+ "so that adult AI and content access is unlocked only after verification is recorded."
269
+ )
270
+ outcome_oracle = [
271
+ "An authenticated but unverified user is blocked from a restricted adult feature and sent to /age-gate.",
272
+ "After the user confirms they are 18+, the profile stores age_verified, age_verified_at, and age_verification_method=self_attestation.",
273
+ "A verified user can retry the restricted feature without being forced through the gate again.",
274
+ ]
275
+ preconditions = [
276
+ "The user is signed in.",
277
+ "The user's profile is not yet age verified.",
278
+ "The requested route or request targets an adult-only Joyride surface.",
279
+ ]
280
+ steps = [
281
+ "Attempt to open a restricted adult feature while signed in but still unverified.",
282
+ "Complete the 18+ self-attestation on /age-gate.",
283
+ "Return to the blocked feature or retry the protected request.",
284
+ ]
285
+ pass_conditions = [
286
+ "The user is redirected to /age-gate before reaching the restricted surface.",
287
+ "Verification is persisted once with age_verified, age_verified_at, and age_verification_method.",
288
+ "Server-side AI/content gateway checks allow the request only after verification is present.",
289
+ ]
290
+ fail_conditions = [
291
+ "An unverified user can reach an adult feature without the age gate.",
292
+ "The self-attestation flow completes but verification fields are missing from the profile.",
293
+ "Restricted AI or content requests succeed while the user is still unverified.",
294
+ ]
295
+ acceptance_criteria = acceptance or [
296
+ "MUST redirect an authenticated but unverified user who requests a restricted adult feature to /age-gate.",
297
+ "MUST persist age_verified, age_verified_at, and age_verification_method when the user completes self-attestation.",
298
+ "MUST allow a verified user to access the previously blocked adult feature without repeating the flow unnecessarily.",
299
+ "MUST deny restricted AI gateway and content gateway requests for unverified users.",
300
+ "MUST record enough verification metadata for auditability without collecting unnecessary sensitive data.",
301
+ ]
302
+ non_goals = out_of_scope or [
303
+ "Third-party age verification provider integration.",
304
+ "Document upload, KYC, or biometric verification.",
305
+ "Collecting more sensitive personal data than the MVP requires.",
306
+ ]
307
+ contract_test_spec = [
308
+ "Given an authenticated user with profile.age_verified=false and a restricted adult feature target.",
309
+ "When the user attempts the feature before verification.",
310
+ "Then the product redirects the user to /age-gate and denies the protected server-side request.",
311
+ "Given the same user completes the 18+ self-attestation flow once.",
312
+ "When the profile stores age_verified=true, age_verified_at, and age_verification_method=self_attestation.",
313
+ "Then the user can access the adult feature and the server-side gateways stop rejecting the verified request.",
314
+ ]
315
+ return {
316
+ "story_statement": story_statement,
317
+ "outcome_oracle": outcome_oracle,
318
+ "preconditions": preconditions,
319
+ "steps": steps,
320
+ "pass_conditions": pass_conditions,
321
+ "fail_conditions": fail_conditions,
322
+ "acceptance_criteria": acceptance_criteria,
323
+ "non_goals": non_goals,
324
+ "contract_test_spec": contract_test_spec,
325
+ }
326
+
327
+ story_statement = f"As a {role}, I want {goal.rstrip('.')}, so that {problem.rstrip('.')} is resolved through an observable user outcome."
328
+ acceptance_criteria = acceptance or [f"MUST deliver {scope[0]}." if scope else "MUST deliver the stated user outcome."]
329
+ non_goals = out_of_scope or constraints or ["Implementation details that are not user-observable."]
330
+ return {
331
+ "story_statement": story_statement,
332
+ "outcome_oracle": done or [goal],
333
+ "preconditions": [f"The actor is {role}."] + ([scope[0]] if scope else []),
334
+ "steps": ["Start from the user entry point.", "Complete the minimal happy path.", "Verify the promised outcome."],
335
+ "pass_conditions": done or acceptance_criteria[:2],
336
+ "fail_conditions": [f"The user still experiences {problem}."] if problem else ["The promised outcome is not observable."],
337
+ "acceptance_criteria": acceptance_criteria,
338
+ "non_goals": non_goals,
339
+ "contract_test_spec": [
340
+ f"Given {role} needs to achieve the promised outcome.",
341
+ "When the user completes the minimum supported flow.",
342
+ f"Then {goal or 'the expected outcome'} is observable without relying on internal implementation details.",
343
+ ],
344
+ }
345
+
346
+
347
+ def generate_draft_set(
348
+ *,
349
+ repo_root: Path,
350
+ idea_id: str,
351
+ analysis_id: str,
352
+ planes: list[str],
353
+ ) -> DraftSet:
354
+ paths = get_idea_paths(repo_root, idea_id=idea_id)
355
+ paths.drafts_root.mkdir(parents=True, exist_ok=True)
356
+
357
+ draft_set_id = uuid.uuid4().hex[:12]
358
+ ds_root = paths.drafts_root / draft_set_id
359
+ ds_root.mkdir(parents=True, exist_ok=True)
360
+
361
+ analysis = _read_analysis(repo_root, idea_id=idea_id, analysis_id=analysis_id)
362
+ evidence = analysis.get("evidence") or []
363
+ analysis_anchors = [e.get("anchor") for e in evidence if isinstance(e, dict) and isinstance(e.get("anchor"), str)]
364
+ context = _idea_context(repo_root, idea_id=idea_id)
365
+ repo_anchors = _find_repo_signal_anchors(repo_root, keywords=_keyword_terms(context))
366
+ anchors = repo_anchors or analysis_anchors
367
+
368
+ draft_paths: list[str] = []
369
+ for plane in planes:
370
+ plane_dir = ds_root / plane
371
+ plane_dir.mkdir(parents=True, exist_ok=True)
372
+ plane_anchor = _plane_anchor(plane, repo_anchors, analysis_anchors)
373
+ draft_content = _build_draft_content(plane=plane, context=context)
374
+
375
+ draft_payload = {
376
+ "story_id": f"STORY:idea:{idea_id}:{plane}",
377
+ "title": f"{context['title']}: {plane} draft",
378
+ "required_planes": [plane],
379
+ "plane_oracles": [
380
+ {"plane": plane, "oracle": _plane_oracle(plane, str(context["title"])), "anchor": plane_anchor}
381
+ ],
382
+ "evidence_anchors": anchors[:10],
383
+ "draft_story_id": f"draft_{plane}_{draft_set_id}",
384
+ "source_analysis_id": analysis_id,
385
+ "plane": plane,
386
+ **draft_content,
387
+ }
388
+
389
+ md = render_draft_story_markdown(draft_payload)
390
+ out_path = plane_dir / f"draft_{plane}_001.md"
391
+ out_path.write_text(md, encoding="utf-8")
392
+ draft_paths.append(str(out_path.relative_to(repo_root)))
393
+
394
+ manifest = {
395
+ "draft_set_id": draft_set_id,
396
+ "idea_id": idea_id,
397
+ "source_analysis_id": analysis_id,
398
+ "created_at": _iso_now(),
399
+ "generator": "devflow-engine",
400
+ "planes": planes,
401
+ "draft_paths": draft_paths,
402
+ }
403
+ (ds_root / "manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8")
404
+
405
+ return DraftSet(draft_set_id=draft_set_id, root=ds_root)
406
+
407
+
408
+ _LOCKED_START = "DEVFLOW:LOCKED_START"
409
+ _LOCKED_END = "DEVFLOW:LOCKED_END"
410
+
411
+
412
+ def parse_locked_header(text: str) -> dict[str, str]:
413
+ lines = [ln.rstrip("\n") for ln in text.splitlines()]
414
+ try:
415
+ i0 = lines.index(_LOCKED_START)
416
+ i1 = lines.index(_LOCKED_END)
417
+ except ValueError:
418
+ return {}
419
+ if i1 <= i0:
420
+ return {}
421
+ header = {}
422
+ for ln in lines[i0 + 1 : i1]:
423
+ if ":" not in ln:
424
+ continue
425
+ k, v = ln.split(":", 1)
426
+ header[k.strip()] = v.strip()
427
+ return header
428
+
429
+
430
+ _DRAFT_NAME_RE = re.compile(r"^DRAFT-([A-Za-z]+)-(\d+)\.md$")
431
+
432
+
433
+ def dest_filename_for_draft(draft_path: Path, *, header: dict[str, str]) -> str:
434
+ if (sid := header.get("story_id")):
435
+ # Safe canonical filename heuristic.
436
+ safe = sid.replace("/", "-").replace(":", "-")
437
+ return f"{safe}.md"
438
+
439
+ m = _DRAFT_NAME_RE.match(draft_path.name)
440
+ if m:
441
+ plane = m.group(1).upper()
442
+ num = m.group(2)
443
+ return f"DF2-IDEA-{plane}-{num}.md"
444
+
445
+ # Fallback: stable mapping based on filename.
446
+ return f"{draft_path.stem}.md"