devcouncil 0.3.1 → 0.4.0
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.
- package/README.md +46 -30
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +49 -30
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +960 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +303 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import logging
|
|
2
3
|
|
|
3
4
|
import typer
|
|
4
5
|
|
|
5
|
-
from devcouncil.
|
|
6
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
6
7
|
|
|
7
8
|
app = typer.Typer()
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
@app.callback(invoke_without_command=True)
|
|
@@ -15,4 +17,12 @@ def mcp_server(ctx: typer.Context):
|
|
|
15
17
|
if ctx.invoked_subcommand is not None:
|
|
16
18
|
return
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
# Deferred import: the MCP SDK (and the server module's large import surface)
|
|
21
|
+
# loads only when this command actually runs, not on every `dev` invocation —
|
|
22
|
+
# main.py imports every command module even for `dev --help`.
|
|
23
|
+
from devcouncil.integrations.mcp.server import run
|
|
24
|
+
|
|
25
|
+
logger.info("dev mcp-server: starting stdio server")
|
|
26
|
+
with log_stage("mcp_server"):
|
|
27
|
+
log_step("mcp_server/1: initializing stdio transport", trace=True)
|
|
28
|
+
asyncio.run(run())
|
|
@@ -19,9 +19,12 @@ from devcouncil.cli.commands.init import initialize_project
|
|
|
19
19
|
from devcouncil.knowledge.okf import read_bundle, validate_bundle
|
|
20
20
|
from devcouncil.storage.db import get_db
|
|
21
21
|
from devcouncil.storage.repositories import ArtifactGraphRepository
|
|
22
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
23
|
+
import logging
|
|
22
24
|
|
|
23
25
|
app = typer.Typer(help="Export, ingest, and validate Open Knowledge Format bundles.")
|
|
24
26
|
console = Console()
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
def _knowledge_okf_dir(root: Path) -> Path:
|
|
@@ -30,8 +33,8 @@ def _knowledge_okf_dir(root: Path) -> Path:
|
|
|
30
33
|
directory = ".devcouncil/knowledge"
|
|
31
34
|
try:
|
|
32
35
|
directory = load_config(root).knowledge.directory
|
|
33
|
-
except Exception:
|
|
34
|
-
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logger.debug("Failed to load knowledge directory from config, using default: %s", e)
|
|
35
38
|
return root / directory / "okf"
|
|
36
39
|
|
|
37
40
|
|
|
@@ -46,8 +49,8 @@ def _knowledge_design_md(root: Path) -> Path | None:
|
|
|
46
49
|
directory = ".devcouncil/knowledge"
|
|
47
50
|
try:
|
|
48
51
|
directory = load_config(root).knowledge.directory
|
|
49
|
-
except Exception:
|
|
50
|
-
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.debug("Failed to load knowledge directory from config, using default: %s", e)
|
|
51
54
|
candidates = [
|
|
52
55
|
root / directory / "design" / "design.md",
|
|
53
56
|
root / ".devcouncil" / "knowledge" / "design" / "design.md",
|
|
@@ -75,12 +78,22 @@ def export(
|
|
|
75
78
|
):
|
|
76
79
|
"""Export the DevCouncil artifact graph as an OKF bundle."""
|
|
77
80
|
root = project_root.expanduser().resolve()
|
|
81
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
82
|
+
set_log_dir(root)
|
|
83
|
+
logger.info("dev okf export")
|
|
78
84
|
initialize_project(root, quiet=True)
|
|
79
85
|
db = get_db(root)
|
|
80
86
|
if not db:
|
|
81
87
|
console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
|
|
82
88
|
raise typer.Exit(code=1)
|
|
83
89
|
|
|
90
|
+
with log_stage("okf", project_root=root, subcommand="export"):
|
|
91
|
+
log_step("okf/1: exporting OKF bundle", project_root=root, trace=True)
|
|
92
|
+
_run_okf_export(root, db, output, skills, design)
|
|
93
|
+
log_step("okf/complete", project_root=root, trace=True)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _run_okf_export(root, db, output, skills, design):
|
|
84
97
|
out_dir = output.expanduser().resolve()
|
|
85
98
|
with db.get_session() as session:
|
|
86
99
|
graph = ArtifactGraphRepository(session).load_graph()
|
|
@@ -90,8 +103,8 @@ def export(
|
|
|
90
103
|
from devcouncil.app.config import load_config
|
|
91
104
|
|
|
92
105
|
project_name = load_config(root).project.name or project_name
|
|
93
|
-
except Exception:
|
|
94
|
-
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.debug("Failed to load project name from config, using directory name: %s", e)
|
|
95
108
|
|
|
96
109
|
# Load the FULL skill set (packaged library + this repo's own skills) so the export is
|
|
97
110
|
# complete; goal-driven selection would only emit the few skills that match a goal.
|
|
@@ -219,6 +232,31 @@ def validate(
|
|
|
219
232
|
raise typer.Exit(code=1)
|
|
220
233
|
|
|
221
234
|
|
|
235
|
+
@app.command("select")
|
|
236
|
+
def select_knowledge(
|
|
237
|
+
goal: str = typer.Option(..., "--goal", "-g", help="Task goal used to rank ingested knowledge sources."),
|
|
238
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
239
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
|
|
240
|
+
) -> None:
|
|
241
|
+
"""Select OKF/design knowledge sources that match a goal."""
|
|
242
|
+
from devcouncil.knowledge.knowledge_select import select_knowledge_payload
|
|
243
|
+
|
|
244
|
+
root = project_root.expanduser().resolve()
|
|
245
|
+
payload = select_knowledge_payload(root, goal)
|
|
246
|
+
if json_output:
|
|
247
|
+
console.print_json(data=payload)
|
|
248
|
+
return
|
|
249
|
+
sources = payload.get("sources")
|
|
250
|
+
if isinstance(sources, list):
|
|
251
|
+
for source in sources:
|
|
252
|
+
if isinstance(source, dict):
|
|
253
|
+
console.print(
|
|
254
|
+
f"- [{source.get('kind')}] {source.get('name')}: {source.get('description')}"
|
|
255
|
+
)
|
|
256
|
+
if payload.get("preamble"):
|
|
257
|
+
console.print("\n" + str(payload["preamble"]))
|
|
258
|
+
|
|
259
|
+
|
|
222
260
|
@app.command("html")
|
|
223
261
|
def html(
|
|
224
262
|
bundle: Path = typer.Argument(..., help="Path to an OKF bundle directory to render."),
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import typer
|
|
2
2
|
import asyncio
|
|
3
|
-
import json
|
|
4
3
|
import datetime
|
|
5
4
|
import logging
|
|
5
|
+
import sys
|
|
6
6
|
from typing import Any
|
|
7
7
|
from rich.console import Console
|
|
8
8
|
from rich.panel import Panel
|
|
@@ -14,13 +14,19 @@ from devcouncil.storage.repositories import (
|
|
|
14
14
|
GapRepository,
|
|
15
15
|
PlanningStateRepository,
|
|
16
16
|
)
|
|
17
|
-
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
18
17
|
from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
|
|
19
18
|
from devcouncil.llm.provider import Provider, MockProvider, ProviderRequestError, build_role_model_config, create_provider, validate_model_provider
|
|
20
19
|
from devcouncil.llm.router import ModelRouter, StructuredOutputError
|
|
21
20
|
from devcouncil.planning.spec_service import SpecService
|
|
22
21
|
from devcouncil.planning.prompt_enhancer_service import PromptEnhancerService, save_active_prompt_enhancement
|
|
23
22
|
from devcouncil.planning.plan_service import PlanService, backfill_acceptance_criteria
|
|
23
|
+
from devcouncil.planning.plan_difficulty import apply_plan_difficulty
|
|
24
|
+
from devcouncil.planning.planned_files_reconcile import (
|
|
25
|
+
expand_scope_with_dependents,
|
|
26
|
+
reconcile_planned_files,
|
|
27
|
+
repo_files_from_map,
|
|
28
|
+
)
|
|
29
|
+
from devcouncil.planning.question_conversion import convert_blocking_questions_to_assumptions
|
|
24
30
|
from devcouncil.planning.critique_service import CritiqueService
|
|
25
31
|
from devcouncil.planning.arbiter_service import ArbiterDecision, ArbiterService
|
|
26
32
|
from devcouncil.gating.policy import GatePolicy
|
|
@@ -29,7 +35,8 @@ from devcouncil.app.state_machine import ProjectPhase
|
|
|
29
35
|
from devcouncil.app.config import ModelRoleConfig, load_config, get_api_key
|
|
30
36
|
from devcouncil.cli.commands.init import initialize_project
|
|
31
37
|
from devcouncil.telemetry.traces import TraceLogger
|
|
32
|
-
from devcouncil.telemetry.stages import log_step
|
|
38
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
39
|
+
from devcouncil.utils.json_persist import dump_json
|
|
33
40
|
|
|
34
41
|
app = typer.Typer()
|
|
35
42
|
console = Console()
|
|
@@ -86,6 +93,46 @@ def _ensure_planning_roles(config) -> None:
|
|
|
86
93
|
for role in REQUIRED_PLANNING_ROLES:
|
|
87
94
|
config.models.roles.setdefault(role, fallback.model_copy())
|
|
88
95
|
|
|
96
|
+
|
|
97
|
+
def _should_auto_convert_blocking_questions(config) -> bool:
|
|
98
|
+
if not config.planning.auto_convert_blocking_questions_in_noninteractive:
|
|
99
|
+
return False
|
|
100
|
+
return not sys.stdin.isatty()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _maybe_convert_blocking_questions(
|
|
104
|
+
spec_output,
|
|
105
|
+
config,
|
|
106
|
+
console,
|
|
107
|
+
*,
|
|
108
|
+
orchestrator=None,
|
|
109
|
+
artifact_path: Path | None = None,
|
|
110
|
+
):
|
|
111
|
+
"""In non-interactive mode, turn blocking questions into non-blocking assumptions."""
|
|
112
|
+
if not _should_auto_convert_blocking_questions(config):
|
|
113
|
+
return spec_output
|
|
114
|
+
if not spec_output.blocking_questions:
|
|
115
|
+
return spec_output
|
|
116
|
+
|
|
117
|
+
question_count = len(spec_output.blocking_questions)
|
|
118
|
+
assumptions, cleared = convert_blocking_questions_to_assumptions(
|
|
119
|
+
spec_output.assumptions,
|
|
120
|
+
spec_output.blocking_questions,
|
|
121
|
+
)
|
|
122
|
+
spec_output = spec_output.model_copy(
|
|
123
|
+
update={"assumptions": assumptions, "blocking_questions": cleared},
|
|
124
|
+
)
|
|
125
|
+
console.print(
|
|
126
|
+
f"[dim]Non-interactive mode: converted {question_count} blocking question(s) "
|
|
127
|
+
"to open assumptions (will not block plan approval).[/dim]"
|
|
128
|
+
)
|
|
129
|
+
if orchestrator is not None and orchestrator.current_run is not None:
|
|
130
|
+
orchestrator.save_run_artifact("requirements.json", spec_output.model_dump())
|
|
131
|
+
elif artifact_path is not None:
|
|
132
|
+
artifact_path.write_text(spec_output.model_dump_json(indent=2), encoding="utf-8")
|
|
133
|
+
return spec_output
|
|
134
|
+
|
|
135
|
+
|
|
89
136
|
async def run_plan_flow(
|
|
90
137
|
goal: str,
|
|
91
138
|
requirements_only: bool = False,
|
|
@@ -129,7 +176,7 @@ async def run_plan_flow(
|
|
|
129
176
|
|
|
130
177
|
provider = MockProvider()
|
|
131
178
|
provider.responses = {
|
|
132
|
-
"mock/prompt_enhancer":
|
|
179
|
+
"mock/prompt_enhancer": dump_json({
|
|
133
180
|
"original_goal": goal,
|
|
134
181
|
"enhanced_goal": (
|
|
135
182
|
f"Plan and implement {goal} using the mapped repository's "
|
|
@@ -139,26 +186,26 @@ async def run_plan_flow(
|
|
|
139
186
|
"debate_focus": ["Compare minimal implementation scope against production-readiness concerns."],
|
|
140
187
|
"constraints": [f"Do not broaden beyond: {goal}."]
|
|
141
188
|
}),
|
|
142
|
-
"mock/spec_writer":
|
|
189
|
+
"mock/spec_writer": dump_json({
|
|
143
190
|
"requirements": [{"id": "REQ-001", "title": "Mock Req", "description": "Desc", "priority": "high", "source": "user", "acceptance_criteria": []}],
|
|
144
191
|
"assumptions": [],
|
|
145
192
|
"blocking_questions": []
|
|
146
193
|
}),
|
|
147
194
|
"mock/planner_a": [
|
|
148
|
-
|
|
195
|
+
dump_json({
|
|
149
196
|
"id": "PLAN-A", "rationale": "Simple", "tasks": [{"id": "TASK-001", "title": "Mock Task", "description": "Desc", "requirement_ids": ["REQ-001"], "acceptance_criterion_ids": [], "planned_files": [], "expected_tests": [], "allowed_commands": [], "status": "planned"}]
|
|
150
197
|
}),
|
|
151
|
-
|
|
198
|
+
dump_json({"rebuttals": []})
|
|
152
199
|
],
|
|
153
200
|
"mock/planner_b": [
|
|
154
|
-
|
|
201
|
+
dump_json({
|
|
155
202
|
"id": "PLAN-B", "rationale": "Robust", "tasks": [{"id": "TASK-001", "title": "Mock Task", "description": "Desc", "requirement_ids": ["REQ-001"], "acceptance_criterion_ids": [], "planned_files": [], "expected_tests": [], "allowed_commands": [], "status": "planned"}]
|
|
156
203
|
}),
|
|
157
|
-
|
|
204
|
+
dump_json({"rebuttals": []})
|
|
158
205
|
],
|
|
159
206
|
"mock/critic_a": '{"findings": []}',
|
|
160
207
|
"mock/critic_b": '{"findings": []}',
|
|
161
|
-
"mock/arbiter":
|
|
208
|
+
"mock/arbiter": dump_json({
|
|
162
209
|
"accepted_finding_ids": [], "rejected_finding_ids": [],
|
|
163
210
|
"final_requirements": [{"id": "REQ-001", "title": "Mock Req", "description": "Desc", "priority": "high", "source": "user", "acceptance_criteria": [{"id": "AC-1", "description": "Test it", "verification_method": "unit_test"}]}],
|
|
164
211
|
"final_tasks": [{"id": "TASK-001", "title": "Mock Task", "description": "Desc", "requirement_ids": ["REQ-001"], "acceptance_criterion_ids": ["AC-1"], "planned_files": [{"path": "test.py", "reason": "logic", "allowed_change": "modify"}], "expected_tests": [], "allowed_commands": [], "status": "planned"}]
|
|
@@ -182,17 +229,37 @@ async def run_plan_flow(
|
|
|
182
229
|
plan_service = PlanService(router)
|
|
183
230
|
critique_service = CritiqueService(router)
|
|
184
231
|
arbiter_service = ArbiterService(router)
|
|
185
|
-
|
|
232
|
+
with log_stage("plan", project_root=root, run_id=run_id, goal=goal, quick=quick, dry_run=dry_run):
|
|
233
|
+
return await _run_plan_body(
|
|
234
|
+
root, goal, requirements_only, dry_run, persist, quick,
|
|
235
|
+
orchestrator, run_id, db, config, router,
|
|
236
|
+
prompt_enhancer, spec_service, plan_service, critique_service,
|
|
237
|
+
arbiter_service,
|
|
238
|
+
)
|
|
186
239
|
|
|
240
|
+
|
|
241
|
+
async def _run_plan_body(
|
|
242
|
+
root, goal, requirements_only, dry_run, persist, quick,
|
|
243
|
+
orchestrator, run_id, db, config, router,
|
|
244
|
+
prompt_enhancer, spec_service, plan_service, critique_service,
|
|
245
|
+
arbiter_service,
|
|
246
|
+
):
|
|
187
247
|
with Progress(
|
|
188
248
|
SpinnerColumn(),
|
|
189
249
|
TextColumn("[progress.description]{task.description}"),
|
|
190
250
|
transient=True,
|
|
191
251
|
) as progress:
|
|
192
252
|
# 1. Repo Map
|
|
193
|
-
log_step("plan/1: mapping repository", project_root=root, run_id=run_id)
|
|
253
|
+
log_step("plan/1: mapping repository", project_root=root, run_id=run_id, trace=True)
|
|
194
254
|
progress.add_task(description="Mapping repository...", total=None)
|
|
195
|
-
|
|
255
|
+
from devcouncil.indexing.map_artifacts import refresh_map_artifacts
|
|
256
|
+
|
|
257
|
+
repo_map = refresh_map_artifacts(
|
|
258
|
+
root,
|
|
259
|
+
root / ".devcouncil" / "repo_map.json",
|
|
260
|
+
goal,
|
|
261
|
+
quiet=True,
|
|
262
|
+
).repo_map
|
|
196
263
|
repo_map_json = repo_map.model_dump_json(indent=2)
|
|
197
264
|
# save_run_artifact re-serializes via json.dump, so pass the dict directly
|
|
198
265
|
# instead of round-tripping the already-serialized JSON back through json.loads.
|
|
@@ -203,7 +270,7 @@ async def run_plan_flow(
|
|
|
203
270
|
await orchestrator.transition_to(ProjectPhase.REPO_MAPPED)
|
|
204
271
|
|
|
205
272
|
# 2. Codebase-specific prompt enhancement
|
|
206
|
-
log_step("plan/2: enhancing prompt for codebase debate", project_root=root, run_id=run_id)
|
|
273
|
+
log_step("plan/2: enhancing prompt for codebase debate", project_root=root, run_id=run_id, trace=True)
|
|
207
274
|
progress.add_task(description="Enhancing prompt for codebase debate...", total=None)
|
|
208
275
|
prompt_enhancement = await prompt_enhancer.enhance_prompt(
|
|
209
276
|
goal,
|
|
@@ -234,7 +301,7 @@ async def run_plan_flow(
|
|
|
234
301
|
)
|
|
235
302
|
|
|
236
303
|
# 3. Spec / Requirements
|
|
237
|
-
log_step("plan/3: generating requirements", project_root=root, run_id=run_id)
|
|
304
|
+
log_step("plan/3: generating requirements", project_root=root, run_id=run_id, trace=True)
|
|
238
305
|
progress.add_task(description="Generating requirements...", total=None)
|
|
239
306
|
spec_output = await spec_service.generate_spec(debate_goal, repo_map_json)
|
|
240
307
|
orchestrator.save_run_artifact("requirements.json", spec_output.model_dump())
|
|
@@ -244,7 +311,7 @@ async def run_plan_flow(
|
|
|
244
311
|
console.print(Panel(f"Found {len(spec_output.requirements)} requirements.", title="Requirements Generated"))
|
|
245
312
|
return []
|
|
246
313
|
|
|
247
|
-
requirements_json =
|
|
314
|
+
requirements_json = dump_json([r.model_dump() for r in spec_output.requirements])
|
|
248
315
|
|
|
249
316
|
if quick:
|
|
250
317
|
# Rigor dial: single pragmatic plan, no A/B debate, critique, rebuttal,
|
|
@@ -253,7 +320,7 @@ async def run_plan_flow(
|
|
|
253
320
|
# adversarial robustness for ~5 fewer model calls — the right setting
|
|
254
321
|
# for small, well-scoped changes where verification (which still gates
|
|
255
322
|
# every diff) is the real safety net, not planning debate.
|
|
256
|
-
log_step("plan/4: generating single plan (quick mode)", project_root=root, run_id=run_id)
|
|
323
|
+
log_step("plan/4: generating single plan (quick mode)", project_root=root, run_id=run_id, trace=True)
|
|
257
324
|
progress.add_task(description="Generating single plan (quick mode)...", total=None)
|
|
258
325
|
plan_a = await plan_service.generate_plan(
|
|
259
326
|
"planner_a", debate_goal, requirements_json, repo_map_json
|
|
@@ -277,7 +344,7 @@ async def run_plan_flow(
|
|
|
277
344
|
final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
|
|
278
345
|
else:
|
|
279
346
|
# 4. Independent Plans (run concurrently — they don't depend on each other)
|
|
280
|
-
log_step("plan/4: generating Plans A (pragmatic) and B (robust)", project_root=root, run_id=run_id)
|
|
347
|
+
log_step("plan/4: generating Plans A (pragmatic) and B (robust)", project_root=root, run_id=run_id, trace=True)
|
|
281
348
|
progress.add_task(description="Generating Plans A (Pragmatic) and B (Robust)...", total=None)
|
|
282
349
|
plan_a, plan_b = await asyncio.gather(
|
|
283
350
|
plan_service.generate_plan("planner_a", debate_goal, requirements_json, repo_map_json),
|
|
@@ -293,7 +360,7 @@ async def run_plan_flow(
|
|
|
293
360
|
plan_b_json = plan_b.model_dump_json()
|
|
294
361
|
|
|
295
362
|
# 5. Cross-Critique (independent — run concurrently)
|
|
296
|
-
log_step("plan/5: cross-critiquing Plans A and B", project_root=root, run_id=run_id)
|
|
363
|
+
log_step("plan/5: cross-critiquing Plans A and B", project_root=root, run_id=run_id, trace=True)
|
|
297
364
|
progress.add_task(description="Critiquing Plans A and B...", total=None)
|
|
298
365
|
critique_a, critique_b = await asyncio.gather(
|
|
299
366
|
critique_service.generate_critique("critic_a", plan_b_json, requirements_json),
|
|
@@ -306,7 +373,7 @@ async def run_plan_flow(
|
|
|
306
373
|
critique_b_json = critique_b.model_dump_json()
|
|
307
374
|
|
|
308
375
|
# 6. Rebuttals (independent — run concurrently)
|
|
309
|
-
log_step("plan/6: generating rebuttals", project_root=root, run_id=run_id)
|
|
376
|
+
log_step("plan/6: generating rebuttals", project_root=root, run_id=run_id, trace=True)
|
|
310
377
|
progress.add_task(description="Generating rebuttals...", total=None)
|
|
311
378
|
rebuttal_a, rebuttal_b = await asyncio.gather(
|
|
312
379
|
critique_service.generate_rebuttal("planner_a", plan_a_json, critique_b_json),
|
|
@@ -316,7 +383,7 @@ async def run_plan_flow(
|
|
|
316
383
|
orchestrator.save_run_artifact("rebuttal_b.json", rebuttal_b.model_dump())
|
|
317
384
|
|
|
318
385
|
# 7. Arbitration
|
|
319
|
-
log_step("plan/7: arbitrating final plan", project_root=root, run_id=run_id)
|
|
386
|
+
log_step("plan/7: arbitrating final plan", project_root=root, run_id=run_id, trace=True)
|
|
320
387
|
progress.add_task(description="Arbitrating final plan...", total=None)
|
|
321
388
|
decision = await arbiter_service.arbitrate(
|
|
322
389
|
debate_goal,
|
|
@@ -350,12 +417,37 @@ async def run_plan_flow(
|
|
|
350
417
|
f"[dim]Linked {len(backfilled_acs)} unmapped acceptance criterion(s) to owning task(s) "
|
|
351
418
|
"so every elaborated behavior is verified.[/dim]"
|
|
352
419
|
)
|
|
420
|
+
# Ground planner-named files in the real repo before they become the scope
|
|
421
|
+
# whitelist: repair a typo'd/renamed modify target to its true path so the
|
|
422
|
+
# agent's correct write isn't reverted for pointing at a path the planner
|
|
423
|
+
# misspelled. Only relaxes/repairs scope — never tightens it.
|
|
424
|
+
final_tasks, planned_file_warnings = reconcile_planned_files(
|
|
425
|
+
final_tasks, repo_files_from_map(repo_map)
|
|
426
|
+
)
|
|
427
|
+
for warning in planned_file_warnings:
|
|
428
|
+
console.print(f"[yellow]{warning}[/yellow]")
|
|
429
|
+
|
|
430
|
+
# The other half: add the real callers (repo-map dependents) of each writable
|
|
431
|
+
# file so a caller the planner omitted isn't reverted mid-run. Only widens scope.
|
|
432
|
+
final_tasks, scope_widen_warnings = expand_scope_with_dependents(
|
|
433
|
+
final_tasks, repo_map.dependents, repo_files_from_map(repo_map)
|
|
434
|
+
)
|
|
435
|
+
for warning in scope_widen_warnings:
|
|
436
|
+
console.print(f"[dim]{warning}[/dim]")
|
|
437
|
+
|
|
438
|
+
final_tasks, difficulty_warnings = apply_plan_difficulty(final_tasks, decision.final_requirements)
|
|
439
|
+
for warning in difficulty_warnings:
|
|
440
|
+
console.print(f"[yellow]{warning}[/yellow]")
|
|
353
441
|
|
|
354
442
|
console.print(f"Final Requirements: [bold]{len(decision.final_requirements)}[/bold]")
|
|
355
443
|
console.print(f"Final Tasks: [bold]{len(final_tasks)}[/bold]")
|
|
356
444
|
|
|
445
|
+
spec_output = _maybe_convert_blocking_questions(
|
|
446
|
+
spec_output, config, console, orchestrator=orchestrator,
|
|
447
|
+
)
|
|
448
|
+
|
|
357
449
|
# 8. Check Gates
|
|
358
|
-
log_step("plan/8: checking plan-approval gates", project_root=root, run_id=run_id)
|
|
450
|
+
log_step("plan/8: checking plan-approval gates", project_root=root, run_id=run_id, trace=True)
|
|
359
451
|
policy = GatePolicy()
|
|
360
452
|
result = policy.check_plan_approval(
|
|
361
453
|
decision.final_requirements,
|
|
@@ -384,6 +476,7 @@ async def run_plan_flow(
|
|
|
384
476
|
console.print("[green]Plan approved by gates.[/green]")
|
|
385
477
|
logger.info("Plan approved by gates: %d task(s)", len(final_tasks))
|
|
386
478
|
await orchestrator.transition_to(ProjectPhase.PLAN_APPROVED)
|
|
479
|
+
log_step("plan/complete: approved", project_root=root, run_id=run_id, trace=True)
|
|
387
480
|
return [task.id for task in final_tasks]
|
|
388
481
|
else:
|
|
389
482
|
if persist:
|
|
@@ -394,6 +487,7 @@ async def run_plan_flow(
|
|
|
394
487
|
console.print("[yellow]Plan generated but failed gates. See status for gaps.[/yellow]")
|
|
395
488
|
logger.warning("Plan failed approval gates with %d gap(s)", len(result.gaps))
|
|
396
489
|
await orchestrator.transition_to(ProjectPhase.AWAITING_USER_DECISIONS)
|
|
490
|
+
log_step("plan/complete: awaiting user decisions", project_root=root, run_id=run_id, trace=True)
|
|
397
491
|
return []
|
|
398
492
|
|
|
399
493
|
def _latest_run_with_decision(root: Path, run_id: str | None) -> Path | None:
|
|
@@ -422,64 +516,81 @@ def approve(
|
|
|
422
516
|
from devcouncil.planning.spec_service import SpecOutput
|
|
423
517
|
|
|
424
518
|
root = project_root.expanduser().resolve()
|
|
519
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
520
|
+
set_log_dir(root)
|
|
521
|
+
logger.info("dev plan approve: run_id=%s force=%s", run_id or "latest", force)
|
|
425
522
|
db = get_db(root)
|
|
426
523
|
if not db:
|
|
427
524
|
console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
|
|
428
525
|
raise typer.Exit(code=1)
|
|
429
526
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
for name in ("critique_a.json", "critique_b.json"):
|
|
443
|
-
critique_path = run_dir / name
|
|
444
|
-
if critique_path.exists():
|
|
445
|
-
findings.extend(CritiqueOutput.model_validate_json(critique_path.read_text(encoding="utf-8")).findings)
|
|
446
|
-
reconciled_findings = _reconcile_findings(findings, decision)
|
|
447
|
-
final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
|
|
448
|
-
assumptions = spec_output.assumptions if spec_output else []
|
|
527
|
+
with log_stage("approve", project_root=root, run_id=run_id or "latest", force=force):
|
|
528
|
+
log_step("approve/1: loading plan decision", project_root=root, trace=True)
|
|
529
|
+
run_dir = _latest_run_with_decision(root, run_id)
|
|
530
|
+
if run_dir is None:
|
|
531
|
+
console.print("[red]No planning run with a decision was found. Run 'dev plan' first.[/red]")
|
|
532
|
+
raise typer.Exit(code=1)
|
|
533
|
+
|
|
534
|
+
decision = ArbiterDecision.model_validate_json((run_dir / "decision.json").read_text(encoding="utf-8"))
|
|
535
|
+
spec_path = run_dir / "requirements.json"
|
|
536
|
+
spec_output = (
|
|
537
|
+
SpecOutput.model_validate_json(spec_path.read_text(encoding="utf-8")) if spec_path.exists() else None
|
|
538
|
+
)
|
|
449
539
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
if not result.passed and not force:
|
|
459
|
-
console.print("[yellow]Plan still fails approval gates:[/yellow]")
|
|
460
|
-
for gap in result.gaps:
|
|
461
|
-
marker = "[red][BLOCKING][/red] " if gap.blocking else ""
|
|
462
|
-
console.print(f" - {marker}{gap.description} (Fix: {gap.recommended_fix})")
|
|
463
|
-
console.print("Resolve the gaps and re-run 'dev plan', or use --force to approve anyway.")
|
|
464
|
-
raise typer.Exit(code=1)
|
|
540
|
+
config = load_config(root)
|
|
541
|
+
if spec_output is not None:
|
|
542
|
+
spec_output = _maybe_convert_blocking_questions(
|
|
543
|
+
spec_output,
|
|
544
|
+
config,
|
|
545
|
+
console,
|
|
546
|
+
artifact_path=spec_path if spec_path.exists() else None,
|
|
547
|
+
)
|
|
465
548
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
549
|
+
findings = []
|
|
550
|
+
for name in ("critique_a.json", "critique_b.json"):
|
|
551
|
+
critique_path = run_dir / name
|
|
552
|
+
if critique_path.exists():
|
|
553
|
+
findings.extend(CritiqueOutput.model_validate_json(critique_path.read_text(encoding="utf-8")).findings)
|
|
554
|
+
reconciled_findings = _reconcile_findings(findings, decision)
|
|
555
|
+
final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
|
|
556
|
+
final_tasks, _ = apply_plan_difficulty(final_tasks, decision.final_requirements)
|
|
557
|
+
assumptions = spec_output.assumptions if spec_output else []
|
|
558
|
+
|
|
559
|
+
policy = GatePolicy()
|
|
560
|
+
log_step("approve/2: checking plan-approval gates", project_root=root)
|
|
561
|
+
result = policy.check_plan_approval(
|
|
469
562
|
decision.final_requirements,
|
|
470
|
-
assumptions,
|
|
471
563
|
final_tasks,
|
|
472
|
-
|
|
564
|
+
assumptions=assumptions,
|
|
565
|
+
findings=reconciled_findings,
|
|
566
|
+
blocking_questions=spec_output.blocking_questions if spec_output else [],
|
|
473
567
|
)
|
|
568
|
+
if not result.passed and not force:
|
|
569
|
+
console.print("[yellow]Plan still fails approval gates:[/yellow]")
|
|
570
|
+
for gap in result.gaps:
|
|
571
|
+
marker = "[red][BLOCKING][/red] " if gap.blocking else ""
|
|
572
|
+
console.print(f" - {marker}{gap.description} (Fix: {gap.recommended_fix})")
|
|
573
|
+
console.print("Resolve the gaps and re-run 'dev plan', or use --force to approve anyway.")
|
|
574
|
+
raise typer.Exit(code=1)
|
|
474
575
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
576
|
+
with db.get_session() as session:
|
|
577
|
+
GapRepository(session).delete_plan_gaps()
|
|
578
|
+
PlanningStateRepository(session).replace_active_plan(
|
|
579
|
+
decision.final_requirements,
|
|
580
|
+
assumptions,
|
|
581
|
+
final_tasks,
|
|
582
|
+
reconciled_findings,
|
|
583
|
+
)
|
|
584
|
+
|
|
585
|
+
orchestrator = Orchestrator(root)
|
|
586
|
+
try:
|
|
587
|
+
asyncio.run(orchestrator.transition_to(ProjectPhase.PLAN_APPROVED))
|
|
588
|
+
except ValueError as exc:
|
|
589
|
+
console.print(f"[red]Cannot approve from the current project phase: {exc}[/red]")
|
|
590
|
+
raise typer.Exit(code=1)
|
|
591
|
+
console.print(f"[green]Plan from run {run_dir.name} approved ({len(final_tasks)} tasks).[/green]")
|
|
592
|
+
console.print("Use 'dev tasks list' to see the planned tasks and 'dev run TASK-ID' to execute one.")
|
|
593
|
+
log_step("approve/complete", project_root=root, run_id=run_dir.name, trace=True)
|
|
483
594
|
|
|
484
595
|
|
|
485
596
|
@app.command()
|
|
@@ -504,6 +615,10 @@ def plan(
|
|
|
504
615
|
Run the full planning cycle (Repo map -> Spec -> Plan A/B -> Critique -> Arbiter).
|
|
505
616
|
"""
|
|
506
617
|
should_persist = persist or not dry_run
|
|
618
|
+
root = project_root.expanduser().resolve()
|
|
619
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
620
|
+
set_log_dir(root)
|
|
621
|
+
logger.info("dev plan: quick=%s dry_run=%s requirements_only=%s", quick, dry_run, requirements_only)
|
|
507
622
|
try:
|
|
508
623
|
asyncio.run(run_plan_flow(goal, requirements_only, dry_run, should_persist, project_root, quick=quick))
|
|
509
624
|
except (ProviderRequestError, StructuredOutputError) as exc:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
from devcouncil.utils.json_persist import dump_json
|
|
2
|
+
import logging
|
|
2
3
|
from typing import NoReturn
|
|
3
4
|
import typer
|
|
4
5
|
from rich.console import Console
|
|
@@ -9,9 +10,11 @@ from devcouncil.storage.repositories import TaskRepository, RequirementRepositor
|
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
|
|
11
12
|
from devcouncil.execution.prompt_builder import PromptBuilder
|
|
13
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
12
14
|
|
|
13
15
|
app = typer.Typer()
|
|
14
16
|
console = Console()
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
15
18
|
|
|
16
19
|
@app.callback(invoke_without_command=True)
|
|
17
20
|
def prompt(
|
|
@@ -29,33 +32,39 @@ def prompt(
|
|
|
29
32
|
|
|
30
33
|
def _fail(message: str) -> NoReturn:
|
|
31
34
|
if json_format:
|
|
32
|
-
typer.echo(
|
|
35
|
+
typer.echo(dump_json({"ok": False, "task_id": task_id, "error": message}, indent=2))
|
|
33
36
|
else:
|
|
34
37
|
console.print(f"[red]{message}[/red]")
|
|
35
38
|
raise typer.Exit(code=1)
|
|
36
39
|
|
|
37
40
|
root = project_root.expanduser().resolve()
|
|
41
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
42
|
+
set_log_dir(root)
|
|
43
|
+
logger.info("dev prompt: task=%s pretty=%s", task_id, pretty)
|
|
38
44
|
initialize_project(root, quiet=True)
|
|
39
45
|
db = get_db(root)
|
|
40
46
|
if not db:
|
|
41
47
|
_fail("DevCouncil state is unavailable in this directory.")
|
|
42
48
|
|
|
43
|
-
with
|
|
44
|
-
|
|
45
|
-
|
|
49
|
+
with log_stage("prompt", project_root=root, task_id=task_id):
|
|
50
|
+
log_step("prompt/1: loading task and building prompt", project_root=root, task_id=task_id, trace=True)
|
|
51
|
+
with db.get_session() as session:
|
|
52
|
+
task_repo = TaskRepository(session)
|
|
53
|
+
req_repo = RequirementRepository(session)
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
task = task_repo.get_by_id(task_id)
|
|
56
|
+
if not task:
|
|
57
|
+
_fail(f"Task {task_id} not found.")
|
|
50
58
|
|
|
51
|
-
|
|
59
|
+
reqs = req_repo.get_all()
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
61
|
+
builder = PromptBuilder(root)
|
|
62
|
+
task_prompt = builder.build_task_prompt(task, reqs)
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
64
|
+
if json_format:
|
|
65
|
+
typer.echo(dump_json({"ok": True, "task_id": task_id, "prompt": task_prompt}, indent=2))
|
|
66
|
+
elif pretty:
|
|
67
|
+
console.print(Markdown(task_prompt))
|
|
68
|
+
else:
|
|
69
|
+
typer.echo(task_prompt, nl=not task_prompt.endswith("\n"))
|
|
70
|
+
log_step("prompt/complete", project_root=root, task_id=task_id, trace=True)
|