devcouncil 0.3.0 → 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,7 +1,9 @@
|
|
|
1
|
+
import logging
|
|
1
2
|
import typer
|
|
2
3
|
import subprocess
|
|
3
4
|
import os
|
|
4
5
|
import shutil
|
|
6
|
+
import sys
|
|
5
7
|
from pathlib import Path
|
|
6
8
|
from rich.console import Console
|
|
7
9
|
from rich.table import Table
|
|
@@ -11,13 +13,18 @@ from devcouncil.executors.agent_registry import (
|
|
|
11
13
|
CODING_CLI_INTEGRATION_INFO,
|
|
12
14
|
CODING_CLI_PROBE_ORDER,
|
|
13
15
|
CODING_CLI_VERSION_COMMANDS,
|
|
16
|
+
DEPRECATED_CODING_CLIS,
|
|
17
|
+
GEMINI_DEPRECATION_MESSAGE,
|
|
14
18
|
detect_available_coding_cli,
|
|
15
19
|
resolve_automated_executor,
|
|
16
20
|
)
|
|
17
21
|
from devcouncil.llm.provider import SUPPORTED_MODEL_PROVIDERS, validate_model_provider
|
|
22
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
18
23
|
|
|
19
24
|
app = typer.Typer()
|
|
25
|
+
|
|
20
26
|
console = Console()
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
21
28
|
|
|
22
29
|
|
|
23
30
|
def _probe_ollama(base_url: str) -> tuple[bool, str]:
|
|
@@ -94,8 +101,8 @@ def _knowledge_dir(project_root: Path, config=None) -> str:
|
|
|
94
101
|
try:
|
|
95
102
|
cfg = config if config is not None else load_config(project_root)
|
|
96
103
|
directory = cfg.knowledge.directory
|
|
97
|
-
except Exception:
|
|
98
|
-
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.debug("Failed to load knowledge directory from config, using default: %s", e)
|
|
99
106
|
return directory
|
|
100
107
|
|
|
101
108
|
|
|
@@ -208,12 +215,663 @@ def check_ingested_knowledge(project_root: Path, config=None) -> list[tuple[str,
|
|
|
208
215
|
return rows
|
|
209
216
|
|
|
210
217
|
|
|
218
|
+
# Explicit map from an area row in docs/project-status.md to the tests/unit/<subsystem>/
|
|
219
|
+
# directory expected to back a "Stable" claim. Deliberately small and in-code (see
|
|
220
|
+
# IMPROVEMENTS.md: doc/status drift check) — extend it as areas graduate to Stable.
|
|
221
|
+
STATUS_DOC_UNIT_TEST_DIRS: tuple[tuple[str, str], ...] = (
|
|
222
|
+
("CLI & Storage", "storage"),
|
|
223
|
+
("Artifact Graph", "artifacts"),
|
|
224
|
+
("Council Debate", "council"),
|
|
225
|
+
("Diff↔Coverage Gate", "verification"),
|
|
226
|
+
("Cost & Run Telemetry", "telemetry"),
|
|
227
|
+
("Security Scanning", "gating"),
|
|
228
|
+
("Manual Executor", "executors"),
|
|
229
|
+
("Lite Check (`dev check --verify`)", "execution"),
|
|
230
|
+
("Next-Actions Contract", "reporting"),
|
|
231
|
+
("Repo Map & Code Graph", "indexing"),
|
|
232
|
+
("LSP / AST Indexing", "indexing"),
|
|
233
|
+
("Live Dashboard", "dashboard"),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Flat tests/unit/test_*.py prefixes that back a subsystem when no per-subsystem dir exists.
|
|
237
|
+
STATUS_DOC_FLAT_TEST_PREFIXES: dict[str, tuple[str, ...]] = {
|
|
238
|
+
"artifacts": ("test_artifact_graph",),
|
|
239
|
+
"council": ("test_orchestrator", "test_state_machine"),
|
|
240
|
+
"verification": ("test_verification", "test_verifier", "test_diff_coverage"),
|
|
241
|
+
"telemetry": ("test_telemetry",),
|
|
242
|
+
"executors": ("test_executors", "test_executor_", "test_claude_sdk_executor"),
|
|
243
|
+
"execution": ("test_execution", "test_go_", "test_cli_check", "test_ad_hoc_check"),
|
|
244
|
+
"reporting": ("test_json_report", "test_export_command", "test_pr_comments"),
|
|
245
|
+
"dashboard": ("test_dashboard",),
|
|
246
|
+
"indexing": (
|
|
247
|
+
"test_indexing",
|
|
248
|
+
"test_repo_mapper",
|
|
249
|
+
"test_repo_map",
|
|
250
|
+
"test_map_",
|
|
251
|
+
"test_graph_",
|
|
252
|
+
"test_graph_html",
|
|
253
|
+
"test_graph_dead",
|
|
254
|
+
"test_graph_cmd",
|
|
255
|
+
"test_graph_intel",
|
|
256
|
+
"test_graph_query",
|
|
257
|
+
"test_graph_incremental",
|
|
258
|
+
"test_graph_schema",
|
|
259
|
+
),
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _subsystem_has_unit_tests(unit_root: Path, subsystem: str) -> bool:
|
|
264
|
+
"""True when tests/unit/<subsystem>/ or mapped flat test files exist."""
|
|
265
|
+
tests_dir = unit_root / subsystem
|
|
266
|
+
try:
|
|
267
|
+
if tests_dir.is_dir() and any(tests_dir.rglob("test_*.py")):
|
|
268
|
+
return True
|
|
269
|
+
if any(unit_root.glob(f"test_{subsystem}*.py")):
|
|
270
|
+
return True
|
|
271
|
+
for prefix in STATUS_DOC_FLAT_TEST_PREFIXES.get(subsystem, ()):
|
|
272
|
+
if any(unit_root.glob(f"{prefix}*.py")):
|
|
273
|
+
return True
|
|
274
|
+
except Exception:
|
|
275
|
+
return False
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _parse_status_doc_areas(status_doc: Path) -> dict[str, str]:
|
|
280
|
+
"""Parse the docs/project-status.md maturity table into {area: status-cell text}.
|
|
281
|
+
|
|
282
|
+
Rows look like ``| **CLI & Storage** | Stable: SQLite + SQLModel, ... |``. The
|
|
283
|
+
header row and the ``| :--- |`` separator row are skipped. Best-effort by design;
|
|
284
|
+
the caller wraps this in try/except so a malformed doc never crashes doctor.
|
|
285
|
+
"""
|
|
286
|
+
areas: dict[str, str] = {}
|
|
287
|
+
for line in status_doc.read_text(encoding="utf-8").splitlines():
|
|
288
|
+
stripped = line.strip()
|
|
289
|
+
if not stripped.startswith("|"):
|
|
290
|
+
continue
|
|
291
|
+
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
|
|
292
|
+
if len(cells) < 2:
|
|
293
|
+
continue
|
|
294
|
+
area = cells[0].strip().strip("*").strip()
|
|
295
|
+
if not area or area.lower() == "area" or set(area) <= {":", "-", " "}:
|
|
296
|
+
continue
|
|
297
|
+
areas[area] = cells[1]
|
|
298
|
+
return areas
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def check_liveness_reliability(project_root: Path) -> list[tuple[str, str, str]]:
|
|
302
|
+
"""Warn when map liveness used empty/unreliable entry roots."""
|
|
303
|
+
ok = "[green]OK[/green]"
|
|
304
|
+
warn = "[yellow]WARN[/yellow]"
|
|
305
|
+
rows: list[tuple[str, str, str]] = []
|
|
306
|
+
try:
|
|
307
|
+
from devcouncil.utils.json_persist import read_json
|
|
308
|
+
|
|
309
|
+
map_path = project_root / ".devcouncil" / "repo_map.json"
|
|
310
|
+
if not map_path.is_file():
|
|
311
|
+
return rows
|
|
312
|
+
data = read_json(map_path)
|
|
313
|
+
payload = data if isinstance(data, dict) else {}
|
|
314
|
+
unreliable = bool(payload.get("liveness_unreachable_unreliable"))
|
|
315
|
+
roots = payload.get("entry_roots") or []
|
|
316
|
+
if unreliable or not roots:
|
|
317
|
+
rows.append((
|
|
318
|
+
"Map liveness",
|
|
319
|
+
warn,
|
|
320
|
+
"Production entry roots are empty or unreachable BFS was skipped. "
|
|
321
|
+
"Add `indexing.entry_roots` in `.devcouncil/config.yaml` (repo-relative "
|
|
322
|
+
"paths) and run `dev map`.",
|
|
323
|
+
))
|
|
324
|
+
else:
|
|
325
|
+
rows.append((
|
|
326
|
+
"Map liveness",
|
|
327
|
+
ok,
|
|
328
|
+
f"{len(roots)} production entry root(s); unreachable lists are reliable.",
|
|
329
|
+
))
|
|
330
|
+
except Exception:
|
|
331
|
+
logger.debug("check_liveness_reliability failed", exc_info=True)
|
|
332
|
+
return rows
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _repo_languages(project_root: Path) -> set[str]:
|
|
336
|
+
"""Repo languages from repo_map.json, else a bounded filesystem sniff."""
|
|
337
|
+
try:
|
|
338
|
+
from devcouncil.utils.json_persist import read_json
|
|
339
|
+
|
|
340
|
+
map_path = project_root / ".devcouncil" / "repo_map.json"
|
|
341
|
+
if map_path.is_file():
|
|
342
|
+
data = read_json(map_path)
|
|
343
|
+
langs = (data or {}).get("languages") if isinstance(data, dict) else None
|
|
344
|
+
if isinstance(langs, list) and langs:
|
|
345
|
+
return {str(lang) for lang in langs}
|
|
346
|
+
except Exception:
|
|
347
|
+
logger.debug("repo language read from map failed", exc_info=True)
|
|
348
|
+
try:
|
|
349
|
+
from devcouncil.indexing.lsp import LspInspector
|
|
350
|
+
|
|
351
|
+
return set(LspInspector(project_root).detect_languages())
|
|
352
|
+
except Exception:
|
|
353
|
+
logger.debug("repo language detection failed", exc_info=True)
|
|
354
|
+
return set()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def check_grammar_coverage(project_root: Path) -> list[tuple[str, str, str]]:
|
|
358
|
+
"""Warn when tree-sitter grammars for the repo's own languages are missing.
|
|
359
|
+
|
|
360
|
+
A missing grammar silently degrades extraction for that language (a
|
|
361
|
+
Python-heavy repo indexed without the Python grammar loses most symbols),
|
|
362
|
+
so surface it with the install action instead of only in graph doctor.
|
|
363
|
+
"""
|
|
364
|
+
ok = "[green]OK[/green]"
|
|
365
|
+
warn = "[yellow]WARN[/yellow]"
|
|
366
|
+
rows: list[tuple[str, str, str]] = []
|
|
367
|
+
try:
|
|
368
|
+
from devcouncil.codeintel.languages import grammar_status
|
|
369
|
+
|
|
370
|
+
status = grammar_status()
|
|
371
|
+
# repo_map languages are lowercase; LANGUAGE_SPECS names are display-cased
|
|
372
|
+
# ("Python") — compare casefolded or nothing ever matches.
|
|
373
|
+
repo_langs = {lang.casefold() for lang in _repo_languages(project_root)}
|
|
374
|
+
if not repo_langs:
|
|
375
|
+
return rows
|
|
376
|
+
missing = sorted(
|
|
377
|
+
row["language"]
|
|
378
|
+
for row in status.get("languages", [])
|
|
379
|
+
if str(row.get("language", "")).casefold() in repo_langs
|
|
380
|
+
and row.get("missing_grammars")
|
|
381
|
+
# Python extraction is native stdlib-ast (cache._grammar_identity);
|
|
382
|
+
# a missing python tree-sitter grammar does not degrade indexing.
|
|
383
|
+
and str(row.get("grammar", "")) != "python"
|
|
384
|
+
)
|
|
385
|
+
if missing:
|
|
386
|
+
action = status.get("action") or (
|
|
387
|
+
"Install the platform-matched devcouncil-codeintel-grammars wheel."
|
|
388
|
+
)
|
|
389
|
+
rows.append((
|
|
390
|
+
"Grammar coverage",
|
|
391
|
+
warn,
|
|
392
|
+
f"Missing tree-sitter grammars for repo language(s): {', '.join(missing)}. "
|
|
393
|
+
f"{action}",
|
|
394
|
+
))
|
|
395
|
+
else:
|
|
396
|
+
rows.append((
|
|
397
|
+
"Grammar coverage",
|
|
398
|
+
ok,
|
|
399
|
+
f"Grammars available for all repo languages ({', '.join(sorted(repo_langs))}).",
|
|
400
|
+
))
|
|
401
|
+
except Exception:
|
|
402
|
+
logger.debug("check_grammar_coverage failed", exc_info=True)
|
|
403
|
+
return rows
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def check_lsp_reference_confirmation(project_root: Path) -> list[tuple[str, str, str]]:
|
|
407
|
+
"""Report repo languages with no language server on PATH.
|
|
408
|
+
|
|
409
|
+
Without a server, ``--lsp-refs`` dead-symbol confirmation cannot run for
|
|
410
|
+
that language; WARN when ``indexing.lsp_refs`` is enabled, informational
|
|
411
|
+
otherwise.
|
|
412
|
+
"""
|
|
413
|
+
ok = "[green]OK[/green]"
|
|
414
|
+
warn = "[yellow]WARN[/yellow]"
|
|
415
|
+
rows: list[tuple[str, str, str]] = []
|
|
416
|
+
try:
|
|
417
|
+
from devcouncil.indexing.lsp import LspInspector
|
|
418
|
+
|
|
419
|
+
candidates = LspInspector(project_root).server_candidates()
|
|
420
|
+
if not candidates:
|
|
421
|
+
return rows
|
|
422
|
+
available_langs = {c.language for c in candidates if c.available}
|
|
423
|
+
missing = sorted({c.language for c in candidates} - available_langs)
|
|
424
|
+
lsp_refs_enabled = False
|
|
425
|
+
try:
|
|
426
|
+
from devcouncil.app.config import load_config
|
|
427
|
+
|
|
428
|
+
lsp_refs_enabled = bool(load_config(project_root).indexing.lsp_refs)
|
|
429
|
+
except Exception:
|
|
430
|
+
logger.debug("lsp_refs config read failed", exc_info=True)
|
|
431
|
+
if not missing:
|
|
432
|
+
rows.append((
|
|
433
|
+
"LSP servers",
|
|
434
|
+
ok,
|
|
435
|
+
f"Language server on PATH for: {', '.join(sorted(available_langs))}.",
|
|
436
|
+
))
|
|
437
|
+
elif lsp_refs_enabled:
|
|
438
|
+
rows.append((
|
|
439
|
+
"LSP servers",
|
|
440
|
+
warn,
|
|
441
|
+
"indexing.lsp_refs is enabled but no language server is on PATH for: "
|
|
442
|
+
f"{', '.join(missing)} — dead-symbol confirmation is skipped there.",
|
|
443
|
+
))
|
|
444
|
+
else:
|
|
445
|
+
rows.append((
|
|
446
|
+
"LSP servers",
|
|
447
|
+
ok,
|
|
448
|
+
f"No server on PATH for: {', '.join(missing)} (detection only; install "
|
|
449
|
+
"one to enable `dev map --lsp-refs` confirmation).",
|
|
450
|
+
))
|
|
451
|
+
except Exception:
|
|
452
|
+
logger.debug("check_lsp_reference_confirmation failed", exc_info=True)
|
|
453
|
+
return rows
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def check_unknown_indexing_keys(project_root: Path) -> list[tuple[str, str, str]]:
|
|
457
|
+
"""Warn on unknown ``indexing.*`` keys in config.yaml (typos/removed options)."""
|
|
458
|
+
warn = "[yellow]WARN[/yellow]"
|
|
459
|
+
rows: list[tuple[str, str, str]] = []
|
|
460
|
+
try:
|
|
461
|
+
import yaml
|
|
462
|
+
|
|
463
|
+
cfg_path = project_root / ".devcouncil" / "config.yaml"
|
|
464
|
+
if not cfg_path.is_file():
|
|
465
|
+
return rows
|
|
466
|
+
payload = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
|
|
467
|
+
indexing = payload.get("indexing")
|
|
468
|
+
if not isinstance(indexing, dict):
|
|
469
|
+
return rows
|
|
470
|
+
from devcouncil.app.config import IndexingConfig
|
|
471
|
+
|
|
472
|
+
unknown = sorted(str(k) for k in indexing if str(k) not in IndexingConfig.model_fields)
|
|
473
|
+
if unknown:
|
|
474
|
+
rows.append((
|
|
475
|
+
"Config keys",
|
|
476
|
+
warn,
|
|
477
|
+
f"Unknown `indexing.*` key(s) ignored: {', '.join(unknown)} — "
|
|
478
|
+
"typo or an option removed in this version.",
|
|
479
|
+
))
|
|
480
|
+
except Exception:
|
|
481
|
+
logger.debug("check_unknown_indexing_keys failed", exc_info=True)
|
|
482
|
+
return rows
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def check_mapping_stack(project_root: Path) -> list[tuple[str, str, str]]:
|
|
486
|
+
"""Doctor rows for repo map + code graph freshness (native mapping stack)."""
|
|
487
|
+
rows: list[tuple[str, str, str]] = []
|
|
488
|
+
ok = "[green]OK[/green]"
|
|
489
|
+
warn = "[yellow]WARN[/yellow]"
|
|
490
|
+
graphify_legacy = project_root / ".devcouncil" / "graphify.yaml"
|
|
491
|
+
if graphify_legacy.is_file():
|
|
492
|
+
rows.append((
|
|
493
|
+
"Legacy graphify.yaml",
|
|
494
|
+
warn,
|
|
495
|
+
"``.devcouncil/graphify.yaml`` is deprecated. Move ``corpus:`` settings into "
|
|
496
|
+
"``.devcouncil/config.yaml`` under ``indexing.corpus`` and remove the file.",
|
|
497
|
+
))
|
|
498
|
+
rows.extend(check_repo_map_freshness(project_root))
|
|
499
|
+
rows.extend(check_liveness_reliability(project_root))
|
|
500
|
+
rows.extend(check_grammar_coverage(project_root))
|
|
501
|
+
rows.extend(check_lsp_reference_confirmation(project_root))
|
|
502
|
+
rows.extend(check_unknown_indexing_keys(project_root))
|
|
503
|
+
graph_path = project_root / ".devcouncil" / "graph" / "code_graph.json"
|
|
504
|
+
if not graph_path.is_file():
|
|
505
|
+
store_has_graph = False
|
|
506
|
+
try:
|
|
507
|
+
from devcouncil.indexing.graph.build import load_code_graph
|
|
508
|
+
|
|
509
|
+
store_has_graph = load_code_graph(project_root) is not None
|
|
510
|
+
except Exception:
|
|
511
|
+
logger.debug("store probe for missing graph JSON failed", exc_info=True)
|
|
512
|
+
if store_has_graph:
|
|
513
|
+
rows.append((
|
|
514
|
+
"Code graph",
|
|
515
|
+
warn,
|
|
516
|
+
"JSON export ``.devcouncil/graph/code_graph.json`` is missing but the "
|
|
517
|
+
"SQLite store has a graph. Run ``dev map`` to re-export it.",
|
|
518
|
+
))
|
|
519
|
+
else:
|
|
520
|
+
rows.append((
|
|
521
|
+
"Code graph",
|
|
522
|
+
warn,
|
|
523
|
+
"Missing ``.devcouncil/graph/code_graph.json``. Run ``dev map`` or ``dev graph ingest``.",
|
|
524
|
+
))
|
|
525
|
+
else:
|
|
526
|
+
try:
|
|
527
|
+
from devcouncil.indexing.graph.build import load_code_graph
|
|
528
|
+
|
|
529
|
+
if load_code_graph(project_root) is None:
|
|
530
|
+
rows.append(("Code graph", warn, "Unreadable or empty code graph export."))
|
|
531
|
+
else:
|
|
532
|
+
rows.append(("Code graph", ok, "Present and loadable."))
|
|
533
|
+
except Exception:
|
|
534
|
+
rows.append(("Code graph", warn, "Could not load code graph export."))
|
|
535
|
+
return rows
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def check_repo_map_freshness(project_root: Path) -> list[tuple[str, str, str]]:
|
|
539
|
+
"""Doctor rows for ``.devcouncil/repo_map.json`` fingerprint freshness."""
|
|
540
|
+
try:
|
|
541
|
+
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
542
|
+
from devcouncil.utils.json_persist import read_json
|
|
543
|
+
|
|
544
|
+
map_path = project_root / ".devcouncil" / "repo_map.json"
|
|
545
|
+
ok = "[green]OK[/green]"
|
|
546
|
+
warn = "[yellow]Stale[/yellow]"
|
|
547
|
+
if not map_path.is_file():
|
|
548
|
+
return [(
|
|
549
|
+
"Repo map",
|
|
550
|
+
warn,
|
|
551
|
+
"Missing `.devcouncil/repo_map.json`. Run `dev map` before verify or checkout.",
|
|
552
|
+
)]
|
|
553
|
+
loaded = read_json(map_path)
|
|
554
|
+
data = loaded if isinstance(loaded, dict) else {}
|
|
555
|
+
mapper = RepoMapper(project_root)
|
|
556
|
+
if mapper.map_is_stale(data):
|
|
557
|
+
head = str(data.get("generated_head") or "(unknown)")[:12]
|
|
558
|
+
current = mapper._git_head()[:12]
|
|
559
|
+
if head == current and head != "(unknown)":
|
|
560
|
+
detail = (
|
|
561
|
+
f"Working-tree content changed since the last map "
|
|
562
|
+
f"(git HEAD {current}). Run `dev map` or rely on auto-refresh "
|
|
563
|
+
f"at checkout/verify."
|
|
564
|
+
)
|
|
565
|
+
else:
|
|
566
|
+
detail = (
|
|
567
|
+
f"Behind current code (map HEAD {head}, repo HEAD {current}). "
|
|
568
|
+
"Run `dev map` or rely on auto-refresh at checkout/verify."
|
|
569
|
+
)
|
|
570
|
+
return [(
|
|
571
|
+
"Repo map",
|
|
572
|
+
warn,
|
|
573
|
+
detail,
|
|
574
|
+
)]
|
|
575
|
+
return [("Repo map", ok, "Fresh — fingerprints match the current repository.")]
|
|
576
|
+
except Exception:
|
|
577
|
+
return []
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def check_execution_containment(project_root: Path, config=None) -> list[tuple[str, str, str]]:
|
|
581
|
+
"""Doctor rows for YOLO/containment posture (scope gate, write hooks, risky profiles)."""
|
|
582
|
+
try:
|
|
583
|
+
from devcouncil.app.config import load_config
|
|
584
|
+
from devcouncil.executors.agent_registry import load_agent_profiles
|
|
585
|
+
|
|
586
|
+
cfg = config if config is not None else load_config(project_root)
|
|
587
|
+
rows: list[tuple[str, str, str]] = []
|
|
588
|
+
ok = "[green]OK[/green]"
|
|
589
|
+
warn = "[yellow]Risky[/yellow]"
|
|
590
|
+
|
|
591
|
+
if cfg.execution.enforce_file_scope_pre_verify:
|
|
592
|
+
rows.append((
|
|
593
|
+
"Pre-verify scope gate",
|
|
594
|
+
ok,
|
|
595
|
+
"execution.enforce_file_scope_pre_verify is enabled — OOS CLI writes are reverted before verify.",
|
|
596
|
+
))
|
|
597
|
+
else:
|
|
598
|
+
rows.append((
|
|
599
|
+
"Pre-verify scope gate",
|
|
600
|
+
warn,
|
|
601
|
+
"Off by default. Enable execution.enforce_file_scope_pre_verify for battle-test containment.",
|
|
602
|
+
))
|
|
603
|
+
|
|
604
|
+
write_gate = bool(getattr(cfg.integrations.claude, "write_gate", False))
|
|
605
|
+
if write_gate:
|
|
606
|
+
rows.append((
|
|
607
|
+
"Claude write-gate",
|
|
608
|
+
ok,
|
|
609
|
+
"integrations.claude.write_gate is enabled. Run `dev integrate hooks --apply` if hooks are missing.",
|
|
610
|
+
))
|
|
611
|
+
else:
|
|
612
|
+
rows.append((
|
|
613
|
+
"Claude write-gate",
|
|
614
|
+
warn,
|
|
615
|
+
"Disabled. Run `dev integrate claude --apply --write-gate` for PreToolUse containment.",
|
|
616
|
+
))
|
|
617
|
+
|
|
618
|
+
for name, profile in load_agent_profiles(project_root).items():
|
|
619
|
+
mode = (profile.permission_mode or "").strip().lower()
|
|
620
|
+
if mode == "bypasspermissions":
|
|
621
|
+
rows.append((
|
|
622
|
+
f"Profile {name}",
|
|
623
|
+
warn,
|
|
624
|
+
"permission_mode bypassPermissions bypasses all CLI edit gates.",
|
|
625
|
+
))
|
|
626
|
+
extra = profile.extra_args or []
|
|
627
|
+
for index, arg in enumerate(extra):
|
|
628
|
+
if arg == "--permission-mode" and index + 1 < len(extra):
|
|
629
|
+
if str(extra[index + 1]).lower() == "bypasspermissions":
|
|
630
|
+
rows.append((
|
|
631
|
+
f"Profile {name}",
|
|
632
|
+
warn,
|
|
633
|
+
"extra_args include --permission-mode bypassPermissions.",
|
|
634
|
+
))
|
|
635
|
+
return rows
|
|
636
|
+
except Exception:
|
|
637
|
+
return []
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def check_local_monitor_sampling(project_root: Path, config=None) -> list[tuple[str, str, str]]:
|
|
641
|
+
"""Doctor rows for local-monitor verification safety.
|
|
642
|
+
|
|
643
|
+
Calibration probes (2026-07-03, benchmarks/results/local_monitor_*) showed a
|
|
644
|
+
local monitor with single-shot acceptance checks rubber-stamping real defects,
|
|
645
|
+
while samples>=3 + per-criterion compilation caught 6/6 with zero false passes.
|
|
646
|
+
Auto-resolution picks the safe local settings; these rows surface EXPLICIT
|
|
647
|
+
overrides that disable them — at setup time, where users actually look, rather
|
|
648
|
+
than only as mid-run log warnings. Cloud monitors produce no rows (single-shot
|
|
649
|
+
is their intended default). Never raises.
|
|
650
|
+
"""
|
|
651
|
+
try:
|
|
652
|
+
from devcouncil.app.config import load_config, role_runs_on_local_provider
|
|
653
|
+
|
|
654
|
+
cfg = config if config is not None else load_config(project_root)
|
|
655
|
+
rows: list[tuple[str, str, str]] = []
|
|
656
|
+
warn = "[yellow]Risky[/yellow]"
|
|
657
|
+
|
|
658
|
+
local_monitor = role_runs_on_local_provider(cfg, "implementation_reviewer")
|
|
659
|
+
for message in cfg.verification.acceptance_checks.unsafe_override_warnings(local_monitor):
|
|
660
|
+
rows.append(("Local monitor (acceptance checks)", warn, message))
|
|
661
|
+
local_reviewer = role_runs_on_local_provider(cfg, "live_reviewer")
|
|
662
|
+
for message in cfg.verification.reviewer_checks.unsafe_override_warnings(local_reviewer):
|
|
663
|
+
rows.append(("Local reviewer (live review)", warn, message))
|
|
664
|
+
|
|
665
|
+
if not rows and (local_monitor or local_reviewer):
|
|
666
|
+
samples, repairs, per_criterion = cfg.verification.acceptance_checks.resolved(local_monitor)
|
|
667
|
+
votes = cfg.verification.reviewer_checks.resolved(local_reviewer)
|
|
668
|
+
rows.append((
|
|
669
|
+
"Local monitor ensembling",
|
|
670
|
+
"[green]OK[/green]",
|
|
671
|
+
f"Acceptance checks: samples={samples}, repair_attempts={repairs}, "
|
|
672
|
+
f"per_criterion={per_criterion}; reviewer votes={votes}.",
|
|
673
|
+
))
|
|
674
|
+
return rows
|
|
675
|
+
except Exception:
|
|
676
|
+
return [] # config problems already surface via other doctor rows
|
|
677
|
+
|
|
678
|
+
|
|
679
|
+
def check_coverage_floor(project_root: Path) -> list[tuple[str, str, str]]:
|
|
680
|
+
"""Doctor row for ``[tool.coverage.report] fail_under`` in pyproject.toml."""
|
|
681
|
+
ok = "[green]OK[/green]"
|
|
682
|
+
warn = "[yellow]WARN[/yellow]"
|
|
683
|
+
pyproject = project_root / "pyproject.toml"
|
|
684
|
+
if not pyproject.is_file():
|
|
685
|
+
return [("Coverage floor", warn, "No pyproject.toml; cannot verify fail_under.")]
|
|
686
|
+
try:
|
|
687
|
+
import tomllib
|
|
688
|
+
|
|
689
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
690
|
+
fail_under = (
|
|
691
|
+
data.get("tool", {})
|
|
692
|
+
.get("coverage", {})
|
|
693
|
+
.get("report", {})
|
|
694
|
+
.get("fail_under")
|
|
695
|
+
)
|
|
696
|
+
except Exception as exc:
|
|
697
|
+
return [("Coverage floor", warn, f"Could not read pyproject.toml: {exc}.")]
|
|
698
|
+
if fail_under is None:
|
|
699
|
+
return [
|
|
700
|
+
(
|
|
701
|
+
"Coverage floor",
|
|
702
|
+
warn,
|
|
703
|
+
"No [tool.coverage.report] fail_under in pyproject.toml; CI will not enforce a coverage minimum.",
|
|
704
|
+
)
|
|
705
|
+
]
|
|
706
|
+
return [
|
|
707
|
+
(
|
|
708
|
+
"Coverage floor",
|
|
709
|
+
ok,
|
|
710
|
+
f"fail_under={fail_under} configured in pyproject.toml (enforced by `coverage report`).",
|
|
711
|
+
)
|
|
712
|
+
]
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _mypy_command(project_root: Path) -> list[str]:
|
|
716
|
+
"""Resolve mypy without making a doctor check depend on network access."""
|
|
717
|
+
project_mypy = project_root / ".venv" / "bin" / "mypy"
|
|
718
|
+
if project_mypy.is_file():
|
|
719
|
+
return [str(project_mypy), "src"]
|
|
720
|
+
if shutil.which("uv"):
|
|
721
|
+
return ["uv", "run", "--python", "3.12", "mypy", "src"]
|
|
722
|
+
return [sys.executable, "-m", "mypy", "src"]
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
def check_mypy_status(project_root: Path) -> list[tuple[str, str, str]]:
|
|
726
|
+
"""Doctor row summarizing canonical mypy health for this repository."""
|
|
727
|
+
ok = "[green]OK[/green]"
|
|
728
|
+
warn = "[yellow]WARN[/yellow]"
|
|
729
|
+
pyproject = project_root / "pyproject.toml"
|
|
730
|
+
if not pyproject.is_file():
|
|
731
|
+
return [("mypy green", warn, "No pyproject.toml; skipping mypy probe.")]
|
|
732
|
+
src_root = project_root / "src"
|
|
733
|
+
if not src_root.is_dir():
|
|
734
|
+
return [("mypy green", warn, "No src/ directory; skipping mypy probe.")]
|
|
735
|
+
command = _mypy_command(project_root)
|
|
736
|
+
command_text = subprocess.list2cmdline(command)
|
|
737
|
+
try:
|
|
738
|
+
proc = subprocess.run(
|
|
739
|
+
command,
|
|
740
|
+
cwd=project_root,
|
|
741
|
+
capture_output=True,
|
|
742
|
+
text=True,
|
|
743
|
+
encoding="utf-8",
|
|
744
|
+
errors="replace",
|
|
745
|
+
timeout=120,
|
|
746
|
+
)
|
|
747
|
+
except FileNotFoundError:
|
|
748
|
+
return [("mypy green", warn, f"{command_text} is unavailable; install uv and development dependencies.")]
|
|
749
|
+
except subprocess.TimeoutExpired:
|
|
750
|
+
return [("mypy green", warn, f"{command_text} timed out after 120s.")]
|
|
751
|
+
except Exception as exc:
|
|
752
|
+
return [("mypy green", warn, f"{command_text} probe failed: {exc}.")]
|
|
753
|
+
|
|
754
|
+
output = (proc.stdout or "") + (proc.stderr or "")
|
|
755
|
+
if "No module named mypy" in output or "No module named 'mypy'" in output:
|
|
756
|
+
return [
|
|
757
|
+
(
|
|
758
|
+
"mypy green",
|
|
759
|
+
warn,
|
|
760
|
+
f"{command_text} is unavailable; install uv and development dependencies.",
|
|
761
|
+
)
|
|
762
|
+
]
|
|
763
|
+
if "INTERNAL ERROR" in output:
|
|
764
|
+
return [
|
|
765
|
+
(
|
|
766
|
+
"mypy green",
|
|
767
|
+
warn,
|
|
768
|
+
f"{command_text} crashed with INTERNAL ERROR; pin or upgrade mypy in dev dependencies.",
|
|
769
|
+
)
|
|
770
|
+
]
|
|
771
|
+
if proc.returncode == 0:
|
|
772
|
+
return [("mypy green", ok, f"{command_text} passed with zero errors.")]
|
|
773
|
+
error_count = output.count(": error:")
|
|
774
|
+
preview = output.strip().splitlines()[-1] if output.strip() else "mypy failed"
|
|
775
|
+
return [
|
|
776
|
+
(
|
|
777
|
+
"mypy green",
|
|
778
|
+
warn,
|
|
779
|
+
f"{command_text} reported {error_count} error(s). Last line: {preview}",
|
|
780
|
+
)
|
|
781
|
+
]
|
|
782
|
+
|
|
783
|
+
|
|
784
|
+
def check_status_doc_drift(project_root: Path) -> list[tuple[str, str, str]]:
|
|
785
|
+
"""Doctor rows verifying docs/project-status.md "Stable" claims against tests/unit/.
|
|
786
|
+
|
|
787
|
+
For each mapped area (``STATUS_DOC_UNIT_TEST_DIRS``) whose status-table row claims
|
|
788
|
+
Stable, require a non-empty ``tests/unit/<subsystem>/`` directory (at least one
|
|
789
|
+
``test_*.py``) to back the claim; mismatches become ``WARN`` rows so the status doc
|
|
790
|
+
cannot drift ahead of the test suite unnoticed. A mapped area missing from the doc
|
|
791
|
+
is also reported — that means the in-code mapping went stale.
|
|
792
|
+
|
|
793
|
+
Never raises: a missing status doc yields a neutral ``INFO`` row (most projects that
|
|
794
|
+
embed DevCouncil have no such doc), and any parse failure becomes a ``WARN`` row.
|
|
795
|
+
"""
|
|
796
|
+
ok = "[green]OK[/green]"
|
|
797
|
+
warn = "[yellow]WARN[/yellow]"
|
|
798
|
+
info = "[cyan]INFO[/cyan]"
|
|
799
|
+
|
|
800
|
+
status_doc = project_root / "docs" / "project-status.md"
|
|
801
|
+
if not status_doc.is_file():
|
|
802
|
+
return [
|
|
803
|
+
(
|
|
804
|
+
"Status-doc drift",
|
|
805
|
+
info,
|
|
806
|
+
"No docs/project-status.md in this project; skipping status-vs-tests drift check.",
|
|
807
|
+
)
|
|
808
|
+
]
|
|
809
|
+
|
|
810
|
+
try:
|
|
811
|
+
areas = _parse_status_doc_areas(status_doc)
|
|
812
|
+
except Exception as exc: # never let a malformed status doc crash doctor
|
|
813
|
+
return [("Status-doc drift", warn, f"Could not parse docs/project-status.md: {exc}.")]
|
|
814
|
+
|
|
815
|
+
mismatches: list[str] = []
|
|
816
|
+
verified = 0
|
|
817
|
+
unit_root = project_root / "tests" / "unit"
|
|
818
|
+
for area, subsystem in STATUS_DOC_UNIT_TEST_DIRS:
|
|
819
|
+
status_text = areas.get(area)
|
|
820
|
+
if status_text is None:
|
|
821
|
+
mismatches.append(
|
|
822
|
+
f"'{area}' is in doctor's drift mapping but not in the status table "
|
|
823
|
+
"(update STATUS_DOC_UNIT_TEST_DIRS)"
|
|
824
|
+
)
|
|
825
|
+
continue
|
|
826
|
+
if not status_text.strip().lower().startswith("stable"):
|
|
827
|
+
continue # only Stable rows claim unit-test-backed maturity
|
|
828
|
+
try:
|
|
829
|
+
has_tests = _subsystem_has_unit_tests(unit_root, subsystem)
|
|
830
|
+
except Exception:
|
|
831
|
+
has_tests = False
|
|
832
|
+
if has_tests:
|
|
833
|
+
verified += 1
|
|
834
|
+
else:
|
|
835
|
+
try:
|
|
836
|
+
flat_matches = any(unit_root.glob(f"test_{subsystem}*.py"))
|
|
837
|
+
except Exception:
|
|
838
|
+
flat_matches = False
|
|
839
|
+
hint = (
|
|
840
|
+
f" (flat tests/unit/test_{subsystem}*.py files exist but no per-subsystem dir)"
|
|
841
|
+
if flat_matches
|
|
842
|
+
else ""
|
|
843
|
+
)
|
|
844
|
+
mismatches.append(
|
|
845
|
+
f"'{area}' claims Stable but tests/unit/{subsystem}/ is missing or empty{hint}"
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
if mismatches:
|
|
849
|
+
preview = "; ".join(mismatches[:5])
|
|
850
|
+
extra = "" if len(mismatches) <= 5 else f" (+{len(mismatches) - 5} more)"
|
|
851
|
+
return [
|
|
852
|
+
(
|
|
853
|
+
"Status-doc drift",
|
|
854
|
+
warn,
|
|
855
|
+
f"{len(mismatches)} mismatch(es) between docs/project-status.md and tests/unit/: "
|
|
856
|
+
f"{preview}{extra}.",
|
|
857
|
+
)
|
|
858
|
+
]
|
|
859
|
+
return [
|
|
860
|
+
(
|
|
861
|
+
"Status-doc drift",
|
|
862
|
+
ok,
|
|
863
|
+
f"{verified} Stable claim(s) in docs/project-status.md backed by non-empty "
|
|
864
|
+
"tests/unit/<subsystem>/ directories.",
|
|
865
|
+
)
|
|
866
|
+
]
|
|
867
|
+
|
|
868
|
+
|
|
211
869
|
def _add_logging_row(table, project_root: Path) -> None:
|
|
212
870
|
"""Append a logging-health row: where the durable run log lives and how big it
|
|
213
871
|
is, so a user chasing a recurring failure knows exactly where to look."""
|
|
214
|
-
from devcouncil.telemetry.logging_setup import
|
|
872
|
+
from devcouncil.telemetry.logging_setup import _resolve_log_path
|
|
215
873
|
|
|
216
|
-
log_path = project_root
|
|
874
|
+
log_path = _resolve_log_path(project_root)
|
|
217
875
|
if log_path.exists():
|
|
218
876
|
size_kb = log_path.stat().st_size / 1024
|
|
219
877
|
detail = f"{log_path} ({size_kb:.0f} KB). View: dev logs tail"
|
|
@@ -222,6 +880,60 @@ def _add_logging_row(table, project_root: Path) -> None:
|
|
|
222
880
|
table.add_row("logging", "[green]OK[/green]", detail)
|
|
223
881
|
|
|
224
882
|
|
|
883
|
+
def _subsystem_maturity_rows() -> list[tuple[str, str, str]]:
|
|
884
|
+
"""Curated maturity tiers aligned with docs/project-status.md."""
|
|
885
|
+
return [
|
|
886
|
+
("CLI & Storage", "stable", "SQLite + SQLModel; core workflow"),
|
|
887
|
+
("Artifact Graph / Coverage", "stable", "Coverage engine and reports"),
|
|
888
|
+
("Council Debate / Planning", "stable", "Multi-agent planning and critique"),
|
|
889
|
+
("Manual Executor", "stable", "Sidecar mode"),
|
|
890
|
+
("Diff↔Coverage Gate", "stable", "Signal-first; opt-in blocking"),
|
|
891
|
+
("Next-Actions / dev check --verify", "stable", "Deterministic evidence gate"),
|
|
892
|
+
("Ollama provider", "stable", "Offline planning without API keys"),
|
|
893
|
+
("Engineering Skills", "stable", "dev skills listing/scaffolding"),
|
|
894
|
+
("Cost & Run Telemetry", "stable", "dev cost show / dev runs"),
|
|
895
|
+
("Security Scanning", "stable", "Secret redaction and detection"),
|
|
896
|
+
("Coding CLI Executors", "preview", "Codex, Claude, OpenCode, Antigravity, Warp, Cursor, …; Gemini deprecated"),
|
|
897
|
+
("Repair Loop (dev go)", "stable", "Bounded self-repair; correction manifest + no-progress"),
|
|
898
|
+
("LLM repair inference", "preview", "Optional RepairService manifest sharpening"),
|
|
899
|
+
("MCP Server (Claude hero loop)", "stable", "Certified checkout→verify loop; golden e2e"),
|
|
900
|
+
("Multi-agent Campaign", "preview", "dev campaign — parallel DAG + Reviewer QC"),
|
|
901
|
+
("OKF / design.md", "preview", "dev okf / dev design commands"),
|
|
902
|
+
("CI Scaffolding", "preview", "dev scaffold-ci starter workflow"),
|
|
903
|
+
("One-command onboarding (dev boot)", "preview", "setup + integrate --apply + go"),
|
|
904
|
+
("GitHub PR Checks / Comments", "preview", "dev report --github*"),
|
|
905
|
+
("Repo Map & Code Graph", "stable", "dev map / dev graph; liveness + query/dead/impact/html"),
|
|
906
|
+
("LSP / AST Indexing", "preview", "dev lsp inspect (detection-only) / dev ast"),
|
|
907
|
+
("Live Dashboard", "stable", "local-only operator UI; loopback + token-guarded apply"),
|
|
908
|
+
("Coding CLI Hooks", "preview", "Stop gate on Claude/Codex Stop hooks; assist seeded on integrate"),
|
|
909
|
+
("Stop gate & claim checks", "preview", "Completion-claim mapper + optional active-task verify"),
|
|
910
|
+
("Corpus side index", "preview", "dev corpus + optional corpus/doc-ref rigor gates"),
|
|
911
|
+
("PDG / CFG / taint", "preview", "Opt-in Python PDG; off by default"),
|
|
912
|
+
("Native Executor", "preview", "Lease-gated writes + shared verify/next-actions loop; sandbox/timeout parity"),
|
|
913
|
+
]
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _render_maturity_section(table) -> None:
|
|
917
|
+
"""Append subsystem maturity rows so preview features are visible upfront."""
|
|
918
|
+
stable = "[green]Stable[/green]"
|
|
919
|
+
preview = "[yellow]Preview[/yellow]"
|
|
920
|
+
experimental = "[magenta]Experimental[/magenta]"
|
|
921
|
+
label_for = {"stable": stable, "preview": preview, "experimental": experimental}
|
|
922
|
+
for area, tier, notes in _subsystem_maturity_rows():
|
|
923
|
+
if tier == "preview":
|
|
924
|
+
notes = f"{notes} — API/output may change."
|
|
925
|
+
table.add_row(area, label_for.get(tier, tier), notes)
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _print_maturity_table() -> None:
|
|
929
|
+
maturity = Table(title="DevCouncil Subsystem Maturity (see docs/project-status.md)")
|
|
930
|
+
maturity.add_column("Area", style="cyan")
|
|
931
|
+
maturity.add_column("Tier", style="magenta")
|
|
932
|
+
maturity.add_column("Notes", style="green")
|
|
933
|
+
_render_maturity_section(maturity)
|
|
934
|
+
console.print(maturity)
|
|
935
|
+
|
|
936
|
+
|
|
225
937
|
def render_doctor_check(project_root: Path = Path(".")):
|
|
226
938
|
# Load config once for the whole invocation; the diagnostic checks below reuse
|
|
227
939
|
# this instead of re-reading config.yaml. None falls back to per-check loading.
|
|
@@ -303,6 +1015,18 @@ def render_doctor_check(project_root: Path = Path(".")):
|
|
|
303
1015
|
f"Optional. Install {info.label}, then use: {info.notes}.",
|
|
304
1016
|
)
|
|
305
1017
|
|
|
1018
|
+
for name in sorted(DEPRECATED_CODING_CLIS):
|
|
1019
|
+
if name in CODING_CLI_PROBE_ORDER:
|
|
1020
|
+
continue
|
|
1021
|
+
info = CODING_CLI_INTEGRATION_INFO.get(name)
|
|
1022
|
+
if info is None:
|
|
1023
|
+
continue
|
|
1024
|
+
table.add_row(
|
|
1025
|
+
info.label,
|
|
1026
|
+
"[dim]Deprecated[/dim]",
|
|
1027
|
+
GEMINI_DEPRECATION_MESSAGE,
|
|
1028
|
+
)
|
|
1029
|
+
|
|
306
1030
|
detected = detect_available_coding_cli(project_root)
|
|
307
1031
|
resolved = resolve_automated_executor(project_root, None)
|
|
308
1032
|
if detected:
|
|
@@ -323,6 +1047,34 @@ def render_doctor_check(project_root: Path = Path(".")):
|
|
|
323
1047
|
for component, status, notes in check_ingested_knowledge(project_root, config=config):
|
|
324
1048
|
table.add_row(component, status, notes)
|
|
325
1049
|
|
|
1050
|
+
from devcouncil.llm.semantic_bridge import check_semantic_layer
|
|
1051
|
+
|
|
1052
|
+
for component, status, notes in check_semantic_layer(project_root, config=config):
|
|
1053
|
+
table.add_row(component, status, notes)
|
|
1054
|
+
|
|
1055
|
+
# Status-doc drift: keep docs/project-status.md "Stable" claims honest against the
|
|
1056
|
+
# actual tests/unit/ layout. Placed with the knowledge rows so it also runs on the
|
|
1057
|
+
# ollama / unsupported-provider early-return paths.
|
|
1058
|
+
for component, status, notes in check_status_doc_drift(project_root):
|
|
1059
|
+
table.add_row(component, status, notes)
|
|
1060
|
+
|
|
1061
|
+
for component, status, notes in check_coverage_floor(project_root):
|
|
1062
|
+
table.add_row(component, status, notes)
|
|
1063
|
+
|
|
1064
|
+
for component, status, notes in check_mypy_status(project_root):
|
|
1065
|
+
table.add_row(component, status, notes)
|
|
1066
|
+
|
|
1067
|
+
for component, status, notes in check_mapping_stack(project_root):
|
|
1068
|
+
table.add_row(component, status, notes)
|
|
1069
|
+
|
|
1070
|
+
for component, status, notes in check_execution_containment(project_root, config=config):
|
|
1071
|
+
table.add_row(component, status, notes)
|
|
1072
|
+
|
|
1073
|
+
# Local-monitor verification safety: flag explicit config that disables the
|
|
1074
|
+
# ensembling a local monitor/reviewer needs (see check docstring for the data).
|
|
1075
|
+
for component, status, notes in check_local_monitor_sampling(project_root, config=config):
|
|
1076
|
+
table.add_row(component, status, notes)
|
|
1077
|
+
|
|
326
1078
|
try:
|
|
327
1079
|
provider = config.models.provider if config is not None else "openrouter"
|
|
328
1080
|
except Exception:
|
|
@@ -338,6 +1090,7 @@ def render_doctor_check(project_root: Path = Path(".")):
|
|
|
338
1090
|
)
|
|
339
1091
|
_add_logging_row(table, project_root)
|
|
340
1092
|
console.print(table)
|
|
1093
|
+
_print_maturity_table()
|
|
341
1094
|
return
|
|
342
1095
|
if provider == "ollama":
|
|
343
1096
|
# Use the provider's own resolver so the displayed URL reflects OLLAMA_HOST
|
|
@@ -410,11 +1163,14 @@ def render_doctor_check(project_root: Path = Path(".")):
|
|
|
410
1163
|
)
|
|
411
1164
|
|
|
412
1165
|
if num_ctx is None:
|
|
1166
|
+
# Only reachable via the explicit OLLAMA_NUM_CTX=0 opt-out: unset now
|
|
1167
|
+
# resolves to the provider's raised DEFAULT_NUM_CTX, never the server default.
|
|
413
1168
|
table.add_row(
|
|
414
1169
|
"OLLAMA num_ctx",
|
|
415
1170
|
"[yellow]WARN[/yellow]",
|
|
416
|
-
f"OLLAMA_NUM_CTX
|
|
417
|
-
f"truncate DevCouncil's large planning prompts.
|
|
1171
|
+
f"OLLAMA_NUM_CTX=0 opts into Ollama's small server default (~2048-4096), "
|
|
1172
|
+
f"which will truncate DevCouncil's large planning prompts. Unset it (default "
|
|
1173
|
+
f"{OllamaProvider.DEFAULT_NUM_CTX}) or set OLLAMA_NUM_CTX={recommended_ctx}.",
|
|
418
1174
|
)
|
|
419
1175
|
elif num_ctx < min_ctx:
|
|
420
1176
|
table.add_row(
|
|
@@ -424,7 +1180,24 @@ def render_doctor_check(project_root: Path = Path(".")):
|
|
|
424
1180
|
f"(~{MAX_PROMPT_CHARS // 4} tokens); recommend >= {recommended_ctx}.",
|
|
425
1181
|
)
|
|
426
1182
|
else:
|
|
427
|
-
table.add_row(
|
|
1183
|
+
table.add_row(
|
|
1184
|
+
"OLLAMA num_ctx",
|
|
1185
|
+
"[green]OK[/green]",
|
|
1186
|
+
f"context window = {num_ctx} tokens (auto-grows per request up to "
|
|
1187
|
+
f"{OllamaProvider._resolve_max_num_ctx()} for oversized prompts; cap with OLLAMA_MAX_NUM_CTX).",
|
|
1188
|
+
)
|
|
1189
|
+
|
|
1190
|
+
think = OllamaProvider._resolve_think()
|
|
1191
|
+
if think is None:
|
|
1192
|
+
table.add_row(
|
|
1193
|
+
"OLLAMA think",
|
|
1194
|
+
"[green]OK[/green]",
|
|
1195
|
+
"server default. On thinking models (qwen3/deepseek-r1/...) the reasoning "
|
|
1196
|
+
"channel can dominate review latency; OLLAMA_THINK=false trades some check "
|
|
1197
|
+
"quality for much faster verification calls.",
|
|
1198
|
+
)
|
|
1199
|
+
else:
|
|
1200
|
+
table.add_row("OLLAMA think", "[green]OK[/green]", f"OLLAMA_THINK={'true' if think else 'false'}.")
|
|
428
1201
|
|
|
429
1202
|
# Local model size is bounded by host memory — unified RAM on Apple Silicon,
|
|
430
1203
|
# VRAM on a discrete-GPU box, system RAM otherwise. Surface a model that will
|
|
@@ -442,6 +1215,7 @@ def render_doctor_check(project_root: Path = Path(".")):
|
|
|
442
1215
|
|
|
443
1216
|
_add_logging_row(table, project_root)
|
|
444
1217
|
console.print(table)
|
|
1218
|
+
_print_maturity_table()
|
|
445
1219
|
return
|
|
446
1220
|
env_var = provider_api_key_env_var(provider)
|
|
447
1221
|
local_secrets = load_local_secrets(project_root)
|
|
@@ -477,6 +1251,7 @@ def render_doctor_check(project_root: Path = Path(".")):
|
|
|
477
1251
|
|
|
478
1252
|
_add_logging_row(table, project_root)
|
|
479
1253
|
console.print(table)
|
|
1254
|
+
_print_maturity_table()
|
|
480
1255
|
|
|
481
1256
|
|
|
482
1257
|
@app.callback(invoke_without_command=True)
|
|
@@ -494,4 +1269,11 @@ def doctor(
|
|
|
494
1269
|
if ctx.invoked_subcommand is not None:
|
|
495
1270
|
return
|
|
496
1271
|
|
|
497
|
-
|
|
1272
|
+
root = project_root.expanduser().resolve()
|
|
1273
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
1274
|
+
set_log_dir(root)
|
|
1275
|
+
logger.info("dev doctor: project_root=%s", root)
|
|
1276
|
+
with log_stage("doctor", project_root=root):
|
|
1277
|
+
log_step("doctor/1: running environment checks", project_root=root, trace=True)
|
|
1278
|
+
render_doctor_check(root)
|
|
1279
|
+
log_step("doctor/complete", project_root=root, trace=True)
|