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,408 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from ..stores.execution_store import ExecutionStore
9
+ from .discovery import canonical_story_reconciliation_doc_path, get_story_source_paths
10
+ from .markdown_contracts import parse_story_contracts_from_markdown, render_canonical_story_markdown
11
+
12
+
13
+ ACTIVE_STORY_QUEUE_STATUSES = ("queued", "claimed", "in_progress", "blocked")
14
+ GENERATED_STORY_ARTIFACT_KIND = "generated_devflow_story_json"
15
+
16
+
17
+ def _iter_live_story_json_paths(repo_root: Path) -> list[Path]:
18
+ ideas_root = repo_root / ".devflow" / "ideas"
19
+ if not ideas_root.exists():
20
+ return []
21
+ return sorted(ideas_root.glob("*/devflow_story_sets/*/story_*.json"))
22
+
23
+
24
+ def _read_json(path: Path) -> dict[str, Any] | None:
25
+ try:
26
+ payload = json.loads(path.read_text(encoding="utf-8"))
27
+ except Exception:
28
+ return None
29
+ return payload if isinstance(payload, dict) else None
30
+
31
+
32
+ def _resolve_artifact_uri(repo_root: Path, artifact_uri: str) -> Path:
33
+ path = Path(artifact_uri).expanduser()
34
+ if not path.is_absolute():
35
+ path = repo_root / path
36
+ return path.resolve()
37
+
38
+
39
+ def _load_live_story_index(repo_root: Path) -> dict[str, list[dict[str, Any]]]:
40
+ grouped: dict[str, list[dict[str, Any]]] = {}
41
+ for path in _iter_live_story_json_paths(repo_root):
42
+ payload = _read_json(path)
43
+ if payload is None:
44
+ continue
45
+ story_id = str(payload.get("story_id") or "").strip()
46
+ if not story_id:
47
+ continue
48
+ grouped.setdefault(story_id, []).append(
49
+ {
50
+ "path": str(path.resolve()),
51
+ "story_uuid": str(payload.get("story_uuid") or "").strip(),
52
+ "title": str(payload.get("title") or "").strip(),
53
+ }
54
+ )
55
+ for story_id in grouped:
56
+ grouped[story_id] = sorted(grouped[story_id], key=lambda item: str(item["path"]))
57
+ return grouped
58
+
59
+
60
+ def _load_live_story_payloads(repo_root: Path) -> dict[str, list[dict[str, Any]]]:
61
+ grouped: dict[str, list[dict[str, Any]]] = {}
62
+ for path in _iter_live_story_json_paths(repo_root):
63
+ payload = _read_json(path)
64
+ if payload is None:
65
+ continue
66
+ story_id = str(payload.get("story_id") or "").strip()
67
+ if not story_id:
68
+ continue
69
+ grouped.setdefault(story_id, []).append({"path": str(path.resolve()), "payload": payload})
70
+ for story_id in grouped:
71
+ grouped[story_id] = sorted(grouped[story_id], key=lambda item: str(item["path"]))
72
+ return grouped
73
+
74
+
75
+ def _load_canonical_doc_story_ids(repo_root: Path) -> dict[str, Any]:
76
+ try:
77
+ source_paths = get_story_source_paths(repo_root)
78
+ except FileNotFoundError:
79
+ return {"available": False, "story_ids": [], "source_paths": []}
80
+
81
+ story_ids: set[str] = set()
82
+ for path in source_paths:
83
+ try:
84
+ text = path.read_text(encoding="utf-8")
85
+ except Exception:
86
+ continue
87
+ for block in parse_story_contracts_from_markdown(text, source_path=str(path)):
88
+ story_id = str(block.contract.story_id or "").strip()
89
+ if story_id:
90
+ story_ids.add(story_id)
91
+ return {
92
+ "available": True,
93
+ "story_ids": sorted(story_ids),
94
+ "source_paths": [str(path.resolve()) for path in source_paths],
95
+ }
96
+
97
+
98
+ def _load_story_queue_rows(store: ExecutionStore, *, project_id: str | None) -> list[dict[str, Any]]:
99
+ query = (
100
+ "SELECT sq.story_queue_id, sq.project_id, sq.enqueue_run_id, sq.story_artifact_id, sq.story_id, sq.title, "
101
+ "sq.status, sq.claimed_by_worker_id, sq.claimed_at, sq.started_run_id, sq.finished_run_id, "
102
+ "sq.failure_message, sq.failure_context_json, sq.created_at, sq.updated_at, "
103
+ "a.kind AS artifact_kind, a.uri AS artifact_uri, a.metadata_json AS artifact_metadata_json "
104
+ "FROM story_queue sq "
105
+ "LEFT JOIN artifacts a ON a.artifact_id = sq.story_artifact_id "
106
+ )
107
+ params: tuple[Any, ...] = ()
108
+ if project_id is not None:
109
+ query += "WHERE sq.project_id=? "
110
+ params = (project_id,)
111
+ query += "ORDER BY sq.story_id ASC, sq.created_at ASC, sq.story_queue_id ASC"
112
+
113
+ with store._connect() as conn:
114
+ rows = conn.execute(query, params).fetchall()
115
+
116
+ payloads: list[dict[str, Any]] = []
117
+ for row in rows:
118
+ payload = dict(row)
119
+ try:
120
+ payload["artifact_metadata"] = json.loads(str(payload.get("artifact_metadata_json") or "")) if payload.get("artifact_metadata_json") else {}
121
+ except Exception:
122
+ payload["artifact_metadata"] = {}
123
+ try:
124
+ payload["failure_context"] = json.loads(str(payload.get("failure_context_json") or "")) if payload.get("failure_context_json") else None
125
+ except Exception:
126
+ payload["failure_context"] = None
127
+ payloads.append(payload)
128
+ return payloads
129
+
130
+
131
+ def _choose_canonical_live_path(*, story_id: str, live_candidates: list[dict[str, Any]], referenced_uris: set[str]) -> str | None:
132
+ if not live_candidates:
133
+ return None
134
+ ranked = sorted(
135
+ live_candidates,
136
+ key=lambda item: (0 if str(item["path"]) in referenced_uris else 1, str(item["path"])),
137
+ )
138
+ return str(ranked[0]["path"])
139
+
140
+
141
+ def _queue_status_priority(status: str) -> int:
142
+ return {"in_progress": 0, "claimed": 1, "queued": 2, "blocked": 3}.get(status, 9)
143
+
144
+
145
+ def _choose_canonical_queue_row(repo_root: Path, rows: list[dict[str, Any]], *, canonical_path: str | None) -> dict[str, Any] | None:
146
+ active_rows = [row for row in rows if str(row.get("status") or "") in ACTIVE_STORY_QUEUE_STATUSES]
147
+ if not active_rows:
148
+ return None
149
+ ranked = sorted(
150
+ active_rows,
151
+ key=lambda row: (
152
+ 0
153
+ if canonical_path
154
+ and str(row.get("artifact_uri") or "").strip()
155
+ and str(_resolve_artifact_uri(repo_root, str(row.get("artifact_uri") or "").strip())) == canonical_path
156
+ else 1,
157
+ _queue_status_priority(str(row.get("status") or "")),
158
+ int(row.get("created_at") or 0),
159
+ str(row.get("story_queue_id") or ""),
160
+ ),
161
+ )
162
+ return ranked[0]
163
+
164
+
165
+ def _inventory_story_path_issue(repo_root: Path, row: dict[str, Any]) -> dict[str, Any] | None:
166
+ artifact_uri = str(row.get("artifact_uri") or "").strip()
167
+ if not artifact_uri:
168
+ return {
169
+ "story_queue_id": row["story_queue_id"],
170
+ "artifact_id": row["story_artifact_id"],
171
+ "issue": "missing_artifact_uri",
172
+ }
173
+ path = _resolve_artifact_uri(repo_root, artifact_uri)
174
+ if not path.exists():
175
+ return {
176
+ "story_queue_id": row["story_queue_id"],
177
+ "artifact_id": row["story_artifact_id"],
178
+ "artifact_uri": artifact_uri,
179
+ "issue": "missing_path",
180
+ }
181
+ payload = _read_json(path)
182
+ if payload is None:
183
+ return {
184
+ "story_queue_id": row["story_queue_id"],
185
+ "artifact_id": row["story_artifact_id"],
186
+ "artifact_uri": artifact_uri,
187
+ "issue": "invalid_json",
188
+ }
189
+ payload_story_id = str(payload.get("story_id") or "").strip()
190
+ queue_story_id = str(row.get("story_id") or "").strip()
191
+ if payload_story_id and queue_story_id and payload_story_id != queue_story_id:
192
+ return {
193
+ "story_queue_id": row["story_queue_id"],
194
+ "artifact_id": row["story_artifact_id"],
195
+ "artifact_uri": artifact_uri,
196
+ "issue": "story_id_mismatch",
197
+ "payload_story_id": payload_story_id,
198
+ "queue_story_id": queue_story_id,
199
+ }
200
+ return None
201
+
202
+
203
+ def _rebuild_canonical_story_doc(
204
+ *,
205
+ repo_root: Path,
206
+ live_story_payloads: dict[str, list[dict[str, Any]]],
207
+ referenced_uris_by_story_id: dict[str, set[str]],
208
+ ) -> dict[str, Any]:
209
+ canonical_doc_path = canonical_story_reconciliation_doc_path(repo_root)
210
+ if not live_story_payloads:
211
+ return {
212
+ "changed": False,
213
+ "path": str(canonical_doc_path.resolve()),
214
+ "story_count": 0,
215
+ "story_ids": [],
216
+ }
217
+
218
+ story_blocks: list[str] = []
219
+ story_ids: list[str] = []
220
+ for story_id in sorted(live_story_payloads):
221
+ candidates = live_story_payloads[story_id]
222
+ canonical_path = _choose_canonical_live_path(
223
+ story_id=story_id,
224
+ live_candidates=[{"path": str(item["path"])} for item in candidates],
225
+ referenced_uris=referenced_uris_by_story_id.get(story_id, set()),
226
+ )
227
+ selected = next((item for item in candidates if str(item["path"]) == canonical_path), candidates[0])
228
+ story_blocks.append(render_canonical_story_markdown(dict(selected["payload"])))
229
+ story_ids.append(story_id)
230
+
231
+ canonical_doc_text = "\n".join(block.rstrip() for block in story_blocks).rstrip() + "\n"
232
+ previous_text = canonical_doc_path.read_text(encoding="utf-8") if canonical_doc_path.exists() else None
233
+ changed = previous_text != canonical_doc_text
234
+ if changed:
235
+ canonical_doc_path.parent.mkdir(parents=True, exist_ok=True)
236
+ canonical_doc_path.write_text(canonical_doc_text, encoding="utf-8")
237
+
238
+ return {
239
+ "changed": changed,
240
+ "path": str(canonical_doc_path.resolve()),
241
+ "story_count": len(story_ids),
242
+ "story_ids": story_ids,
243
+ }
244
+
245
+
246
+ def reconcile_story_queue(
247
+ *,
248
+ repo_root: Path,
249
+ project_id: str | None = None,
250
+ apply: bool = False,
251
+ ) -> dict[str, Any]:
252
+ repo_root = repo_root.expanduser().resolve()
253
+ store = ExecutionStore(repo_root / ".devflow" / "execution.sqlite")
254
+ live_story_index = _load_live_story_index(repo_root)
255
+ live_story_payloads = _load_live_story_payloads(repo_root)
256
+ doc_index = _load_canonical_doc_story_ids(repo_root)
257
+ queue_rows = _load_story_queue_rows(store, project_id=project_id)
258
+
259
+ grouped_rows: dict[str, list[dict[str, Any]]] = {}
260
+ for row in queue_rows:
261
+ story_id = str(row.get("story_id") or "").strip()
262
+ if not story_id:
263
+ continue
264
+ grouped_rows.setdefault(story_id, []).append(row)
265
+
266
+ report_stories: list[dict[str, Any]] = []
267
+ artifact_updates: list[dict[str, Any]] = []
268
+ queue_inertions: list[dict[str, Any]] = []
269
+ canonical_doc_repair = {
270
+ "changed": False,
271
+ "path": str(canonical_story_reconciliation_doc_path(repo_root).resolve()),
272
+ "story_count": 0,
273
+ "story_ids": [],
274
+ }
275
+ now = int(time.time())
276
+ referenced_uris_by_story_id: dict[str, set[str]] = {}
277
+
278
+ with store._connect() as conn:
279
+ for story_id in sorted(grouped_rows):
280
+ rows = grouped_rows[story_id]
281
+ live_candidates = live_story_index.get(story_id, [])
282
+ referenced_uris = {
283
+ str(_resolve_artifact_uri(repo_root, str(row.get("artifact_uri") or "").strip()))
284
+ for row in rows
285
+ if str(row.get("artifact_uri") or "").strip()
286
+ }
287
+ referenced_uris_by_story_id[story_id] = referenced_uris
288
+ canonical_path = _choose_canonical_live_path(
289
+ story_id=story_id,
290
+ live_candidates=live_candidates,
291
+ referenced_uris=referenced_uris,
292
+ )
293
+ canonical_queue_row = _choose_canonical_queue_row(repo_root, rows, canonical_path=canonical_path)
294
+ active_rows = [row for row in rows if str(row.get("status") or "") in ACTIVE_STORY_QUEUE_STATUSES]
295
+ path_issues = [issue for issue in (_inventory_story_path_issue(repo_root, row) for row in rows) if issue is not None]
296
+ unknown_story_id_risk = canonical_path is None
297
+ doc_mismatch = bool(doc_index["available"] and canonical_path and story_id not in set(doc_index["story_ids"]))
298
+
299
+ story_report = {
300
+ "story_id": story_id,
301
+ "project_ids": sorted({str(row.get("project_id") or "") for row in rows if str(row.get("project_id") or "")}),
302
+ "story_queue_ids": [str(row["story_queue_id"]) for row in rows],
303
+ "duplicate_queue_rows": len(rows) > 1,
304
+ "duplicate_active_queue_rows": len(active_rows) > 1,
305
+ "distinct_artifact_uris": sorted(referenced_uris),
306
+ "broken_story_json_paths": path_issues,
307
+ "canonical_story_json_path": canonical_path,
308
+ "canonical_queue_row_id": None if canonical_queue_row is None else str(canonical_queue_row["story_queue_id"]),
309
+ "unknown_story_id_risk": unknown_story_id_risk,
310
+ "canonical_doc_mismatch": doc_mismatch,
311
+ "live_story_json_candidates": [str(item["path"]) for item in live_candidates],
312
+ }
313
+
314
+ if apply and canonical_path:
315
+ seen_artifact_ids: set[str] = set()
316
+ for row in rows:
317
+ artifact_id = str(row.get("story_artifact_id") or "").strip()
318
+ if not artifact_id or artifact_id in seen_artifact_ids:
319
+ continue
320
+ seen_artifact_ids.add(artifact_id)
321
+ artifact_uri = str(_resolve_artifact_uri(repo_root, str(row.get("artifact_uri") or "").strip())) if str(row.get("artifact_uri") or "").strip() else ""
322
+ if artifact_uri == canonical_path:
323
+ continue
324
+ conn.execute(
325
+ "UPDATE artifacts SET uri=?, updated_at=? WHERE artifact_id=?",
326
+ (canonical_path, now, artifact_id),
327
+ )
328
+ artifact_updates.append(
329
+ {
330
+ "story_id": story_id,
331
+ "artifact_id": artifact_id,
332
+ "from_uri": artifact_uri,
333
+ "to_uri": canonical_path,
334
+ }
335
+ )
336
+
337
+ if canonical_queue_row is not None:
338
+ for row in active_rows:
339
+ story_queue_id = str(row["story_queue_id"])
340
+ if story_queue_id == str(canonical_queue_row["story_queue_id"]):
341
+ continue
342
+ failure_context = row.get("failure_context") if isinstance(row.get("failure_context"), dict) else {}
343
+ failure_context["reconciled_duplicate"] = True
344
+ failure_context["canonical_story_queue_id"] = str(canonical_queue_row["story_queue_id"])
345
+ failure_context["canonical_story_json_path"] = canonical_path
346
+ message = (
347
+ "story queue reconciled: duplicate active row inerted; "
348
+ f"canonical_story_queue_id={canonical_queue_row['story_queue_id']}"
349
+ )
350
+ conn.execute(
351
+ (
352
+ "UPDATE story_queue SET status='cancelled', claimed_by_worker_id=NULL, claimed_at=NULL, "
353
+ "failure_message=?, failure_context_json=?, updated_at=? WHERE story_queue_id=?"
354
+ ),
355
+ (
356
+ message,
357
+ json.dumps(failure_context, sort_keys=True),
358
+ now,
359
+ story_queue_id,
360
+ ),
361
+ )
362
+ queue_inertions.append(
363
+ {
364
+ "story_id": story_id,
365
+ "story_queue_id": story_queue_id,
366
+ "canonical_story_queue_id": str(canonical_queue_row["story_queue_id"]),
367
+ }
368
+ )
369
+
370
+ report_stories.append(story_report)
371
+
372
+ if apply:
373
+ canonical_doc_repair = _rebuild_canonical_story_doc(
374
+ repo_root=repo_root,
375
+ live_story_payloads=live_story_payloads,
376
+ referenced_uris_by_story_id=referenced_uris_by_story_id,
377
+ )
378
+ doc_index = _load_canonical_doc_story_ids(repo_root)
379
+ doc_story_ids = set(doc_index["story_ids"])
380
+ for story_report in report_stories:
381
+ story_id = str(story_report.get("story_id") or "").strip()
382
+ story_report["canonical_doc_mismatch"] = bool(story_report["canonical_story_json_path"] and story_id not in doc_story_ids)
383
+ story_report["canonical_doc_repaired"] = bool(canonical_doc_repair["changed"] and not story_report["canonical_doc_mismatch"])
384
+
385
+ summary = {
386
+ "story_count": len(report_stories),
387
+ "story_ids_with_duplicate_queue_rows": sum(1 for item in report_stories if item["duplicate_queue_rows"]),
388
+ "story_ids_with_duplicate_active_queue_rows": sum(1 for item in report_stories if item["duplicate_active_queue_rows"]),
389
+ "story_ids_with_broken_story_json_paths": sum(1 for item in report_stories if item["broken_story_json_paths"]),
390
+ "story_ids_with_unknown_story_id_risk": sum(1 for item in report_stories if item["unknown_story_id_risk"]),
391
+ "story_ids_with_canonical_doc_mismatch": sum(1 for item in report_stories if item["canonical_doc_mismatch"]),
392
+ "artifact_uri_updates": len(artifact_updates),
393
+ "queue_rows_inerted": len(queue_inertions),
394
+ "canonical_doc_repairs": 1 if canonical_doc_repair["changed"] else 0,
395
+ }
396
+
397
+ return {
398
+ "repo_root": str(repo_root),
399
+ "db_path": str((repo_root / ".devflow" / "execution.sqlite").resolve()),
400
+ "project_id": project_id,
401
+ "apply": apply,
402
+ "canonical_doc_index": doc_index,
403
+ "summary": summary,
404
+ "stories": report_stories,
405
+ "artifact_updates": artifact_updates,
406
+ "queue_inertions": queue_inertions,
407
+ "canonical_doc_repair": canonical_doc_repair,
408
+ }
@@ -0,0 +1,149 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic story contract validator for devflow_engine.
3
+
4
+ Hard gates:
5
+ - No silent skips: every canonical story file must contain ≥1 parseable story block.
6
+ - Contract formation mode line present (verbatim)
7
+ - story_uuid uuid-v4, required_planes valid
8
+ - plane_oracles present with 1 entry per required plane
9
+ - each plane_oracles item has oracle + anchor
10
+ - anchor must match a stable-address allowlist (prefix-based)
11
+
12
+ Output:
13
+ - human-readable diagnostics to stdout
14
+ - optional JSON report via --json <path>
15
+
16
+ Exit codes:
17
+ - 0 pass
18
+ - 2 validation failed
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import json
25
+ import re
26
+ from dataclasses import asdict, dataclass
27
+ from pathlib import Path
28
+
29
+ from devflow_engine.story.contracts import validate_parsed_story_block
30
+ from devflow_engine.story.markdown_contracts import iter_canonical_story_paths, parse_story_contracts_from_markdown
31
+
32
+
33
+ ANCHOR_PREFIX_ALLOWLIST = (
34
+ # UI / routing
35
+ "route:",
36
+ # API contracts
37
+ "api:",
38
+ # Integrations boundary oracles
39
+ "outbox:",
40
+ "audit:",
41
+ # CLI and operational commands (common in devflow_engine stories)
42
+ "cli:",
43
+ "command:",
44
+ # Artifacts and files
45
+ "artifact:",
46
+ "file:",
47
+ # Environment + docs + scripts are legitimate stable anchors in story specs
48
+ "env:",
49
+ "docs:",
50
+ "script:",
51
+ )
52
+
53
+ # Also allow short-form HTTP anchors like: "GET /health" or "POST /api/v1/foo"
54
+ HTTP_ANCHOR_RE = re.compile(r"^(GET|POST|PUT|PATCH|DELETE)\s+/\S+")
55
+ SCHEME_ANCHOR_RE = re.compile(r"^[a-z][a-z0-9_-]{1,32}:")
56
+
57
+
58
+ @dataclass(frozen=True)
59
+ class Issue:
60
+ code: str
61
+ message: str
62
+ path: str
63
+
64
+
65
+ def _anchor_is_allowed(anchor: str) -> bool:
66
+ a = (anchor or "").strip()
67
+ if not a:
68
+ return False
69
+ # Prefer scheme-like anchors (stable, parseable addresses), e.g.
70
+ # cli:..., route:..., api:..., outbox:..., docs:..., tests:...
71
+ if SCHEME_ANCHOR_RE.match(a):
72
+ return True
73
+ # Also allow short-form HTTP anchors like "GET /health".
74
+ if HTTP_ANCHOR_RE.match(a):
75
+ return True
76
+ return False
77
+
78
+
79
+ def main() -> int:
80
+ ap = argparse.ArgumentParser()
81
+ ap.add_argument("--repo-root", default=".")
82
+ ap.add_argument("--json", default=None, help="Write JSON report to this path")
83
+ args = ap.parse_args()
84
+
85
+ repo_root = Path(args.repo_root).resolve()
86
+
87
+ issues: list[Issue] = []
88
+ story_count = 0
89
+
90
+ for p in iter_canonical_story_paths(repo_root):
91
+ text = p.read_text(encoding="utf-8")
92
+ blocks = parse_story_contracts_from_markdown(text, source_path=str(p))
93
+
94
+ if not blocks:
95
+ issues.append(
96
+ Issue(
97
+ code="no_story_blocks",
98
+ message="No parseable story blocks found (expected a story_uuid: header). This file would otherwise be silently skipped.",
99
+ path=str(p),
100
+ )
101
+ )
102
+ continue
103
+
104
+ for b in blocks:
105
+ story_count += 1
106
+ contract_issues = validate_parsed_story_block(b)
107
+ for ci in contract_issues:
108
+ issues.append(Issue(code=ci.code, message=ci.message, path=ci.path))
109
+
110
+ # Additional deterministic semantic gate: anchor must be stable-address formatted.
111
+ c = b.contract
112
+ for idx, po in enumerate(c.plane_oracles):
113
+ if po.anchor and not _anchor_is_allowed(po.anchor):
114
+ issues.append(
115
+ Issue(
116
+ code="invalid_plane_oracle_anchor_format",
117
+ message=(
118
+ f"plane_oracles[{idx}].anchor must be a stable address; "
119
+ f"allowed prefixes={list(ANCHOR_PREFIX_ALLOWLIST)} or HTTP like 'GET /health'"
120
+ ),
121
+ path=f"{c.source_path}:{b.start_line}:plane_oracles[{idx}].anchor",
122
+ )
123
+ )
124
+
125
+ report = {
126
+ "ok": len(issues) == 0,
127
+ "story_count": story_count,
128
+ "issue_count": len(issues),
129
+ "issues": [asdict(i) for i in issues],
130
+ }
131
+
132
+ if args.json:
133
+ Path(args.json).write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
134
+
135
+ if issues:
136
+ print(f"STORY VALIDATION FAILED: {len(issues)} issue(s) across {story_count} story block(s)")
137
+ # Print up to 200 issues to avoid drowning CI logs.
138
+ for i, iss in enumerate(issues[:200]):
139
+ print(f"- {iss.code}: {iss.path}: {iss.message}")
140
+ if len(issues) > 200:
141
+ print(f"... ({len(issues) - 200} more)")
142
+ return 2
143
+
144
+ print(f"STORY VALIDATION OK: {story_count} story block(s)")
145
+ return 0
146
+
147
+
148
+ if __name__ == "__main__":
149
+ raise SystemExit(main())