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,1069 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import UTC, datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
from urllib.parse import quote
|
|
12
|
+
from urllib.request import Request, urlopen
|
|
13
|
+
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
|
|
16
|
+
from ..devflow_state import publish_devflow_state
|
|
17
|
+
from ..idea.paths import get_idea_paths
|
|
18
|
+
from ..stores.execution_store import ExecutionStore
|
|
19
|
+
from ..vendor.datalumina_genai.core.nodes.agent import AgentConfig, AgentNode
|
|
20
|
+
from ..vendor.datalumina_genai.core.nodes.base import Node
|
|
21
|
+
from ..vendor.datalumina_genai.core.nodes.router import BaseRouter, RouterNode
|
|
22
|
+
from ..vendor.datalumina_genai.core.schema import NodeConfig, WorkflowSchema
|
|
23
|
+
from ..vendor.datalumina_genai.core.task import TaskContext
|
|
24
|
+
from ..vendor.datalumina_genai.core.workflow import Workflow
|
|
25
|
+
from . import agentic as scope_idea_agentic
|
|
26
|
+
from .models import (
|
|
27
|
+
ArtifactLineage,
|
|
28
|
+
GoldilocksAssessmentArtifact,
|
|
29
|
+
GoldilocksDecisionArtifact,
|
|
30
|
+
IdeaCandidateArtifact,
|
|
31
|
+
IdeaCandidateSetArtifact,
|
|
32
|
+
IdeaRegistryRecordArtifact,
|
|
33
|
+
IdeaResolutionPackageArtifact,
|
|
34
|
+
IdeaSplitPlanArtifact,
|
|
35
|
+
NarrowScopeReviewArtifact,
|
|
36
|
+
RegisteredIdeaArtifact,
|
|
37
|
+
ScopeContextArtifact,
|
|
38
|
+
ScopeIdeaDagSummary,
|
|
39
|
+
SourceEvidenceRef,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
DAG_ID = "scope_to_idea_dag"
|
|
43
|
+
|
|
44
|
+
_CURRENT_STORE: ExecutionStore | None = None
|
|
45
|
+
_CURRENT_RUN_ID: str | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _dfs_running(*, project_id: str, run_id: str, scope_id: str, summary: str = "Generating ideas from scopes") -> None:
|
|
49
|
+
publish_devflow_state(
|
|
50
|
+
project_id=project_id,
|
|
51
|
+
run_id=run_id,
|
|
52
|
+
current_state="running",
|
|
53
|
+
current_status="processing",
|
|
54
|
+
run_summary=summary,
|
|
55
|
+
display="project",
|
|
56
|
+
display_path=f"scope:{scope_id}",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _dfs_terminal(
|
|
61
|
+
*,
|
|
62
|
+
project_id: str,
|
|
63
|
+
run_id: str,
|
|
64
|
+
scope_id: str,
|
|
65
|
+
current_state: str,
|
|
66
|
+
current_status: str,
|
|
67
|
+
summary: str,
|
|
68
|
+
error_message: str | None = None,
|
|
69
|
+
) -> None:
|
|
70
|
+
publish_devflow_state(
|
|
71
|
+
project_id=project_id,
|
|
72
|
+
run_id=run_id,
|
|
73
|
+
current_state=current_state,
|
|
74
|
+
current_status=current_status,
|
|
75
|
+
run_summary=summary,
|
|
76
|
+
error_message=error_message,
|
|
77
|
+
display="project",
|
|
78
|
+
display_path=f"scope:{scope_id}",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _keychain_get(service: str, account: str) -> str | None:
|
|
83
|
+
try:
|
|
84
|
+
proc = subprocess.run(
|
|
85
|
+
["security", "find-generic-password", "-s", service, "-a", account, "-w"],
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
check=False,
|
|
89
|
+
timeout=10,
|
|
90
|
+
)
|
|
91
|
+
except Exception:
|
|
92
|
+
return None
|
|
93
|
+
if proc.returncode != 0:
|
|
94
|
+
return None
|
|
95
|
+
value = proc.stdout.strip()
|
|
96
|
+
return value or None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _resolve_supabase_rest_config() -> tuple[str, str] | None:
|
|
100
|
+
url = (
|
|
101
|
+
os.environ.get("DEVFLOW_SUPABASE_URL")
|
|
102
|
+
or os.environ.get("SUPABASE_URL")
|
|
103
|
+
or _keychain_get("Supabase URL", "Clarity")
|
|
104
|
+
)
|
|
105
|
+
key = (
|
|
106
|
+
os.environ.get("DEVFLOW_SUPABASE_SERVICE_KEY")
|
|
107
|
+
or os.environ.get("SUPABASE_SERVICE_ROLE_KEY")
|
|
108
|
+
or os.environ.get("SUPABASE_SERVICE_KEY")
|
|
109
|
+
or _keychain_get("Supabase Service Key", "Clarity")
|
|
110
|
+
)
|
|
111
|
+
if not url or not key:
|
|
112
|
+
return None
|
|
113
|
+
return url.rstrip("/"), key
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _postgrest_request(*, method: str, url: str, key: str, body: Any | None = None, prefer: str | None = None) -> Any:
|
|
117
|
+
payload = None if body is None else json.dumps(body).encode("utf-8")
|
|
118
|
+
req = Request(url, data=payload, method=method)
|
|
119
|
+
req.add_header("apikey", key)
|
|
120
|
+
req.add_header("Authorization", f"Bearer {key}")
|
|
121
|
+
if body is not None:
|
|
122
|
+
req.add_header("Content-Type", "application/json")
|
|
123
|
+
if prefer:
|
|
124
|
+
req.add_header("Prefer", prefer)
|
|
125
|
+
with urlopen(req, timeout=30) as resp:
|
|
126
|
+
raw = resp.read().decode("utf-8")
|
|
127
|
+
return json.loads(raw) if raw else None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _sync_registered_ideas_to_supabase(*, project_id: str, scope_id: str, run_id: str, pipeline_dir: Path) -> None:
|
|
131
|
+
config = _resolve_supabase_rest_config()
|
|
132
|
+
if config is None:
|
|
133
|
+
return
|
|
134
|
+
url, key = config
|
|
135
|
+
registry_path = pipeline_dir / "idea_registry_record.json"
|
|
136
|
+
if not registry_path.exists():
|
|
137
|
+
return
|
|
138
|
+
registry = json.loads(registry_path.read_text(encoding="utf-8"))
|
|
139
|
+
ideas = registry.get("registered_ideas") or []
|
|
140
|
+
rows: list[dict[str, Any]] = []
|
|
141
|
+
for idea in ideas:
|
|
142
|
+
if not isinstance(idea, dict) or not idea.get("idea_id"):
|
|
143
|
+
continue
|
|
144
|
+
persisted: dict[str, Any] = {}
|
|
145
|
+
idea_ref = idea.get("idea_ref")
|
|
146
|
+
if isinstance(idea_ref, str) and Path(idea_ref).exists():
|
|
147
|
+
persisted = json.loads(Path(idea_ref).read_text(encoding="utf-8"))
|
|
148
|
+
payload = persisted.get("idea") if isinstance(persisted.get("idea"), dict) else {}
|
|
149
|
+
rows.append(
|
|
150
|
+
{
|
|
151
|
+
"idea_id": str(idea.get("idea_id")),
|
|
152
|
+
"project_id": project_id,
|
|
153
|
+
"scope_set_id": registry.get("scope_set_id") or persisted.get("scope_set_id"),
|
|
154
|
+
"scope_id": scope_id,
|
|
155
|
+
"run_id": run_id,
|
|
156
|
+
"title": str(
|
|
157
|
+
idea.get("title")
|
|
158
|
+
or persisted.get("title")
|
|
159
|
+
or payload.get("title")
|
|
160
|
+
or payload.get("goal")
|
|
161
|
+
or payload.get("problem")
|
|
162
|
+
or idea.get("idea_id")
|
|
163
|
+
),
|
|
164
|
+
"summary": (
|
|
165
|
+
idea.get("summary")
|
|
166
|
+
or persisted.get("summary")
|
|
167
|
+
or payload.get("summary")
|
|
168
|
+
or payload.get("goal")
|
|
169
|
+
or payload.get("problem")
|
|
170
|
+
),
|
|
171
|
+
"status": idea.get("status") or persisted.get("status"),
|
|
172
|
+
"shape": idea.get("shape") or registry.get("scope_shape"),
|
|
173
|
+
"resolution_status": registry.get("resolution_status"),
|
|
174
|
+
"origin": "scope_to_idea",
|
|
175
|
+
"artifact_path": str(idea_ref or registry_path),
|
|
176
|
+
"updated_at": datetime.now(UTC).isoformat(),
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
delete_url = f"{url}/rest/v1/devflow_project_ideas?project_id=eq.{quote(project_id)}&scope_id=eq.{quote(scope_id)}"
|
|
180
|
+
_postgrest_request(method="DELETE", url=delete_url, key=key)
|
|
181
|
+
if rows:
|
|
182
|
+
upsert_url = f"{url}/rest/v1/devflow_project_ideas?on_conflict=idea_id"
|
|
183
|
+
_postgrest_request(method="POST", url=upsert_url, key=key, body=rows, prefer="resolution=merge-duplicates")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass(frozen=True)
|
|
187
|
+
class ScopeToIdeaDagResult:
|
|
188
|
+
exit_code: int
|
|
189
|
+
run_id: str
|
|
190
|
+
pipeline_dir: Path
|
|
191
|
+
message: str
|
|
192
|
+
outcome: dict[str, Any]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class ScopeToIdeaDagEvent(BaseModel):
|
|
196
|
+
repo_root: str
|
|
197
|
+
project_id: str
|
|
198
|
+
scope_set_id: str
|
|
199
|
+
scope_id: str
|
|
200
|
+
scope_payload_path: str | None = None
|
|
201
|
+
scope_payload_inline: dict[str, Any] | None = None
|
|
202
|
+
pipeline_key: str
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _store_run() -> tuple[ExecutionStore, str]:
|
|
206
|
+
if _CURRENT_STORE is None or _CURRENT_RUN_ID is None:
|
|
207
|
+
raise RuntimeError("scope->idea dag missing runtime store/run_id")
|
|
208
|
+
return _CURRENT_STORE, _CURRENT_RUN_ID
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _stable_hash(payload: Any) -> str:
|
|
212
|
+
return hashlib.sha256(json.dumps(payload, sort_keys=True).encode("utf-8")).hexdigest()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _stable_id(prefix: str, payload: Any, *, size: int = 12) -> str:
|
|
216
|
+
return f"{prefix}{_stable_hash(payload)[:size]}"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
220
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _append_node_trace(*, pipeline_root: Path, node: str, event: str, detail: dict[str, Any] | None = None) -> None:
|
|
225
|
+
path = pipeline_root / "node_trace.jsonl"
|
|
226
|
+
record = {
|
|
227
|
+
"ts": datetime.now(UTC).isoformat(),
|
|
228
|
+
"node": node,
|
|
229
|
+
"event": event,
|
|
230
|
+
}
|
|
231
|
+
if detail:
|
|
232
|
+
record["detail"] = detail
|
|
233
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
234
|
+
with path.open("a", encoding="utf-8") as fh:
|
|
235
|
+
fh.write(json.dumps(record, sort_keys=True) + "\n")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _agent_timeout_seconds() -> int:
|
|
239
|
+
raw = os.environ.get("DEVFLOW_SCOPE_IDEA_AGENT_TIMEOUT")
|
|
240
|
+
if not raw:
|
|
241
|
+
return 300
|
|
242
|
+
try:
|
|
243
|
+
value = int(raw)
|
|
244
|
+
except ValueError:
|
|
245
|
+
return 300
|
|
246
|
+
return value if value > 0 else 300
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _relative_to_repo(repo_root: Path, path: Path) -> str:
|
|
250
|
+
try:
|
|
251
|
+
return str(path.relative_to(repo_root))
|
|
252
|
+
except ValueError:
|
|
253
|
+
return str(path)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _lineage_entry(
|
|
257
|
+
*,
|
|
258
|
+
stage: str,
|
|
259
|
+
origin: str,
|
|
260
|
+
mode: str,
|
|
261
|
+
repo_root: Path,
|
|
262
|
+
artifact_path: Path | None = None,
|
|
263
|
+
agent_run_path: Path | None = None,
|
|
264
|
+
generated_from: list[str] | None = None,
|
|
265
|
+
notes: list[str] | None = None,
|
|
266
|
+
metadata: dict[str, Any] | None = None,
|
|
267
|
+
) -> ArtifactLineage:
|
|
268
|
+
return ArtifactLineage(
|
|
269
|
+
stage=stage,
|
|
270
|
+
origin=origin, # type: ignore[arg-type]
|
|
271
|
+
mode=mode,
|
|
272
|
+
artifact_path=_relative_to_repo(repo_root, artifact_path) if artifact_path else None,
|
|
273
|
+
agent_run_ref=_relative_to_repo(repo_root, agent_run_path) if agent_run_path else None,
|
|
274
|
+
generated_from=generated_from or [],
|
|
275
|
+
notes=notes or [],
|
|
276
|
+
metadata=metadata or {},
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _run_scope_idea_agent(
|
|
281
|
+
*,
|
|
282
|
+
repo_root: Path,
|
|
283
|
+
pipeline_root: Path,
|
|
284
|
+
stage_name: str,
|
|
285
|
+
output_model: type[BaseModel],
|
|
286
|
+
context_payload: dict[str, Any],
|
|
287
|
+
guidance: list[str],
|
|
288
|
+
) -> tuple[BaseModel, ArtifactLineage]:
|
|
289
|
+
artifact, envelope = scope_idea_agentic.run_scope_idea_agent_step(
|
|
290
|
+
repo_root=repo_root,
|
|
291
|
+
stage_name=stage_name,
|
|
292
|
+
output_model=output_model,
|
|
293
|
+
context_payload=context_payload,
|
|
294
|
+
guidance=guidance,
|
|
295
|
+
timeout_seconds=_agent_timeout_seconds(),
|
|
296
|
+
)
|
|
297
|
+
agent_run_path = scope_idea_agentic.persist_agent_run(pipeline_root=pipeline_root, node_id=stage_name, envelope=envelope)
|
|
298
|
+
return artifact, _lineage_entry(
|
|
299
|
+
stage=stage_name,
|
|
300
|
+
origin="model",
|
|
301
|
+
mode="agent",
|
|
302
|
+
repo_root=repo_root,
|
|
303
|
+
agent_run_path=agent_run_path,
|
|
304
|
+
notes=["Artifact was produced from the model-backed execution path."],
|
|
305
|
+
metadata={"model_backed": True},
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _pipeline_root(repo_root: Path, *, scope_id: str, pipeline_key: str) -> Path:
|
|
310
|
+
return repo_root / ".devflow" / "scopes" / scope_id / "pipelines" / DAG_ID / pipeline_key
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def build_pipeline_key(*, repo_root: Path, project_id: str, scope_set_id: str, scope_id: str, scope_payload: dict[str, Any]) -> str:
|
|
314
|
+
return _stable_id(
|
|
315
|
+
"run_",
|
|
316
|
+
{
|
|
317
|
+
"repo_root": str(repo_root),
|
|
318
|
+
"project_id": project_id,
|
|
319
|
+
"scope_set_id": scope_set_id,
|
|
320
|
+
"scope_id": scope_id,
|
|
321
|
+
"scope_payload": scope_payload,
|
|
322
|
+
},
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _load_scope_payload(*, inline_payload: dict[str, Any] | None, payload_path: Path | None) -> dict[str, Any]:
|
|
327
|
+
if inline_payload and payload_path:
|
|
328
|
+
raise ValueError("Provide exactly one of scope_payload_inline or scope_payload_path")
|
|
329
|
+
if payload_path:
|
|
330
|
+
return json.loads(payload_path.read_text(encoding="utf-8"))
|
|
331
|
+
if inline_payload:
|
|
332
|
+
return dict(inline_payload)
|
|
333
|
+
raise ValueError("Missing scope payload for scope->idea DAG")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _scope_payload_item(payload: dict[str, Any]) -> dict[str, Any]:
|
|
337
|
+
scope_item = payload.get("scope_item")
|
|
338
|
+
if isinstance(scope_item, dict):
|
|
339
|
+
return scope_item
|
|
340
|
+
return payload
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _coerce_source_support(items: Any) -> list[SourceEvidenceRef]:
|
|
344
|
+
if not isinstance(items, list):
|
|
345
|
+
return []
|
|
346
|
+
refs: list[SourceEvidenceRef] = []
|
|
347
|
+
for item in items:
|
|
348
|
+
if isinstance(item, dict):
|
|
349
|
+
refs.append(SourceEvidenceRef.model_validate(item))
|
|
350
|
+
elif isinstance(item, str) and item.strip():
|
|
351
|
+
refs.append(SourceEvidenceRef(ref=item.strip()))
|
|
352
|
+
return refs
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _scope_context_from_payload(*, payload: dict[str, Any], project_id: str, scope_set_id: str, scope_id: str) -> ScopeContextArtifact:
|
|
356
|
+
scope_item = _scope_payload_item(payload)
|
|
357
|
+
scope_title = str(scope_item.get("title") or payload.get("scope_title") or payload.get("title") or scope_id).strip()
|
|
358
|
+
scope_description = str(scope_item.get("description") or payload.get("scope_description") or payload.get("description") or "").strip()
|
|
359
|
+
assumptions = scope_item.get("assumptions")
|
|
360
|
+
if not isinstance(assumptions, list):
|
|
361
|
+
assumptions = payload.get("assumptions")
|
|
362
|
+
constraints = scope_item.get("cross_cutting_constraints")
|
|
363
|
+
if not isinstance(constraints, list):
|
|
364
|
+
constraints = payload.get("cross_cutting_constraints")
|
|
365
|
+
neighbor_scope_refs = scope_item.get("neighbor_scope_refs")
|
|
366
|
+
if not isinstance(neighbor_scope_refs, list):
|
|
367
|
+
neighbor_scope_refs = payload.get("neighbor_scope_refs")
|
|
368
|
+
source_support = scope_item.get("source_support")
|
|
369
|
+
if not isinstance(source_support, list):
|
|
370
|
+
source_support = payload.get("source_support")
|
|
371
|
+
|
|
372
|
+
artifact = ScopeContextArtifact(
|
|
373
|
+
project_id=project_id,
|
|
374
|
+
scope_set_id=scope_set_id,
|
|
375
|
+
scope_id=scope_id,
|
|
376
|
+
scope_title=scope_title,
|
|
377
|
+
scope_description=scope_description,
|
|
378
|
+
source_support=_coerce_source_support(source_support),
|
|
379
|
+
assumptions=[str(item) for item in assumptions or []],
|
|
380
|
+
cross_cutting_constraints=[str(item) for item in constraints or []],
|
|
381
|
+
neighbor_scope_refs=[str(item) for item in neighbor_scope_refs or []],
|
|
382
|
+
approval_status=str(scope_item.get("approval_status") or payload.get("approval_status") or payload.get("status") or "approved"),
|
|
383
|
+
)
|
|
384
|
+
if not artifact.scope_title:
|
|
385
|
+
raise ValueError(f"Scope payload for {scope_id} is missing scope_item.title/title")
|
|
386
|
+
if not artifact.scope_description:
|
|
387
|
+
raise ValueError(f"Scope payload for {scope_id} is missing scope_item.description/description")
|
|
388
|
+
return artifact
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _build_constraints(scope: ScopeContextArtifact, *, default_constraint: str) -> list[str]:
|
|
392
|
+
constraints = [item for item in scope.cross_cutting_constraints if item]
|
|
393
|
+
if default_constraint not in constraints:
|
|
394
|
+
constraints.append(default_constraint)
|
|
395
|
+
return constraints
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _build_traceability(scope: ScopeContextArtifact, *, extras: list[str] | None = None) -> list[str]:
|
|
399
|
+
traceability = [
|
|
400
|
+
f"project:{scope.project_id}",
|
|
401
|
+
f"scope_set:{scope.scope_set_id}",
|
|
402
|
+
f"scope:{scope.scope_id}",
|
|
403
|
+
]
|
|
404
|
+
for source_ref in scope.source_support:
|
|
405
|
+
traceability.append(f"source:{source_ref.ref}")
|
|
406
|
+
if extras:
|
|
407
|
+
traceability.extend(extras)
|
|
408
|
+
return traceability
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
class LoadApprovedScopeItemNode(Node):
|
|
412
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
413
|
+
event = task_context.event
|
|
414
|
+
repo_root = Path(event.repo_root)
|
|
415
|
+
_dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Loading approved scope")
|
|
416
|
+
payload = _load_scope_payload(
|
|
417
|
+
inline_payload=event.scope_payload_inline,
|
|
418
|
+
payload_path=Path(event.scope_payload_path) if event.scope_payload_path else None,
|
|
419
|
+
)
|
|
420
|
+
artifact = _scope_context_from_payload(
|
|
421
|
+
payload=payload,
|
|
422
|
+
project_id=event.project_id,
|
|
423
|
+
scope_set_id=event.scope_set_id,
|
|
424
|
+
scope_id=event.scope_id,
|
|
425
|
+
)
|
|
426
|
+
stage_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "scope_context.json"
|
|
427
|
+
_write_json(stage_path, artifact.model_dump())
|
|
428
|
+
task_context.metadata["scope_context"] = artifact
|
|
429
|
+
self.save_output(artifact)
|
|
430
|
+
return task_context
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class AssessScopeGoldilocksNode(AgentNode):
|
|
434
|
+
def get_agent_config(self) -> AgentConfig:
|
|
435
|
+
return AgentConfig(
|
|
436
|
+
instructions="Assess whether the approved scope is too broad, just right, or too narrow.",
|
|
437
|
+
output_type=GoldilocksAssessmentArtifact,
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
441
|
+
event = task_context.event
|
|
442
|
+
repo_root = Path(event.repo_root)
|
|
443
|
+
_dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Assessing scope goldilocks")
|
|
444
|
+
pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
|
|
445
|
+
scope = task_context.metadata["scope_context"]
|
|
446
|
+
artifact, execution_lineage = _run_scope_idea_agent(
|
|
447
|
+
repo_root=repo_root,
|
|
448
|
+
pipeline_root=pipeline_root,
|
|
449
|
+
stage_name="assess_scope_goldilocks",
|
|
450
|
+
output_model=GoldilocksAssessmentArtifact,
|
|
451
|
+
context_payload={
|
|
452
|
+
"project_id": scope.project_id,
|
|
453
|
+
"scope_set_id": scope.scope_set_id,
|
|
454
|
+
"scope_id": scope.scope_id,
|
|
455
|
+
"approved_scope": scope.model_dump(),
|
|
456
|
+
},
|
|
457
|
+
guidance=[
|
|
458
|
+
"Assess whether the approved scope is too broad, just right, or too narrow for one idea candidate.",
|
|
459
|
+
"Treat the approved scope as the anchor. Do not rewrite or broaden its intent.",
|
|
460
|
+
"Set split_recommended true only when the approved scope clearly bundles multiple distinct business outcomes that a human would recognize as separate planning units.",
|
|
461
|
+
"Do not split merely because the scope contains multiple sub-workflows, metrics, linked entities, lifecycle steps, or supporting artifact types.",
|
|
462
|
+
"Should-not-split examples: customer management, reporting, documents and photos, and job management when they still describe one shared business slice.",
|
|
463
|
+
"Should-split example: quotes when internal composition and customer delivery/approval are clearly separate outcome surfaces.",
|
|
464
|
+
"Keep canonical idea sufficiency downstream; this node judges boundary size only.",
|
|
465
|
+
],
|
|
466
|
+
)
|
|
467
|
+
artifact_data = artifact.model_dump()
|
|
468
|
+
notes = list(artifact_data.get("notes") or [])
|
|
469
|
+
notes.extend(execution_lineage.notes)
|
|
470
|
+
artifact_data["notes"] = list(dict.fromkeys(notes))
|
|
471
|
+
artifact = GoldilocksAssessmentArtifact.model_validate(artifact_data)
|
|
472
|
+
stage_path = pipeline_root / "goldilocks_assessment.json"
|
|
473
|
+
artifact.execution_lineage = execution_lineage.model_copy(
|
|
474
|
+
update={
|
|
475
|
+
"artifact_path": _relative_to_repo(repo_root, stage_path),
|
|
476
|
+
"generated_from": [_relative_to_repo(repo_root, pipeline_root / "scope_context.json")],
|
|
477
|
+
}
|
|
478
|
+
)
|
|
479
|
+
_write_json(stage_path, artifact.model_dump())
|
|
480
|
+
task_context.metadata["goldilocks_assessment"] = artifact
|
|
481
|
+
self.save_output(artifact)
|
|
482
|
+
return task_context
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class GoldilocksDecisionNode(Node):
|
|
486
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
487
|
+
event = task_context.event
|
|
488
|
+
repo_root = Path(event.repo_root)
|
|
489
|
+
_dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Routing scope shape")
|
|
490
|
+
assessment = task_context.metadata["goldilocks_assessment"]
|
|
491
|
+
next_node = {
|
|
492
|
+
"too_broad": "SplitScopeIntoIdeaCandidatesNode",
|
|
493
|
+
"just_right": "DraftIdeaFromScopeNode",
|
|
494
|
+
"too_narrow": "NarrowScopeReviewPackageNode",
|
|
495
|
+
}[assessment.scope_shape]
|
|
496
|
+
artifact = GoldilocksDecisionArtifact(
|
|
497
|
+
decision=assessment.scope_shape,
|
|
498
|
+
reason=assessment.reasoning,
|
|
499
|
+
next_node=next_node,
|
|
500
|
+
)
|
|
501
|
+
stage_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "goldilocks_decision.json"
|
|
502
|
+
_write_json(stage_path, artifact.model_dump())
|
|
503
|
+
task_context.metadata["goldilocks_decision"] = artifact
|
|
504
|
+
self.save_output(artifact)
|
|
505
|
+
return task_context
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class _RouteTooBroad(RouterNode):
|
|
509
|
+
def determine_next_node(self, task_context: TaskContext) -> Node | None:
|
|
510
|
+
decision = task_context.metadata["goldilocks_decision"]
|
|
511
|
+
if decision.decision == "too_broad":
|
|
512
|
+
return SplitScopeIntoIdeaCandidatesNode(task_context=task_context)
|
|
513
|
+
return None
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
class _RouteJustRight(RouterNode):
|
|
517
|
+
def determine_next_node(self, task_context: TaskContext) -> Node | None:
|
|
518
|
+
decision = task_context.metadata["goldilocks_decision"]
|
|
519
|
+
if decision.decision == "just_right":
|
|
520
|
+
return DraftIdeaFromScopeNode(task_context=task_context)
|
|
521
|
+
return None
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
class _RouteTooNarrow(RouterNode):
|
|
525
|
+
def determine_next_node(self, task_context: TaskContext) -> Node | None:
|
|
526
|
+
decision = task_context.metadata["goldilocks_decision"]
|
|
527
|
+
if decision.decision == "too_narrow":
|
|
528
|
+
return NarrowScopeReviewPackageNode(task_context=task_context)
|
|
529
|
+
return None
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
class GoldilocksDecisionRouter(BaseRouter):
|
|
533
|
+
def __init__(self) -> None:
|
|
534
|
+
self.routes = [_RouteTooBroad(), _RouteJustRight(), _RouteTooNarrow()]
|
|
535
|
+
self.fallback = NarrowScopeReviewPackageNode()
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
class SplitScopeIntoIdeaCandidatesNode(AgentNode):
|
|
539
|
+
def get_agent_config(self) -> AgentConfig:
|
|
540
|
+
return AgentConfig(
|
|
541
|
+
instructions="Split broad approved scope into child idea candidates while preserving parent traceability.",
|
|
542
|
+
output_type=IdeaSplitPlanArtifact,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
546
|
+
event = task_context.event
|
|
547
|
+
repo_root = Path(event.repo_root)
|
|
548
|
+
_dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Splitting broad scope into ideas")
|
|
549
|
+
pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
|
|
550
|
+
scope = task_context.metadata["scope_context"]
|
|
551
|
+
assessment = task_context.metadata["goldilocks_assessment"]
|
|
552
|
+
artifact, execution_lineage = _run_scope_idea_agent(
|
|
553
|
+
repo_root=repo_root,
|
|
554
|
+
pipeline_root=pipeline_root,
|
|
555
|
+
stage_name="split_scope_into_idea_candidates",
|
|
556
|
+
output_model=IdeaSplitPlanArtifact,
|
|
557
|
+
context_payload={
|
|
558
|
+
"project_id": scope.project_id,
|
|
559
|
+
"scope_set_id": scope.scope_set_id,
|
|
560
|
+
"scope_id": scope.scope_id,
|
|
561
|
+
"approved_scope": scope.model_dump(),
|
|
562
|
+
"goldilocks_assessment": assessment.model_dump(),
|
|
563
|
+
},
|
|
564
|
+
guidance=[
|
|
565
|
+
"Split the approved parent scope into 2-3 coherent idea candidates only when the goldilocks assessment already says the scope is too broad.",
|
|
566
|
+
"Each child idea must stay fully inside the approved parent scope and preserve parent_scope_id plus traceability.",
|
|
567
|
+
"Do not silently create story-ready slices; produce idea candidates suitable for downstream idea sufficiency review.",
|
|
568
|
+
"Coverage_of_parent_scope should account for the parent scope bullets without drifting beyond them.",
|
|
569
|
+
"Preserve the assessment split rationale unless stronger evidence from the approved scope justifies a clearer phrasing of the same reason.",
|
|
570
|
+
"Do not split into foundation-vs-linkage ideas, metric-family ideas, or artifact-type ideas unless the approved scope itself clearly frames those as separate business outcomes.",
|
|
571
|
+
"If one child idea mostly exists to support another child idea, keep them together instead of splitting.",
|
|
572
|
+
],
|
|
573
|
+
)
|
|
574
|
+
artifact_data = artifact.model_dump()
|
|
575
|
+
risks = list(artifact_data.get("remaining_risks") or [])
|
|
576
|
+
risks.extend(execution_lineage.notes)
|
|
577
|
+
artifact_data["remaining_risks"] = list(dict.fromkeys(risks))
|
|
578
|
+
artifact = IdeaSplitPlanArtifact.model_validate(artifact_data)
|
|
579
|
+
stage_path = pipeline_root / "idea_split_plan.json"
|
|
580
|
+
artifact.execution_lineage = execution_lineage.model_copy(
|
|
581
|
+
update={
|
|
582
|
+
"artifact_path": _relative_to_repo(repo_root, stage_path),
|
|
583
|
+
"generated_from": [
|
|
584
|
+
_relative_to_repo(repo_root, pipeline_root / "scope_context.json"),
|
|
585
|
+
_relative_to_repo(repo_root, pipeline_root / "goldilocks_assessment.json"),
|
|
586
|
+
],
|
|
587
|
+
}
|
|
588
|
+
)
|
|
589
|
+
for child in artifact.child_ideas:
|
|
590
|
+
child.execution_lineage = artifact.execution_lineage
|
|
591
|
+
_write_json(stage_path, artifact.model_dump())
|
|
592
|
+
task_context.metadata["idea_split_plan"] = artifact
|
|
593
|
+
self.save_output(artifact)
|
|
594
|
+
return task_context
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class DraftIdeaFromScopeNode(AgentNode):
|
|
598
|
+
def get_agent_config(self) -> AgentConfig:
|
|
599
|
+
return AgentConfig(
|
|
600
|
+
instructions="Draft one idea from a Goldilocks-sized approved scope.",
|
|
601
|
+
output_type=IdeaCandidateArtifact,
|
|
602
|
+
)
|
|
603
|
+
|
|
604
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
605
|
+
event = task_context.event
|
|
606
|
+
repo_root = Path(event.repo_root)
|
|
607
|
+
_dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Drafting idea from scope")
|
|
608
|
+
pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
|
|
609
|
+
_append_node_trace(pipeline_root=pipeline_root, node="DraftIdeaFromScopeNode", event="enter")
|
|
610
|
+
scope = task_context.metadata["scope_context"]
|
|
611
|
+
assessment = task_context.metadata["goldilocks_assessment"]
|
|
612
|
+
try:
|
|
613
|
+
artifact, execution_lineage = _run_scope_idea_agent(
|
|
614
|
+
repo_root=repo_root,
|
|
615
|
+
pipeline_root=pipeline_root,
|
|
616
|
+
stage_name="draft_idea_from_scope",
|
|
617
|
+
output_model=IdeaCandidateArtifact,
|
|
618
|
+
context_payload={
|
|
619
|
+
"project_id": scope.project_id,
|
|
620
|
+
"scope_set_id": scope.scope_set_id,
|
|
621
|
+
"scope_id": scope.scope_id,
|
|
622
|
+
"approved_scope": scope.model_dump(),
|
|
623
|
+
"goldilocks_assessment": assessment.model_dump(),
|
|
624
|
+
},
|
|
625
|
+
guidance=[
|
|
626
|
+
"Draft exactly one idea candidate from the approved scope when the goldilocks assessment says the scope is just_right.",
|
|
627
|
+
"Keep the approved scope as the anchor; do not broaden or compress it into a different intent.",
|
|
628
|
+
"Return a real idea candidate artifact with explicit problem, users, goal, scope bullets, constraints, acceptance criteria, assumptions, and traceability.",
|
|
629
|
+
"Do not treat the result as canonically sufficient for stories; downstream idea sufficiency still decides that.",
|
|
630
|
+
"Preserve refs and traceability back to the approved scope and source evidence.",
|
|
631
|
+
],
|
|
632
|
+
)
|
|
633
|
+
except Exception as exc:
|
|
634
|
+
_append_node_trace(pipeline_root=pipeline_root, node="DraftIdeaFromScopeNode", event="failure", detail={"error": str(exc)})
|
|
635
|
+
raise
|
|
636
|
+
artifact = IdeaCandidateArtifact.model_validate(artifact.model_dump())
|
|
637
|
+
stage_path = pipeline_root / "idea_candidate.json"
|
|
638
|
+
artifact.execution_lineage = execution_lineage.model_copy(
|
|
639
|
+
update={
|
|
640
|
+
"artifact_path": _relative_to_repo(repo_root, stage_path),
|
|
641
|
+
"generated_from": [
|
|
642
|
+
_relative_to_repo(repo_root, pipeline_root / "scope_context.json"),
|
|
643
|
+
_relative_to_repo(repo_root, pipeline_root / "goldilocks_assessment.json"),
|
|
644
|
+
],
|
|
645
|
+
}
|
|
646
|
+
)
|
|
647
|
+
_write_json(stage_path, artifact.model_dump())
|
|
648
|
+
_append_node_trace(pipeline_root=pipeline_root, node="DraftIdeaFromScopeNode", event="success", detail={"artifact": _relative_to_repo(repo_root, stage_path)})
|
|
649
|
+
task_context.metadata["idea_candidate"] = artifact
|
|
650
|
+
self.save_output(artifact)
|
|
651
|
+
return task_context
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
class NarrowScopeReviewPackageNode(Node):
|
|
655
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
656
|
+
event = task_context.event
|
|
657
|
+
repo_root = Path(event.repo_root)
|
|
658
|
+
_dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Packaging narrow-scope review")
|
|
659
|
+
scope = task_context.metadata["scope_context"]
|
|
660
|
+
assessment = task_context.metadata["goldilocks_assessment"]
|
|
661
|
+
stage_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "narrow_scope_review.json"
|
|
662
|
+
artifact = NarrowScopeReviewArtifact(
|
|
663
|
+
project_id=scope.project_id,
|
|
664
|
+
scope_set_id=scope.scope_set_id,
|
|
665
|
+
scope_id=scope.scope_id,
|
|
666
|
+
parent_scope_id=scope.scope_id,
|
|
667
|
+
refs=_build_traceability(scope),
|
|
668
|
+
reason_too_narrow=assessment.reasoning,
|
|
669
|
+
suggested_merge_targets=scope.neighbor_scope_refs,
|
|
670
|
+
recommended_next_action="Request human review or explicit merge recommendation; do not auto-merge.",
|
|
671
|
+
execution_lineage=_lineage_entry(
|
|
672
|
+
stage="narrow_scope_review",
|
|
673
|
+
origin="deterministic",
|
|
674
|
+
mode="deterministic",
|
|
675
|
+
repo_root=repo_root,
|
|
676
|
+
artifact_path=stage_path,
|
|
677
|
+
generated_from=[
|
|
678
|
+
_relative_to_repo(repo_root, _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "scope_context.json"),
|
|
679
|
+
_relative_to_repo(repo_root, _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "goldilocks_assessment.json"),
|
|
680
|
+
],
|
|
681
|
+
notes=["Review package is deterministic packaging of a too-narrow boundary decision."],
|
|
682
|
+
metadata={"model_backed": False},
|
|
683
|
+
),
|
|
684
|
+
)
|
|
685
|
+
_write_json(stage_path, artifact.model_dump())
|
|
686
|
+
task_context.metadata["narrow_scope_review"] = artifact
|
|
687
|
+
self.save_output(artifact)
|
|
688
|
+
return task_context
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class NormalizeIdeaCandidateSetNode(Node):
|
|
692
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
693
|
+
event = task_context.event
|
|
694
|
+
repo_root = Path(event.repo_root)
|
|
695
|
+
_dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Normalizing idea candidate set")
|
|
696
|
+
pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
|
|
697
|
+
_append_node_trace(pipeline_root=pipeline_root, node="NormalizeIdeaCandidateSetNode", event="enter")
|
|
698
|
+
scope = task_context.metadata["scope_context"]
|
|
699
|
+
split_plan = task_context.metadata.get("idea_split_plan")
|
|
700
|
+
direct_candidate = task_context.metadata.get("idea_candidate")
|
|
701
|
+
pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
|
|
702
|
+
if split_plan is not None:
|
|
703
|
+
artifact = IdeaCandidateSetArtifact(
|
|
704
|
+
project_id=scope.project_id,
|
|
705
|
+
scope_set_id=scope.scope_set_id,
|
|
706
|
+
scope_id=scope.scope_id,
|
|
707
|
+
parent_scope_id=scope.scope_id,
|
|
708
|
+
refs=_build_traceability(scope),
|
|
709
|
+
idea_candidates=split_plan.child_ideas,
|
|
710
|
+
candidate_count=len(split_plan.child_ideas),
|
|
711
|
+
split_applied=True,
|
|
712
|
+
split_rationale=split_plan.split_rationale,
|
|
713
|
+
lineage=[split_plan.execution_lineage] if split_plan.execution_lineage else [],
|
|
714
|
+
)
|
|
715
|
+
else:
|
|
716
|
+
candidates = [direct_candidate] if direct_candidate is not None else []
|
|
717
|
+
artifact = IdeaCandidateSetArtifact(
|
|
718
|
+
project_id=scope.project_id,
|
|
719
|
+
scope_set_id=scope.scope_set_id,
|
|
720
|
+
scope_id=scope.scope_id,
|
|
721
|
+
parent_scope_id=scope.scope_id,
|
|
722
|
+
refs=_build_traceability(scope),
|
|
723
|
+
idea_candidates=candidates,
|
|
724
|
+
candidate_count=len(candidates),
|
|
725
|
+
split_applied=False,
|
|
726
|
+
split_rationale=None,
|
|
727
|
+
lineage=[direct_candidate.execution_lineage] if direct_candidate is not None and direct_candidate.execution_lineage else [],
|
|
728
|
+
)
|
|
729
|
+
stage_path = pipeline_root / "idea_candidate_set.json"
|
|
730
|
+
artifact.lineage.append(
|
|
731
|
+
_lineage_entry(
|
|
732
|
+
stage="normalize_idea_candidate_set",
|
|
733
|
+
origin="deterministic",
|
|
734
|
+
mode="deterministic",
|
|
735
|
+
repo_root=repo_root,
|
|
736
|
+
artifact_path=stage_path,
|
|
737
|
+
generated_from=[line.artifact_path for line in artifact.lineage if line and line.artifact_path],
|
|
738
|
+
notes=["Candidate set normalizes one-vs-many shaping results without changing their origin."],
|
|
739
|
+
metadata={"model_backed": False},
|
|
740
|
+
)
|
|
741
|
+
)
|
|
742
|
+
_write_json(stage_path, artifact.model_dump())
|
|
743
|
+
_append_node_trace(pipeline_root=pipeline_root, node="NormalizeIdeaCandidateSetNode", event="success", detail={"artifact": _relative_to_repo(repo_root, stage_path), "candidate_count": artifact.candidate_count})
|
|
744
|
+
task_context.metadata["idea_candidate_set"] = artifact
|
|
745
|
+
self.save_output(artifact)
|
|
746
|
+
return task_context
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
class RegisterIdeasNode(Node):
|
|
750
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
751
|
+
event = task_context.event
|
|
752
|
+
repo_root = Path(event.repo_root)
|
|
753
|
+
_dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Registering ideas")
|
|
754
|
+
pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
|
|
755
|
+
_append_node_trace(pipeline_root=pipeline_root, node="RegisterIdeasNode", event="enter")
|
|
756
|
+
scope = task_context.metadata["scope_context"]
|
|
757
|
+
candidate_set = task_context.metadata["idea_candidate_set"]
|
|
758
|
+
registry_root = repo_root / ".devflow" / "ideas"
|
|
759
|
+
registry_root.mkdir(parents=True, exist_ok=True)
|
|
760
|
+
registered: list[RegisteredIdeaArtifact] = []
|
|
761
|
+
assessment = task_context.metadata["goldilocks_assessment"]
|
|
762
|
+
for candidate in candidate_set.idea_candidates:
|
|
763
|
+
idea_id = _stable_id("idea_", {"scope_id": scope.scope_id, "candidate": candidate.model_dump()})
|
|
764
|
+
idea_paths = get_idea_paths(repo_root, idea_id=idea_id)
|
|
765
|
+
idea_paths.idea_dir.mkdir(parents=True, exist_ok=True)
|
|
766
|
+
registry_path = idea_paths.idea_dir / "idea.json"
|
|
767
|
+
candidate_lineage = candidate.execution_lineage
|
|
768
|
+
if candidate_lineage is None:
|
|
769
|
+
raise ValueError(f"Idea candidate {candidate.idea_candidate_id} is missing execution lineage")
|
|
770
|
+
summary = candidate.goal.strip() or candidate.problem.strip() or candidate.title.strip() or idea_id
|
|
771
|
+
persisted = {
|
|
772
|
+
"idea_id": idea_id,
|
|
773
|
+
"project_id": scope.project_id,
|
|
774
|
+
"scope_set_id": scope.scope_set_id,
|
|
775
|
+
"source_scope_id": scope.scope_id,
|
|
776
|
+
"parent_scope_id": candidate.parent_scope_id,
|
|
777
|
+
"status": "ready_for_story_sufficiency",
|
|
778
|
+
"title": candidate.title,
|
|
779
|
+
"summary": summary,
|
|
780
|
+
"idea": candidate.model_dump(),
|
|
781
|
+
"traceability": {
|
|
782
|
+
"scope_id": scope.scope_id,
|
|
783
|
+
"scope_artifact": str((_pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "scope_context.json").relative_to(repo_root)),
|
|
784
|
+
"candidate_artifact": str((_pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "idea_candidate_set.json").relative_to(repo_root)),
|
|
785
|
+
},
|
|
786
|
+
"lineage": {
|
|
787
|
+
"scope_to_idea_origin": candidate_lineage.origin,
|
|
788
|
+
"scope_to_idea_mode": candidate_lineage.mode,
|
|
789
|
+
"scope_to_idea_stage": candidate_lineage.stage,
|
|
790
|
+
"scope_to_idea_artifact": candidate_lineage.artifact_path,
|
|
791
|
+
"scope_to_idea_agent_run_ref": candidate_lineage.agent_run_ref,
|
|
792
|
+
"model_backed": bool(candidate_lineage.metadata.get("model_backed")),
|
|
793
|
+
},
|
|
794
|
+
}
|
|
795
|
+
_write_json(registry_path, persisted)
|
|
796
|
+
registered.append(
|
|
797
|
+
RegisteredIdeaArtifact(
|
|
798
|
+
idea_id=idea_id,
|
|
799
|
+
source_scope_id=scope.scope_id,
|
|
800
|
+
parent_scope_id=candidate.parent_scope_id,
|
|
801
|
+
idea_candidate_id=candidate.idea_candidate_id,
|
|
802
|
+
title=candidate.title,
|
|
803
|
+
summary=summary,
|
|
804
|
+
registry_ref=str(registry_path),
|
|
805
|
+
idea_ref=str(registry_path),
|
|
806
|
+
status="ready_for_story_sufficiency",
|
|
807
|
+
execution_lineage=candidate_lineage,
|
|
808
|
+
)
|
|
809
|
+
)
|
|
810
|
+
stage_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "idea_registry_record.json"
|
|
811
|
+
artifact = IdeaRegistryRecordArtifact(
|
|
812
|
+
project_id=scope.project_id,
|
|
813
|
+
scope_set_id=scope.scope_set_id,
|
|
814
|
+
scope_id=scope.scope_id,
|
|
815
|
+
parent_scope_id=scope.scope_id,
|
|
816
|
+
refs=_build_traceability(scope),
|
|
817
|
+
scope_shape=assessment.scope_shape,
|
|
818
|
+
registered_ideas=registered,
|
|
819
|
+
registration_timestamp=datetime.now(UTC).isoformat(),
|
|
820
|
+
registry_root=str(registry_root),
|
|
821
|
+
lineage=list(candidate_set.lineage) + [
|
|
822
|
+
_lineage_entry(
|
|
823
|
+
stage="register_ideas",
|
|
824
|
+
origin="deterministic",
|
|
825
|
+
mode="deterministic",
|
|
826
|
+
repo_root=repo_root,
|
|
827
|
+
artifact_path=stage_path,
|
|
828
|
+
generated_from=[_relative_to_repo(repo_root, _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "idea_candidate_set.json")],
|
|
829
|
+
notes=["Registry write preserves prior shaping lineage and exposes persisted idea records."],
|
|
830
|
+
metadata={"model_backed": False},
|
|
831
|
+
)
|
|
832
|
+
],
|
|
833
|
+
)
|
|
834
|
+
_write_json(stage_path, artifact.model_dump())
|
|
835
|
+
_append_node_trace(pipeline_root=pipeline_root, node="RegisterIdeasNode", event="success", detail={"artifact": _relative_to_repo(repo_root, stage_path), "registered_idea_count": len(artifact.registered_ideas)})
|
|
836
|
+
task_context.metadata["idea_registry_record"] = artifact
|
|
837
|
+
self.save_output(artifact)
|
|
838
|
+
return task_context
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
class IdeaResolutionPackageNode(Node):
|
|
842
|
+
async def process(self, task_context: TaskContext) -> TaskContext:
|
|
843
|
+
event = task_context.event
|
|
844
|
+
repo_root = Path(event.repo_root)
|
|
845
|
+
_dfs_running(project_id=event.project_id, run_id=_store_run()[1], scope_id=event.scope_id, summary="Packaging scope-to-idea resolution")
|
|
846
|
+
pipeline_root = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)
|
|
847
|
+
_append_node_trace(pipeline_root=pipeline_root, node="IdeaResolutionPackageNode", event="enter")
|
|
848
|
+
scope = task_context.metadata["scope_context"]
|
|
849
|
+
assessment = task_context.metadata["goldilocks_assessment"]
|
|
850
|
+
registry = task_context.metadata.get("idea_registry_record")
|
|
851
|
+
narrow = task_context.metadata.get("narrow_scope_review")
|
|
852
|
+
|
|
853
|
+
if registry is not None:
|
|
854
|
+
artifact = IdeaResolutionPackageArtifact(
|
|
855
|
+
project_id=scope.project_id,
|
|
856
|
+
scope_set_id=scope.scope_set_id,
|
|
857
|
+
scope_id=scope.scope_id,
|
|
858
|
+
parent_scope_id=scope.scope_id,
|
|
859
|
+
refs=_build_traceability(scope),
|
|
860
|
+
scope_shape=assessment.scope_shape,
|
|
861
|
+
split_applied=bool(task_context.metadata.get("idea_split_plan")),
|
|
862
|
+
resolution_status="ideas_registered",
|
|
863
|
+
registered_idea_count=len(registry.registered_ideas),
|
|
864
|
+
registered_ideas=registry.registered_ideas,
|
|
865
|
+
assumptions_added=scope.assumptions,
|
|
866
|
+
remaining_risks=["Idea sufficiency is intentionally deferred to idea->DevFlow stories DAG."],
|
|
867
|
+
recommended_next_action="Submit registered idea(s) to the idea->story sufficiency DAG.",
|
|
868
|
+
lineage=list(registry.lineage),
|
|
869
|
+
)
|
|
870
|
+
else:
|
|
871
|
+
review_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "narrow_scope_review.json"
|
|
872
|
+
artifact = IdeaResolutionPackageArtifact(
|
|
873
|
+
project_id=scope.project_id,
|
|
874
|
+
scope_set_id=scope.scope_set_id,
|
|
875
|
+
scope_id=scope.scope_id,
|
|
876
|
+
parent_scope_id=scope.scope_id,
|
|
877
|
+
refs=_build_traceability(scope),
|
|
878
|
+
scope_shape=assessment.scope_shape,
|
|
879
|
+
split_applied=False,
|
|
880
|
+
resolution_status="narrow_scope_review_required",
|
|
881
|
+
registered_idea_count=0,
|
|
882
|
+
assumptions_added=scope.assumptions,
|
|
883
|
+
remaining_risks=[narrow.reason_too_narrow] if narrow is not None else [],
|
|
884
|
+
recommended_next_action="Human review required before any merge or rescoping.",
|
|
885
|
+
review_required=True,
|
|
886
|
+
review_package_ref=str(review_path) if review_path.exists() else None,
|
|
887
|
+
lineage=[narrow.execution_lineage] if narrow is not None and narrow.execution_lineage else [],
|
|
888
|
+
)
|
|
889
|
+
stage_path = _pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "idea_resolution_package.json"
|
|
890
|
+
artifact.lineage.append(
|
|
891
|
+
_lineage_entry(
|
|
892
|
+
stage="idea_resolution_package",
|
|
893
|
+
origin="deterministic",
|
|
894
|
+
mode="deterministic",
|
|
895
|
+
repo_root=repo_root,
|
|
896
|
+
artifact_path=stage_path,
|
|
897
|
+
generated_from=[line.artifact_path for line in artifact.lineage if line and line.artifact_path],
|
|
898
|
+
notes=["Resolution package summarizes the run without changing the shaped idea lineage."],
|
|
899
|
+
metadata={"model_backed": False},
|
|
900
|
+
)
|
|
901
|
+
)
|
|
902
|
+
_write_json(stage_path, artifact.model_dump())
|
|
903
|
+
_append_node_trace(pipeline_root=pipeline_root, node="IdeaResolutionPackageNode", event="success", detail={"artifact": _relative_to_repo(repo_root, stage_path), "resolution_status": artifact.resolution_status})
|
|
904
|
+
summary = ScopeIdeaDagSummary(
|
|
905
|
+
exit_code=0,
|
|
906
|
+
run_id=_store_run()[1],
|
|
907
|
+
pipeline_dir=str(_pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key)),
|
|
908
|
+
message="scope->idea run complete",
|
|
909
|
+
outcome={
|
|
910
|
+
"project_id": scope.project_id,
|
|
911
|
+
"scope_id": scope.scope_id,
|
|
912
|
+
"scope_shape": assessment.scope_shape,
|
|
913
|
+
"registered_idea_count": artifact.registered_idea_count,
|
|
914
|
+
"resolution_status": artifact.resolution_status,
|
|
915
|
+
},
|
|
916
|
+
)
|
|
917
|
+
_write_json(_pipeline_root(repo_root, scope_id=event.scope_id, pipeline_key=event.pipeline_key) / "summary.json", summary.model_dump())
|
|
918
|
+
task_context.metadata["outcome"] = dict(summary.outcome)
|
|
919
|
+
task_context.metadata["message"] = json.dumps({**summary.outcome, "run_id": summary.run_id, "pipeline_dir": summary.pipeline_dir}, sort_keys=True) + "\n"
|
|
920
|
+
task_context.metadata["exit_code"] = 0
|
|
921
|
+
self.save_output(artifact)
|
|
922
|
+
return task_context
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
class ScopeToIdeaWorkflow(Workflow):
|
|
926
|
+
workflow_schema = WorkflowSchema(
|
|
927
|
+
description="Approved scope -> idea DAG (load -> goldilocks -> route -> shape/register -> resolution)",
|
|
928
|
+
event_schema=ScopeToIdeaDagEvent,
|
|
929
|
+
start=LoadApprovedScopeItemNode,
|
|
930
|
+
nodes=[
|
|
931
|
+
NodeConfig(node=LoadApprovedScopeItemNode, connections=[AssessScopeGoldilocksNode]),
|
|
932
|
+
NodeConfig(node=AssessScopeGoldilocksNode, connections=[GoldilocksDecisionNode]),
|
|
933
|
+
NodeConfig(node=GoldilocksDecisionNode, connections=[GoldilocksDecisionRouter]),
|
|
934
|
+
NodeConfig(node=GoldilocksDecisionRouter, connections=[SplitScopeIntoIdeaCandidatesNode, DraftIdeaFromScopeNode, NarrowScopeReviewPackageNode], is_router=True),
|
|
935
|
+
NodeConfig(node=SplitScopeIntoIdeaCandidatesNode, connections=[NormalizeIdeaCandidateSetNode]),
|
|
936
|
+
NodeConfig(node=DraftIdeaFromScopeNode, connections=[NormalizeIdeaCandidateSetNode]),
|
|
937
|
+
NodeConfig(node=NarrowScopeReviewPackageNode, connections=[IdeaResolutionPackageNode]),
|
|
938
|
+
NodeConfig(node=NormalizeIdeaCandidateSetNode, connections=[RegisterIdeasNode]),
|
|
939
|
+
NodeConfig(node=RegisterIdeasNode, connections=[IdeaResolutionPackageNode]),
|
|
940
|
+
NodeConfig(node=IdeaResolutionPackageNode, connections=[]),
|
|
941
|
+
],
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
|
|
945
|
+
def run_scope_to_idea_dag(
|
|
946
|
+
*,
|
|
947
|
+
repo_root: Path,
|
|
948
|
+
store: ExecutionStore,
|
|
949
|
+
project_id: str,
|
|
950
|
+
scope_set_id: str,
|
|
951
|
+
scope_id: str,
|
|
952
|
+
scope_payload_inline: dict[str, Any] | None = None,
|
|
953
|
+
scope_payload_path: Path | None = None,
|
|
954
|
+
) -> ScopeToIdeaDagResult:
|
|
955
|
+
payload = _load_scope_payload(inline_payload=scope_payload_inline, payload_path=scope_payload_path)
|
|
956
|
+
_scope_context_from_payload(
|
|
957
|
+
payload=payload,
|
|
958
|
+
project_id=project_id,
|
|
959
|
+
scope_set_id=scope_set_id,
|
|
960
|
+
scope_id=scope_id,
|
|
961
|
+
)
|
|
962
|
+
pipeline_key = build_pipeline_key(
|
|
963
|
+
repo_root=repo_root,
|
|
964
|
+
project_id=project_id,
|
|
965
|
+
scope_set_id=scope_set_id,
|
|
966
|
+
scope_id=scope_id,
|
|
967
|
+
scope_payload=payload,
|
|
968
|
+
)
|
|
969
|
+
pipeline_dir = _pipeline_root(repo_root, scope_id=scope_id, pipeline_key=pipeline_key)
|
|
970
|
+
pipeline_dir.mkdir(parents=True, exist_ok=True)
|
|
971
|
+
|
|
972
|
+
run_id = store.create_run(
|
|
973
|
+
dag_id=DAG_ID,
|
|
974
|
+
dag_version="v1_scaffold",
|
|
975
|
+
root_correlation_id=f"corr_{pipeline_key}",
|
|
976
|
+
config={
|
|
977
|
+
"project_id": project_id,
|
|
978
|
+
"scope_set_id": scope_set_id,
|
|
979
|
+
"scope_id": scope_id,
|
|
980
|
+
"pipeline_key": pipeline_key,
|
|
981
|
+
},
|
|
982
|
+
)
|
|
983
|
+
store.mark_run_started(run_id=run_id)
|
|
984
|
+
_dfs_running(project_id=project_id, run_id=run_id, scope_id=scope_id)
|
|
985
|
+
|
|
986
|
+
wf = ScopeToIdeaWorkflow()
|
|
987
|
+
global _CURRENT_STORE, _CURRENT_RUN_ID
|
|
988
|
+
_CURRENT_STORE = store
|
|
989
|
+
_CURRENT_RUN_ID = run_id
|
|
990
|
+
try:
|
|
991
|
+
ctx = wf.run(
|
|
992
|
+
{
|
|
993
|
+
"repo_root": str(repo_root),
|
|
994
|
+
"project_id": project_id,
|
|
995
|
+
"scope_set_id": scope_set_id,
|
|
996
|
+
"scope_id": scope_id,
|
|
997
|
+
"scope_payload_inline": None if scope_payload_path else payload,
|
|
998
|
+
"scope_payload_path": str(scope_payload_path) if scope_payload_path else None,
|
|
999
|
+
"pipeline_key": pipeline_key,
|
|
1000
|
+
}
|
|
1001
|
+
)
|
|
1002
|
+
except Exception as exc:
|
|
1003
|
+
store.mark_run_finished(run_id=run_id, status="failed")
|
|
1004
|
+
_dfs_terminal(
|
|
1005
|
+
project_id=project_id,
|
|
1006
|
+
run_id=run_id,
|
|
1007
|
+
scope_id=scope_id,
|
|
1008
|
+
current_state="error",
|
|
1009
|
+
current_status="failed",
|
|
1010
|
+
summary="Scope to idea failed",
|
|
1011
|
+
error_message=str(exc),
|
|
1012
|
+
)
|
|
1013
|
+
raise
|
|
1014
|
+
finally:
|
|
1015
|
+
_CURRENT_STORE = None
|
|
1016
|
+
_CURRENT_RUN_ID = None
|
|
1017
|
+
|
|
1018
|
+
exit_code = int(ctx.metadata.get("exit_code") or 0)
|
|
1019
|
+
store.mark_run_finished(run_id=run_id, status="succeeded" if exit_code == 0 else "failed")
|
|
1020
|
+
if exit_code == 0:
|
|
1021
|
+
_dfs_terminal(
|
|
1022
|
+
project_id=project_id,
|
|
1023
|
+
run_id=run_id,
|
|
1024
|
+
scope_id=scope_id,
|
|
1025
|
+
current_state="idle",
|
|
1026
|
+
current_status="completed",
|
|
1027
|
+
summary="Ideas generated from scope",
|
|
1028
|
+
)
|
|
1029
|
+
_sync_registered_ideas_to_supabase(
|
|
1030
|
+
project_id=project_id,
|
|
1031
|
+
scope_id=scope_id,
|
|
1032
|
+
run_id=run_id,
|
|
1033
|
+
pipeline_dir=pipeline_dir,
|
|
1034
|
+
)
|
|
1035
|
+
registry_path = pipeline_dir / "idea_registry_record.json"
|
|
1036
|
+
if registry_path.exists():
|
|
1037
|
+
registry = json.loads(registry_path.read_text(encoding="utf-8"))
|
|
1038
|
+
for idea in registry.get("registered_ideas") or []:
|
|
1039
|
+
if not isinstance(idea, dict):
|
|
1040
|
+
continue
|
|
1041
|
+
idea_id = str(idea.get("idea_id") or "").strip()
|
|
1042
|
+
idea_ref = str(idea.get("idea_ref") or "").strip()
|
|
1043
|
+
if not idea_id or not idea_ref:
|
|
1044
|
+
continue
|
|
1045
|
+
store.enqueue_idea_task(
|
|
1046
|
+
project_id=project_id,
|
|
1047
|
+
enqueue_run_id=run_id,
|
|
1048
|
+
idea_id=idea_id,
|
|
1049
|
+
title=str(idea.get("title") or idea_id or "idea"),
|
|
1050
|
+
idea_payload_path=idea_ref,
|
|
1051
|
+
candidate_planes=[],
|
|
1052
|
+
)
|
|
1053
|
+
else:
|
|
1054
|
+
_dfs_terminal(
|
|
1055
|
+
project_id=project_id,
|
|
1056
|
+
run_id=run_id,
|
|
1057
|
+
scope_id=scope_id,
|
|
1058
|
+
current_state="error",
|
|
1059
|
+
current_status="failed",
|
|
1060
|
+
summary="Scope to idea failed",
|
|
1061
|
+
error_message=str(ctx.metadata.get("message") or "scope_to_idea failed"),
|
|
1062
|
+
)
|
|
1063
|
+
return ScopeToIdeaDagResult(
|
|
1064
|
+
exit_code=exit_code,
|
|
1065
|
+
run_id=run_id,
|
|
1066
|
+
pipeline_dir=pipeline_dir,
|
|
1067
|
+
message=str(ctx.metadata.get("message") or ""),
|
|
1068
|
+
outcome=dict(ctx.metadata.get("outcome") or {}),
|
|
1069
|
+
)
|