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.
- devflow_engine/__init__.py +3 -0
- devflow_engine/agentic_prompts.py +100 -0
- devflow_engine/agentic_runtime.py +398 -0
- devflow_engine/api_key_flow_harness.py +539 -0
- devflow_engine/api_keys.py +357 -0
- devflow_engine/bootstrap/__init__.py +2 -0
- devflow_engine/bootstrap/provision_from_template.py +84 -0
- devflow_engine/cli/__init__.py +0 -0
- devflow_engine/cli/app.py +7270 -0
- devflow_engine/core/__init__.py +0 -0
- devflow_engine/core/config.py +86 -0
- devflow_engine/core/logging.py +29 -0
- devflow_engine/core/paths.py +45 -0
- devflow_engine/core/toml_kv.py +33 -0
- devflow_engine/devflow_event_worker.py +1292 -0
- devflow_engine/devflow_state.py +201 -0
- devflow_engine/devin2/__init__.py +9 -0
- devflow_engine/devin2/agent_definition.py +120 -0
- devflow_engine/devin2/pi_runner.py +204 -0
- devflow_engine/devin_orchestration.py +69 -0
- devflow_engine/docs/prompts/anti-patterns.md +42 -0
- devflow_engine/docs/prompts/devin-agent-prompt.md +55 -0
- devflow_engine/docs/prompts/devin2-agent-prompt.md +81 -0
- devflow_engine/docs/prompts/examples/devin-vapi-clone-reference-exchange.json +85 -0
- devflow_engine/doctor/__init__.py +2 -0
- devflow_engine/doctor/triage.py +140 -0
- devflow_engine/error/__init__.py +0 -0
- devflow_engine/error/remediation.py +21 -0
- devflow_engine/errors/error_solver_dag.py +522 -0
- devflow_engine/errors/runtime_observability.py +67 -0
- devflow_engine/idea/__init__.py +4 -0
- devflow_engine/idea/actors.py +481 -0
- devflow_engine/idea/agentic.py +465 -0
- devflow_engine/idea/analyze.py +93 -0
- devflow_engine/idea/devin_chat_dag.py +1 -0
- devflow_engine/idea/diff.py +99 -0
- devflow_engine/idea/drafts.py +446 -0
- devflow_engine/idea/idea_creation_dag.py +643 -0
- devflow_engine/idea/ideation_enrichment.py +355 -0
- devflow_engine/idea/ideation_enrichment_worker.py +19 -0
- devflow_engine/idea/paths.py +28 -0
- devflow_engine/idea/promote.py +53 -0
- devflow_engine/idea/redaction.py +27 -0
- devflow_engine/idea/repo_tools.py +1277 -0
- devflow_engine/idea/response_mode.py +30 -0
- devflow_engine/idea/story_pipeline.py +1585 -0
- devflow_engine/idea/sufficiency.py +376 -0
- devflow_engine/idea/traditional_stories.py +1257 -0
- devflow_engine/implementation/__init__.py +0 -0
- devflow_engine/implementation/alembic_preflight.py +700 -0
- devflow_engine/implementation/dag.py +8450 -0
- devflow_engine/implementation/green_gate.py +93 -0
- devflow_engine/implementation/prompts.py +108 -0
- devflow_engine/implementation/test_runtime.py +623 -0
- devflow_engine/integration/__init__.py +19 -0
- devflow_engine/integration/agentic.py +66 -0
- devflow_engine/integration/dag.py +3539 -0
- devflow_engine/integration/prompts.py +114 -0
- devflow_engine/integration/supabase_schema.sql +31 -0
- devflow_engine/integration/supabase_sync.py +177 -0
- devflow_engine/llm/__init__.py +1 -0
- devflow_engine/llm/cli_one_shot.py +84 -0
- devflow_engine/llm/cli_stream.py +371 -0
- devflow_engine/llm/execution_context.py +26 -0
- devflow_engine/llm/invoke.py +1322 -0
- devflow_engine/llm/provider_api.py +304 -0
- devflow_engine/llm/repo_knowledge.py +588 -0
- devflow_engine/llm_primitives.py +315 -0
- devflow_engine/orchestration.py +62 -0
- devflow_engine/planning/__init__.py +0 -0
- devflow_engine/planning/analyze_repo.py +92 -0
- devflow_engine/planning/render_drafts.py +133 -0
- devflow_engine/playground/__init__.py +0 -0
- devflow_engine/playground/hooks.py +26 -0
- devflow_engine/playwright_workflow/__init__.py +5 -0
- devflow_engine/playwright_workflow/dag.py +1317 -0
- devflow_engine/process/__init__.py +5 -0
- devflow_engine/process/dag.py +59 -0
- devflow_engine/project_registration/__init__.py +3 -0
- devflow_engine/project_registration/dag.py +1581 -0
- devflow_engine/project_registry.py +109 -0
- devflow_engine/prompts/devin/generic/prompt.md +6 -0
- devflow_engine/prompts/devin/ideation/prompt.md +263 -0
- devflow_engine/prompts/devin/ideation/scenarios.md +5 -0
- devflow_engine/prompts/devin/ideation_loop/prompt.md +6 -0
- devflow_engine/prompts/devin/insight/prompt.md +11 -0
- devflow_engine/prompts/devin/insight/scenarios.md +5 -0
- devflow_engine/prompts/devin/intake/prompt.md +15 -0
- devflow_engine/prompts/devin/iterate/prompt.md +12 -0
- devflow_engine/prompts/devin/shared/eval_doctrine.md +9 -0
- devflow_engine/prompts/devin/shared/principles.md +246 -0
- devflow_engine/prompts/devin_eval/assessment/prompt.md +18 -0
- devflow_engine/prompts/idea/api_ideation_agent/prompt.md +8 -0
- devflow_engine/prompts/idea/api_insight_agent/prompt.md +8 -0
- devflow_engine/prompts/idea/response_doctrine/prompt.md +18 -0
- devflow_engine/prompts/implementation/dependency_assessment/prompt.md +12 -0
- devflow_engine/prompts/implementation/green/green/prompt.md +11 -0
- devflow_engine/prompts/implementation/green/node_config/prompt.md +3 -0
- devflow_engine/prompts/implementation/green_review/outcome_review/prompt.md +5 -0
- devflow_engine/prompts/implementation/green_review/prior_run_review/prompt.md +5 -0
- devflow_engine/prompts/implementation/red/prompt.md +27 -0
- devflow_engine/prompts/implementation/redreview/prompt.md +23 -0
- devflow_engine/prompts/implementation/redreview_repair/prompt.md +16 -0
- devflow_engine/prompts/implementation/setupdoc/prompt.md +10 -0
- devflow_engine/prompts/implementation/story_planning/prompt.md +13 -0
- devflow_engine/prompts/implementation/test_design/prompt.md +27 -0
- devflow_engine/prompts/integration/README.md +185 -0
- devflow_engine/prompts/integration/green/example.md +67 -0
- devflow_engine/prompts/integration/green/green/prompt.md +10 -0
- devflow_engine/prompts/integration/green/node_config/prompt.md +42 -0
- devflow_engine/prompts/integration/green/past_prompts/20260417T212300/green/prompt.md +15 -0
- devflow_engine/prompts/integration/green/past_prompts/20260417T212300/node_config/prompt.md +42 -0
- devflow_engine/prompts/integration/green_enrich/example.md +79 -0
- devflow_engine/prompts/integration/green_enrich/green_enrich/prompt.md +9 -0
- devflow_engine/prompts/integration/green_enrich/node_config/prompt.md +41 -0
- devflow_engine/prompts/integration/green_enrich/past_prompts/20260417T212300/green_enrich/prompt.md +14 -0
- devflow_engine/prompts/integration/green_enrich/past_prompts/20260417T212300/node_config/prompt.md +41 -0
- devflow_engine/prompts/integration/red/code_repair/prompt.md +12 -0
- devflow_engine/prompts/integration/red/example.md +152 -0
- devflow_engine/prompts/integration/red/node_config/prompt.md +86 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T212300/code_repair/prompt.md +19 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T212300/node_config/prompt.md +84 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T212300/red/prompt.md +16 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T212300/red_repair/prompt.md +15 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T215032/code_repair/prompt.md +10 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T215032/node_config/prompt.md +84 -0
- devflow_engine/prompts/integration/red/past_prompts/20260417T215032/red_repair/prompt.md +11 -0
- devflow_engine/prompts/integration/red/red/prompt.md +11 -0
- devflow_engine/prompts/integration/red/red_repair/prompt.md +12 -0
- devflow_engine/prompts/integration/red_review/example.md +71 -0
- devflow_engine/prompts/integration/red_review/node_config/prompt.md +41 -0
- devflow_engine/prompts/integration/red_review/past_prompts/20260417T212300/node_config/prompt.md +41 -0
- devflow_engine/prompts/integration/red_review/past_prompts/20260417T212300/red_review/prompt.md +15 -0
- devflow_engine/prompts/integration/red_review/red_review/prompt.md +9 -0
- devflow_engine/prompts/integration/resolve/example.md +111 -0
- devflow_engine/prompts/integration/resolve/node_config/prompt.md +64 -0
- devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/node_config/prompt.md +64 -0
- devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/resolve_implicated_users/prompt.md +15 -0
- devflow_engine/prompts/integration/resolve/past_prompts/20260417T212300/resolve_side_effects/prompt.md +15 -0
- devflow_engine/prompts/integration/resolve/resolve_implicated_users/prompt.md +10 -0
- devflow_engine/prompts/integration/resolve/resolve_side_effects/prompt.md +10 -0
- devflow_engine/prompts/integration/validate/build_idea_acceptance_coverage/prompt.md +12 -0
- devflow_engine/prompts/integration/validate/code_repair/prompt.md +13 -0
- devflow_engine/prompts/integration/validate/example.md +143 -0
- devflow_engine/prompts/integration/validate/node_config/prompt.md +87 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/code_repair/prompt.md +19 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/node_config/prompt.md +67 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/validate_enrich_gate/prompt.md +17 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T212300/validate_repair/prompt.md +16 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/code_repair/prompt.md +10 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/node_config/prompt.md +67 -0
- devflow_engine/prompts/integration/validate/past_prompts/20260417T215032/validate_repair/prompt.md +9 -0
- devflow_engine/prompts/integration/validate/validate_enrich_gate/prompt.md +10 -0
- devflow_engine/prompts/integration/validate/validate_repair/prompt.md +20 -0
- devflow_engine/prompts/integration/write_workflows/example.md +100 -0
- devflow_engine/prompts/integration/write_workflows/node_config/prompt.md +44 -0
- devflow_engine/prompts/integration/write_workflows/past_prompts/20260417T212300/node_config/prompt.md +44 -0
- devflow_engine/prompts/integration/write_workflows/past_prompts/20260417T212300/write_workflows/prompt.md +17 -0
- devflow_engine/prompts/integration/write_workflows/write_workflows/prompt.md +11 -0
- devflow_engine/prompts/iterate/README.md +7 -0
- devflow_engine/prompts/iterate/coder/prompt.md +11 -0
- devflow_engine/prompts/iterate/framer/prompt.md +11 -0
- devflow_engine/prompts/iterate/iterator/prompt.md +13 -0
- devflow_engine/prompts/iterate/observer/prompt.md +11 -0
- devflow_engine/prompts/recovery/diagnosis/prompt.md +7 -0
- devflow_engine/prompts/recovery/execution/prompt.md +8 -0
- devflow_engine/prompts/recovery/execution_verification/prompt.md +7 -0
- devflow_engine/prompts/recovery/failure_investigation/prompt.md +10 -0
- devflow_engine/prompts/recovery/preflight_health_repo_repair/prompt.md +8 -0
- devflow_engine/prompts/recovery/remediation_execution/prompt.md +11 -0
- devflow_engine/prompts/recovery/root_cause_investigation/prompt.md +12 -0
- devflow_engine/prompts/scope_idea/doctrine/prompt.md +7 -0
- devflow_engine/prompts/source_doc_eval/document/prompt.md +6 -0
- devflow_engine/prompts/source_doc_eval/targeted_mutation/prompt.md +9 -0
- devflow_engine/prompts/source_doc_mutation/domain_entities/prompt.md +6 -0
- devflow_engine/prompts/source_doc_mutation/product_brief/prompt.md +6 -0
- devflow_engine/prompts/source_doc_mutation/project_doc_coherence/prompt.md +7 -0
- devflow_engine/prompts/source_doc_mutation/project_doc_render/prompt.md +9 -0
- devflow_engine/prompts/source_doc_mutation/source_doc_coherence/prompt.md +5 -0
- devflow_engine/prompts/source_doc_mutation/source_doc_enrichment_coherence/prompt.md +6 -0
- devflow_engine/prompts/source_doc_mutation/user_workflows/prompt.md +6 -0
- devflow_engine/prompts/source_scope/doctrine/prompt.md +10 -0
- devflow_engine/prompts/ui_grounding/doctrine/prompt.md +7 -0
- devflow_engine/recovery/__init__.py +3 -0
- devflow_engine/recovery/dag.py +2609 -0
- devflow_engine/recovery/models.py +220 -0
- devflow_engine/refactor.py +93 -0
- devflow_engine/registry/__init__.py +1 -0
- devflow_engine/registry/cards.py +238 -0
- devflow_engine/registry/domain_normalize.py +60 -0
- devflow_engine/registry/effects.py +65 -0
- devflow_engine/registry/enforce_report.py +150 -0
- devflow_engine/registry/module_cards_classify.py +164 -0
- devflow_engine/registry/module_cards_draft.py +184 -0
- devflow_engine/registry/module_cards_gate.py +59 -0
- devflow_engine/registry/packages.py +347 -0
- devflow_engine/registry/pathways.py +323 -0
- devflow_engine/review/__init__.py +11 -0
- devflow_engine/review/dag.py +588 -0
- devflow_engine/review/review_story.py +67 -0
- devflow_engine/scope_idea/__init__.py +3 -0
- devflow_engine/scope_idea/agentic.py +39 -0
- devflow_engine/scope_idea/dag.py +1069 -0
- devflow_engine/scope_idea/models.py +175 -0
- devflow_engine/skills/builtins/devflow/queue_failure_investigation/SKILL.md +112 -0
- devflow_engine/skills/builtins/devflow/queue_idea_to_story/SKILL.md +120 -0
- devflow_engine/skills/builtins/devflow/queue_integration/SKILL.md +105 -0
- devflow_engine/skills/builtins/devflow/queue_recovery/SKILL.md +108 -0
- devflow_engine/skills/builtins/devflow/queue_runtime_core/SKILL.md +155 -0
- devflow_engine/skills/builtins/devflow/queue_story_implementation/SKILL.md +122 -0
- devflow_engine/skills/builtins/devin/idea_to_story_handoff/SKILL.md +120 -0
- devflow_engine/skills/builtins/devin/ideation/SKILL.md +168 -0
- devflow_engine/skills/builtins/devin/ideation/state-and-phrasing-reference.md +18 -0
- devflow_engine/skills/builtins/devin/insight/SKILL.md +22 -0
- devflow_engine/skills/registry.example.yaml +42 -0
- devflow_engine/source_doc_assumptions.py +291 -0
- devflow_engine/source_doc_mutation_dag.py +1606 -0
- devflow_engine/source_doc_mutation_eval.py +417 -0
- devflow_engine/source_doc_mutation_worker.py +25 -0
- devflow_engine/source_docs_schema.py +207 -0
- devflow_engine/source_docs_updater.py +309 -0
- devflow_engine/source_scope/__init__.py +15 -0
- devflow_engine/source_scope/agentic.py +45 -0
- devflow_engine/source_scope/dag.py +1626 -0
- devflow_engine/source_scope/models.py +177 -0
- devflow_engine/stores/__init__.py +0 -0
- devflow_engine/stores/execution_store.py +3534 -0
- devflow_engine/story/__init__.py +0 -0
- devflow_engine/story/contracts.py +160 -0
- devflow_engine/story/discovery.py +47 -0
- devflow_engine/story/evidence.py +118 -0
- devflow_engine/story/hashing.py +27 -0
- devflow_engine/story/implemented_queue_purge.py +148 -0
- devflow_engine/story/indexer.py +105 -0
- devflow_engine/story/io.py +20 -0
- devflow_engine/story/markdown_contracts.py +298 -0
- devflow_engine/story/reconciliation.py +408 -0
- devflow_engine/story/validate_stories.py +149 -0
- devflow_engine/story/validate_tests_story.py +512 -0
- devflow_engine/story/validation.py +133 -0
- devflow_engine/ui_grounding/__init__.py +11 -0
- devflow_engine/ui_grounding/agentic.py +31 -0
- devflow_engine/ui_grounding/dag.py +874 -0
- devflow_engine/ui_grounding/models.py +224 -0
- devflow_engine/ui_grounding/pencil_bridge.py +247 -0
- devflow_engine/vendor/__init__.py +0 -0
- devflow_engine/vendor/datalumina_genai/__init__.py +11 -0
- devflow_engine/vendor/datalumina_genai/core/__init__.py +0 -0
- devflow_engine/vendor/datalumina_genai/core/exceptions.py +9 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/__init__.py +0 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/agent.py +48 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/agent_streaming_node.py +26 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/base.py +89 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/concurrent.py +30 -0
- devflow_engine/vendor/datalumina_genai/core/nodes/router.py +69 -0
- devflow_engine/vendor/datalumina_genai/core/schema.py +72 -0
- devflow_engine/vendor/datalumina_genai/core/task.py +52 -0
- devflow_engine/vendor/datalumina_genai/core/validate.py +139 -0
- devflow_engine/vendor/datalumina_genai/core/workflow.py +200 -0
- devflow_engine/worker.py +1086 -0
- devflow_engine/worker_guard.py +233 -0
- devflow_engine-1.0.0.dist-info/METADATA +235 -0
- devflow_engine-1.0.0.dist-info/RECORD +393 -0
- devflow_engine-1.0.0.dist-info/WHEEL +4 -0
- devflow_engine-1.0.0.dist-info/entry_points.txt +3 -0
- devin/__init__.py +6 -0
- devin/dag.py +58 -0
- devin/dag_two_arm.py +138 -0
- devin/devin_chat_scenario_catalog.json +588 -0
- devin/devin_eval.py +677 -0
- devin/nodes/__init__.py +0 -0
- devin/nodes/ideation/__init__.py +0 -0
- devin/nodes/ideation/node.py +195 -0
- devin/nodes/ideation/playground.py +267 -0
- devin/nodes/ideation/prompt.md +65 -0
- devin/nodes/ideation/scenarios/continue_refinement.py +13 -0
- devin/nodes/ideation/scenarios/continue_refinement_evals.py +18 -0
- devin/nodes/ideation/scenarios/idea_fits_existing_patterns.py +17 -0
- devin/nodes/ideation/scenarios/idea_fits_existing_patterns_evals.py +16 -0
- devin/nodes/ideation/scenarios/large_idea_split.py +4 -0
- devin/nodes/ideation/scenarios/large_idea_split_evals.py +17 -0
- devin/nodes/ideation/scenarios/source_documentation_added.py +4 -0
- devin/nodes/ideation/scenarios/source_documentation_added_evals.py +16 -0
- devin/nodes/ideation/scenarios/user_says_create_it.py +30 -0
- devin/nodes/ideation/scenarios/user_says_create_it_evals.py +23 -0
- devin/nodes/ideation/scenarios/vague_idea.py +16 -0
- devin/nodes/ideation/scenarios/vague_idea_evals.py +47 -0
- devin/nodes/ideation/tools.json +312 -0
- devin/nodes/insight/__init__.py +0 -0
- devin/nodes/insight/node.py +49 -0
- devin/nodes/insight/playground.py +154 -0
- devin/nodes/insight/prompt.md +61 -0
- devin/nodes/insight/scenarios/architecture_pattern_query.py +15 -0
- devin/nodes/insight/scenarios/architecture_pattern_query_evals.py +25 -0
- devin/nodes/insight/scenarios/codebase_exploration.py +15 -0
- devin/nodes/insight/scenarios/codebase_exploration_evals.py +23 -0
- devin/nodes/insight/scenarios/devin_ideation_routing.py +19 -0
- devin/nodes/insight/scenarios/devin_ideation_routing_evals.py +39 -0
- devin/nodes/insight/scenarios/devin_insight_routing.py +20 -0
- devin/nodes/insight/scenarios/devin_insight_routing_evals.py +40 -0
- devin/nodes/insight/scenarios/operational_debugging.py +15 -0
- devin/nodes/insight/scenarios/operational_debugging_evals.py +23 -0
- devin/nodes/insight/scenarios/operational_question.py +9 -0
- devin/nodes/insight/scenarios/operational_question_evals.py +8 -0
- devin/nodes/insight/scenarios/queue_status.py +15 -0
- devin/nodes/insight/scenarios/queue_status_evals.py +23 -0
- devin/nodes/insight/scenarios/source_doc_explanation.py +14 -0
- devin/nodes/insight/scenarios/source_doc_explanation_evals.py +21 -0
- devin/nodes/insight/scenarios/worker_state_check.py +15 -0
- devin/nodes/insight/scenarios/worker_state_check_evals.py +22 -0
- devin/nodes/insight/tools.json +126 -0
- devin/nodes/intake/__init__.py +0 -0
- devin/nodes/intake/node.py +27 -0
- devin/nodes/intake/playground.py +47 -0
- devin/nodes/intake/prompt.md +12 -0
- devin/nodes/intake/scenarios/ideation_routing.py +4 -0
- devin/nodes/intake/scenarios/ideation_routing_evals.py +5 -0
- devin/nodes/intake/scenarios/insight_routing.py +4 -0
- devin/nodes/intake/scenarios/insight_routing_evals.py +5 -0
- devin/nodes/iterate/README.md +44 -0
- devin/nodes/iterate/__init__.py +1 -0
- devin/nodes/iterate/_archived_design_stages/01-objectives-requirements.md +112 -0
- devin/nodes/iterate/_archived_design_stages/02-evals.md +131 -0
- devin/nodes/iterate/_archived_design_stages/03-tools-and-boundaries.md +110 -0
- devin/nodes/iterate/_archived_design_stages/04-harness-and-playground.md +32 -0
- devin/nodes/iterate/_archived_design_stages/05-prompt-deferred.md +11 -0
- devin/nodes/iterate/_archived_design_stages/coder_agent_design/01-objectives-requirements.md +20 -0
- devin/nodes/iterate/_archived_design_stages/coder_agent_design/02-evals.md +8 -0
- devin/nodes/iterate/_archived_design_stages/coder_agent_design/03-tools-and-boundaries.md +14 -0
- devin/nodes/iterate/_archived_design_stages/coder_agent_design/04-harness-and-playground.md +12 -0
- devin/nodes/iterate/_archived_design_stages/framer_agent_design/01-objectives-requirements.md +20 -0
- devin/nodes/iterate/_archived_design_stages/framer_agent_design/02-evals.md +8 -0
- devin/nodes/iterate/_archived_design_stages/framer_agent_design/03-tools-and-boundaries.md +13 -0
- devin/nodes/iterate/_archived_design_stages/framer_agent_design/04-harness-and-playground.md +12 -0
- devin/nodes/iterate/_archived_design_stages/iterator_agent_design/01-objectives-requirements.md +25 -0
- devin/nodes/iterate/_archived_design_stages/iterator_agent_design/02-evals.md +9 -0
- devin/nodes/iterate/_archived_design_stages/iterator_agent_design/03-tools-and-boundaries.md +14 -0
- devin/nodes/iterate/_archived_design_stages/iterator_agent_design/04-harness-and-playground.md +12 -0
- devin/nodes/iterate/_archived_design_stages/observer_agent_design/01-objectives-requirements.md +20 -0
- devin/nodes/iterate/_archived_design_stages/observer_agent_design/02-evals.md +8 -0
- devin/nodes/iterate/_archived_design_stages/observer_agent_design/03-tools-and-boundaries.md +14 -0
- devin/nodes/iterate/_archived_design_stages/observer_agent_design/04-harness-and-playground.md +13 -0
- devin/nodes/iterate/agent-roles.md +89 -0
- devin/nodes/iterate/agents/README.md +10 -0
- devin/nodes/iterate/artifacts.md +504 -0
- devin/nodes/iterate/contract.md +100 -0
- devin/nodes/iterate/eval-plan.md +74 -0
- devin/nodes/iterate/node.py +100 -0
- devin/nodes/iterate/pipeline/README.md +13 -0
- devin/nodes/iterate/playground-contract.md +76 -0
- devin/nodes/iterate/prompt.md +11 -0
- devin/nodes/iterate/scenarios/README.md +38 -0
- devin/nodes/iterate/scenarios/artifact-and-loop-scenarios.md +101 -0
- devin/nodes/iterate/scenarios/coder_artifact_alignment.py +32 -0
- devin/nodes/iterate/scenarios/coder_artifact_alignment_evals.py +45 -0
- devin/nodes/iterate/scenarios/coder_bounded_fix.py +27 -0
- devin/nodes/iterate/scenarios/coder_bounded_fix_evals.py +45 -0
- devin/nodes/iterate/scenarios/devin_iterate_routing.py +21 -0
- devin/nodes/iterate/scenarios/devin_iterate_routing_evals.py +36 -0
- devin/nodes/iterate/scenarios/framer_scope_boundary.py +25 -0
- devin/nodes/iterate/scenarios/framer_scope_boundary_evals.py +57 -0
- devin/nodes/iterate/scenarios/framer_task_framing.py +25 -0
- devin/nodes/iterate/scenarios/framer_task_framing_evals.py +58 -0
- devin/nodes/iterate/scenarios/iterate_error_fix.py +21 -0
- devin/nodes/iterate/scenarios/iterate_error_fix_evals.py +39 -0
- devin/nodes/iterate/scenarios/iterate_quick_change.py +21 -0
- devin/nodes/iterate/scenarios/iterate_quick_change_evals.py +35 -0
- devin/nodes/iterate/scenarios/iterate_to_idea_promotion.py +23 -0
- devin/nodes/iterate/scenarios/iterate_to_idea_promotion_evals.py +53 -0
- devin/nodes/iterate/scenarios/iterate_to_insight_reroute.py +23 -0
- devin/nodes/iterate/scenarios/iterate_to_insight_reroute_evals.py +53 -0
- devin/nodes/iterate/scenarios/observer_evidence_seam.py +28 -0
- devin/nodes/iterate/scenarios/observer_evidence_seam_evals.py +55 -0
- devin/nodes/iterate/scenarios/observer_repro_creation.py +28 -0
- devin/nodes/iterate/scenarios/observer_repro_creation_evals.py +45 -0
- devin/nodes/iterate/scenarios/routing-matrix.md +45 -0
- devin/nodes/shared/__init__.py +0 -0
- devin/nodes/shared/filemaker_expert.md +80 -0
- devin/nodes/shared/filemaker_expert.py +354 -0
- devin/nodes/shared/filemaker_expert_eval/runner.py +176 -0
- devin/nodes/shared/filemaker_expert_eval/scenarios.json +65 -0
- devin/nodes/shared/goldilocks_advisor_eval/runner.py +214 -0
- devin/nodes/shared/goldilocks_advisor_eval/scenarios.json +58 -0
- devin/nodes/shared/helpers.py +156 -0
- devin/nodes/shared/idea_compliance_advisor_eval/runner.py +252 -0
- devin/nodes/shared/idea_compliance_advisor_eval/scenarios.json +75 -0
- devin/nodes/shared/models.py +44 -0
- devin/nodes/shared/post.py +40 -0
- devin/nodes/shared/router.py +107 -0
- devin/nodes/shared/tools.py +191 -0
- devin/shared/devin-chat-rubric.md +237 -0
- devin/shared/devin-chat-scenario-suite.md +90 -0
- 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
|