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