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,1277 @@
1
+ """Composable repo tool surfaces for ideation and insight agent arms."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import re
8
+ import shutil
9
+ import subprocess
10
+ from contextlib import contextmanager
11
+ from pathlib import Path
12
+ from typing import Any, Literal
13
+
14
+ from ..llm.repo_knowledge import load_repo_knowledge_index, write_repo_knowledge_index
15
+ from ..stores.execution_store import ExecutionStore
16
+ from .paths import get_idea_paths
17
+
18
+
19
+ # ---------------------------------------------------------------------------
20
+ # Constants
21
+ # ---------------------------------------------------------------------------
22
+
23
+ _REPOS_ROOT = Path(os.environ.get("DEVFLOW_REPOS_ROOT", "")) or Path.home() / "repos"
24
+
25
+ _KEY_FILE_PATTERNS = {
26
+ "package.json",
27
+ "pyproject.toml",
28
+ "setup.py",
29
+ "setup.cfg",
30
+ "Cargo.toml",
31
+ "go.mod",
32
+ "Gemfile",
33
+ "Dockerfile",
34
+ "docker-compose.yml",
35
+ "docker-compose.yaml",
36
+ "Makefile",
37
+ "CMakeLists.txt",
38
+ ".env.example",
39
+ ".env.sample",
40
+ "requirements.txt",
41
+ "pom.xml",
42
+ "build.gradle",
43
+ "tsconfig.json",
44
+ "vite.config.ts",
45
+ "next.config.js",
46
+ "next.config.mjs",
47
+ "tailwind.config.js",
48
+ "tailwind.config.ts",
49
+ "webpack.config.js",
50
+ "angular.json",
51
+ "nuxt.config.ts",
52
+ "svelte.config.js",
53
+ "remix.config.js",
54
+ "astro.config.mjs",
55
+ }
56
+
57
+ _STACK_INDICATORS: dict[str, dict[str, str]] = {
58
+ "package.json": {"stack": "node", "framework": "detect_from_deps"},
59
+ "pyproject.toml": {"stack": "python", "framework": "detect_from_deps"},
60
+ "setup.py": {"stack": "python", "framework": "unknown"},
61
+ "Cargo.toml": {"stack": "rust", "framework": "unknown"},
62
+ "go.mod": {"stack": "go", "framework": "unknown"},
63
+ "Gemfile": {"stack": "ruby", "framework": "detect_from_deps"},
64
+ "pom.xml": {"stack": "java", "framework": "maven"},
65
+ "build.gradle": {"stack": "java", "framework": "gradle"},
66
+ }
67
+
68
+ _FRONTEND_FRAMEWORK_MARKERS = {
69
+ "next.config.js": "nextjs",
70
+ "next.config.mjs": "nextjs",
71
+ "nuxt.config.ts": "nuxt",
72
+ "svelte.config.js": "svelte",
73
+ "remix.config.js": "remix",
74
+ "astro.config.mjs": "astro",
75
+ "angular.json": "angular",
76
+ "vite.config.ts": "vite",
77
+ }
78
+
79
+ _ACTIVE_REPO_ROOT: Path | None = None
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # Helpers
84
+ # ---------------------------------------------------------------------------
85
+
86
+
87
+ def _repos_root() -> Path:
88
+ """Return the repos root, respecting DEVFLOW_REPOS_ROOT env override."""
89
+ env = os.environ.get("DEVFLOW_REPOS_ROOT", "").strip()
90
+ if env:
91
+ return Path(env)
92
+ return Path.home() / "repos"
93
+
94
+
95
+ @contextmanager
96
+ def activate_repo_tools(repo_root: Path):
97
+ """Bind the ideation tool surface to the active workspace repo."""
98
+ global _ACTIVE_REPO_ROOT
99
+ previous = _ACTIVE_REPO_ROOT
100
+ _ACTIVE_REPO_ROOT = repo_root.resolve()
101
+ try:
102
+ yield
103
+ finally:
104
+ _ACTIVE_REPO_ROOT = previous
105
+
106
+
107
+ def _require_active_repo_root() -> Path:
108
+ if _ACTIVE_REPO_ROOT is None:
109
+ raise RuntimeError("Ideation repo tools are not active outside an ideation agent run.")
110
+ return _ACTIVE_REPO_ROOT
111
+
112
+
113
+ def _resolve_within_repo(repo_root: Path, path: str | None = None) -> Path:
114
+ target = repo_root if not path or path == "." else repo_root / path
115
+ try:
116
+ resolved = target.resolve()
117
+ resolved.relative_to(repo_root.resolve())
118
+ except ValueError as exc:
119
+ raise ValueError(f"path must stay within the active repo: {path!r}") from exc
120
+ return resolved
121
+
122
+
123
+ def _directory_entries(target: Path, *, max_entries: int = 200) -> list[dict[str, str]]:
124
+ entries: list[dict[str, str]] = []
125
+ try:
126
+ for item in sorted(target.iterdir()):
127
+ if item.name == ".git":
128
+ continue
129
+ entries.append({
130
+ "name": item.name,
131
+ "type": "dir" if item.is_dir() else "file",
132
+ })
133
+ if len(entries) >= max_entries:
134
+ break
135
+ except OSError:
136
+ pass
137
+ return entries
138
+
139
+
140
+ def _parse_repo_ref(repo_ref: str) -> tuple[str, str]:
141
+ """Extract (owner, repo_name) from a GitHub URL or owner/repo string."""
142
+ # https://github.com/owner/repo or git@github.com:owner/repo.git
143
+ m = re.match(r"(?:https?://github\.com/|git@github\.com:)([^/]+)/([^/.]+?)(?:\.git)?/?$", repo_ref)
144
+ if m:
145
+ return m.group(1), m.group(2)
146
+ # owner/repo
147
+ m = re.match(r"^([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)$", repo_ref)
148
+ if m:
149
+ return m.group(1), m.group(2)
150
+ raise ValueError(f"Cannot parse repo_ref: {repo_ref!r}. Expected GitHub URL or owner/repo.")
151
+
152
+
153
+ def _local_candidates(owner: str, repo_name: str, suggested_local_name: str | None = None) -> list[Path]:
154
+ """Return candidate local paths to check for an existing clone, in priority order."""
155
+ root = _repos_root()
156
+ candidates = []
157
+ if suggested_local_name:
158
+ candidates.append(root / suggested_local_name)
159
+ candidates.append(root / repo_name)
160
+ candidates.append(root / f"{owner}_{repo_name}")
161
+ candidates.append(root / f"{owner}-{repo_name}")
162
+ return candidates
163
+
164
+
165
+ def _top_level_inventory(local_path: Path, max_entries: int = 200) -> list[dict[str, str]]:
166
+ """Return top-level files/dirs with type annotation."""
167
+ entries: list[dict[str, str]] = []
168
+ try:
169
+ for item in sorted(local_path.iterdir()):
170
+ if item.name.startswith(".") and item.name in {".git"}:
171
+ continue
172
+ entries.append({
173
+ "name": item.name,
174
+ "type": "dir" if item.is_dir() else "file",
175
+ })
176
+ if len(entries) >= max_entries:
177
+ break
178
+ except OSError:
179
+ pass
180
+ return entries
181
+
182
+
183
+ def _find_key_files(local_path: Path) -> list[str]:
184
+ """Return key candidate files found at repo root."""
185
+ found = []
186
+ for name in sorted(_KEY_FILE_PATTERNS):
187
+ if (local_path / name).exists():
188
+ found.append(name)
189
+ return found
190
+
191
+
192
+ # ---------------------------------------------------------------------------
193
+ # Tool 1: acquire_and_inventory_repo
194
+ # ---------------------------------------------------------------------------
195
+
196
+
197
+ def acquire_and_inventory_repo(
198
+ *,
199
+ repo_ref: str,
200
+ suggested_local_name: str | None = None,
201
+ mode: Literal["frontend", "backend", "generic"] | None = None,
202
+ ) -> dict[str, Any]:
203
+ """Acquire a GitHub repo (reuse local clone or clone via gh) and return inventory.
204
+
205
+ Returns structured dict with local_path, repo_ref, cloned, inventory, key_files.
206
+ Raises on clone failure — no fake success.
207
+ """
208
+ owner, repo_name = _parse_repo_ref(repo_ref)
209
+
210
+ # Check for existing local clone
211
+ for candidate in _local_candidates(owner, repo_name, suggested_local_name):
212
+ if candidate.is_dir() and (candidate / ".git").is_dir():
213
+ return {
214
+ "local_path": str(candidate),
215
+ "repo_ref": f"{owner}/{repo_name}",
216
+ "cloned": False,
217
+ "inventory": _top_level_inventory(candidate),
218
+ "key_files": _find_key_files(candidate),
219
+ }
220
+
221
+ # Clone via gh
222
+ if not shutil.which("gh"):
223
+ raise RuntimeError("GitHub CLI (gh) is not installed or not on PATH. Cannot clone repo.")
224
+
225
+ root = _repos_root()
226
+ root.mkdir(parents=True, exist_ok=True)
227
+ target_name = suggested_local_name or repo_name
228
+ target_path = root / target_name
229
+
230
+ result = subprocess.run(
231
+ ["gh", "repo", "clone", f"{owner}/{repo_name}", str(target_path)],
232
+ capture_output=True,
233
+ text=True,
234
+ timeout=300,
235
+ )
236
+ if result.returncode != 0:
237
+ raise RuntimeError(
238
+ f"gh repo clone failed (rc={result.returncode}):\n"
239
+ f"stdout: {result.stdout.strip()}\n"
240
+ f"stderr: {result.stderr.strip()}"
241
+ )
242
+
243
+ return {
244
+ "local_path": str(target_path),
245
+ "repo_ref": f"{owner}/{repo_name}",
246
+ "cloned": True,
247
+ "inventory": _top_level_inventory(target_path),
248
+ "key_files": _find_key_files(target_path),
249
+ }
250
+
251
+
252
+ # ---------------------------------------------------------------------------
253
+ # Tool 2: inspect_repo_surface
254
+ # ---------------------------------------------------------------------------
255
+
256
+
257
+ def _detect_stack_and_frameworks(local_path: Path) -> tuple[list[str], list[str]]:
258
+ """Detect stacks and frameworks from key files."""
259
+ stacks: list[str] = []
260
+ frameworks: list[str] = []
261
+
262
+ for filename, info in _STACK_INDICATORS.items():
263
+ if (local_path / filename).exists():
264
+ if info["stack"] not in stacks:
265
+ stacks.append(info["stack"])
266
+
267
+ for filename, fw in _FRONTEND_FRAMEWORK_MARKERS.items():
268
+ if (local_path / filename).exists():
269
+ if fw not in frameworks:
270
+ frameworks.append(fw)
271
+
272
+ # Detect from package.json deps
273
+ pkg_json = local_path / "package.json"
274
+ if pkg_json.exists():
275
+ try:
276
+ import json
277
+ pkg = json.loads(pkg_json.read_text(encoding="utf-8"))
278
+ all_deps = {**pkg.get("dependencies", {}), **pkg.get("devDependencies", {})}
279
+ for dep, fw in [
280
+ ("react", "react"), ("vue", "vue"), ("svelte", "svelte"),
281
+ ("@angular/core", "angular"), ("express", "express"),
282
+ ("fastify", "fastify"), ("next", "nextjs"), ("nuxt", "nuxt"),
283
+ ("remix", "remix"), ("django", "django"), ("flask", "flask"),
284
+ ("tailwindcss", "tailwind"),
285
+ ]:
286
+ if dep in all_deps and fw not in frameworks:
287
+ frameworks.append(fw)
288
+ except (OSError, ValueError):
289
+ pass
290
+
291
+ # Detect from pyproject.toml deps
292
+ pyproj = local_path / "pyproject.toml"
293
+ if pyproj.exists():
294
+ try:
295
+ content = pyproj.read_text(encoding="utf-8")
296
+ for marker, fw in [
297
+ ("django", "django"), ("flask", "flask"), ("fastapi", "fastapi"),
298
+ ("starlette", "starlette"), ("streamlit", "streamlit"),
299
+ ]:
300
+ if marker in content.lower() and fw not in frameworks:
301
+ frameworks.append(fw)
302
+ except OSError:
303
+ pass
304
+
305
+ return stacks, frameworks
306
+
307
+
308
+ def _detect_patterns(local_path: Path) -> dict[str, Any]:
309
+ """Detect auth, API, env, and deployment patterns."""
310
+ auth_pattern: str | None = None
311
+ api_pattern: str | None = None
312
+ env_files: list[str] = []
313
+ deployment_files: list[str] = []
314
+
315
+ # Auth detection
316
+ auth_dirs = ["auth", "authentication", "lib/auth", "src/auth", "app/auth", "middleware/auth"]
317
+ for d in auth_dirs:
318
+ if (local_path / d).exists():
319
+ auth_pattern = d
320
+ break
321
+
322
+ # API pattern detection
323
+ api_dirs = ["api", "routes", "src/api", "app/api", "src/routes", "endpoints", "controllers"]
324
+ for d in api_dirs:
325
+ if (local_path / d).exists():
326
+ api_pattern = d
327
+ break
328
+
329
+ # Env files
330
+ for name in sorted(local_path.iterdir()) if local_path.is_dir() else []:
331
+ if name.name.startswith(".env") and name.is_file():
332
+ env_files.append(name.name)
333
+
334
+ # Deployment files
335
+ deploy_patterns = [
336
+ "Dockerfile", "docker-compose.yml", "docker-compose.yaml",
337
+ "fly.toml", "vercel.json", "netlify.toml", "railway.json",
338
+ "render.yaml", "Procfile", "app.yaml", "serverless.yml",
339
+ "terraform", "k8s", "kubernetes", ".github/workflows",
340
+ "Jenkinsfile", ".circleci",
341
+ ]
342
+ for pat in deploy_patterns:
343
+ p = local_path / pat
344
+ if p.exists():
345
+ deployment_files.append(pat)
346
+
347
+ return {
348
+ "auth_pattern": auth_pattern,
349
+ "api_pattern": api_pattern,
350
+ "env_files": env_files,
351
+ "deployment_files": deployment_files,
352
+ }
353
+
354
+
355
+ def inspect_repo_surface(
356
+ *,
357
+ local_path: str,
358
+ mode: Literal["frontend", "backend", "generic"] = "generic",
359
+ ) -> dict[str, Any]:
360
+ """Inspect a local repo for stack, framework, auth, API, env, and deployment clues.
361
+
362
+ Returns structured JSON summary.
363
+ """
364
+ repo = Path(local_path)
365
+ if not repo.is_dir():
366
+ raise FileNotFoundError(f"Repo path does not exist: {local_path}")
367
+
368
+ stacks, frameworks = _detect_stack_and_frameworks(repo)
369
+ patterns = _detect_patterns(repo)
370
+ key_files = _find_key_files(repo)
371
+
372
+ notes: list[str] = []
373
+ if mode == "frontend" and not any(f in frameworks for f in ["react", "vue", "svelte", "angular", "nextjs", "nuxt"]):
374
+ notes.append("No recognized frontend framework detected despite frontend mode.")
375
+ if mode == "backend" and not any(f in frameworks for f in ["express", "fastify", "django", "flask", "fastapi"]):
376
+ notes.append("No recognized backend framework detected despite backend mode.")
377
+
378
+ return {
379
+ "local_path": local_path,
380
+ "mode": mode,
381
+ "stack": stacks,
382
+ "frameworks": frameworks,
383
+ "auth_pattern": patterns["auth_pattern"],
384
+ "api_pattern": patterns["api_pattern"],
385
+ "env_files": patterns["env_files"],
386
+ "deployment_files": patterns["deployment_files"],
387
+ "key_files": key_files,
388
+ "notes": notes,
389
+ }
390
+
391
+
392
+ # ---------------------------------------------------------------------------
393
+ # Tool 3: search_repo
394
+ # ---------------------------------------------------------------------------
395
+
396
+
397
+ def search_repo(
398
+ *,
399
+ local_path: str,
400
+ query: str,
401
+ glob: str | None = None,
402
+ max_results: int = 50,
403
+ ) -> dict[str, Any]:
404
+ """Search a local repo using rg (ripgrep) with grep fallback.
405
+
406
+ Returns structured matches with file path, line numbers, and snippets.
407
+ """
408
+ repo = Path(local_path)
409
+ if not repo.is_dir():
410
+ raise FileNotFoundError(f"Repo path does not exist: {local_path}")
411
+
412
+ matches: list[dict[str, Any]] = []
413
+
414
+ if shutil.which("rg"):
415
+ cmd = ["rg", "--no-heading", "--line-number", "--max-count", str(max_results), "--color", "never"]
416
+ if glob:
417
+ cmd.extend(["--glob", glob])
418
+ cmd.extend([query, str(repo)])
419
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
420
+ # rg returns 1 on no match, 2 on error
421
+ if result.returncode == 2:
422
+ raise RuntimeError(f"rg search error: {result.stderr.strip()}")
423
+ for line in result.stdout.strip().splitlines():
424
+ # Format: /path/to/file:line_num:content
425
+ parts = line.split(":", 2)
426
+ if len(parts) >= 3:
427
+ file_path = parts[0]
428
+ # Make path relative to repo root
429
+ try:
430
+ rel = str(Path(file_path).relative_to(repo))
431
+ except ValueError:
432
+ rel = file_path
433
+ matches.append({
434
+ "file": rel,
435
+ "line": int(parts[1]) if parts[1].isdigit() else parts[1],
436
+ "snippet": parts[2],
437
+ })
438
+ if len(matches) >= max_results:
439
+ break
440
+ elif shutil.which("grep"):
441
+ cmd = ["grep", "-rn", "--color=never"]
442
+ if glob:
443
+ cmd.extend(["--include", glob])
444
+ cmd.extend([query, str(repo)])
445
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
446
+ if result.returncode == 2:
447
+ raise RuntimeError(f"grep search error: {result.stderr.strip()}")
448
+ for line in result.stdout.strip().splitlines():
449
+ parts = line.split(":", 2)
450
+ if len(parts) >= 3:
451
+ try:
452
+ rel = str(Path(parts[0]).relative_to(repo))
453
+ except ValueError:
454
+ rel = parts[0]
455
+ matches.append({
456
+ "file": rel,
457
+ "line": int(parts[1]) if parts[1].isdigit() else parts[1],
458
+ "snippet": parts[2],
459
+ })
460
+ if len(matches) >= max_results:
461
+ break
462
+ else:
463
+ raise RuntimeError("Neither rg (ripgrep) nor grep is available on PATH.")
464
+
465
+ return {
466
+ "local_path": local_path,
467
+ "query": query,
468
+ "glob": glob,
469
+ "match_count": len(matches),
470
+ "matches": matches,
471
+ }
472
+
473
+
474
+ # ---------------------------------------------------------------------------
475
+ # Tool 4: read_repo_file
476
+ # ---------------------------------------------------------------------------
477
+
478
+
479
+ def read_repo_file(
480
+ *,
481
+ local_path: str,
482
+ file_path: str,
483
+ start_line: int | None = None,
484
+ end_line: int | None = None,
485
+ ) -> dict[str, Any]:
486
+ """Read a file from a local repo.
487
+
488
+ Returns text content and path metadata.
489
+ Paths are resolved relative to local_path if not absolute.
490
+ """
491
+ repo = Path(local_path)
492
+ target = Path(file_path)
493
+
494
+ # Resolve relative paths against repo root
495
+ if not target.is_absolute():
496
+ target = repo / target
497
+
498
+ # Security: ensure target is within repo
499
+ try:
500
+ target.resolve().relative_to(repo.resolve())
501
+ except ValueError:
502
+ raise ValueError(f"file_path must be within the repo: {file_path!r} is outside {local_path!r}")
503
+
504
+ if not target.is_file():
505
+ raise FileNotFoundError(f"File not found: {target}")
506
+
507
+ try:
508
+ content = target.read_text(encoding="utf-8")
509
+ except UnicodeDecodeError:
510
+ raise ValueError(f"File is not valid UTF-8 text: {target}")
511
+
512
+ lines = content.splitlines(keepends=True)
513
+ total_lines = len(lines)
514
+
515
+ if start_line is not None or end_line is not None:
516
+ s = (start_line or 1) - 1 # 1-indexed to 0-indexed
517
+ e = end_line or total_lines
518
+ lines = lines[s:e]
519
+ content = "".join(lines)
520
+
521
+ try:
522
+ rel = str(target.relative_to(repo))
523
+ except ValueError:
524
+ rel = str(target)
525
+
526
+ return {
527
+ "local_path": local_path,
528
+ "file_path": rel,
529
+ "absolute_path": str(target),
530
+ "total_lines": total_lines,
531
+ "returned_lines": len(lines),
532
+ "start_line": start_line or 1,
533
+ "end_line": end_line or total_lines,
534
+ "content": content,
535
+ }
536
+
537
+
538
+ def project_overview(
539
+ *,
540
+ focus: Literal["generic", "architecture", "product", "codebase"] = "generic",
541
+ ) -> dict[str, Any]:
542
+ repo_root = _require_active_repo_root()
543
+ surface = inspect_repo_surface(local_path=str(repo_root), mode="generic")
544
+ return {
545
+ "repo_root": str(repo_root),
546
+ "focus": focus,
547
+ "top_level_entries": _top_level_inventory(repo_root),
548
+ "key_files": surface["key_files"],
549
+ "stack": surface["stack"],
550
+ "frameworks": surface["frameworks"],
551
+ "auth_surface": surface["auth_pattern"],
552
+ "api_surface": surface["api_pattern"],
553
+ "env_files": surface["env_files"],
554
+ "deployment_files": surface["deployment_files"],
555
+ "notes": surface["notes"],
556
+ }
557
+
558
+
559
+ def read_repo_knowledge_index(
560
+ *,
561
+ refresh: bool = False,
562
+ max_files: int = 2_000,
563
+ ) -> dict[str, Any]:
564
+ repo_root = _require_active_repo_root()
565
+ payload = (
566
+ write_repo_knowledge_index(repo_root, max_files=max_files)
567
+ if refresh
568
+ else load_repo_knowledge_index(repo_root)
569
+ )
570
+ if payload is None:
571
+ payload = write_repo_knowledge_index(repo_root, max_files=max_files)
572
+ return {
573
+ "repo_root": str(repo_root),
574
+ "schema_version": payload.get("schema_version"),
575
+ "generated_at": payload.get("generated_at"),
576
+ "git_head": payload.get("git_head"),
577
+ "indexed_file_count": payload.get("indexed_file_count"),
578
+ "skipped_file_count": payload.get("skipped_file_count"),
579
+ "directories": payload.get("directories") or [],
580
+ "story_planes": payload.get("story_planes") or [],
581
+ "registry": payload.get("registry") or {},
582
+ "search_hints": payload.get("search_hints") or {},
583
+ "files": payload.get("files") or [],
584
+ }
585
+
586
+
587
+ def list_directory(
588
+ *,
589
+ path: str = ".",
590
+ max_entries: int = 200,
591
+ ) -> dict[str, Any]:
592
+ repo_root = _require_active_repo_root()
593
+ target = _resolve_within_repo(repo_root, path)
594
+ if not target.is_dir():
595
+ raise FileNotFoundError(f"Directory not found: {path}")
596
+ relative_path = "." if target == repo_root else str(target.relative_to(repo_root))
597
+ entries = _directory_entries(target, max_entries=max_entries)
598
+ return {
599
+ "repo_root": str(repo_root),
600
+ "path": relative_path,
601
+ "absolute_path": str(target),
602
+ "entry_count": len(entries),
603
+ "entries": entries,
604
+ }
605
+
606
+
607
+ def search_code(
608
+ *,
609
+ query: str,
610
+ glob: str | None = None,
611
+ max_results: int = 50,
612
+ path: str = ".",
613
+ ) -> dict[str, Any]:
614
+ repo_root = _require_active_repo_root()
615
+ target = _resolve_within_repo(repo_root, path)
616
+ if not target.exists():
617
+ raise FileNotFoundError(f"Path not found: {path}")
618
+ if target.is_file():
619
+ search_root = target.parent
620
+ effective_glob = target.name
621
+ else:
622
+ search_root = target
623
+ effective_glob = glob
624
+ result = search_repo(
625
+ local_path=str(search_root),
626
+ query=query,
627
+ glob=effective_glob,
628
+ max_results=max_results,
629
+ )
630
+ return {
631
+ "repo_root": str(repo_root),
632
+ "path": "." if target == repo_root else str(target.relative_to(repo_root)),
633
+ "query": result["query"],
634
+ "glob": glob,
635
+ "match_count": result["match_count"],
636
+ "matches": result["matches"],
637
+ }
638
+
639
+
640
+ def read_file(
641
+ *,
642
+ file_path: str,
643
+ start_line: int | None = None,
644
+ end_line: int | None = None,
645
+ ) -> dict[str, Any]:
646
+ repo_root = _require_active_repo_root()
647
+ result = read_repo_file(
648
+ local_path=str(repo_root),
649
+ file_path=file_path,
650
+ start_line=start_line,
651
+ end_line=end_line,
652
+ )
653
+ return {
654
+ "repo_root": str(repo_root),
655
+ "file_path": result["file_path"],
656
+ "absolute_path": result["absolute_path"],
657
+ "total_lines": result["total_lines"],
658
+ "returned_lines": result["returned_lines"],
659
+ "start_line": result["start_line"],
660
+ "end_line": result["end_line"],
661
+ "content": result["content"],
662
+ }
663
+
664
+
665
+ def read_docs_context(
666
+ *,
667
+ query: str,
668
+ max_results: int = 8,
669
+ ) -> dict[str, Any]:
670
+ repo_root = _require_active_repo_root()
671
+ docs_root = repo_root / "ai_docs" / "context"
672
+ if not docs_root.exists():
673
+ raise FileNotFoundError(f"Docs context not found: {docs_root}")
674
+ result = search_repo(
675
+ local_path=str(docs_root),
676
+ query=query,
677
+ glob="*.md",
678
+ max_results=max_results,
679
+ )
680
+ return {
681
+ "repo_root": str(repo_root),
682
+ "docs_root": str(docs_root),
683
+ "query": result["query"],
684
+ "match_count": result["match_count"],
685
+ "matches": result["matches"],
686
+ }
687
+
688
+
689
+ def read_recent_artifacts(
690
+ *,
691
+ limit: int = 10,
692
+ kind: str | None = None,
693
+ ) -> dict[str, Any]:
694
+ repo_root = _require_active_repo_root()
695
+ db_path = repo_root / ".devflow" / "execution.sqlite"
696
+ if not db_path.exists():
697
+ raise FileNotFoundError(f"Execution store not found: {db_path}")
698
+ store = ExecutionStore(db_path)
699
+ sql = (
700
+ "SELECT artifact_id, run_id, node_exec_id, kind, uri, metadata_json, created_at, updated_at "
701
+ "FROM artifacts"
702
+ )
703
+ params: list[Any] = []
704
+ if kind:
705
+ sql += " WHERE kind=?"
706
+ params.append(kind)
707
+ sql += " ORDER BY created_at DESC LIMIT ?"
708
+ params.append(max(1, int(limit)))
709
+ with store._connect() as conn:
710
+ rows = conn.execute(sql, tuple(params)).fetchall()
711
+ artifacts: list[dict[str, Any]] = []
712
+ for row in rows:
713
+ try:
714
+ metadata = json.loads(str(row["metadata_json"] or "{}"))
715
+ except Exception:
716
+ metadata = {}
717
+ artifacts.append(
718
+ {
719
+ "artifact_id": str(row["artifact_id"]),
720
+ "run_id": str(row["run_id"]),
721
+ "node_exec_id": None if row["node_exec_id"] is None else str(row["node_exec_id"]),
722
+ "kind": str(row["kind"]),
723
+ "uri": str(row["uri"]),
724
+ "metadata": metadata if isinstance(metadata, dict) else {},
725
+ "created_at": int(row["created_at"]),
726
+ "updated_at": int(row["updated_at"]),
727
+ }
728
+ )
729
+ return {
730
+ "repo_root": str(repo_root),
731
+ "db_path": str(db_path),
732
+ "artifact_count": len(artifacts),
733
+ "artifacts": artifacts,
734
+ }
735
+
736
+
737
+ def inspect_routes(
738
+ *,
739
+ path: str = ".",
740
+ max_results: int = 20,
741
+ ) -> dict[str, Any]:
742
+ repo_root = _require_active_repo_root()
743
+ target = _resolve_within_repo(repo_root, path)
744
+ if not target.exists():
745
+ raise FileNotFoundError(f"Path not found: {path}")
746
+ route_globs = ["*route*", "*routes*", "*router*", "*api*", "*endpoint*", "*controller*"]
747
+ route_queries = ["router", "route", "endpoint", "controller", "APIRouter", "FastAPI", "Blueprint", "express.Router", "app.get", "app.post", "app.put", "app.delete", "app.patch"]
748
+ matches: list[dict[str, Any]] = []
749
+ seen: set[tuple[str, Any, str]] = set()
750
+ search_root = target if target.is_dir() else target.parent
751
+ for glob in route_globs:
752
+ for query in route_queries:
753
+ result = search_repo(
754
+ local_path=str(search_root),
755
+ query=query,
756
+ glob=glob if target.is_dir() else target.name,
757
+ max_results=max_results,
758
+ )
759
+ for item in result["matches"]:
760
+ key = (str(item.get("file")), item.get("line"), str(item.get("snippet")))
761
+ if key in seen:
762
+ continue
763
+ seen.add(key)
764
+ matches.append(item)
765
+ if len(matches) >= max_results:
766
+ break
767
+ if len(matches) >= max_results:
768
+ break
769
+ if len(matches) >= max_results:
770
+ break
771
+ return {
772
+ "repo_root": str(repo_root),
773
+ "path": "." if target == repo_root else str(target.relative_to(repo_root)),
774
+ "match_count": len(matches),
775
+ "matches": matches,
776
+ }
777
+
778
+
779
+ def inspect_data_model(
780
+ *,
781
+ path: str = ".",
782
+ max_results: int = 20,
783
+ ) -> dict[str, Any]:
784
+ repo_root = _require_active_repo_root()
785
+ target = _resolve_within_repo(repo_root, path)
786
+ if not target.exists():
787
+ raise FileNotFoundError(f"Path not found: {path}")
788
+ model_queries = ["model", "schema", "entity", "table", "migration", "sqlalchemy", "BaseModel", "dataclass"]
789
+ matches: list[dict[str, Any]] = []
790
+ seen: set[tuple[str, Any, str]] = set()
791
+ search_root = target if target.is_dir() else target.parent
792
+ effective_glob = None if target.is_dir() else target.name
793
+ for query in model_queries:
794
+ result = search_repo(
795
+ local_path=str(search_root),
796
+ query=query,
797
+ glob=effective_glob,
798
+ max_results=max_results,
799
+ )
800
+ for item in result["matches"]:
801
+ key = (str(item.get("file")), item.get("line"), str(item.get("snippet")))
802
+ if key in seen:
803
+ continue
804
+ seen.add(key)
805
+ matches.append(item)
806
+ if len(matches) >= max_results:
807
+ break
808
+ if len(matches) >= max_results:
809
+ break
810
+ return {
811
+ "repo_root": str(repo_root),
812
+ "path": "." if target == repo_root else str(target.relative_to(repo_root)),
813
+ "match_count": len(matches),
814
+ "matches": matches,
815
+ }
816
+
817
+
818
+ def devflow_create_idea(
819
+ *,
820
+ idea_id: str,
821
+ summary: str,
822
+ project_id: str | None = None,
823
+ problem_statement: str | None = None,
824
+ target_users: list[str] | None = None,
825
+ desired_outcomes: list[str] | None = None,
826
+ in_scope: list[str] | None = None,
827
+ out_of_scope: list[str] | None = None,
828
+ constraints: list[str] | None = None,
829
+ acceptance_criteria: list[str] | None = None,
830
+ ) -> dict[str, Any]:
831
+ repo_root = _require_active_repo_root()
832
+ normalized_idea_id = str(idea_id).strip()
833
+ if not normalized_idea_id:
834
+ raise ValueError("idea_id is required")
835
+ payload: dict[str, Any] = {
836
+ "idea_id": normalized_idea_id,
837
+ "project_id": str(project_id).strip() or None if project_id is not None else None,
838
+ "title": str(summary).strip(),
839
+ "summary": str(summary).strip(),
840
+ "problem_statement": str(problem_statement or "").strip(),
841
+ "target_users": [str(item).strip() for item in (target_users or []) if str(item).strip()],
842
+ "desired_outcomes": [str(item).strip() for item in (desired_outcomes or []) if str(item).strip()],
843
+ "initial_scope": {
844
+ "in_scope": [str(item).strip() for item in (in_scope or []) if str(item).strip()],
845
+ "out_of_scope": [str(item).strip() for item in (out_of_scope or []) if str(item).strip()],
846
+ },
847
+ "constraints": [str(item).strip() for item in (constraints or []) if str(item).strip()],
848
+ "acceptance_criteria": [str(item).strip() for item in (acceptance_criteria or []) if str(item).strip()],
849
+ "latest_message": str(summary).strip(),
850
+ }
851
+ idea_path = get_idea_paths(repo_root, idea_id=normalized_idea_id).idea_dir / "idea.json"
852
+ idea_path.parent.mkdir(parents=True, exist_ok=True)
853
+ idea_path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
854
+
855
+ store = ExecutionStore(repo_root / ".devflow" / "execution.sqlite")
856
+ run = store.start_run(
857
+ kind="ideation_arm_devflow_create_idea",
858
+ repo_root=repo_root,
859
+ args={"idea_id": normalized_idea_id, "project_id": payload["project_id"]},
860
+ )
861
+ queue_id = store.enqueue_idea_creation_task(
862
+ project_id=payload["project_id"],
863
+ enqueue_run_id=run.run_id,
864
+ idea_id=normalized_idea_id,
865
+ title=str(payload["title"]),
866
+ idea_payload_path=str(idea_path),
867
+ )
868
+ return {
869
+ "repo_root": str(repo_root),
870
+ "run_id": run.run_id,
871
+ "idea_id": normalized_idea_id,
872
+ "idea_path": str(idea_path),
873
+ "idea_creation_queue_id": queue_id,
874
+ "status": "queued",
875
+ }
876
+
877
+
878
+ def devflow_generate_stories(
879
+ *,
880
+ idea_id: str,
881
+ project_id: str | None = None,
882
+ candidate_planes: list[str] | None = None,
883
+ ) -> dict[str, Any]:
884
+ repo_root = _require_active_repo_root()
885
+ normalized_idea_id = str(idea_id).strip()
886
+ if not normalized_idea_id:
887
+ raise ValueError("idea_id is required")
888
+ idea_path = get_idea_paths(repo_root, idea_id=normalized_idea_id).idea_dir / "idea.json"
889
+ if not idea_path.exists():
890
+ raise FileNotFoundError(f"Idea payload not found: {idea_path}")
891
+ payload = json.loads(idea_path.read_text(encoding="utf-8"))
892
+ title = str(payload.get("title") or payload.get("summary") or normalized_idea_id).strip() or normalized_idea_id
893
+ resolved_project_id = str(project_id).strip() or None if project_id is not None else (str(payload.get("project_id") or "").strip() or None)
894
+ store = ExecutionStore(repo_root / ".devflow" / "execution.sqlite")
895
+ run = store.start_run(
896
+ kind="ideation_arm_devflow_generate_stories",
897
+ repo_root=repo_root,
898
+ args={"idea_id": normalized_idea_id, "project_id": resolved_project_id, "candidate_planes": candidate_planes or []},
899
+ )
900
+ queue_id = store.enqueue_idea_task(
901
+ project_id=resolved_project_id,
902
+ enqueue_run_id=run.run_id,
903
+ idea_id=normalized_idea_id,
904
+ title=title,
905
+ idea_payload_path=str(idea_path),
906
+ candidate_planes=candidate_planes or [],
907
+ )
908
+ return {
909
+ "repo_root": str(repo_root),
910
+ "run_id": run.run_id,
911
+ "idea_id": normalized_idea_id,
912
+ "idea_path": str(idea_path),
913
+ "idea_queue_id": queue_id,
914
+ "status": "queued",
915
+ "candidate_planes": list(candidate_planes or []),
916
+ }
917
+
918
+
919
+ def devflow_kick_queue(
920
+ *,
921
+ project_id: str,
922
+ ) -> dict[str, Any]:
923
+ repo_root = _require_active_repo_root()
924
+ normalized_project_id = str(project_id).strip()
925
+ if not normalized_project_id:
926
+ raise ValueError("project_id is required")
927
+ cmd = ["devflow", "worker", "start", "--project", normalized_project_id, "--once"]
928
+ result = subprocess.run(
929
+ cmd,
930
+ cwd=str(repo_root),
931
+ capture_output=True,
932
+ text=True,
933
+ timeout=300,
934
+ check=False,
935
+ )
936
+ if result.returncode != 0:
937
+ raise RuntimeError(
938
+ "devflow worker start --once failed: "
939
+ f"rc={result.returncode} stdout={result.stdout.strip()} stderr={result.stderr.strip()}"
940
+ )
941
+ return {
942
+ "repo_root": str(repo_root),
943
+ "project_id": normalized_project_id,
944
+ "command": cmd,
945
+ "status": "completed",
946
+ "stdout": result.stdout.strip(),
947
+ }
948
+
949
+
950
+ def devflow_status(
951
+ *,
952
+ project_id: str | None = None,
953
+ ) -> dict[str, Any]:
954
+ repo_root = _require_active_repo_root()
955
+ db_path = repo_root / ".devflow" / "execution.sqlite"
956
+ if not db_path.exists():
957
+ raise FileNotFoundError(f"Execution store not found: {db_path}")
958
+ store = ExecutionStore(db_path)
959
+ where = ""
960
+ params: list[Any] = []
961
+ if project_id is not None and str(project_id).strip():
962
+ where = " WHERE project_id=?"
963
+ params.append(str(project_id).strip())
964
+ queues: dict[str, dict[str, int]] = {}
965
+ with store._connect() as conn:
966
+ for table, key in [
967
+ ("scope_queue", "scope"),
968
+ ("idea_creation_queue", "idea_creation"),
969
+ ("idea_queue", "idea"),
970
+ ("story_queue", "story"),
971
+ ("source_doc_mutation_queue", "source_doc_mutation"),
972
+ ]:
973
+ rows = conn.execute(
974
+ f"SELECT status, COUNT(*) AS total FROM {table}{where} GROUP BY status",
975
+ tuple(params),
976
+ ).fetchall()
977
+ counts = {str(row["status"]): int(row["total"]) for row in rows}
978
+ queues[key] = {
979
+ "queued": int(counts.get("queued", 0)),
980
+ "claimed": int(counts.get("claimed", 0)),
981
+ "in_progress": int(counts.get("in_progress", 0)),
982
+ "completed": int(counts.get("completed", 0)),
983
+ "failed": int(counts.get("failed", 0)),
984
+ "total": sum(counts.values()),
985
+ }
986
+ return {
987
+ "repo_root": str(repo_root),
988
+ "project_id": None if project_id is None else str(project_id).strip() or None,
989
+ "queues": queues,
990
+ }
991
+
992
+
993
+ def propose_idea(
994
+ *,
995
+ idea_id: str,
996
+ summary: str,
997
+ project_id: str | None = None,
998
+ problem_statement: str | None = None,
999
+ target_users: list[str] | None = None,
1000
+ desired_outcomes: list[str] | None = None,
1001
+ in_scope: list[str] | None = None,
1002
+ out_of_scope: list[str] | None = None,
1003
+ constraints: list[str] | None = None,
1004
+ acceptance_criteria: list[str] | None = None,
1005
+ ) -> dict[str, Any]:
1006
+ payload = {
1007
+ "idea_id": str(idea_id).strip() or "idea",
1008
+ "project_id": str(project_id).strip() or None if project_id is not None else None,
1009
+ "summary": str(summary).strip(),
1010
+ "problem_statement": str(problem_statement or "").strip(),
1011
+ "target_users": [str(item).strip() for item in (target_users or []) if str(item).strip()],
1012
+ "desired_outcomes": [str(item).strip() for item in (desired_outcomes or []) if str(item).strip()],
1013
+ "initial_scope": {
1014
+ "in_scope": [str(item).strip() for item in (in_scope or []) if str(item).strip()],
1015
+ "out_of_scope": [str(item).strip() for item in (out_of_scope or []) if str(item).strip()],
1016
+ },
1017
+ "constraints": [str(item).strip() for item in (constraints or []) if str(item).strip()],
1018
+ "acceptance_criteria": [str(item).strip() for item in (acceptance_criteria or []) if str(item).strip()],
1019
+ }
1020
+ missing_fields: list[str] = []
1021
+ for key in ("summary", "target_users", "desired_outcomes", "constraints", "acceptance_criteria"):
1022
+ if payload.get(key) in (None, "", [], {}):
1023
+ missing_fields.append(key)
1024
+ if not list((payload.get("initial_scope") or {}).get("in_scope") or []):
1025
+ missing_fields.append("initial_scope")
1026
+ decision = "sufficient" if not missing_fields else "too_thin"
1027
+ preview = {
1028
+ "decision": decision,
1029
+ "passed": decision == "sufficient",
1030
+ "rationale": "The idea is specific enough to move into DevFlow." if decision == "sufficient" else "The idea is still too thin to move into DevFlow cleanly.",
1031
+ "missing_fields": missing_fields,
1032
+ "follow_up": None if decision == "sufficient" else f"This is still too thin to build cleanly. Biggest gap: {missing_fields[0]}.",
1033
+ }
1034
+ return {
1035
+ "idea_id": payload["idea_id"],
1036
+ "project_id": payload["project_id"],
1037
+ "summary": payload["summary"],
1038
+ "preview": preview,
1039
+ }
1040
+
1041
+
1042
+ # ---------------------------------------------------------------------------
1043
+ # Tool specs by arm
1044
+ # ---------------------------------------------------------------------------
1045
+
1046
+ _SHARED_READ_TOOL_SPECS: list[dict[str, Any]] = [
1047
+ {
1048
+ "name": "project_overview",
1049
+ "description": "Summarize the active project at a high level, including top-level structure, stack signals, key files, and major surfaces relevant to the current question.",
1050
+ "parameters": {
1051
+ "focus": {
1052
+ "type": "string",
1053
+ "required": False,
1054
+ "enum": ["generic", "architecture", "product", "codebase"],
1055
+ "description": "Optional lens for the summary.",
1056
+ },
1057
+ },
1058
+ },
1059
+ {
1060
+ "name": "read_repo_knowledge_index",
1061
+ "description": (
1062
+ "Read or refresh the fast PageIndex-style repo knowledge index: directory map, "
1063
+ "key files, story-plane anchors, registry canonicals, and search hints."
1064
+ ),
1065
+ "parameters": {
1066
+ "refresh": {"type": "boolean", "required": False, "description": "Refresh the index before reading it."},
1067
+ "max_files": {
1068
+ "type": "integer",
1069
+ "required": False,
1070
+ "description": "Maximum files to summarize during refresh.",
1071
+ },
1072
+ },
1073
+ },
1074
+ {
1075
+ "name": "search_code",
1076
+ "description": "Search the active project for code or text matches and return file paths, line numbers, and snippets.",
1077
+ "parameters": {
1078
+ "query": {"type": "string", "required": True, "description": "Search pattern (regex supported)"},
1079
+ "path": {"type": "string", "required": False, "description": "Optional file or directory path relative to the active repo root."},
1080
+ "glob": {"type": "string", "required": False, "description": "File glob filter (e.g. '*.py')"},
1081
+ "max_results": {"type": "integer", "required": False, "description": "Max matches to return (default 50)"},
1082
+ },
1083
+ },
1084
+ {
1085
+ "name": "read_file",
1086
+ "description": "Read a file from the active project. Supports line range selection.",
1087
+ "parameters": {
1088
+ "file_path": {"type": "string", "required": True, "description": "File path relative to the active repo root."},
1089
+ "start_line": {"type": "integer", "required": False, "description": "First line to read (1-indexed)"},
1090
+ "end_line": {"type": "integer", "required": False, "description": "Last line to read (inclusive)"},
1091
+ },
1092
+ },
1093
+ {
1094
+ "name": "read_docs_context",
1095
+ "description": "Search the repo's docs context for relevant documentation matches.",
1096
+ "parameters": {
1097
+ "query": {"type": "string", "required": True, "description": "Search pattern for docs context."},
1098
+ "max_results": {"type": "integer", "required": False, "description": "Max matches to return (default 8)"},
1099
+ },
1100
+ },
1101
+ {
1102
+ "name": "read_recent_artifacts",
1103
+ "description": "Read the most recent execution artifacts from the local DevFlow execution store.",
1104
+ "parameters": {
1105
+ "limit": {"type": "integer", "required": False, "description": "Max artifacts to return (default 10)."},
1106
+ "kind": {"type": "string", "required": False, "description": "Optional artifact kind filter."},
1107
+ },
1108
+ },
1109
+ {
1110
+ "name": "inspect_routes",
1111
+ "description": "Inspect likely route, API, controller, or endpoint surfaces in the active repo.",
1112
+ "parameters": {
1113
+ "path": {"type": "string", "required": False, "description": "Optional file or directory path relative to the active repo root."},
1114
+ "max_results": {"type": "integer", "required": False, "description": "Max matches to return (default 20)."},
1115
+ },
1116
+ },
1117
+ {
1118
+ "name": "inspect_data_model",
1119
+ "description": "Inspect likely data-model, schema, entity, migration, or table definitions in the active repo.",
1120
+ "parameters": {
1121
+ "path": {"type": "string", "required": False, "description": "Optional file or directory path relative to the active repo root."},
1122
+ "max_results": {"type": "integer", "required": False, "description": "Max matches to return (default 20)."},
1123
+ },
1124
+ },
1125
+ ]
1126
+
1127
+ _IDEATION_ONLY_TOOL_SPECS: list[dict[str, Any]] = [
1128
+ {
1129
+ "name": "devflow_create_idea",
1130
+ "description": "Persist an idea payload locally and enqueue it onto the DevFlow idea-creation queue.",
1131
+ "parameters": {
1132
+ "idea_id": {"type": "string", "required": True, "description": "Canonical idea id to create."},
1133
+ "summary": {"type": "string", "required": True, "description": "Short summary of the idea."},
1134
+ "project_id": {"type": "string", "required": False, "description": "Optional project id."},
1135
+ "problem_statement": {"type": "string", "required": False, "description": "Optional problem statement."},
1136
+ "target_users": {"type": "array", "required": False, "description": "Optional target users list."},
1137
+ "desired_outcomes": {"type": "array", "required": False, "description": "Optional desired outcomes list."},
1138
+ "in_scope": {"type": "array", "required": False, "description": "Optional in-scope items."},
1139
+ "out_of_scope": {"type": "array", "required": False, "description": "Optional out-of-scope items."},
1140
+ "constraints": {"type": "array", "required": False, "description": "Optional constraints list."},
1141
+ "acceptance_criteria": {"type": "array", "required": False, "description": "Optional acceptance criteria list."},
1142
+ },
1143
+ },
1144
+ {
1145
+ "name": "devflow_generate_stories",
1146
+ "description": "Enqueue an existing idea for DevFlow story generation.",
1147
+ "parameters": {
1148
+ "idea_id": {"type": "string", "required": True, "description": "Idea id to enqueue for story generation."},
1149
+ "project_id": {"type": "string", "required": False, "description": "Optional project id override."},
1150
+ "candidate_planes": {"type": "array", "required": False, "description": "Optional candidate planes for downstream generation."},
1151
+ },
1152
+ },
1153
+ {
1154
+ "name": "devflow_kick_queue",
1155
+ "description": "Run one DevFlow worker pass for the given project.",
1156
+ "parameters": {
1157
+ "project_id": {"type": "string", "required": True, "description": "Project id whose queue should be processed once."},
1158
+ },
1159
+ },
1160
+ {
1161
+ "name": "devflow_status",
1162
+ "description": "Read current DevFlow queue counts for the active repo, optionally scoped to one project.",
1163
+ "parameters": {
1164
+ "project_id": {"type": "string", "required": False, "description": "Optional project id filter."},
1165
+ },
1166
+ },
1167
+ {
1168
+ "name": "propose_idea",
1169
+ "description": "Synthesize a lightweight idea proposal preview from the provided idea details.",
1170
+ "parameters": {
1171
+ "idea_id": {"type": "string", "required": True, "description": "Idea id for the synthesized proposal."},
1172
+ "summary": {"type": "string", "required": True, "description": "Short summary of the idea."},
1173
+ "project_id": {"type": "string", "required": False, "description": "Optional project id."},
1174
+ "problem_statement": {"type": "string", "required": False, "description": "Optional problem statement."},
1175
+ "target_users": {"type": "array", "required": False, "description": "Optional target users list."},
1176
+ "desired_outcomes": {"type": "array", "required": False, "description": "Optional desired outcomes list."},
1177
+ "in_scope": {"type": "array", "required": False, "description": "Optional in-scope items."},
1178
+ "out_of_scope": {"type": "array", "required": False, "description": "Optional out-of-scope items."},
1179
+ "constraints": {"type": "array", "required": False, "description": "Optional constraints list."},
1180
+ "acceptance_criteria": {"type": "array", "required": False, "description": "Optional acceptance criteria list."},
1181
+ },
1182
+ },
1183
+ ]
1184
+
1185
+ _INSIGHT_ONLY_TOOL_SPECS: list[dict[str, Any]] = [
1186
+ {
1187
+ "name": "devflow_status",
1188
+ "description": "Read current DevFlow queue counts for the active repo, optionally scoped to one project.",
1189
+ "parameters": {
1190
+ "project_id": {"type": "string", "required": False, "description": "Optional project id filter."},
1191
+ },
1192
+ },
1193
+ ]
1194
+
1195
+ REPO_TOOL_SPECS: list[dict[str, Any]] = [*_SHARED_READ_TOOL_SPECS, *_IDEATION_ONLY_TOOL_SPECS]
1196
+ INSIGHT_REPO_TOOL_SPECS: list[dict[str, Any]] = [*_SHARED_READ_TOOL_SPECS, *_INSIGHT_ONLY_TOOL_SPECS]
1197
+
1198
+ _ALL_REPO_TOOL_DISPATCH: dict[str, Any] = {
1199
+ "project_overview": project_overview,
1200
+ "read_repo_knowledge_index": read_repo_knowledge_index,
1201
+ "search_code": search_code,
1202
+ "read_file": read_file,
1203
+ "read_docs_context": read_docs_context,
1204
+ "read_recent_artifacts": read_recent_artifacts,
1205
+ "inspect_routes": inspect_routes,
1206
+ "inspect_data_model": inspect_data_model,
1207
+ "devflow_create_idea": devflow_create_idea,
1208
+ "devflow_generate_stories": devflow_generate_stories,
1209
+ "devflow_kick_queue": devflow_kick_queue,
1210
+ "devflow_status": devflow_status,
1211
+ "propose_idea": propose_idea,
1212
+ }
1213
+
1214
+ REPO_TOOL_DISPATCH: dict[str, Any] = {name: _ALL_REPO_TOOL_DISPATCH[name] for name in (spec["name"] for spec in REPO_TOOL_SPECS)}
1215
+ INSIGHT_REPO_TOOL_DISPATCH: dict[str, Any] = {
1216
+ name: _ALL_REPO_TOOL_DISPATCH[name] for name in (spec["name"] for spec in INSIGHT_REPO_TOOL_SPECS)
1217
+ }
1218
+
1219
+
1220
+ def detect_repo_refs_in_text(text: str) -> list[str]:
1221
+ """Detect likely GitHub repo references in free text.
1222
+
1223
+ Returns list of owner/repo strings found.
1224
+ """
1225
+ refs: list[str] = []
1226
+ # GitHub URLs
1227
+ for m in re.finditer(r"https?://github\.com/([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)", text):
1228
+ ref = m.group(1).rstrip("/").removesuffix(".git")
1229
+ if ref not in refs:
1230
+ refs.append(ref)
1231
+ # owner/repo patterns (conservative: require surrounding whitespace or start/end)
1232
+ for m in re.finditer(r"(?:^|\s)([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)(?:\s|$)", text):
1233
+ candidate = m.group(1)
1234
+ # Filter out obvious non-repo patterns
1235
+ if "/" in candidate and not candidate.startswith("/") and "." not in candidate.split("/")[0]:
1236
+ if candidate not in refs:
1237
+ refs.append(candidate)
1238
+ return refs
1239
+
1240
+
1241
+ def build_repo_tool_guidance(repo_refs: list[str] | None = None) -> list[str]:
1242
+ """Build guidance strings for the ideation agent about available repo tools."""
1243
+ guidance = [
1244
+ "You have access to the ideation-arm tool surface for the active project.",
1245
+ (
1246
+ "Read / understand: read_repo_knowledge_index, project_overview, search_code, read_file, "
1247
+ "read_docs_context, read_recent_artifacts, inspect_routes, inspect_data_model."
1248
+ ),
1249
+ "Act (DevFlow only): devflow_create_idea, devflow_generate_stories, devflow_kick_queue, devflow_status.",
1250
+ "Synthesis: propose_idea.",
1251
+ "Do not use anything outside that approved ideation-arm tool surface.",
1252
+ ]
1253
+ if repo_refs:
1254
+ refs_str = ", ".join(repo_refs)
1255
+ guidance.append(
1256
+ f"Repo references mentioned in context: {refs_str}. Treat them as discussion context unless the relevant files are present in the active workspace."
1257
+ )
1258
+ return guidance
1259
+
1260
+
1261
+ def build_insight_repo_tool_guidance(repo_refs: list[str] | None = None) -> list[str]:
1262
+ """Build guidance strings for the insight agent about available repo tools."""
1263
+ guidance = [
1264
+ "You have access to the insight-arm tool surface for the active project.",
1265
+ (
1266
+ "Read / investigate: read_repo_knowledge_index, project_overview, search_code, read_file, "
1267
+ "read_docs_context, read_recent_artifacts, inspect_routes, inspect_data_model, devflow_status."
1268
+ ),
1269
+ "Use these tools for repo understanding, evidence gathering, and DevFlow status checks only.",
1270
+ "Do not use anything outside that approved insight-arm tool surface.",
1271
+ ]
1272
+ if repo_refs:
1273
+ refs_str = ", ".join(repo_refs)
1274
+ guidance.append(
1275
+ f"Repo references mentioned in context: {refs_str}. Treat them as discussion context unless the relevant files are present in the active workspace."
1276
+ )
1277
+ return guidance