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,1257 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from dataclasses import dataclass
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ try:
13
+ import tomllib # py3.11+
14
+ except Exception: # pragma: no cover
15
+ tomllib = None # type: ignore
16
+
17
+ from ..llm.cli_one_shot import run_one_shot
18
+ from .actors import canonicalize_story_actor, load_actor_registry, normalize_actor_label, resolve_actor_entry, validate_canonical_actor_label
19
+ from .paths import get_idea_paths
20
+
21
+
22
+ MAX_COVERAGE_ATTEMPTS = 3
23
+ MAX_DECOMPOSITION_ITERATIONS = 2
24
+ MAX_JSON_REPAIR_ATTEMPTS = 1
25
+
26
+
27
+ def _iso_now() -> str:
28
+ return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class TraditionalStorySet:
33
+ story_set_id: str
34
+ root: Path
35
+ story_paths: list[Path]
36
+ sufficiency_report: dict[str, Any]
37
+
38
+
39
+ class TraditionalStoryInsufficiencyError(RuntimeError):
40
+ def __init__(self, *, root: Path, story_set_id: str, report_path: Path, report: dict[str, Any]) -> None:
41
+ self.root = root
42
+ self.story_set_id = story_set_id
43
+ self.report_path = report_path
44
+ self.report = report
45
+ findings = report.get("final_findings") or report.get("history", [{}])[-1].get("findings") or []
46
+ summary = "; ".join(str(item) for item in findings[:4]) or "traditional story set remained insufficient"
47
+ super().__init__(summary)
48
+
49
+
50
+ class TraditionalStoryGenerationError(RuntimeError):
51
+ def __init__(self, *, root: Path, story_set_id: str, resume_cursor: dict[str, Any] | None, cause: Exception) -> None:
52
+ self.root = root
53
+ self.story_set_id = story_set_id
54
+ self.resume_cursor = resume_cursor
55
+ self.failure_context = {
56
+ 'story_set_id': story_set_id,
57
+ 'stories_dir': str(root),
58
+ 'resume_cursor': resume_cursor,
59
+ }
60
+ super().__init__(str(cause))
61
+
62
+
63
+ def _traditional_root(repo_root: Path, *, idea_id: str) -> Path:
64
+ return get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "traditional_user_stories"
65
+
66
+
67
+ def _read_idea_payload(repo_root: Path, *, idea_id: str) -> dict[str, Any]:
68
+ idea_json = get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "idea.json"
69
+ return json.loads(idea_json.read_text(encoding="utf-8"))
70
+
71
+
72
+ def _extract_sufficient_idea(payload: dict[str, Any]) -> dict[str, Any]:
73
+ candidates = [
74
+ payload.get("sufficient_idea"),
75
+ payload.get("ideation_output"),
76
+ payload.get("ideation_artifact"),
77
+ ]
78
+ for candidate in candidates:
79
+ if isinstance(candidate, dict):
80
+ if isinstance(candidate.get("sufficient_idea"), dict):
81
+ return dict(candidate["sufficient_idea"])
82
+ return dict(candidate)
83
+ return payload
84
+
85
+
86
+ def _resolved_actor_entries(actor_registry: dict[str, Any]) -> list[dict[str, Any]]:
87
+ entries: list[dict[str, Any]] = []
88
+ for item in (actor_registry.get("resolved_actors") or actor_registry.get("actors") or []):
89
+ if not isinstance(item, dict):
90
+ continue
91
+ label = str(item.get("label") or "").strip()
92
+ actor_id = str(item.get("id") or "").strip()
93
+ if not label or not actor_id:
94
+ continue
95
+ entries.append({
96
+ "id": actor_id,
97
+ "label": label,
98
+ "kind": str(item.get("kind") or "human").strip() or "human",
99
+ "inherits_from": str(item.get("inherits_from") or "").strip() or None,
100
+ "description": str(item.get("description") or "").strip(),
101
+ "aliases": [str(alias).strip() for alias in (item.get("aliases") or []) if str(alias).strip()],
102
+ })
103
+ return entries
104
+
105
+
106
+ def _resolved_actor_ids(actor_registry: dict[str, Any]) -> list[str]:
107
+ return [str(item.get("id") or "").strip() for item in _resolved_actor_entries(actor_registry)]
108
+
109
+
110
+ def _primary_actor_contract(*, actor_registry: dict[str, Any], actor_id: str | None = None, actor_label: str | None = None, field_name: str = "primary actor") -> dict[str, Any]:
111
+ resolved = None
112
+ if actor_id:
113
+ for item in _resolved_actor_entries(actor_registry):
114
+ if str(item.get("id") or "").strip() == str(actor_id).strip():
115
+ resolved = item
116
+ break
117
+ if resolved is None and actor_label:
118
+ resolved = resolve_actor_entry(actor=str(actor_label), actor_registry=actor_registry)
119
+ if resolved is None:
120
+ allowed = ", ".join(f"{item['id']}:{item['label']}" for item in _resolved_actor_entries(actor_registry)) or "<none>"
121
+ raise RuntimeError(f"{field_name.title()} must resolve from the canonical actor registry. Allowed actors: {allowed}")
122
+ if actor_id and str(resolved.get("id") or "").strip() != str(actor_id).strip():
123
+ raise RuntimeError(f"{field_name.title()} id {actor_id!r} does not match canonical actor {resolved.get('id')!r}.")
124
+ if actor_label and str(resolved.get("label") or "").strip() != str(actor_label).strip():
125
+ raise RuntimeError(f"{field_name.title()} label {actor_label!r} does not match canonical actor label {resolved.get('label')!r}.")
126
+ return {
127
+ "id": str(resolved.get("id") or "").strip(),
128
+ "label": str(resolved.get("label") or "").strip(),
129
+ "kind": str(resolved.get("kind") or "human").strip() or "human",
130
+ "inherits_from": str(resolved.get("inherits_from") or "").strip() or None,
131
+ }
132
+
133
+
134
+ def _normalize_list(value: object) -> list[str]:
135
+ if isinstance(value, str):
136
+ text = value.strip()
137
+ return [text] if text else []
138
+ if isinstance(value, list):
139
+ items: list[str] = []
140
+ for item in value:
141
+ text = str(item).strip()
142
+ if text:
143
+ items.append(text)
144
+ return items
145
+ return []
146
+
147
+
148
+ def _required_fields(sufficient_idea: dict[str, Any]) -> list[str]:
149
+ missing: list[str] = []
150
+ if not _normalize_list(sufficient_idea.get("target_users")):
151
+ missing.append("target_users")
152
+ if not str(sufficient_idea.get("problem") or "").strip():
153
+ missing.append("problem")
154
+ if not _normalize_list(sufficient_idea.get("user_outcomes")):
155
+ missing.append("user_outcomes")
156
+ return missing
157
+
158
+
159
+ def validate_sufficient_idea(sufficient_idea: dict[str, Any]) -> list[str]:
160
+ return _required_fields(sufficient_idea)
161
+
162
+
163
+ def _stable_story_set_id(*, idea_id: str, sufficient_idea: dict[str, Any], coverage_requirements: dict[str, Any] | None = None) -> str:
164
+ payload = {
165
+ "idea_id": idea_id,
166
+ "sufficient_idea": sufficient_idea,
167
+ "coverage_requirements": coverage_requirements or {},
168
+ "story_generation_version": 2,
169
+ }
170
+ digest = hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
171
+ return f"trad_{digest[:12]}"
172
+
173
+
174
+ def load_sufficient_idea(repo_root: Path, *, idea_id: str) -> tuple[dict[str, Any], dict[str, Any]]:
175
+ payload = _read_idea_payload(repo_root, idea_id=idea_id)
176
+ sufficient_idea = _extract_sufficient_idea(payload)
177
+ missing = validate_sufficient_idea(sufficient_idea)
178
+ if missing:
179
+ raise ValueError("Sufficient idea missing required fields: " + ", ".join(missing))
180
+ return payload, sufficient_idea
181
+
182
+
183
+ def _global_devflow_dir() -> Path:
184
+ home = os.environ.get("DEVFLOW_HOME")
185
+ if home:
186
+ return Path(home) / ".devflow"
187
+ return Path.home() / ".devflow"
188
+
189
+
190
+ def _read_toml(path: Path) -> dict[str, object]:
191
+ if tomllib is None or not path.exists():
192
+ return {}
193
+ try:
194
+ with path.open("rb") as fh:
195
+ data = tomllib.load(fh)
196
+ except Exception:
197
+ # Fall back: if tomllib fails (e.g. Python dict-string format in tiers key),
198
+ # read the raw file, extract all scalar key=value pairs as-is, and separately
199
+ # parse the tiers key as a Python literal so tier config works.
200
+ data = {}
201
+ try:
202
+ import re
203
+ raw = path.read_text(encoding="utf-8")
204
+ # Extract tiers separately as a Python literal.
205
+ m = re.search(r"^tiers\s*=\s*(.+)$", raw, re.MULTILINE | re.DOTALL)
206
+ if m:
207
+ tiers_src = m.group(1).strip()
208
+ parsed = ast.literal_eval(tiers_src)
209
+ if isinstance(parsed, dict):
210
+ data["tiers"] = parsed
211
+ # Extract all other scalar top-level key=value pairs directly.
212
+ for line in raw.splitlines():
213
+ line = line.strip()
214
+ if not line or line.startswith("#"):
215
+ continue
216
+ if "=" not in line:
217
+ continue
218
+ key, _, raw_val = line.partition("=")
219
+ key = key.strip()
220
+ if key in data or key == "tiers":
221
+ continue
222
+ val = raw_val.strip()
223
+ # Parse scalar TOML values: "string", number, boolean, null.
224
+ if val in ("true", "false"):
225
+ data[key] = val == "true"
226
+ elif val == "null":
227
+ data[key] = None
228
+ elif val.startswith('"') and val.endswith('"'):
229
+ data[key] = val[1:-1]
230
+ elif val.startswith("'") and val.endswith("'"):
231
+ data[key] = val[1:-1]
232
+ else:
233
+ try:
234
+ data[key] = int(val)
235
+ except ValueError:
236
+ try:
237
+ data[key] = float(val)
238
+ except ValueError:
239
+ pass
240
+ except Exception:
241
+ pass
242
+ return data if isinstance(data, dict) else {}
243
+
244
+
245
+
246
+
247
+ def _agent_timeout_seconds() -> int:
248
+ raw = os.environ.get("DEVFLOW_IDEA_STORY_AGENT_TIMEOUT")
249
+ if not raw:
250
+ return 3600
251
+ try:
252
+ value = int(raw)
253
+ except ValueError:
254
+ return 3600
255
+ return value if value > 0 else 3600
256
+
257
+ def _load_llm_cli_config() -> tuple[str, str]:
258
+ cfg = _read_toml(_global_devflow_dir() / "config.toml")
259
+ llm_mode = str(cfg.get("llm_mode") or "").strip().lower()
260
+ if llm_mode != "cli":
261
+ raise RuntimeError("LLM not configured for CLI mode. Run: devflow config llm-set-provider --mode cli --provider <...>")
262
+ base_cmd = str(cfg.get("llm_cli_base") or "").strip()
263
+ if not base_cmd:
264
+ raise RuntimeError("LLM CLI base command not set. Run: devflow config llm-set-provider ...")
265
+ delivery = str(cfg.get("llm_cli_delivery") or "argument").strip().lower()
266
+ return base_cmd, delivery
267
+
268
+
269
+ def _extract_json(text: str) -> str | None:
270
+ if "```" in text:
271
+ parts = text.split("```")
272
+ for i in range(len(parts) - 1):
273
+ body = parts[i + 1]
274
+ body_lines = body.splitlines()
275
+ if body_lines and body_lines[0].strip().lower() in {"json", "javascript"}:
276
+ body = "\n".join(body_lines[1:])
277
+ body = body.strip()
278
+ if body.startswith("{") and body.endswith("}"):
279
+ return body
280
+ start = text.find("{")
281
+ end = text.rfind("}")
282
+ if start != -1 and end != -1 and end > start:
283
+ return text[start : end + 1]
284
+ return None
285
+
286
+
287
+ def _json_attempt_artifact_path(*, root: Path | None, task: str, attempt: int) -> Path | None:
288
+ if root is None:
289
+ return None
290
+ safe_task = ''.join(ch if ch.isalnum() or ch in {'_', '-'} else '_' for ch in task).strip('_') or 'task'
291
+ agent_root = root / 'agent_runs'
292
+ agent_root.mkdir(parents=True, exist_ok=True)
293
+ return agent_root / f"{safe_task}_{attempt:03d}.json"
294
+
295
+
296
+ def _write_agent_attempt_artifact(*, path: Path | None, prompt: dict[str, Any], base_cmd: str, delivery: str, stdout: str, stderr: str, ok: bool, parsed: dict[str, Any] | None = None, parse_error: str | None = None, repaired_from_attempt: int | None = None) -> None:
297
+ if path is None:
298
+ return
299
+ payload: dict[str, Any] = {'task': str(prompt.get('task') or ''), 'ok': ok, 'base_cmd': base_cmd, 'delivery': delivery, 'prompt': prompt, 'raw_stdout': stdout, 'raw_stderr': stderr}
300
+ if parsed is not None:
301
+ payload['parsed'] = parsed
302
+ if parse_error:
303
+ payload['parse_error'] = parse_error
304
+ if repaired_from_attempt is not None:
305
+ payload['repaired_from_attempt'] = repaired_from_attempt
306
+ _write_json(path, payload)
307
+
308
+
309
+ def _repair_json_prompt(*, original_prompt: dict[str, Any], raw_stdout: str) -> dict[str, Any]:
310
+ return {
311
+ 'task': f"repair_{str(original_prompt.get('task') or 'json_output')}",
312
+ 'instructions': [
313
+ 'Return JSON only. No markdown. No prose outside JSON.',
314
+ 'Repair or extract the prior model output into one valid JSON object matching the original requested schema.',
315
+ 'Do not add new semantics. Preserve the original meaning as closely as possible.',
316
+ ],
317
+ 'original_prompt': original_prompt,
318
+ 'raw_output_to_repair': raw_stdout,
319
+ 'output_schema': original_prompt.get('output_schema') or {},
320
+ }
321
+
322
+
323
+ def _parse_json_agent_output(*, stdout: str, task: str) -> dict[str, Any]:
324
+ raw_json = _extract_json(stdout)
325
+ if raw_json is None:
326
+ raise RuntimeError(f"Failed to locate JSON in agent output for task={task!r}.")
327
+ try:
328
+ parsed = json.loads(raw_json)
329
+ except Exception as exc:
330
+ raise RuntimeError(f"Failed to parse JSON from agent output for task={task!r}.") from exc
331
+ if not isinstance(parsed, dict):
332
+ raise RuntimeError(f"Agent output for task={task!r} must be a JSON object.")
333
+ return parsed
334
+
335
+
336
+ def _call_json_agent(*, repo_root: Path, prompt: dict[str, Any], error_message: str, artifact_root: Path | None = None) -> dict[str, Any]:
337
+ base_cmd, delivery = _load_llm_cli_config()
338
+ task = str(prompt.get('task') or 'json_task')
339
+ result = run_one_shot(base_cmd=base_cmd, delivery=delivery, prompt=json.dumps(prompt, indent=2, sort_keys=True), cwd=repo_root, timeout_seconds=_agent_timeout_seconds())
340
+ attempt_path = _json_attempt_artifact_path(root=artifact_root, task=task, attempt=1)
341
+ if not result.ok:
342
+ _write_agent_attempt_artifact(path=attempt_path, prompt=prompt, base_cmd=base_cmd, delivery=delivery, stdout=result.stdout, stderr=result.stderr, ok=False, parse_error=result.stderr or result.stdout or error_message)
343
+ raise RuntimeError(result.stderr or result.stdout or error_message)
344
+ try:
345
+ parsed = _parse_json_agent_output(stdout=result.stdout, task=task)
346
+ _write_agent_attempt_artifact(path=attempt_path, prompt=prompt, base_cmd=base_cmd, delivery=delivery, stdout=result.stdout, stderr=result.stderr, ok=True, parsed=parsed)
347
+ return parsed
348
+ except RuntimeError as exc:
349
+ _write_agent_attempt_artifact(path=attempt_path, prompt=prompt, base_cmd=base_cmd, delivery=delivery, stdout=result.stdout, stderr=result.stderr, ok=False, parse_error=str(exc))
350
+ repair_error: RuntimeError = exc
351
+ for repair_attempt in range(1, MAX_JSON_REPAIR_ATTEMPTS + 1):
352
+ repair_prompt = _repair_json_prompt(original_prompt=prompt, raw_stdout=result.stdout)
353
+ repair_result = run_one_shot(base_cmd=base_cmd, delivery=delivery, prompt=json.dumps(repair_prompt, indent=2, sort_keys=True), cwd=repo_root, timeout_seconds=_agent_timeout_seconds())
354
+ repair_path = _json_attempt_artifact_path(root=artifact_root, task=task, attempt=repair_attempt + 1)
355
+ if not repair_result.ok:
356
+ _write_agent_attempt_artifact(path=repair_path, prompt=repair_prompt, base_cmd=base_cmd, delivery=delivery, stdout=repair_result.stdout, stderr=repair_result.stderr, ok=False, parse_error=repair_result.stderr or repair_result.stdout or error_message, repaired_from_attempt=1)
357
+ repair_error = RuntimeError(repair_result.stderr or repair_result.stdout or error_message)
358
+ continue
359
+ try:
360
+ repaired = _parse_json_agent_output(stdout=repair_result.stdout, task=task)
361
+ _write_agent_attempt_artifact(path=repair_path, prompt=repair_prompt, base_cmd=base_cmd, delivery=delivery, stdout=repair_result.stdout, stderr=repair_result.stderr, ok=True, parsed=repaired, repaired_from_attempt=1)
362
+ return repaired
363
+ except RuntimeError as inner_exc:
364
+ _write_agent_attempt_artifact(path=repair_path, prompt=repair_prompt, base_cmd=base_cmd, delivery=delivery, stdout=repair_result.stdout, stderr=repair_result.stderr, ok=False, parse_error=str(inner_exc), repaired_from_attempt=1)
365
+ repair_error = inner_exc
366
+ raise repair_error
367
+
368
+
369
+ def _normalize_requirement(requirement: dict[str, Any], *, index: int, actor_registry: dict[str, Any] | None = None) -> dict[str, Any]:
370
+ category = str(requirement.get("category") or "coverage").strip() or "coverage"
371
+ requirement_id = str(requirement.get("requirement_id") or f"req_{index:03d}").strip() or f"req_{index:03d}"
372
+ actor_requirement_id = str(requirement.get("actor_requirement_id") or "").strip() or None
373
+ subject = str(requirement.get("subject") or "").strip()
374
+ actor_id = str(requirement.get("actor_id") or "").strip() or None
375
+ actor_label = str(requirement.get("actor_label") or "").strip() or None
376
+ if category == "actor" and actor_registry is not None and _resolved_actor_entries(actor_registry):
377
+ primary_actor = _primary_actor_contract(
378
+ actor_registry=actor_registry,
379
+ actor_id=actor_id,
380
+ actor_label=actor_label or subject,
381
+ field_name="coverage requirement actor",
382
+ )
383
+ actor_id = primary_actor["id"]
384
+ actor_label = primary_actor["label"]
385
+ subject = actor_label
386
+ return {
387
+ "requirement_id": requirement_id,
388
+ "category": category,
389
+ "actor_requirement_id": actor_requirement_id,
390
+ "actor_id": actor_id,
391
+ "actor_label": actor_label,
392
+ "subject": subject,
393
+ "requirement": str(requirement.get("requirement") or "").strip(),
394
+ "rationale": str(requirement.get("rationale") or "").strip(),
395
+ }
396
+
397
+
398
+ def _coverage_requirements_prompt(*, idea_id: str, payload: dict[str, Any], sufficient_idea: dict[str, Any], actor_registry: dict[str, Any]) -> dict[str, Any]:
399
+ resolved_actor_catalog = [
400
+ {
401
+ "id": str(item.get("id") or "").strip(),
402
+ "label": str(item.get("label") or "").strip(),
403
+ "kind": str(item.get("kind") or "human").strip() or "human",
404
+ "inherits_from": str(item.get("inherits_from") or "").strip() or None,
405
+ }
406
+ for item in _resolved_actor_entries(actor_registry)
407
+ ]
408
+ return {
409
+ "task": "generate_traditional_story_coverage_requirements",
410
+ "idea_id": idea_id,
411
+ "idea_title": str(payload.get("title") or sufficient_idea.get("summary") or idea_id),
412
+ "sufficient_idea": sufficient_idea,
413
+ "actor_registry": actor_registry,
414
+ "resolved_actor_catalog": resolved_actor_catalog,
415
+ "instructions": [
416
+ "Return JSON only. No markdown. No prose outside JSON.",
417
+ "Generate the atomic coverage requirements that must be represented by the traditional user story set.",
418
+ "Every requirement must be specific, testable at the story-planning level, and traceable to the sufficient idea.",
419
+ "Decompose capability-first / process-first by default: prefer one requirement per distinct user-visible capability or workflow.",
420
+ "Do not clone the same requirement across multiple user types or actors when the capability, workflow, and acceptance remain materially the same.",
421
+ "Create actor-scoped requirements only when auth, permissions, approvals, data scope, or the workflow materially differs for that actor.",
422
+ "Use only actors from actor_registry.canonical_actors / actor_registry.actors when generating actor-scoped requirements." if _resolved_actor_entries(actor_registry) else "If no actor registry is defined yet, use singular canonical actor labels and avoid merged actor classes.",
423
+ f"Use only these resolved actor ids when actor_id is present: {_resolved_actor_ids(actor_registry)}" if _resolved_actor_entries(actor_registry) else "If actor_id is omitted, use a singular actor label only.",
424
+ "Canonical actor labels must name exactly one actor class. Never use slash-separated aliases, merged synonym labels, or hybrid names like employee/respondent or admin/operator.",
425
+ "When a requirement concerns a specific actor's distinct outcome, gate, or materially different flow, include actor_requirement_id pointing at that actor requirement.",
426
+ "Do not use heuristics or fallback behavior. Derive requirements directly from the sufficient idea and canonical actor registry.",
427
+ ],
428
+ "output_schema": {
429
+ "requirements": [
430
+ {
431
+ "requirement_id": "string",
432
+ "category": "actor|actor_outcome|actor_flow|actor_gate|cross_cutting",
433
+ "actor_requirement_id": "string|null",
434
+ "actor_id": "string|null",
435
+ "actor_label": "string|null",
436
+ "subject": "string",
437
+ "requirement": "string",
438
+ "rationale": "string",
439
+ }
440
+ ]
441
+ },
442
+ }
443
+
444
+
445
+ def generate_story_coverage_requirements(*, repo_root: Path, idea_id: str, payload: dict[str, Any], sufficient_idea: dict[str, Any], actor_registry: dict[str, Any] | None = None) -> dict[str, Any]:
446
+ actor_registry = actor_registry or load_actor_registry(repo_root)
447
+ prompt = _coverage_requirements_prompt(
448
+ idea_id=idea_id,
449
+ payload=payload,
450
+ sufficient_idea=sufficient_idea,
451
+ actor_registry=actor_registry,
452
+ )
453
+ raw = _call_json_agent(repo_root=repo_root, prompt=prompt, error_message="story coverage requirements LLM command failed", artifact_root=get_idea_paths(repo_root, idea_id=idea_id).idea_dir)
454
+ requirements_raw = raw.get("requirements") if isinstance(raw.get("requirements"), list) else []
455
+ requirements = [_normalize_requirement(item, index=index, actor_registry=actor_registry) for index, item in enumerate(requirements_raw, start=1) if isinstance(item, dict)]
456
+ if not requirements:
457
+ raise RuntimeError("Coverage requirements agent returned no requirements.")
458
+ registry_actors = _resolved_actor_entries(actor_registry)
459
+ if registry_actors:
460
+ for requirement in requirements:
461
+ if str(requirement.get("category") or "") != "actor":
462
+ continue
463
+ primary_actor = _primary_actor_contract(
464
+ actor_registry=actor_registry,
465
+ actor_id=str(requirement.get("actor_id") or "").strip() or None,
466
+ actor_label=str(requirement.get("actor_label") or requirement.get("subject") or "").strip() or None,
467
+ field_name="coverage requirement actor",
468
+ )
469
+ requirement["actor_id"] = primary_actor["id"]
470
+ requirement["actor_label"] = primary_actor["label"]
471
+ if str(requirement.get("category") or "") == "actor":
472
+ requirement["subject"] = primary_actor["label"]
473
+ return {
474
+ "idea_id": idea_id,
475
+ "generated_at": _iso_now(),
476
+ "evaluation_mode": "agentic",
477
+ "requirements": requirements,
478
+ }
479
+
480
+
481
+ def _validate_story_shape(story: dict[str, Any], *, index: int) -> list[str]:
482
+ findings: list[str] = []
483
+ required = ("title", "actor", "goal", "benefit", "user_value", "acceptance_criteria")
484
+ for field in required:
485
+ if field == "acceptance_criteria":
486
+ values = _normalize_list(story.get(field))
487
+ if len(values) < 2:
488
+ findings.append(f"story {index} needs at least two acceptance criteria")
489
+ continue
490
+ value = str(story.get(field) or "").strip()
491
+ if not value:
492
+ findings.append(f"story {index} missing {field}")
493
+ if value.lower() in {"placeholder", "todo", "tbd", "template"}:
494
+ findings.append(f"story {index} contains placeholder {field}")
495
+ return findings
496
+
497
+
498
+ def _coverage_deficiency_prompt(*, sufficient_idea: dict[str, Any], coverage_requirements: dict[str, Any], stories: list[dict[str, Any]], pass_index: int) -> dict[str, Any]:
499
+ return {
500
+ "task": "evaluate_traditional_story_coverage_deficiencies",
501
+ "pass_index": pass_index,
502
+ "sufficient_idea": sufficient_idea,
503
+ "coverage_requirements": coverage_requirements,
504
+ "stories": stories,
505
+ "instructions": [
506
+ "Return JSON only. No markdown. No prose outside JSON.",
507
+ "Evaluate whether the traditional story set fully covers the atomic coverage requirements.",
508
+ "Judge coverage semantically, but report deficiencies only against the provided requirement IDs.",
509
+ "Maintain actor correctness, but do not require separate stories per actor when the same capability/workflow is shared.",
510
+ "Only flag actor-specific gaps when auth, permissions, approvals, routing, or other workflow behavior materially changes for that actor.",
511
+ "If coverage is insufficient, list the uncovered or weakly covered requirement IDs and concise findings.",
512
+ "Do not invent fallback requirements.",
513
+ ],
514
+ "output_schema": {
515
+ "passed": True,
516
+ "findings": ["string"],
517
+ "uncovered_requirement_ids": ["string"],
518
+ "weak_requirement_ids": ["string"],
519
+ "covered_requirement_ids": ["string"],
520
+ "deficiencies": [
521
+ {
522
+ "requirement_id": "string",
523
+ "severity": "missing|weak",
524
+ "finding": "string",
525
+ }
526
+ ],
527
+ },
528
+ }
529
+
530
+
531
+ def _build_actor_specificity_repair_plan(*, coverage_requirements: dict[str, Any], weak_requirement_ids: list[str]) -> list[dict[str, Any]]:
532
+ by_id = _requirements_by_id(coverage_requirements)
533
+ repairs: list[dict[str, Any]] = []
534
+ seen: set[tuple[str, str]] = set()
535
+ for req_id in weak_requirement_ids:
536
+ requirement = by_id.get(str(req_id).strip()) or {}
537
+ actor_id = str(requirement.get("actor_id") or "").strip()
538
+ actor_label = str(requirement.get("actor_label") or requirement.get("subject") or "").strip()
539
+ if not actor_id and not actor_label:
540
+ continue
541
+ key = (actor_id, actor_label)
542
+ if key in seen:
543
+ continue
544
+ seen.add(key)
545
+ repairs.append({
546
+ "type": "clarify_actor_distinction",
547
+ "actor_id": actor_id or None,
548
+ "actor_label": actor_label or None,
549
+ "requirement_ids": [
550
+ rid for rid in weak_requirement_ids
551
+ if ((str((by_id.get(str(rid).strip()) or {}).get("actor_id") or "").strip() == actor_id) and actor_id)
552
+ or ((str((by_id.get(str(rid).strip()) or {}).get("actor_label") or (by_id.get(str(rid).strip()) or {}).get("subject") or "").strip() == actor_label) and actor_label)
553
+ ],
554
+ "instruction": (
555
+ f"Keep the shared capability-first story shape for {actor_label or actor_id} unless that actor has a materially different permission, approval, "
556
+ "or workflow branch. Prefer capturing the role distinction in acceptance criteria or story notes before splitting into a separate story."
557
+ ),
558
+ })
559
+ return repairs
560
+
561
+
562
+ def evaluate_traditional_story_sufficiency(*, repo_root: Path, stories: list[dict[str, Any]], sufficient_idea: dict[str, Any], coverage_requirements: dict[str, Any], pass_index: int) -> dict[str, Any]:
563
+ findings: list[str] = []
564
+ if not stories:
565
+ findings.append("missing stories")
566
+ return {"passed": False, "findings": findings, "coverage": {"story_count": 0}, "evaluation_mode": "agentic"}
567
+
568
+ for index, story in enumerate(stories, start=1):
569
+ findings.extend(_validate_story_shape(story, index=index))
570
+ if findings:
571
+ return {"passed": False, "findings": findings, "coverage": {"story_count": len(stories)}, "evaluation_mode": "agentic"}
572
+
573
+ prompt = _coverage_deficiency_prompt(
574
+ sufficient_idea=sufficient_idea,
575
+ coverage_requirements=coverage_requirements,
576
+ stories=stories,
577
+ pass_index=pass_index,
578
+ )
579
+ evaluation = _call_json_agent(repo_root=repo_root, prompt=prompt, error_message="story coverage deficiency evaluator LLM command failed")
580
+ eval_findings = [str(item) for item in (evaluation.get("findings") or []) if str(item).strip()]
581
+ covered_requirement_ids = [str(item).strip() for item in (evaluation.get("covered_requirement_ids") or []) if str(item).strip()]
582
+ uncovered_requirement_ids = [str(item).strip() for item in (evaluation.get("uncovered_requirement_ids") or []) if str(item).strip()]
583
+ weak_requirement_ids = [str(item).strip() for item in (evaluation.get("weak_requirement_ids") or []) if str(item).strip()]
584
+ deficiencies = [
585
+ {
586
+ "requirement_id": str(item.get("requirement_id") or "").strip(),
587
+ "severity": str(item.get("severity") or "").strip(),
588
+ "finding": str(item.get("finding") or "").strip(),
589
+ }
590
+ for item in (evaluation.get("deficiencies") or [])
591
+ if isinstance(item, dict)
592
+ ]
593
+
594
+ actor_coverage_ids: set[str] = set()
595
+ for requirement in (coverage_requirements.get("requirements") or []):
596
+ if not isinstance(requirement, dict) or str(requirement.get("category") or "") != "actor":
597
+ continue
598
+ req_id = str(requirement.get("requirement_id") or "").strip()
599
+ req_actor_id = str(requirement.get("actor_id") or "").strip()
600
+ req_actor_label = str(requirement.get("actor_label") or requirement.get("subject") or "").strip()
601
+ for story in stories:
602
+ story_actor_id = str(story.get("actor_id") or "").strip()
603
+ story_actor = str(story.get("actor") or "").strip()
604
+ if req_actor_id and story_actor_id and req_actor_id == story_actor_id:
605
+ actor_coverage_ids.add(req_id)
606
+ break
607
+ if req_actor_label and story_actor and normalize_actor_label(req_actor_label) == normalize_actor_label(story_actor):
608
+ actor_coverage_ids.add(req_id)
609
+ break
610
+
611
+ for req_id in sorted(actor_coverage_ids):
612
+ if req_id and req_id not in covered_requirement_ids:
613
+ covered_requirement_ids.append(req_id)
614
+ if req_id in uncovered_requirement_ids:
615
+ uncovered_requirement_ids.remove(req_id)
616
+ if req_id in weak_requirement_ids:
617
+ weak_requirement_ids.remove(req_id)
618
+ deficiencies = [item for item in deficiencies if item.get("requirement_id") != req_id]
619
+ eval_findings = [item for item in eval_findings if req_id not in item]
620
+
621
+ weak_requirements = _select_requirement_subset(coverage_requirements=coverage_requirements, requirement_ids=weak_requirement_ids, limit=20)
622
+ actor_specificity_warning = bool(weak_requirement_ids) and not uncovered_requirement_ids
623
+ repair_plan = _build_actor_specificity_repair_plan(coverage_requirements=coverage_requirements, weak_requirement_ids=weak_requirement_ids)
624
+ coverage = {
625
+ "story_count": len(stories),
626
+ "requirement_count": len(coverage_requirements.get("requirements") or []),
627
+ "covered_requirement_ids": covered_requirement_ids,
628
+ "uncovered_requirement_ids": uncovered_requirement_ids,
629
+ "weak_requirement_ids": weak_requirement_ids,
630
+ "deficiencies": deficiencies,
631
+ "weak_requirements": weak_requirements,
632
+ "actor_specificity_warning": actor_specificity_warning,
633
+ "repair_plan": repair_plan,
634
+ }
635
+ passed = not coverage["uncovered_requirement_ids"]
636
+ if actor_specificity_warning:
637
+ eval_findings.append(
638
+ "Behavioral coverage is complete, but some actor-specific requirements remain weakly attributed; passing with actor-specificity warning and repair plan."
639
+ )
640
+ return {
641
+ "passed": passed,
642
+ "findings": eval_findings,
643
+ "coverage": coverage,
644
+ "evaluation_mode": "agentic",
645
+ "warnings": ["actor_specificity_weak"] if actor_specificity_warning else [],
646
+ "repair_plan": repair_plan,
647
+ }
648
+
649
+
650
+
651
+
652
+ def _compact_requirement(requirement: dict[str, Any]) -> dict[str, Any]:
653
+ return {
654
+ "requirement_id": str(requirement.get("requirement_id") or "").strip(),
655
+ "category": str(requirement.get("category") or "").strip(),
656
+ "actor_id": str(requirement.get("actor_id") or "").strip() or None,
657
+ "actor_label": str(requirement.get("actor_label") or "").strip() or None,
658
+ "subject": str(requirement.get("subject") or "").strip(),
659
+ "requirement": str(requirement.get("requirement") or "").strip(),
660
+ }
661
+
662
+
663
+ def _compact_story(story: dict[str, Any]) -> dict[str, Any]:
664
+ ac = [str(item).strip() for item in (story.get("acceptance_criteria") or []) if str(item).strip()]
665
+ return {
666
+ "story_id": str(story.get("story_id") or story.get("id") or "").strip() or None,
667
+ "title": str(story.get("title") or "").strip(),
668
+ "actor_id": str(story.get("actor_id") or "").strip() or None,
669
+ "actor": str(story.get("actor") or "").strip(),
670
+ "goal": str(story.get("goal") or "").strip(),
671
+ "benefit": str(story.get("benefit") or "").strip(),
672
+ "coverage_tags": [str(item).strip() for item in (story.get("coverage_tags") or []) if str(item).strip()],
673
+ "acceptance_criteria": ac[:4],
674
+ }
675
+
676
+
677
+ def _compact_sufficient_idea(sufficient_idea: dict[str, Any]) -> dict[str, Any]:
678
+ return {
679
+ "summary": str(sufficient_idea.get("summary") or sufficient_idea.get("description") or "").strip(),
680
+ "target_users": [str(item).strip() for item in (sufficient_idea.get("target_users") or sufficient_idea.get("users") or []) if str(item).strip()],
681
+ "key_constraints": [str(item).strip() for item in (sufficient_idea.get("constraints") or []) if str(item).strip()][:10],
682
+ }
683
+
684
+
685
+ def _requirements_by_id(coverage_requirements: dict[str, Any]) -> dict[str, dict[str, Any]]:
686
+ return {str(item.get("requirement_id") or "").strip(): item for item in (coverage_requirements.get("requirements") or []) if isinstance(item, dict) and str(item.get("requirement_id") or "").strip()}
687
+
688
+
689
+ def _select_requirement_subset(*, coverage_requirements: dict[str, Any], requirement_ids: list[str] | None = None, limit: int = 8) -> list[dict[str, Any]]:
690
+ reqs = [item for item in (coverage_requirements.get("requirements") or []) if isinstance(item, dict)]
691
+ if not requirement_ids:
692
+ return [_compact_requirement(item) for item in reqs[:limit]]
693
+ by_id = _requirements_by_id(coverage_requirements)
694
+ subset=[]
695
+ seen=set()
696
+ for req_id in requirement_ids:
697
+ rid=str(req_id).strip()
698
+ if rid and rid in by_id and rid not in seen:
699
+ subset.append(_compact_requirement(by_id[rid]))
700
+ seen.add(rid)
701
+ return subset[:limit]
702
+
703
+
704
+ def _select_story_subset(*, stories: list[dict[str, Any]], titles: list[str] | None = None, limit: int = 6) -> list[dict[str, Any]]:
705
+ if not titles:
706
+ return [_compact_story(item) for item in stories[:limit]]
707
+ wanted={str(item).strip().lower() for item in titles if str(item).strip()}
708
+ subset=[]
709
+ for story in stories:
710
+ title=str(story.get("title") or "").strip()
711
+ if title.lower() in wanted:
712
+ subset.append(_compact_story(story))
713
+ if not subset:
714
+ subset=[_compact_story(item) for item in stories[:limit]]
715
+ return subset[:limit]
716
+
717
+ def _decomposition_check_prompt(*, sufficient_idea: dict[str, Any], coverage_requirements: dict[str, Any], stories: list[dict[str, Any]], iteration: int) -> dict[str, Any]:
718
+ return {
719
+ "task": "evaluate_traditional_story_decomposition",
720
+ "iteration": iteration,
721
+ "sufficient_idea": sufficient_idea,
722
+ "coverage_requirements": coverage_requirements,
723
+ "stories": stories,
724
+ "instructions": [
725
+ "Return JSON only. No markdown. No prose outside JSON.",
726
+ "Evaluate whether the covered traditional story set is cleanly decomposed into atomic, non-overlapping user-visible stories.",
727
+ "Identify overlaps, duplicate coverage, assembly stories, and stories that are not independently meaningful.",
728
+ "Treat actor-first clones of the same underlying capability or workflow as overlap unless the actor changes permissions, approvals, or flow semantics in a material way.",
729
+ "Do not use heuristics or fallback behavior.",
730
+ ],
731
+ "output_schema": {
732
+ "passed": True,
733
+ "findings": ["string"],
734
+ "issues": [
735
+ {
736
+ "story_title": "string",
737
+ "issue": "string",
738
+ }
739
+ ],
740
+ },
741
+ }
742
+
743
+
744
+ def _decomposition_refine_prompt(*, sufficient_idea: dict[str, Any], coverage_requirements: dict[str, Any], stories: list[dict[str, Any]], iteration: int, check_findings: list[str], check_issues: list[dict[str, str]]) -> dict[str, Any]:
745
+ return {
746
+ "task": "refine_traditional_story_decomposition",
747
+ "iteration": iteration,
748
+ "sufficient_idea": sufficient_idea,
749
+ "coverage_requirements": coverage_requirements,
750
+ "stories": stories,
751
+ "check_findings": check_findings,
752
+ "check_issues": check_issues,
753
+ "instructions": [
754
+ "Return JSON only. No markdown. No prose outside JSON.",
755
+ "Revise the story set to improve decomposition while preserving semantic coverage of the provided coverage requirements.",
756
+ "Merge overlapping stories when they represent the same user-visible outcome.",
757
+ "Collapse parallel actor-first clones into capability-first stories unless the actor changes permissions, approvals, routing, or flow semantics in a materially different way.",
758
+ "When multiple actors can perform the same capability, keep one primary actor and capture the other allowed actors or constraints in acceptance criteria.",
759
+ "Fold technical or infrastructure-only concerns into acceptance criteria on user-visible stories instead of keeping them as standalone stories.",
760
+ "Preserve actor specificity and requirement traceability.",
761
+ "Return the full revised story set, not a patch.",
762
+ "Do not use heuristics or fallback behavior.",
763
+ ],
764
+ "output_schema": {
765
+ "stories": [
766
+ {
767
+ "title": "string",
768
+ "actor_id": "string",
769
+ "actor_id": "string",
770
+ "actor": "string",
771
+ "goal": "string",
772
+ "benefit": "string",
773
+ "user_value": "string",
774
+ "acceptance_criteria": ["string", "string"],
775
+ "coverage_tags": ["string"],
776
+ }
777
+ ],
778
+ "merge_log": [
779
+ {
780
+ "action": "merge|drop|rewrite|retain",
781
+ "story_title": "string",
782
+ "reason": "string",
783
+ }
784
+ ],
785
+ },
786
+ }
787
+
788
+
789
+ def _normalize_decomposition_check(check: dict[str, Any]) -> dict[str, Any]:
790
+ return {
791
+ "passed": bool(check.get("passed")),
792
+ "findings": [str(item) for item in (check.get("findings") or []) if str(item).strip()],
793
+ "issues": [
794
+ {
795
+ "story_title": str(item.get("story_title") or "").strip(),
796
+ "issue": str(item.get("issue") or "").strip(),
797
+ }
798
+ for item in (check.get("issues") or [])
799
+ if isinstance(item, dict)
800
+ ],
801
+ }
802
+
803
+
804
+ def _normalize_refine_payload(refined: dict[str, Any], revised_stories: list[dict[str, Any]]) -> dict[str, Any]:
805
+ return {
806
+ "merge_log": [
807
+ {
808
+ "action": str(item.get("action") or "").strip(),
809
+ "story_title": str(item.get("story_title") or "").strip(),
810
+ "reason": str(item.get("reason") or "").strip(),
811
+ }
812
+ for item in (refined.get("merge_log") or [])
813
+ if isinstance(item, dict)
814
+ ],
815
+ "stories": revised_stories,
816
+ }
817
+
818
+
819
+ def _build_resume_cursor(*, story_set_id: str, iteration: int, phase: str, artifact_path: Path | None = None) -> dict[str, Any]:
820
+ payload: dict[str, Any] = {'kind': 'traditional_story_decomposition', 'story_set_id': story_set_id, 'iteration': iteration, 'phase': phase}
821
+ if artifact_path is not None:
822
+ payload['artifact'] = artifact_path.name
823
+ return payload
824
+
825
+
826
+ def run_traditional_story_decomposition_loop(*, repo_root: Path, root: Path, stories: list[dict[str, Any]], sufficient_idea: dict[str, Any], coverage_requirements: dict[str, Any]) -> tuple[list[dict[str, Any]], dict[str, Any]]:
827
+ if not stories:
828
+ report = {
829
+ "passed": False, "approved": True, "hit_max_iterations": False, "approved_by_iteration_cap": False, "iteration_count": 0,
830
+ "findings": ["missing stories"], "issues": [], "history": [], "evaluation_mode": "agentic",
831
+ "resume_cursor": _build_resume_cursor(story_set_id=root.name, iteration=1, phase='check'),
832
+ }
833
+ return stories, report
834
+
835
+ current_stories = list(stories)
836
+ history: list[dict[str, Any]] = []
837
+ latest_check = {"passed": True, "findings": [], "issues": []}
838
+ resume_cursor: dict[str, Any] | None = None
839
+
840
+ for iteration in range(1, MAX_DECOMPOSITION_ITERATIONS + 1):
841
+ check_path = root / f"decomposition_check_{iteration:03d}.json"
842
+ refine_path = root / f"decomposition_refine_{iteration:03d}.json"
843
+ validate_path = root / f"decomposition_validate_{iteration:03d}.json"
844
+ iteration_payload: dict[str, Any] = {"iteration": iteration}
845
+
846
+ if check_path.exists():
847
+ normalized_check = json.loads(check_path.read_text(encoding='utf-8'))
848
+ else:
849
+ check_prompt = _decomposition_check_prompt(sufficient_idea=sufficient_idea, coverage_requirements=coverage_requirements, stories=current_stories, iteration=iteration)
850
+ check = _call_json_agent(repo_root=repo_root, prompt=check_prompt, error_message="story decomposition evaluator LLM command failed", artifact_root=root)
851
+ normalized_check = _normalize_decomposition_check(check)
852
+ _write_json(check_path, normalized_check)
853
+ latest_check = normalized_check
854
+ iteration_payload.update({"check": normalized_check, "check_artifact": check_path.name})
855
+ if normalized_check["passed"]:
856
+ history.append(iteration_payload)
857
+ resume_cursor = None
858
+ break
859
+
860
+ if refine_path.exists():
861
+ refine_payload = json.loads(refine_path.read_text(encoding='utf-8'))
862
+ revised_stories = list(refine_payload.get('stories') or [])
863
+ else:
864
+ refine_prompt = _decomposition_refine_prompt(sufficient_idea=sufficient_idea, coverage_requirements=coverage_requirements, stories=current_stories, iteration=iteration, check_findings=normalized_check["findings"], check_issues=normalized_check["issues"])
865
+ refined = _call_json_agent(repo_root=repo_root, prompt=refine_prompt, error_message="story decomposition refinement LLM command failed", artifact_root=root)
866
+ revised_stories = _call_story_agent(repo_root=repo_root, prompt=refine_prompt, artifact_root=root)
867
+ refine_payload = _normalize_refine_payload(refined, revised_stories)
868
+ _write_json(refine_path, refine_payload)
869
+
870
+ if validate_path.exists():
871
+ validate = json.loads(validate_path.read_text(encoding='utf-8'))
872
+ else:
873
+ validate = evaluate_traditional_story_sufficiency(repo_root=repo_root, stories=revised_stories, sufficient_idea=sufficient_idea, coverage_requirements=coverage_requirements, pass_index=MAX_COVERAGE_ATTEMPTS + iteration)
874
+ _write_json(validate_path, validate)
875
+
876
+ iteration_payload.update({"refine_artifact": refine_path.name, "validate_artifact": validate_path.name, "validation": validate})
877
+ history.append(iteration_payload)
878
+ current_stories = revised_stories
879
+ latest_check = {"passed": bool(validate.get("passed")), "findings": [str(item) for item in (validate.get("findings") or []) if str(item).strip()], "issues": normalized_check["issues"]}
880
+ resume_cursor = None if latest_check['passed'] else _build_resume_cursor(story_set_id=root.name, iteration=min(iteration + 1, MAX_DECOMPOSITION_ITERATIONS), phase='check', artifact_path=validate_path)
881
+
882
+ hit_max_iterations = not bool(latest_check.get("passed")) and len(history) >= MAX_DECOMPOSITION_ITERATIONS
883
+ report = {
884
+ "passed": bool(latest_check.get("passed")), "approved": True, "hit_max_iterations": hit_max_iterations, "approved_by_iteration_cap": hit_max_iterations,
885
+ "iteration_count": len(history), "findings": list(latest_check.get("findings") or []), "issues": list(latest_check.get("issues") or []),
886
+ "history": history, "evaluation_mode": "agentic", "resume_cursor": resume_cursor,
887
+ }
888
+ return current_stories, report
889
+
890
+
891
+ def _resolved_actor_labels(actor_registry: dict[str, Any]) -> list[str]:
892
+ return [str(item.get("label") or "").strip() for item in _resolved_actor_entries(actor_registry)]
893
+
894
+
895
+ def _lint_story_actor(*, actor: str, actor_registry: dict[str, Any]) -> str:
896
+ validate_canonical_actor_label(actor)
897
+ canonical_actor = canonicalize_story_actor(actor=actor, actor_registry=actor_registry)
898
+ if canonical_actor is None:
899
+ allowed = ", ".join(_resolved_actor_labels(actor_registry)) or "<none>"
900
+ raise RuntimeError(f"Story actor {actor!r} is not present in canonical actor registry. Allowed actors: {allowed}")
901
+ return canonical_actor
902
+
903
+
904
+ def _story_prompt(*, idea_id: str, payload: dict[str, Any], sufficient_idea: dict[str, Any], coverage_requirements: dict[str, Any], actor_registry: dict[str, Any], pass_index: int, prior_stories: list[dict[str, Any]], current_findings: list[str], current_deficiencies: list[dict[str, Any]]) -> dict[str, Any]:
905
+ resolved_actor_labels = _resolved_actor_labels(actor_registry)
906
+ resolved_actor_catalog = [
907
+ {
908
+ "id": str(item.get("id") or "").strip(),
909
+ "label": str(item.get("label") or "").strip(),
910
+ "kind": str(item.get("kind") or "human").strip() or "human",
911
+ "inherits_from": str(item.get("inherits_from") or "").strip() or None,
912
+ }
913
+ for item in _resolved_actor_entries(actor_registry)
914
+ ]
915
+ return {
916
+ "task": "generate_traditional_user_stories",
917
+ "idea_id": idea_id,
918
+ "pass_index": pass_index,
919
+ "sufficient_idea": sufficient_idea,
920
+ "coverage_requirements": coverage_requirements,
921
+ "actor_registry": actor_registry,
922
+ "resolved_actor_catalog": resolved_actor_catalog,
923
+ "idea_title": str(payload.get("title") or sufficient_idea.get("summary") or idea_id),
924
+ "current_findings": current_findings,
925
+ "current_deficiencies": current_deficiencies,
926
+ "prior_stories": prior_stories,
927
+ "instructions": [
928
+ "Return JSON only. No markdown. No prose outside JSON.",
929
+ "Generate materially useful traditional user stories that satisfy every atomic coverage requirement.",
930
+ "Default to capability-first / process-first decomposition: organize stories around distinct user-visible capabilities or workflow steps.",
931
+ "Do not clone the same story across user types or actors when the capability, workflow, and outcome are materially the same.",
932
+ "Split stories by actor only when permissions, approvals, routing, data scope, or workflow behavior materially differs for that actor.",
933
+ f"Use only these resolved actor labels in the story actor field: {resolved_actor_labels}",
934
+ f"Use only these resolved actor ids in the story actor_id field: {_resolved_actor_ids(actor_registry)}",
935
+ "Every story must have exactly one primary actor from the resolved actor set for this run.",
936
+ "Canonical actor labels must name exactly one actor class. Never use slash-separated aliases, merged synonym labels, 'or'-joined labels, or hybrid names like employee/respondent or admin/operator.",
937
+ "If multiple roles are involved in the same shared capability, keep one representative primary actor in the story actor field and mention the other permitted roles or constraints only inside acceptance criteria.",
938
+ "Stay at the user-visible story level. Do not drift into implementation details.",
939
+ "When current_deficiencies is non-empty, revise the full story set to close those exact requirement gaps.",
940
+ "Do not use placeholder text.",
941
+ ],
942
+ "output_schema": {
943
+ "stories": [
944
+ {
945
+ "title": "string",
946
+ "actor_id": "string",
947
+ "actor": "string",
948
+ "goal": "string",
949
+ "benefit": "string",
950
+ "user_value": "string",
951
+ "acceptance_criteria": ["string", "string"],
952
+ "coverage_tags": ["string"],
953
+ }
954
+ ]
955
+ },
956
+ }
957
+
958
+
959
+ def _call_story_agent(*, repo_root: Path, prompt: dict[str, Any], artifact_root: Path | None = None) -> list[dict[str, Any]]:
960
+ parsed = _call_json_agent(repo_root=repo_root, prompt=prompt, error_message="story generation LLM command failed", artifact_root=artifact_root)
961
+ stories = parsed.get("stories") if isinstance(parsed, dict) else None
962
+ if not isinstance(stories, list):
963
+ raise RuntimeError("LLM output missing stories[].")
964
+ actor_registry = prompt.get("actor_registry") if isinstance(prompt.get("actor_registry"), dict) else {}
965
+ registry_actors = list(actor_registry.get("canonical_actors") or actor_registry.get("actors") or [])
966
+ normalized: list[dict[str, Any]] = []
967
+ for story in stories:
968
+ if not isinstance(story, dict):
969
+ continue
970
+ actor = str(story.get("actor") or "").strip()
971
+ actor_id = str(story.get("actor_id") or "").strip() or None
972
+ if registry_actors:
973
+ primary_actor = _primary_actor_contract(actor_registry=actor_registry, actor_id=actor_id, actor_label=actor, field_name="story primary actor")
974
+ actor = primary_actor["label"]
975
+ actor_id = primary_actor["id"]
976
+ else:
977
+ validate_canonical_actor_label(actor)
978
+ primary_actor = {"id": actor_id or actor.lower().replace(" ", "_"), "label": actor, "kind": "human", "inherits_from": None}
979
+ normalized.append(
980
+ {
981
+ "title": str(story.get("title") or "").strip(),
982
+ "actor_id": actor_id,
983
+ "actor": actor,
984
+ "primary_actor": primary_actor,
985
+ "goal": str(story.get("goal") or "").strip(),
986
+ "benefit": str(story.get("benefit") or "").strip(),
987
+ "user_value": str(story.get("user_value") or "").strip(),
988
+ "acceptance_criteria": _normalize_list(story.get("acceptance_criteria")),
989
+ "coverage_tags": _normalize_list(story.get("coverage_tags")),
990
+ }
991
+ )
992
+ return normalized
993
+
994
+
995
+ def _render_story_markdown(*, index: int, story: dict[str, Any]) -> str:
996
+ lines = [
997
+ f"# User Story {index}",
998
+ "",
999
+ f"**Title:** {story['title']}",
1000
+ "",
1001
+ f"As a {story['actor']},",
1002
+ f"I want {story['goal'].rstrip('.')},",
1003
+ f"so that {story['benefit'].rstrip('.')}",
1004
+ "",
1005
+ "## User Value",
1006
+ story["user_value"],
1007
+ "",
1008
+ "## Acceptance Criteria",
1009
+ ]
1010
+ for idx, criterion in enumerate(_normalize_list(story.get("acceptance_criteria")), start=1):
1011
+ lines.append(f"{idx}. {criterion.rstrip('.') }.")
1012
+ lines.append("")
1013
+ return "\n".join(lines)
1014
+
1015
+
1016
+ def _clear_previous_story_files(root: Path) -> None:
1017
+ for candidate in root.glob("US-*.md"):
1018
+ candidate.unlink()
1019
+
1020
+
1021
+ def _write_json(path: Path, payload: dict[str, Any]) -> None:
1022
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
1023
+
1024
+
1025
+ def _report_history_entry(*, pass_index: int, evaluation: dict[str, Any], stories: list[dict[str, Any]], attempt_path: Path) -> dict[str, Any]:
1026
+ return {
1027
+ "pass_index": pass_index,
1028
+ "passed": bool(evaluation.get("passed")),
1029
+ "findings": list(evaluation.get("findings") or []),
1030
+ "coverage": dict(evaluation.get("coverage") or {}),
1031
+ "story_count": len(stories),
1032
+ "attempt_artifact": attempt_path.name,
1033
+ }
1034
+
1035
+
1036
+ def generate_traditional_user_story_set(*, repo_root: Path, idea_id: str, max_stories: int | None = None, story_set_id: str | None = None, coverage_requirements: dict[str, Any] | None = None, actor_registry: dict[str, Any] | None = None) -> TraditionalStorySet:
1037
+ del max_stories # agentic story generation currently decides cardinality from coverage requirements.
1038
+ payload, sufficient_idea = load_sufficient_idea(repo_root, idea_id=idea_id)
1039
+ actor_registry = actor_registry or load_actor_registry(repo_root)
1040
+ coverage_requirements = coverage_requirements or generate_story_coverage_requirements(
1041
+ repo_root=repo_root,
1042
+ idea_id=idea_id,
1043
+ payload=payload,
1044
+ sufficient_idea=sufficient_idea,
1045
+ actor_registry=actor_registry,
1046
+ )
1047
+ resolved_story_set_id = story_set_id or _stable_story_set_id(
1048
+ idea_id=idea_id,
1049
+ sufficient_idea=sufficient_idea,
1050
+ coverage_requirements=coverage_requirements,
1051
+ )
1052
+ root = _traditional_root(repo_root, idea_id=idea_id) / resolved_story_set_id
1053
+ manifest_path = root / "manifest.json"
1054
+ report_path = root / "sufficiency_report.json"
1055
+
1056
+ if manifest_path.exists():
1057
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
1058
+ story_paths = [repo_root / rel_path for rel_path in manifest.get("story_paths", [])]
1059
+ report = json.loads(report_path.read_text(encoding="utf-8")) if report_path.exists() else dict(manifest.get("sufficiency") or {})
1060
+ return TraditionalStorySet(story_set_id=resolved_story_set_id, root=root, story_paths=story_paths, sufficiency_report=report)
1061
+
1062
+ root.mkdir(parents=True, exist_ok=True)
1063
+ coverage_requirements_path = root / "coverage_requirements.json"
1064
+ _write_json(coverage_requirements_path, coverage_requirements)
1065
+
1066
+ resolved_actor_catalog = [
1067
+ {
1068
+ "id": str(item.get("id") or "").strip(),
1069
+ "label": str(item.get("label") or "").strip(),
1070
+ "kind": str(item.get("kind") or "human").strip() or "human",
1071
+ "inherits_from": str(item.get("inherits_from") or "").strip() or None,
1072
+ }
1073
+ for item in _resolved_actor_entries(actor_registry)
1074
+ ]
1075
+ history: list[dict[str, Any]] = []
1076
+ prior_stories: list[dict[str, Any]] = []
1077
+ current_deficiencies: list[dict[str, Any]] = []
1078
+ stories: list[dict[str, Any]] = []
1079
+ evaluation: dict[str, Any] = {"passed": False, "findings": ["generation did not run"], "coverage": {}}
1080
+
1081
+ for pass_index in range(1, MAX_COVERAGE_ATTEMPTS + 1):
1082
+ prompt = _story_prompt(
1083
+ idea_id=idea_id,
1084
+ payload=payload,
1085
+ sufficient_idea=sufficient_idea,
1086
+ coverage_requirements=coverage_requirements,
1087
+ actor_registry=actor_registry,
1088
+ pass_index=pass_index,
1089
+ prior_stories=prior_stories,
1090
+ current_findings=list(evaluation.get("findings") or []) if pass_index > 1 else [],
1091
+ current_deficiencies=current_deficiencies,
1092
+ )
1093
+ stories = _call_story_agent(repo_root=repo_root, prompt=prompt, artifact_root=root)
1094
+ evaluation = evaluate_traditional_story_sufficiency(
1095
+ repo_root=repo_root,
1096
+ stories=stories,
1097
+ sufficient_idea=sufficient_idea,
1098
+ coverage_requirements=coverage_requirements,
1099
+ pass_index=pass_index,
1100
+ )
1101
+ attempt_payload = {
1102
+ "pass_index": pass_index,
1103
+ "prompt": prompt,
1104
+ "stories": stories,
1105
+ "coverage_evaluation": evaluation,
1106
+ }
1107
+ attempt_path = root / f"attempt_{pass_index:03d}.json"
1108
+ _write_json(attempt_path, attempt_payload)
1109
+ history.append(_report_history_entry(pass_index=pass_index, evaluation=evaluation, stories=stories, attempt_path=attempt_path))
1110
+ current_deficiencies = list(evaluation.get("coverage", {}).get("deficiencies") or [])
1111
+ prior_stories = stories
1112
+ if evaluation.get("passed"):
1113
+ break
1114
+
1115
+ try:
1116
+ decomposed_stories, decomposition_report = run_traditional_story_decomposition_loop(
1117
+ repo_root=repo_root,
1118
+ root=root,
1119
+ stories=stories,
1120
+ sufficient_idea=sufficient_idea,
1121
+ coverage_requirements=coverage_requirements,
1122
+ )
1123
+ except Exception as exc:
1124
+ resume_cursor = _build_resume_cursor(story_set_id=resolved_story_set_id, iteration=1, phase='check')
1125
+ for iteration in range(1, MAX_DECOMPOSITION_ITERATIONS + 1):
1126
+ check_path = root / f"decomposition_check_{iteration:03d}.json"
1127
+ refine_path = root / f"decomposition_refine_{iteration:03d}.json"
1128
+ validate_path = root / f"decomposition_validate_{iteration:03d}.json"
1129
+ if check_path.exists() and not refine_path.exists():
1130
+ resume_cursor = _build_resume_cursor(story_set_id=resolved_story_set_id, iteration=iteration, phase='refine', artifact_path=check_path)
1131
+ break
1132
+ if refine_path.exists() and not validate_path.exists():
1133
+ resume_cursor = _build_resume_cursor(story_set_id=resolved_story_set_id, iteration=iteration, phase='validate', artifact_path=refine_path)
1134
+ break
1135
+ if validate_path.exists():
1136
+ resume_cursor = _build_resume_cursor(story_set_id=resolved_story_set_id, iteration=min(iteration + 1, MAX_DECOMPOSITION_ITERATIONS), phase='check', artifact_path=validate_path)
1137
+ raise TraditionalStoryGenerationError(root=root, story_set_id=resolved_story_set_id, resume_cursor=resume_cursor, cause=exc) from exc
1138
+ final_coverage_report = evaluate_traditional_story_sufficiency(
1139
+ repo_root=repo_root,
1140
+ stories=decomposed_stories,
1141
+ sufficient_idea=sufficient_idea,
1142
+ coverage_requirements=coverage_requirements,
1143
+ pass_index=MAX_COVERAGE_ATTEMPTS + MAX_DECOMPOSITION_ITERATIONS + 1,
1144
+ )
1145
+ final_coverage_path = root / "final_coverage_validation.json"
1146
+ decomposition_report_path = root / "decomposition_report.json"
1147
+ _write_json(final_coverage_path, final_coverage_report)
1148
+ _write_json(decomposition_report_path, decomposition_report)
1149
+
1150
+ passed = bool(evaluation.get("passed")) and bool(final_coverage_report.get("passed")) and bool(decomposition_report.get("passed"))
1151
+ report = {
1152
+ "story_set_id": resolved_story_set_id,
1153
+ "idea_id": idea_id,
1154
+ "created_at": _iso_now(),
1155
+ "passed": passed,
1156
+ "coverage_passed": bool(evaluation.get("passed")),
1157
+ "final_coverage_passed": bool(final_coverage_report.get("passed")),
1158
+ "decomposition_passed": bool(decomposition_report.get("passed")),
1159
+ "approved": True,
1160
+ "pass_count": len(history),
1161
+ "history": history,
1162
+ "coverage_requirements_path": coverage_requirements_path.name,
1163
+ "decomposition_report_path": decomposition_report_path.name,
1164
+ "final_coverage_validation_path": final_coverage_path.name,
1165
+ "final_findings": list(evaluation.get("findings") or []) + list(decomposition_report.get("findings") or []) + list(final_coverage_report.get("findings") or []),
1166
+ "final_coverage": dict(final_coverage_report.get("coverage") or {}),
1167
+ "coverage_requirements": coverage_requirements,
1168
+ "actor_registry": actor_registry,
1169
+ "resolved_actor_catalog": resolved_actor_catalog,
1170
+ "decomposition": decomposition_report,
1171
+ }
1172
+ _write_json(report_path, report)
1173
+
1174
+ _clear_previous_story_files(root)
1175
+ story_paths: list[Path] = []
1176
+ for index, story in enumerate(decomposed_stories, start=1):
1177
+ out_path = root / f"US-{index:03d}.md"
1178
+ out_path.write_text(_render_story_markdown(index=index, story=story), encoding="utf-8")
1179
+ story_paths.append(out_path)
1180
+
1181
+ manifest = {
1182
+ "story_set_id": resolved_story_set_id,
1183
+ "idea_id": idea_id,
1184
+ "kind": "traditional_user_stories",
1185
+ "created_at": _iso_now(),
1186
+ "source": "sufficient_idea",
1187
+ "canonical_story_contract": False,
1188
+ "traceability": {
1189
+ "idea_id": idea_id,
1190
+ "idea_artifact": str((get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "idea.json").relative_to(repo_root)),
1191
+ "handoff": "promote_or_dag_to_canonical_devflow_story",
1192
+ },
1193
+ "coverage_requirements_path": str(coverage_requirements_path.relative_to(repo_root)),
1194
+ "decomposition_report_path": str(decomposition_report_path.relative_to(repo_root)),
1195
+ "story_paths": [str(path.relative_to(repo_root)) for path in story_paths],
1196
+ "sufficiency": report,
1197
+ }
1198
+ _write_json(manifest_path, manifest)
1199
+
1200
+ report = {
1201
+ "story_set_id": resolved_story_set_id,
1202
+ "idea_id": idea_id,
1203
+ "created_at": _iso_now(),
1204
+ "passed": passed,
1205
+ "coverage_passed": bool(evaluation.get("passed")),
1206
+ "final_coverage_passed": bool(final_coverage_report.get("passed")),
1207
+ "decomposition_passed": bool(decomposition_report.get("passed")),
1208
+ "approved": True,
1209
+ "pass_count": len(history),
1210
+ "history": history,
1211
+ "coverage_requirements_path": coverage_requirements_path.name,
1212
+ "decomposition_report_path": decomposition_report_path.name,
1213
+ "final_coverage_validation_path": final_coverage_path.name,
1214
+ "final_findings": list(evaluation.get("findings") or []) + list(decomposition_report.get("findings") or []) + list(final_coverage_report.get("findings") or []),
1215
+ "final_coverage": dict(final_coverage_report.get("coverage") or {}),
1216
+ "coverage_requirements": coverage_requirements,
1217
+ "actor_registry": actor_registry,
1218
+ "resolved_actor_catalog": resolved_actor_catalog,
1219
+ "decomposition": decomposition_report,
1220
+ }
1221
+ _write_json(report_path, report)
1222
+
1223
+ _clear_previous_story_files(root)
1224
+ story_paths: list[Path] = []
1225
+ for index, story in enumerate(decomposed_stories, start=1):
1226
+ out_path = root / f"US-{index:03d}.md"
1227
+ out_path.write_text(_render_story_markdown(index=index, story=story), encoding="utf-8")
1228
+ story_paths.append(out_path)
1229
+
1230
+ manifest = {
1231
+ "story_set_id": resolved_story_set_id,
1232
+ "idea_id": idea_id,
1233
+ "kind": "traditional_user_stories",
1234
+ "created_at": _iso_now(),
1235
+ "source": "sufficient_idea",
1236
+ "canonical_story_contract": False,
1237
+ "traceability": {
1238
+ "idea_id": idea_id,
1239
+ "idea_artifact": str((get_idea_paths(repo_root, idea_id=idea_id).idea_dir / "idea.json").relative_to(repo_root)),
1240
+ "handoff": "promote_or_dag_to_canonical_devflow_story",
1241
+ },
1242
+ "coverage_requirements_path": str(coverage_requirements_path.relative_to(repo_root)),
1243
+ "decomposition_report_path": str(decomposition_report_path.relative_to(repo_root)),
1244
+ "story_paths": [str(path.relative_to(repo_root)) for path in story_paths],
1245
+ "sufficiency": report,
1246
+ }
1247
+ _write_json(manifest_path, manifest)
1248
+
1249
+ if not passed:
1250
+ raise TraditionalStoryInsufficiencyError(
1251
+ root=root,
1252
+ story_set_id=resolved_story_set_id,
1253
+ report_path=report_path,
1254
+ report=report,
1255
+ )
1256
+
1257
+ return TraditionalStorySet(story_set_id=resolved_story_set_id, root=root, story_paths=story_paths, sufficiency_report=report)