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,481 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from devflow_engine.llm.cli_one_shot import run_one_shot
10
+
11
+ from .paths import get_idea_paths
12
+
13
+
14
+ _REGISTRY_JSON_FILENAME = "users.json"
15
+ _REGISTRY_MD_FILENAME = "users.md"
16
+
17
+
18
+ def _load_llm_cli_config() -> tuple[str, str]:
19
+ base_cmd = os.environ.get("DEVFLOW_LLM_CLI_BASE", "claude --print")
20
+ delivery = os.environ.get("DEVFLOW_LLM_CLI_DELIVERY", "stdin").strip().lower()
21
+ if delivery not in {"stdin", "argument"}:
22
+ raise RuntimeError("DEVFLOW_LLM_CLI_DELIVERY must be stdin or argument")
23
+ return base_cmd, delivery
24
+
25
+
26
+ def _extract_json(text: str) -> str | None:
27
+ if "```" in text:
28
+ parts = text.split("```")
29
+ for i in range(len(parts) - 1):
30
+ body = parts[i + 1]
31
+ body_lines = body.splitlines()
32
+ if body_lines and body_lines[0].strip().lower() in {"json", "javascript"}:
33
+ body = "\n".join(body_lines[1:])
34
+ body = body.strip()
35
+ if body.startswith("{") and body.endswith("}"):
36
+ return body
37
+ start = text.find("{")
38
+ end = text.rfind("}")
39
+ if start != -1 and end != -1 and end > start:
40
+ return text[start:end+1]
41
+ return None
42
+
43
+
44
+ def _call_json_llm(*, repo_root: Path, prompt: dict[str, Any], error_message: str) -> dict[str, Any]:
45
+ base_cmd, delivery = _load_llm_cli_config()
46
+ result = run_one_shot(base_cmd=base_cmd, delivery=delivery, prompt=json.dumps(prompt, indent=2, sort_keys=True), cwd=repo_root, timeout_seconds=900)
47
+ if not result.ok:
48
+ raise RuntimeError(result.stderr or result.stdout or error_message)
49
+ raw_json = _extract_json(result.stdout)
50
+ if raw_json is None:
51
+ raise RuntimeError(f"Failed to locate JSON in actor normalization output for task={prompt.get('task')!r}.")
52
+ parsed = json.loads(raw_json)
53
+ if not isinstance(parsed, dict):
54
+ raise RuntimeError(f"Actor normalization output for task={prompt.get('task')!r} must be a JSON object.")
55
+ return parsed
56
+
57
+
58
+ def _llm_normalize_actor_entries(*, repo_root: Path, idea_actors: list[str], existing_entries: list[dict[str, Any]], sufficient_idea: dict[str, Any]) -> dict[str, Any]:
59
+ prompt = {
60
+ "task": "normalize_idea_actors",
61
+ "instructions": [
62
+ "Map each raw actor string from the idea into the existing canonical actor registry when possible.",
63
+ "Use semantic matching, aliases, descriptions, kind, and inheritance.",
64
+ "If a source actor phrase semantically refers to one or more existing canonical actors, return decision='matched' and canonical_actor_ids.",
65
+ "A single source actor phrase may legitimately map to multiple canonical actors when it explicitly combines roles like Regular Manager / Super Manager.",
66
+ "If no existing actor fits, return decision='proposed' with one or more singular canonical proposed actors.",
67
+ "Never return merged actor labels as canonical outputs. Split combined source phrases into multiple singular canonical actors when appropriate.",
68
+ "Return JSON only."
69
+ ],
70
+ "existing_actors": existing_entries,
71
+ "idea_actors": idea_actors,
72
+ "idea_summary": str(sufficient_idea.get("summary") or sufficient_idea.get("description") or ""),
73
+ "output_schema": {
74
+ "mappings": [
75
+ {
76
+ "source_actor_text": "string",
77
+ "decision": "matched|proposed",
78
+ "canonical_actor_ids": ["string"],
79
+ "canonical_actor_labels": ["string"],
80
+ "proposed_actors": [{"proposed_id": "string", "proposed_label": "string", "kind": "human|system|consumer|null", "inherits_from": "string|null"}],
81
+ "rationale": "string"
82
+ }
83
+ ]
84
+ }
85
+ }
86
+ return _call_json_llm(repo_root=repo_root, prompt=prompt, error_message="actor normalization LLM command failed")
87
+
88
+
89
+ def actor_registry_json_path(repo_root: Path) -> Path:
90
+ return repo_root / _REGISTRY_JSON_FILENAME
91
+
92
+
93
+ def actor_registry_markdown_path(repo_root: Path) -> Path:
94
+ return repo_root / _REGISTRY_MD_FILENAME
95
+
96
+
97
+ def actor_registry_path(repo_root: Path) -> Path:
98
+ json_path = actor_registry_json_path(repo_root)
99
+ if json_path.exists():
100
+ return json_path
101
+ return actor_registry_markdown_path(repo_root)
102
+
103
+
104
+ def normalize_actor_label(value: str) -> str:
105
+ lowered = value.strip().lower()
106
+ lowered = re.sub(r"[^a-z0-9]+", " ", lowered)
107
+ lowered = re.sub(r"\s+", " ", lowered).strip()
108
+ return lowered
109
+
110
+
111
+ def validate_canonical_actor_label(label: str) -> None:
112
+ text = str(label).strip()
113
+ if not text:
114
+ raise RuntimeError("Canonical actor labels must not be empty.")
115
+ if "/" in text:
116
+ raise RuntimeError(
117
+ f"Canonical actor label {text!r} is invalid: use one actor class only and never slash-separated aliases."
118
+ )
119
+ lowered = text.lower()
120
+ if " and/or " in lowered:
121
+ raise RuntimeError(
122
+ f"Canonical actor label {text!r} is invalid: do not use merged actor aliases like and/or."
123
+ )
124
+ if re.search(r"\bor\b", lowered):
125
+ raise RuntimeError(
126
+ f"Canonical actor label {text!r} is invalid: use exactly one primary actor and do not merge actors with 'or'."
127
+ )
128
+
129
+
130
+ def _coerce_actor_list(value: object) -> list[str]:
131
+ if isinstance(value, str):
132
+ text = value.strip()
133
+ return [text] if text else []
134
+ if isinstance(value, list):
135
+ out: list[str] = []
136
+ for item in value:
137
+ text = str(item).strip()
138
+ if text:
139
+ out.append(text)
140
+ return out
141
+ return []
142
+
143
+
144
+ def _contains_any(text: str, terms: set[str]) -> bool:
145
+ return any(term in text for term in terms)
146
+
147
+
148
+ def _slash_variants(label: str) -> list[str]:
149
+ if "/" not in label:
150
+ return []
151
+ parts = [part.strip(" -\t") for part in re.split(r"\s*/\s*", label) if part.strip(" -\t")]
152
+ return parts
153
+
154
+
155
+ def _actor_entry(*, label: str, description: str = "", aliases: list[str] | None = None, actor_id: str | None = None, kind: str | None = None, inherits_from: str | None = None) -> dict[str, Any]:
156
+ clean_label = str(label).strip()
157
+ validate_canonical_actor_label(clean_label)
158
+ clean_description = str(description).strip()
159
+ norm_label = normalize_actor_label(clean_label)
160
+ dedup_aliases: list[str] = []
161
+ seen: set[str] = set()
162
+ for alias in [clean_label, *(aliases or [])]:
163
+ alias_text = str(alias).strip()
164
+ alias_norm = normalize_actor_label(alias_text)
165
+ if not alias_text or not alias_norm or alias_norm in seen:
166
+ continue
167
+ seen.add(alias_norm)
168
+ dedup_aliases.append(alias_text)
169
+ actor_kind = str(kind or "human").strip().lower() or "human"
170
+ if actor_kind not in {"human", "system", "consumer"}:
171
+ raise RuntimeError(f"Actor kind {actor_kind!r} is invalid. Allowed kinds: human, system, consumer.")
172
+ inherited_actor = str(inherits_from or "").strip() or None
173
+ return {
174
+ "id": str(actor_id or norm_label.replace(" ", "_")).strip() or norm_label.replace(" ", "_"),
175
+ "label": clean_label,
176
+ "description": clean_description,
177
+ "kind": actor_kind,
178
+ "inherits_from": inherited_actor,
179
+ "aliases": dedup_aliases,
180
+ }
181
+
182
+
183
+ def _markdown_value_to_actor_entry(value: str) -> dict[str, Any]:
184
+ text = value.strip()
185
+ if not text:
186
+ raise RuntimeError("Actor registry entry must not be empty.")
187
+ match = re.match(r"^(.*?)\s+[—-]\s+(.*)$", text)
188
+ if match:
189
+ return _actor_entry(label=match.group(1).strip(), description=match.group(2).strip())
190
+ if ":" in text:
191
+ label, description = text.split(":", 1)
192
+ return _actor_entry(label=label.strip(), description=description.strip())
193
+ return _actor_entry(label=text)
194
+
195
+
196
+ def _repair_actor_label(label: str, *, existing_actors: list[str]) -> tuple[str, str | None]:
197
+ text = str(label).strip()
198
+ try:
199
+ validate_canonical_actor_label(text)
200
+ return text, None
201
+ except RuntimeError:
202
+ pass
203
+
204
+ existing_labels = [actor.strip() for actor in existing_actors if actor.strip()]
205
+ existing_norms = {normalize_actor_label(actor): actor for actor in existing_labels}
206
+ norm = normalize_actor_label(text)
207
+ manager_labels = [actor for actor in existing_labels if "manager" in normalize_actor_label(actor)]
208
+ if manager_labels and "manager" in norm:
209
+ super_like = {"super", "org wide", "org-wide", "all groups", "all group", "global"}
210
+ if _contains_any(norm, super_like):
211
+ for actor in manager_labels:
212
+ if "super manager" in normalize_actor_label(actor):
213
+ return actor, f"repaired {text!r} to existing canonical manager role {actor!r}"
214
+ for actor in manager_labels:
215
+ if "regular manager" in normalize_actor_label(actor):
216
+ return actor, f"repaired {text!r} to existing canonical manager role {actor!r}"
217
+ if len(manager_labels) == 1:
218
+ return manager_labels[0], f"repaired {text!r} to existing canonical manager role {manager_labels[0]!r}"
219
+
220
+ variants = _slash_variants(text)
221
+ for variant in sorted(variants, key=lambda item: len(normalize_actor_label(item).split()), reverse=True):
222
+ variant_norm = normalize_actor_label(variant)
223
+ if variant_norm in existing_norms:
224
+ repaired = existing_norms[variant_norm]
225
+ return repaired, f"repaired {text!r} to existing canonical actor {repaired!r}"
226
+
227
+ generic_terms = {"admin", "administrator", "user", "operator", "team", "org", "organization", "level"}
228
+ if variants:
229
+ ranked = sorted(
230
+ variants,
231
+ key=lambda item: (
232
+ -len([tok for tok in normalize_actor_label(item).split() if tok not in generic_terms]),
233
+ -len(normalize_actor_label(item).split()),
234
+ ),
235
+ )
236
+ repaired = ranked[0].strip()
237
+ validate_canonical_actor_label(repaired)
238
+ return repaired, f"repaired slash-merged actor {text!r} to canonical actor {repaired!r}"
239
+
240
+ raise RuntimeError(
241
+ f"Canonical actor label {text!r} is invalid and could not be repaired to an existing or canonical actor label."
242
+ )
243
+
244
+
245
+ def _load_json_registry(path: Path) -> dict[str, Any]:
246
+ payload = json.loads(path.read_text(encoding="utf-8"))
247
+ raw_actors = payload.get("actors") if isinstance(payload, dict) else None
248
+ raw_notes = payload.get("notes") if isinstance(payload, dict) else None
249
+ actors: list[dict[str, Any]] = []
250
+ for item in raw_actors or []:
251
+ if not isinstance(item, dict):
252
+ continue
253
+ actors.append(
254
+ _actor_entry(
255
+ label=str(item.get("label") or "").strip(),
256
+ description=str(item.get("description") or "").strip(),
257
+ aliases=[str(alias).strip() for alias in (item.get("aliases") or []) if str(alias).strip()],
258
+ actor_id=str(item.get("id") or "").strip() or None,
259
+ kind=str(item.get("kind") or "").strip() or None,
260
+ inherits_from=str(item.get("inherits_from") or "").strip() or None,
261
+ )
262
+ )
263
+ notes = [str(item).strip() for item in (raw_notes or []) if str(item).strip()]
264
+ return {"path": str(path), "actors": actors, "notes": notes}
265
+
266
+
267
+ def _load_markdown_registry(path: Path) -> dict[str, Any]:
268
+ actors: list[dict[str, Any]] = []
269
+ notes: list[str] = []
270
+ if path.exists():
271
+ section: str | None = None
272
+ for raw_line in path.read_text(encoding="utf-8").splitlines():
273
+ line = raw_line.strip()
274
+ lowered = line.lower()
275
+ if lowered.startswith("## "):
276
+ if "actor" in lowered:
277
+ section = "actors"
278
+ elif "note" in lowered:
279
+ section = "notes"
280
+ else:
281
+ section = None
282
+ continue
283
+ if not line.startswith("- "):
284
+ continue
285
+ value = line[2:].strip()
286
+ if not value:
287
+ continue
288
+ if section == "actors":
289
+ actors.append(_markdown_value_to_actor_entry(value))
290
+ elif section == "notes":
291
+ notes.append(value)
292
+ return {"path": str(path), "actors": actors, "notes": notes}
293
+
294
+
295
+ def load_actor_registry(repo_root: Path) -> dict[str, Any]:
296
+ json_path = actor_registry_json_path(repo_root)
297
+ if json_path.exists():
298
+ return _load_json_registry(json_path)
299
+ return _load_markdown_registry(actor_registry_markdown_path(repo_root))
300
+
301
+
302
+ def write_actor_registry(repo_root: Path, *, actors: list[str] | list[dict[str, Any]], notes: list[str] | None = None) -> Path:
303
+ path = actor_registry_json_path(repo_root)
304
+ unique: list[dict[str, Any]] = []
305
+ seen: set[str] = set()
306
+ for actor in actors:
307
+ if isinstance(actor, dict):
308
+ entry = _actor_entry(
309
+ label=str(actor.get("label") or "").strip(),
310
+ description=str(actor.get("description") or "").strip(),
311
+ aliases=[str(alias).strip() for alias in (actor.get("aliases") or []) if str(alias).strip()],
312
+ actor_id=str(actor.get("id") or "").strip() or None,
313
+ kind=str(actor.get("kind") or "").strip() or None,
314
+ inherits_from=str(actor.get("inherits_from") or "").strip() or None,
315
+ )
316
+ else:
317
+ entry = _actor_entry(label=str(actor).strip())
318
+ norm = normalize_actor_label(str(entry["label"]))
319
+ if not norm or norm in seen:
320
+ continue
321
+ seen.add(norm)
322
+ unique.append(entry)
323
+ payload = {
324
+ "version": 1,
325
+ "actors": unique,
326
+ "notes": [str(item).strip() for item in (notes or []) if str(item).strip()],
327
+ }
328
+ path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
329
+ return path
330
+
331
+
332
+ def normalize_idea_actors(*, repo_root: Path, idea_id: str, sufficient_idea: dict[str, Any], existing_registry: dict[str, Any] | None = None) -> dict[str, Any]:
333
+ del idea_id
334
+ registry = existing_registry or load_actor_registry(repo_root)
335
+ actor_entries = [item for item in (registry.get("actors") or []) if isinstance(item, dict)]
336
+ existing_entries_by_id = {str(item.get("id") or "").strip(): dict(item) for item in actor_entries if str(item.get("id") or "").strip()}
337
+ idea_actors = _coerce_actor_list(sufficient_idea.get("target_users") or sufficient_idea.get("users"))
338
+ if not idea_actors:
339
+ return {
340
+ "canonical_actors": [str(item.get("label") or "").strip() for item in actor_entries if str(item.get("label") or "").strip()],
341
+ "resolved_actors": actor_entries,
342
+ "actors": actor_entries,
343
+ "additions": [],
344
+ "repairs": [],
345
+ "notes": list(registry.get("notes") or []),
346
+ "path": str(actor_registry_path(repo_root).name),
347
+ }
348
+
349
+ llm_result = _llm_normalize_actor_entries(repo_root=repo_root, idea_actors=idea_actors, existing_entries=actor_entries, sufficient_idea=sufficient_idea)
350
+ mappings = [item for item in (llm_result.get("mappings") or []) if isinstance(item, dict)]
351
+ resolved_entries = list(actor_entries)
352
+ additions: list[str] = []
353
+ repairs: list[dict[str, str]] = []
354
+ seen_ids = {str(item.get("id") or "").strip() for item in resolved_entries if str(item.get("id") or "").strip()}
355
+
356
+ for mapping in mappings:
357
+ source_text = str(mapping.get("source_actor_text") or "").strip()
358
+ decision = str(mapping.get("decision") or "").strip().lower()
359
+ if decision == "matched":
360
+ actor_ids = [str(item).strip() for item in (mapping.get("canonical_actor_ids") or []) if str(item).strip()]
361
+ if not actor_ids:
362
+ raise RuntimeError(f"Actor normalization returned no canonical_actor_ids for matched source actor {source_text!r}")
363
+ matched_labels: list[str] = []
364
+ for actor_id in actor_ids:
365
+ if actor_id not in existing_entries_by_id:
366
+ raise RuntimeError(f"Actor normalization returned unknown canonical_actor_id {actor_id!r} for source actor {source_text!r}")
367
+ matched = existing_entries_by_id[actor_id]
368
+ matched_labels.append(str(matched.get("label") or "").strip())
369
+ repairs.append({"original": source_text, "repaired": ", ".join(matched_labels), "reason": str(mapping.get("rationale") or "semantic match").strip()})
370
+ continue
371
+ if decision == "proposed":
372
+ proposed_actors = [item for item in (mapping.get("proposed_actors") or []) if isinstance(item, dict)]
373
+ if not proposed_actors:
374
+ raise RuntimeError(f"Actor normalization returned no proposed_actors for source actor {source_text!r}")
375
+ proposed_labels: list[str] = []
376
+ for proposed in proposed_actors:
377
+ proposed_label = str(proposed.get("proposed_label") or "").strip()
378
+ proposed_id = str(proposed.get("proposed_id") or "").strip() or normalize_actor_label(proposed_label).replace(" ", "_")
379
+ entry = _actor_entry(
380
+ label=proposed_label,
381
+ actor_id=proposed_id,
382
+ kind=str(proposed.get("kind") or "human").strip() or None,
383
+ inherits_from=str(proposed.get("inherits_from") or "").strip() or None,
384
+ )
385
+ proposed_labels.append(entry["label"])
386
+ if entry["id"] not in seen_ids:
387
+ resolved_entries.append(entry)
388
+ seen_ids.add(entry["id"])
389
+ additions.append(entry["label"])
390
+ repairs.append({"original": source_text, "repaired": ", ".join(proposed_labels), "reason": str(mapping.get("rationale") or "proposed canonical actor").strip()})
391
+ continue
392
+ raise RuntimeError(f"Actor normalization returned invalid decision {decision!r} for source actor {source_text!r}")
393
+
394
+ return {
395
+ "canonical_actors": [str(item.get("label") or "").strip() for item in resolved_entries if str(item.get("label") or "").strip()],
396
+ "resolved_actors": resolved_entries,
397
+ "actors": resolved_entries,
398
+ "additions": additions,
399
+ "repairs": repairs,
400
+ "notes": list(registry.get("notes") or []),
401
+ "path": str(actor_registry_path(repo_root).name),
402
+ }
403
+
404
+ def persist_actor_registry_artifact(*, repo_root: Path, idea_id: str, payload: dict[str, Any]) -> Path:
405
+ idea_dir = get_idea_paths(repo_root, idea_id=idea_id).idea_dir
406
+ idea_dir.mkdir(parents=True, exist_ok=True)
407
+ out = idea_dir / "actor_registry.json"
408
+ out.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
409
+ return out
410
+
411
+
412
+ def resolve_actor_entry(*, actor: str, actor_registry: dict[str, Any]) -> dict[str, Any] | None:
413
+ norm = normalize_actor_label(actor)
414
+ if not norm:
415
+ return None
416
+ raw_entries = actor_registry.get("resolved_actors") or actor_registry.get("actors") or actor_registry.get("canonical_actors") or []
417
+ entries: list[dict[str, Any]] = []
418
+ alias_to_entry: dict[str, dict[str, Any]] = {}
419
+ for item in raw_entries:
420
+ if isinstance(item, dict):
421
+ entry = dict(item)
422
+ label = str(entry.get("label") or "").strip()
423
+ aliases = [str(alias).strip() for alias in (entry.get("aliases") or []) if str(alias).strip()]
424
+ else:
425
+ label = str(item).strip()
426
+ aliases = []
427
+ entry = {"id": normalize_actor_label(label).replace(" ", "_"), "label": label, "description": "", "aliases": [label]}
428
+ if not label:
429
+ continue
430
+ entries.append(entry)
431
+ for alias in [label, *aliases]:
432
+ alias_norm = normalize_actor_label(alias)
433
+ if alias_norm and alias_norm not in alias_to_entry:
434
+ alias_to_entry[alias_norm] = entry
435
+ if norm in alias_to_entry:
436
+ return dict(alias_to_entry[norm])
437
+ actor_tokens = set(norm.split())
438
+ fuzzy: list[dict[str, Any]] = []
439
+ for entry in entries:
440
+ label_norm = normalize_actor_label(str(entry.get("label") or ""))
441
+ if not label_norm:
442
+ continue
443
+ label_tokens = set(label_norm.split())
444
+ if norm in label_norm or label_norm in norm or actor_tokens.issubset(label_tokens) or label_tokens.issubset(actor_tokens):
445
+ fuzzy.append(entry)
446
+ if len(fuzzy) == 1:
447
+ return dict(fuzzy[0])
448
+ return None
449
+
450
+
451
+ def upsert_runtime_actor_entry(*, actor: str, actor_registry: dict[str, Any]) -> tuple[dict[str, Any], dict[str, Any]]:
452
+ resolved = resolve_actor_entry(actor=actor, actor_registry=actor_registry)
453
+ if resolved is not None:
454
+ return actor_registry, resolved
455
+ label = str(actor).strip()
456
+ validate_canonical_actor_label(label)
457
+ raw_entries = list(actor_registry.get("resolved_actors") or actor_registry.get("actors") or [])
458
+ new_entry = _actor_entry(label=label)
459
+ raw_entries.append(new_entry)
460
+ deduped: list[dict[str, Any]] = []
461
+ seen: set[str] = set()
462
+ for item in raw_entries:
463
+ if not isinstance(item, dict):
464
+ continue
465
+ norm = normalize_actor_label(str(item.get("label") or ""))
466
+ if not norm or norm in seen:
467
+ continue
468
+ seen.add(norm)
469
+ deduped.append(dict(item))
470
+ updated = {
471
+ **actor_registry,
472
+ "resolved_actors": deduped,
473
+ "actors": deduped,
474
+ "canonical_actors": [str(item.get("label") or "").strip() for item in deduped if str(item.get("label") or "").strip()],
475
+ }
476
+ return updated, new_entry
477
+
478
+
479
+ def canonicalize_story_actor(*, actor: str, actor_registry: dict[str, Any]) -> str | None:
480
+ entry = resolve_actor_entry(actor=actor, actor_registry=actor_registry)
481
+ return None if entry is None else str(entry.get("label") or "").strip() or None