devcouncil 0.3.1 → 0.4.1
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 +92 -33
- 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 +56 -40
- 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 +964 -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 +281 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -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 +87 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +103 -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 +69 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -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 +53 -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 +325 -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/summary.py +21 -3
- 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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""`dev checkout`, `dev release`, and `dev lease` — task lease CLI service layer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from devcouncil.execution.lease_ops import (
|
|
11
|
+
checkout_task_payload,
|
|
12
|
+
list_leases_payload,
|
|
13
|
+
release_task_payload,
|
|
14
|
+
renew_lease_payload,
|
|
15
|
+
)
|
|
16
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
17
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
18
|
+
from devcouncil.utils.json_persist import dump_json
|
|
19
|
+
|
|
20
|
+
lease_app = typer.Typer(help="List and renew task leases.")
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def checkout(
|
|
25
|
+
task_id: str = typer.Argument(..., help="Task ID to acquire."),
|
|
26
|
+
client_id: str = typer.Option(..., "--client-id", help="Stable client id for the lease owner."),
|
|
27
|
+
agent: str | None = typer.Option(None, "--agent", help="Optional agent profile name."),
|
|
28
|
+
force: bool = typer.Option(False, "--force", help="Reclaim a stale lease."),
|
|
29
|
+
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
30
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Acquire a task lease and return scope for gated write tools."""
|
|
33
|
+
root = project_root.expanduser().resolve()
|
|
34
|
+
set_log_dir(root)
|
|
35
|
+
logger.info("dev checkout: task=%s client=%s", task_id, client_id)
|
|
36
|
+
|
|
37
|
+
with log_stage("checkout", project_root=root, task_id=task_id):
|
|
38
|
+
payload = checkout_task_payload(root, task_id=task_id, client_id=client_id, agent=agent, force=force)
|
|
39
|
+
if json_format:
|
|
40
|
+
typer.echo(dump_json(payload, indent=2))
|
|
41
|
+
log_step("checkout/complete", project_root=root, task_id=task_id, trace=True)
|
|
42
|
+
if not payload.get("ok"):
|
|
43
|
+
raise typer.Exit(code=1)
|
|
44
|
+
return
|
|
45
|
+
if not payload.get("ok"):
|
|
46
|
+
typer.echo(payload.get("error", "checkout failed"))
|
|
47
|
+
raise typer.Exit(code=1)
|
|
48
|
+
typer.echo(f"Checked out {task_id} (expires {payload.get('expires_at')})")
|
|
49
|
+
log_step("checkout/complete", project_root=root, task_id=task_id, trace=True)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def release(
|
|
53
|
+
task_id: str = typer.Argument(..., help="Task ID to release."),
|
|
54
|
+
lease_token: str = typer.Option(..., "--lease-token", help="Lease token from checkout."),
|
|
55
|
+
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
56
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Release a held task lease."""
|
|
59
|
+
root = project_root.expanduser().resolve()
|
|
60
|
+
set_log_dir(root)
|
|
61
|
+
logger.info("dev release: task=%s", task_id)
|
|
62
|
+
|
|
63
|
+
with log_stage("release", project_root=root, task_id=task_id):
|
|
64
|
+
payload = release_task_payload(root, task_id=task_id, lease_token=lease_token)
|
|
65
|
+
if json_format:
|
|
66
|
+
typer.echo(dump_json(payload, indent=2))
|
|
67
|
+
log_step("release/complete", project_root=root, task_id=task_id, trace=True)
|
|
68
|
+
if not payload.get("ok"):
|
|
69
|
+
raise typer.Exit(code=1)
|
|
70
|
+
return
|
|
71
|
+
if not payload.get("ok"):
|
|
72
|
+
typer.echo(payload.get("error", "release failed"))
|
|
73
|
+
raise typer.Exit(code=1)
|
|
74
|
+
typer.echo(f"Released {task_id}")
|
|
75
|
+
log_step("release/complete", project_root=root, task_id=task_id, trace=True)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@lease_app.command("list")
|
|
79
|
+
def lease_list(
|
|
80
|
+
active_only: bool = typer.Option(True, "--active-only/--all", help="Only active leases."),
|
|
81
|
+
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
82
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
83
|
+
) -> None:
|
|
84
|
+
"""List task leases for fleet supervision."""
|
|
85
|
+
root = project_root.expanduser().resolve()
|
|
86
|
+
set_log_dir(root)
|
|
87
|
+
payload = list_leases_payload(root, active_only=active_only)
|
|
88
|
+
if json_format:
|
|
89
|
+
typer.echo(dump_json(payload, indent=2))
|
|
90
|
+
if not payload.get("ok"):
|
|
91
|
+
raise typer.Exit(code=1)
|
|
92
|
+
return
|
|
93
|
+
if not payload.get("ok"):
|
|
94
|
+
typer.echo(payload.get("error", "lease list failed"))
|
|
95
|
+
raise typer.Exit(code=1)
|
|
96
|
+
for item in payload.get("leases") or []:
|
|
97
|
+
typer.echo(f"{item['task_id']} {item['owner']} expires={item['expires_at']}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@lease_app.command("renew")
|
|
101
|
+
def lease_renew(
|
|
102
|
+
task_id: str = typer.Argument(..., help="Task ID."),
|
|
103
|
+
lease_token: str = typer.Option(..., "--lease-token", help="Lease token to renew."),
|
|
104
|
+
ttl_seconds: int | None = typer.Option(None, "--ttl-seconds", help="New TTL in seconds."),
|
|
105
|
+
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
106
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
107
|
+
) -> None:
|
|
108
|
+
"""Extend a held task lease's TTL."""
|
|
109
|
+
root = project_root.expanduser().resolve()
|
|
110
|
+
set_log_dir(root)
|
|
111
|
+
payload = renew_lease_payload(root, task_id=task_id, lease_token=lease_token, ttl_seconds=ttl_seconds)
|
|
112
|
+
if json_format:
|
|
113
|
+
typer.echo(dump_json(payload, indent=2))
|
|
114
|
+
if not payload.get("ok"):
|
|
115
|
+
raise typer.Exit(code=1)
|
|
116
|
+
return
|
|
117
|
+
if not payload.get("ok"):
|
|
118
|
+
typer.echo(payload.get("error", "renew failed"))
|
|
119
|
+
raise typer.Exit(code=1)
|
|
120
|
+
typer.echo(f"Renewed {task_id} until {payload.get('expires_at')}")
|
|
@@ -5,6 +5,13 @@ breaks. This command surfaces the always-on shared log
|
|
|
5
5
|
(``.devcouncil/logs/devcouncil.log``) and the per-run logs
|
|
6
6
|
(``.devcouncil/runs/<run_id>/run.log``) without the user needing to remember
|
|
7
7
|
paths or hand-roll ``tail``/``grep``.
|
|
8
|
+
|
|
9
|
+
DELIBERATELY UNINSTRUMENTED: this command's subject *is* the log. Emitting log
|
|
10
|
+
records (``logger.info`` / ``log_stage`` / ``log_step``) from here mutates what
|
|
11
|
+
the user asked to inspect — ``tail`` displaces the trailing lines it was asked
|
|
12
|
+
to show with its own records, and ``path`` creates the very file it is supposed
|
|
13
|
+
to report as "not created yet". An introspection command must not modify what
|
|
14
|
+
it introspects, so no stage instrumentation belongs in this file.
|
|
8
15
|
"""
|
|
9
16
|
|
|
10
17
|
import time
|
|
@@ -14,14 +21,16 @@ from typing import Optional
|
|
|
14
21
|
import typer
|
|
15
22
|
from rich.console import Console
|
|
16
23
|
|
|
17
|
-
from devcouncil.telemetry.logging_setup import
|
|
24
|
+
from devcouncil.telemetry.logging_setup import _resolve_log_path
|
|
18
25
|
|
|
19
26
|
app = typer.Typer(help="View DevCouncil runtime logs (shared and per-run).")
|
|
20
27
|
console = Console()
|
|
21
28
|
|
|
22
29
|
|
|
23
30
|
def _shared_log(root: Path) -> Path:
|
|
24
|
-
|
|
31
|
+
# Honors DEVCOUNCIL_LOG_DIR so `dev logs` inspects the same file the
|
|
32
|
+
# handlers actually write to.
|
|
33
|
+
return _resolve_log_path(root)
|
|
25
34
|
|
|
26
35
|
|
|
27
36
|
def _runs_dir(root: Path) -> Path:
|
|
@@ -32,7 +41,6 @@ def _print_tail(path: Path, limit: int, grep: Optional[str]) -> None:
|
|
|
32
41
|
if not path.exists():
|
|
33
42
|
console.print(f"[yellow]No log at {path}. Run a command first (or pass --project-root).[/yellow]")
|
|
34
43
|
raise typer.Exit(code=0)
|
|
35
|
-
# Read only the tail rather than the whole (potentially large, rotated) file.
|
|
36
44
|
lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
37
45
|
if grep:
|
|
38
46
|
lines = [line for line in lines if grep.lower() in line.lower()]
|
|
@@ -56,11 +64,10 @@ def tail(
|
|
|
56
64
|
if not follow:
|
|
57
65
|
return
|
|
58
66
|
|
|
59
|
-
# Follow mode: poll for appended bytes (the file is append-only between rotations).
|
|
60
67
|
console.print(f"[dim]— following {path} (Ctrl-C to stop) —[/dim]")
|
|
61
68
|
try:
|
|
62
69
|
with open(path, "r", encoding="utf-8", errors="replace") as handle:
|
|
63
|
-
handle.seek(0, 2)
|
|
70
|
+
handle.seek(0, 2)
|
|
64
71
|
while True:
|
|
65
72
|
line = handle.readline()
|
|
66
73
|
if line:
|
|
@@ -1,20 +1,55 @@
|
|
|
1
|
-
import
|
|
1
|
+
from devcouncil.utils.json_persist import dump_json
|
|
2
|
+
import logging
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
|
|
4
5
|
import typer
|
|
5
6
|
|
|
6
7
|
from devcouncil.indexing.lsp import LspInspector
|
|
8
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
7
9
|
|
|
8
|
-
app = typer.Typer(
|
|
10
|
+
app = typer.Typer(
|
|
11
|
+
help=(
|
|
12
|
+
"Inspect optional LSP integration readiness. "
|
|
13
|
+
"Default is detection-only (PATH check). When indexing.lsp_refs is enabled, "
|
|
14
|
+
"reports mode: client for the live references/definition client."
|
|
15
|
+
),
|
|
16
|
+
)
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
9
18
|
|
|
10
19
|
|
|
11
20
|
@app.command("inspect")
|
|
12
21
|
def inspect(
|
|
13
22
|
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
|
|
23
|
+
json_format: bool = typer.Option(False, "--json", help="Emit {mode, servers_detected, note} JSON."),
|
|
14
24
|
):
|
|
15
|
-
"""Print detected language servers
|
|
25
|
+
"""Print detected language servers; mode is detection-only or client."""
|
|
16
26
|
root = project_root.expanduser().resolve()
|
|
27
|
+
# Existence check FIRST: set_log_dir/log_stage create .devcouncil/ under the
|
|
28
|
+
# root, which would materialize a missing/typo'd path and turn the "does not
|
|
29
|
+
# exist" report into a false success.
|
|
17
30
|
if not root.exists():
|
|
18
|
-
typer.echo(
|
|
31
|
+
typer.echo(dump_json({"languages": [], "servers": [], "initialize_requests": {}, "error": f"{root} does not exist"}, indent=2))
|
|
19
32
|
raise typer.Exit(code=1)
|
|
20
|
-
|
|
33
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
34
|
+
set_log_dir(root)
|
|
35
|
+
logger.info("dev lsp inspect")
|
|
36
|
+
with log_stage("lsp", project_root=root):
|
|
37
|
+
log_step("lsp/1: inspecting language servers", project_root=root, trace=True)
|
|
38
|
+
client_enabled = False
|
|
39
|
+
try:
|
|
40
|
+
from devcouncil.indexing.lsp_client import lsp_refs_enabled
|
|
41
|
+
|
|
42
|
+
client_enabled = lsp_refs_enabled(root)
|
|
43
|
+
except Exception:
|
|
44
|
+
client_enabled = False
|
|
45
|
+
summary = LspInspector(root).summary(client_enabled=client_enabled)
|
|
46
|
+
if json_format:
|
|
47
|
+
compact = {
|
|
48
|
+
"mode": summary.get("mode", "detection-only"),
|
|
49
|
+
"servers_detected": summary.get("servers_detected", summary.get("detected_servers", [])),
|
|
50
|
+
"note": summary.get("note", LspInspector._DETECTION_ONLY_NOTE),
|
|
51
|
+
}
|
|
52
|
+
typer.echo(dump_json(compact, indent=2))
|
|
53
|
+
else:
|
|
54
|
+
typer.echo(dump_json(summary, indent=2))
|
|
55
|
+
log_step("lsp/complete", project_root=root, trace=True)
|
|
@@ -1,88 +1,63 @@
|
|
|
1
|
-
import
|
|
1
|
+
import logging
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
5
|
from rich.console import Console
|
|
6
6
|
|
|
7
|
-
from devcouncil.
|
|
7
|
+
from devcouncil.indexing.map_artifacts import (
|
|
8
|
+
_important_surfaces as _important_surfaces,
|
|
9
|
+
_wiki_index_rel as _wiki_index_rel,
|
|
10
|
+
generate_map_artifacts as generate_map_artifacts,
|
|
11
|
+
write_agent_guides,
|
|
12
|
+
)
|
|
8
13
|
from devcouncil.indexing.repo_mapper import RepoMap, RepoMapper
|
|
9
14
|
from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
|
|
10
15
|
from devcouncil.storage.db import get_db
|
|
16
|
+
from devcouncil.telemetry.stages import log_stage, log_step
|
|
17
|
+
from devcouncil.utils.json_persist import dump_json
|
|
18
|
+
|
|
19
|
+
# Back-compat aliases for tests / external importers.
|
|
20
|
+
_write_agent_guides = write_agent_guides
|
|
11
21
|
|
|
12
22
|
console = Console()
|
|
13
23
|
status_console = Console(stderr=True)
|
|
14
|
-
|
|
15
|
-
AGENT_GUIDE_MARKER = "<!-- Managed by dev map: keep this file in sync with .devcouncil/repo_map.json. -->"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _important_surfaces(repo_map: RepoMap) -> list[str]:
|
|
19
|
-
"""Derive the 'important surfaces' list from the computed map, so the guide points
|
|
20
|
-
at THIS repo's real subsystems instead of hardcoded DevCouncil paths."""
|
|
21
|
-
lines: list[str] = []
|
|
22
|
-
for index, subsystem in enumerate(repo_map.subsystems[:6], start=1):
|
|
23
|
-
lines.append(f"{index}. `{subsystem.area}/` — {subsystem.summary}")
|
|
24
|
-
if not lines:
|
|
25
|
-
for index, path in enumerate(repo_map.important_files[:6], start=1):
|
|
26
|
-
lines.append(f"{index}. `{path}`")
|
|
27
|
-
return lines or ["1. See `.devcouncil/repo_map.json` for the file index."]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
def _agent_guide_text(repo_map_path: Path, repo_root: Path, repo_map: RepoMap) -> str:
|
|
31
|
-
return "\n".join(
|
|
32
|
-
[
|
|
33
|
-
AGENT_GUIDE_MARKER,
|
|
34
|
-
"",
|
|
35
|
-
"# Agent Workspace Guide",
|
|
36
|
-
"",
|
|
37
|
-
"Use `.devcouncil/repo_map.json` as the primary file index for this workspace.",
|
|
38
|
-
f"Repo map: `{repo_map_path.relative_to(repo_root).as_posix() if repo_map_path.is_relative_to(repo_root) else repo_map_path}`",
|
|
39
|
-
"",
|
|
40
|
-
"Workflow for agents:",
|
|
41
|
-
"1. Open `.devcouncil/repo_map.json` before guessing at file locations.",
|
|
42
|
-
"2. Use the `files` list to resolve module ownership and nearby siblings.",
|
|
43
|
-
"3. Use `subsystems` for subsystem-level navigation.",
|
|
44
|
-
"4. In `subsystems`, use `entry_points` + `critical_files` for entry points and starting context.",
|
|
45
|
-
"5. Use `role_files` in `subsystems` for subsystem role buckets (entry, runtime, policy, adapters, etc.).",
|
|
46
|
-
"6. Use `neighbors` and `handoff_paths` in `subsystems` to follow cross-subsystem flow.",
|
|
47
|
-
"7. Run `dev map` again after large refactors to refresh the map.",
|
|
48
|
-
"",
|
|
49
|
-
"Important surfaces:",
|
|
50
|
-
*_important_surfaces(repo_map),
|
|
51
|
-
"",
|
|
52
|
-
"If the map and source disagree, trust the source and regenerate the map.",
|
|
53
|
-
]
|
|
54
|
-
)
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
55
25
|
|
|
56
26
|
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
27
|
+
def _liveness_summary(repo_map: RepoMap) -> str | None:
|
|
28
|
+
unreliable = bool(getattr(repo_map, "liveness_unreachable_unreliable", False))
|
|
29
|
+
if not (
|
|
30
|
+
repo_map.entry_roots
|
|
31
|
+
or repo_map.unwired_candidates
|
|
32
|
+
or repo_map.unreachable_files
|
|
33
|
+
or repo_map.dead_symbol_candidates
|
|
34
|
+
or unreliable
|
|
35
|
+
):
|
|
36
|
+
return None
|
|
37
|
+
if not repo_map.entry_roots or unreliable:
|
|
38
|
+
import sys
|
|
65
39
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
40
|
+
print(
|
|
41
|
+
"warning: unreachable_files low-confidence "
|
|
42
|
+
"(liveness_unreachable_unreliable); prefer unwired_candidates / "
|
|
43
|
+
"dead_symbol_candidates and `dev graph dead --confidence extracted`",
|
|
44
|
+
file=sys.stderr,
|
|
45
|
+
)
|
|
46
|
+
samples = repo_map.unwired_candidates[:3] + repo_map.dead_symbol_candidates[:2]
|
|
47
|
+
sample_txt = (", " + ", ".join(samples)) if samples else ""
|
|
48
|
+
unreachable_txt = (
|
|
49
|
+
"omitted (unreliable)"
|
|
50
|
+
if unreliable
|
|
51
|
+
else str(len(repo_map.unreachable_files))
|
|
52
|
+
)
|
|
53
|
+
return (
|
|
54
|
+
f"liveness: {len(repo_map.entry_roots)} entry roots, "
|
|
55
|
+
f"{len(repo_map.unwired_candidates)} unwired, "
|
|
56
|
+
f"{unreachable_txt} unreachable, "
|
|
57
|
+
f"{len(repo_map.dead_symbol_candidates)} dead symbols"
|
|
58
|
+
f"{sample_txt}"
|
|
59
|
+
" (prefer unwired + extracted dead; map clears on any non-test importer)"
|
|
60
|
+
)
|
|
86
61
|
|
|
87
62
|
|
|
88
63
|
def map_repo(
|
|
@@ -99,14 +74,282 @@ def map_repo(
|
|
|
99
74
|
"--scan-deps",
|
|
100
75
|
help="Run available dependency auditors (pip-audit/npm audit/osv-scanner) and record dependency_risks in the map. Off by default.",
|
|
101
76
|
),
|
|
77
|
+
liveness: bool = typer.Option(
|
|
78
|
+
True,
|
|
79
|
+
"--liveness/--no-liveness",
|
|
80
|
+
help="Compute entry_roots / unwired / unreachable / dead_symbol candidate lists (on by default).",
|
|
81
|
+
),
|
|
82
|
+
lsp_refs: bool = typer.Option(
|
|
83
|
+
False,
|
|
84
|
+
"--lsp-refs/--no-lsp-refs",
|
|
85
|
+
help=(
|
|
86
|
+
"Confirm dead-symbol candidates via live LSP references when a language "
|
|
87
|
+
"server is on PATH. Also set indexing.lsp_refs in config.yaml. Off by default."
|
|
88
|
+
),
|
|
89
|
+
),
|
|
90
|
+
refresh_wiki: bool = typer.Option(
|
|
91
|
+
True,
|
|
92
|
+
"--wiki/--no-wiki",
|
|
93
|
+
help="After mapping, refresh stale codebase-wiki page skeletons when a wiki exists (no LLM calls).",
|
|
94
|
+
),
|
|
95
|
+
watch: bool = typer.Option(
|
|
96
|
+
False,
|
|
97
|
+
"--watch",
|
|
98
|
+
help="After the initial map, watch the tree and incrementally refresh on code edits (Ctrl-C to stop).",
|
|
99
|
+
),
|
|
100
|
+
if_stale: bool = typer.Option(
|
|
101
|
+
False,
|
|
102
|
+
"--if-stale",
|
|
103
|
+
help="Fingerprint-check first and exit 0 without rebuilding when the on-disk map is still fresh.",
|
|
104
|
+
),
|
|
105
|
+
pdg: bool = typer.Option(
|
|
106
|
+
False,
|
|
107
|
+
"--pdg/--no-pdg",
|
|
108
|
+
help="Build opt-in PDG/CFG/taint layer after map (Python-only, intra-procedural).",
|
|
109
|
+
),
|
|
102
110
|
):
|
|
103
111
|
"""Build the deterministic repository map without calling an LLM."""
|
|
104
112
|
root = project_root.expanduser().resolve()
|
|
113
|
+
# Reject missing roots before set_log_dir / initialize_project mkdir(parents=True)
|
|
114
|
+
# silently creates an empty project and maps zero files with exit 0.
|
|
115
|
+
if not root.is_dir():
|
|
116
|
+
status_console.print(f"[red]Project root does not exist: {root}[/red]")
|
|
117
|
+
raise typer.Exit(code=1)
|
|
118
|
+
# `dev map /some/repo` parses the path as the GOAL and maps the CWD repo with
|
|
119
|
+
# exit 0 — surface the likely intent instead of silently mapping the wrong repo.
|
|
120
|
+
if goal and project_root == Path("."):
|
|
121
|
+
goal_path = Path(goal).expanduser()
|
|
122
|
+
if goal_path.is_absolute() and goal_path.is_dir() and goal_path.resolve() != root:
|
|
123
|
+
status_console.print(
|
|
124
|
+
f"[yellow]Goal argument is a directory ({goal}). Did you mean "
|
|
125
|
+
f"`dev map --project-root {goal}`? Mapping {root} with it as goal text.[/yellow]"
|
|
126
|
+
)
|
|
127
|
+
from devcouncil.telemetry.logging_setup import set_log_dir
|
|
128
|
+
set_log_dir(root)
|
|
129
|
+
# CLI flag OR config; flag alone is enough without rewriting config.
|
|
130
|
+
use_lsp = lsp_refs
|
|
131
|
+
if not use_lsp:
|
|
132
|
+
try:
|
|
133
|
+
from devcouncil.app.config import load_config
|
|
134
|
+
|
|
135
|
+
use_lsp = bool(load_config(root).indexing.lsp_refs)
|
|
136
|
+
except Exception:
|
|
137
|
+
use_lsp = False
|
|
138
|
+
logger.info(
|
|
139
|
+
"dev map: goal=%r scan_deps=%s liveness=%s lsp_refs=%s if_stale=%s",
|
|
140
|
+
goal, scan_deps, liveness, use_lsp, if_stale,
|
|
141
|
+
)
|
|
142
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
143
|
+
|
|
105
144
|
initialize_project(root, quiet=True, with_map=False)
|
|
106
145
|
if not get_db(root):
|
|
107
146
|
raise typer.Exit(code=1)
|
|
108
147
|
|
|
109
148
|
output = output if output.is_absolute() else root / output
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
149
|
+
if if_stale and output.is_file():
|
|
150
|
+
try:
|
|
151
|
+
from devcouncil.utils.json_persist import read_json
|
|
152
|
+
|
|
153
|
+
data = read_json(output) or {}
|
|
154
|
+
if isinstance(data, dict) and not RepoMapper(root).map_is_stale(data):
|
|
155
|
+
status_console.print(f"[dim]Map is fresh; skipping rebuild ({output})[/dim]")
|
|
156
|
+
raise typer.Exit(code=0)
|
|
157
|
+
except typer.Exit:
|
|
158
|
+
raise
|
|
159
|
+
except Exception:
|
|
160
|
+
logger.debug("if-stale freshness check failed; rebuilding", exc_info=True)
|
|
161
|
+
|
|
162
|
+
with log_stage("map", project_root=root, scan_deps=scan_deps):
|
|
163
|
+
log_step("map/1: generating repository map", project_root=root, trace=True)
|
|
164
|
+
from devcouncil.codeintel.build_control import GraphBuildBusy
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
from devcouncil.indexing.map_artifacts import refresh_map_artifacts
|
|
168
|
+
|
|
169
|
+
refresh = refresh_map_artifacts(
|
|
170
|
+
root,
|
|
171
|
+
output,
|
|
172
|
+
goal,
|
|
173
|
+
scan_dependencies=scan_deps,
|
|
174
|
+
liveness=liveness,
|
|
175
|
+
lsp_refs=use_lsp,
|
|
176
|
+
)
|
|
177
|
+
repo_map = refresh.repo_map
|
|
178
|
+
except GraphBuildBusy as exc:
|
|
179
|
+
status_console.print(f"[red]{exc}[/red]")
|
|
180
|
+
raise typer.Exit(code=1) from exc
|
|
181
|
+
if refresh.degraded:
|
|
182
|
+
status_console.print(
|
|
183
|
+
f"[red]Map wrote lean/degraded artifacts: {refresh.reason or refresh.mode}[/red]"
|
|
184
|
+
)
|
|
185
|
+
raise typer.Exit(code=1)
|
|
186
|
+
if refresh.compatibility_export_degraded:
|
|
187
|
+
status_console.print(
|
|
188
|
+
f"[yellow]Compatibility export degraded (canonical SQLite ok): "
|
|
189
|
+
f"{refresh.reason}[/yellow]"
|
|
190
|
+
)
|
|
191
|
+
try:
|
|
192
|
+
typer.echo(dump_json(repo_map.model_dump(), indent=2))
|
|
193
|
+
except BrokenPipeError:
|
|
194
|
+
# Consumer closed stdout early (`dev map | head`). The artifacts are
|
|
195
|
+
# already written — a closed pipe must not turn the stage red.
|
|
196
|
+
import os
|
|
197
|
+
import sys
|
|
198
|
+
|
|
199
|
+
logger.debug("stdout pipe closed while streaming repo map JSON")
|
|
200
|
+
try:
|
|
201
|
+
os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
|
|
202
|
+
except OSError:
|
|
203
|
+
pass
|
|
204
|
+
status_console.print(f"[green]Wrote repository map to {output}[/green]")
|
|
205
|
+
graph_out = root / ".devcouncil" / "graph" / "code_graph.json"
|
|
206
|
+
if not graph_out.is_file():
|
|
207
|
+
# The export write failed or an external cleanup removed the JSON
|
|
208
|
+
# while SQLite (canonical) still holds the graph — re-export instead
|
|
209
|
+
# of exiting green without the documented artifact.
|
|
210
|
+
from devcouncil.indexing.graph.build import export_code_graph_json
|
|
211
|
+
|
|
212
|
+
if export_code_graph_json(root) is not None:
|
|
213
|
+
status_console.print(
|
|
214
|
+
f"[yellow]Code graph JSON was missing; re-exported from store to {graph_out}[/yellow]"
|
|
215
|
+
)
|
|
216
|
+
else:
|
|
217
|
+
status_console.print(
|
|
218
|
+
f"[yellow]Code graph JSON missing and store re-export failed ({graph_out}); "
|
|
219
|
+
"run `dev graph doctor`[/yellow]"
|
|
220
|
+
)
|
|
221
|
+
if graph_out.is_file():
|
|
222
|
+
status_console.print(f"[green]Wrote code graph to {graph_out}[/green]")
|
|
223
|
+
if pdg:
|
|
224
|
+
try:
|
|
225
|
+
from devcouncil.indexing.graph.build import (
|
|
226
|
+
build_pdg_for_paths,
|
|
227
|
+
load_code_graph,
|
|
228
|
+
merge_pdg_into_graph,
|
|
229
|
+
write_code_graph,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
graph = load_code_graph(root)
|
|
233
|
+
if graph is not None:
|
|
234
|
+
layer = build_pdg_for_paths(root, graph)
|
|
235
|
+
shards = merge_pdg_into_graph(graph, layer)
|
|
236
|
+
merged: dict = {}
|
|
237
|
+
try:
|
|
238
|
+
from devcouncil.codeintel import get_codeintel_service
|
|
239
|
+
|
|
240
|
+
merged = dict(get_codeintel_service(root).store.analysis_shards())
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
for path, payload in shards.items():
|
|
244
|
+
merged.setdefault(path, {}).update(payload)
|
|
245
|
+
write_code_graph(root, graph, analysis_shards=merged)
|
|
246
|
+
stats = (graph.meta.get("pdg") or {}).get("stats") or {}
|
|
247
|
+
status_console.print(
|
|
248
|
+
f"[green]Wrote PDG layer[/green] "
|
|
249
|
+
f"({stats.get('function_count', 0)} functions, "
|
|
250
|
+
f"{stats.get('taint_count', 0)} taint findings)"
|
|
251
|
+
)
|
|
252
|
+
except Exception as exc:
|
|
253
|
+
logger.warning("PDG build after map failed: %s", exc)
|
|
254
|
+
try:
|
|
255
|
+
from devcouncil.app.config import load_config
|
|
256
|
+
|
|
257
|
+
if bool(load_config(root).indexing.write_graph_html):
|
|
258
|
+
from devcouncil.indexing.viz import write_graph_html
|
|
259
|
+
|
|
260
|
+
html_out = write_graph_html(root, open_browser=False)
|
|
261
|
+
status_console.print(f"[green]Wrote graph HTML to {html_out}[/green]")
|
|
262
|
+
except Exception as exc:
|
|
263
|
+
logger.warning("Failed to write graph.html after map: %s", exc)
|
|
264
|
+
summary = _liveness_summary(repo_map)
|
|
265
|
+
if summary:
|
|
266
|
+
status_console.print(f"[cyan]{summary}[/cyan]")
|
|
267
|
+
if refresh_wiki:
|
|
268
|
+
_refresh_wiki_skeletons(root, repo_map)
|
|
269
|
+
log_step("map/complete", project_root=root, trace=True)
|
|
270
|
+
if watch:
|
|
271
|
+
_watch_map(root, liveness=liveness)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def graph_context_cmd(
|
|
275
|
+
files: list[str] = typer.Option([], "--file", help="Changed files to scope blast-radius (repeatable)."),
|
|
276
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
|
|
277
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
278
|
+
) -> None:
|
|
279
|
+
"""Return code-review-graph context for the given files."""
|
|
280
|
+
root = project_root.expanduser().resolve()
|
|
281
|
+
context = CodeReviewGraphAdapter(root).get_context(files)
|
|
282
|
+
if json_output:
|
|
283
|
+
typer.echo(context.model_dump_json(indent=2))
|
|
284
|
+
return
|
|
285
|
+
if not context.available:
|
|
286
|
+
console.print("[dim]Code-review graph integration is not available.[/dim]")
|
|
287
|
+
return
|
|
288
|
+
if context.impacted_files:
|
|
289
|
+
console.print("[cyan]Impacted files:[/cyan] " + ", ".join(context.impacted_files[:20]))
|
|
290
|
+
if context.related_tests:
|
|
291
|
+
console.print("[cyan]Related tests:[/cyan] " + ", ".join(context.related_tests[:20]))
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _refresh_wiki_skeletons(root: Path, repo_map: RepoMap) -> None:
|
|
295
|
+
"""Keep the codebase wiki in step with the map — seamlessly, but deterministically.
|
|
296
|
+
|
|
297
|
+
Only runs when a wiki was already generated (`dev wiki update` opted the repo in),
|
|
298
|
+
and only rewrites pages whose repo-map slice changed. No model calls here: `dev map`
|
|
299
|
+
must stay fast and offline; run `dev wiki update` for LLM-enriched prose on the
|
|
300
|
+
refreshed pages.
|
|
301
|
+
"""
|
|
302
|
+
try:
|
|
303
|
+
from devcouncil.knowledge.wiki import (
|
|
304
|
+
_project_name,
|
|
305
|
+
generate_wiki,
|
|
306
|
+
wiki_dir_for,
|
|
307
|
+
wiki_stale_pages,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
wiki_dir = wiki_dir_for(root)
|
|
311
|
+
if not (wiki_dir / "index.md").is_file():
|
|
312
|
+
return
|
|
313
|
+
stale = wiki_stale_pages(root, repo_map, wiki_dir)
|
|
314
|
+
if not stale:
|
|
315
|
+
return
|
|
316
|
+
result = generate_wiki(root, repo_map, wiki_dir, project_name=_project_name(root))
|
|
317
|
+
status_console.print(
|
|
318
|
+
f"[green]Refreshed {len(result.changed)} stale wiki page(s)[/green] "
|
|
319
|
+
"(skeleton only — run `dev wiki update` for LLM-enriched prose)."
|
|
320
|
+
)
|
|
321
|
+
except Exception as exc:
|
|
322
|
+
# The wiki is a convenience layer; never let it fail `dev map`.
|
|
323
|
+
logger.warning("Wiki refresh after map failed: %s", exc)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _watch_map(root: Path, *, liveness: bool = True) -> None:
|
|
327
|
+
"""Run the shared code-intelligence coordinator for map compatibility."""
|
|
328
|
+
import time
|
|
329
|
+
|
|
330
|
+
from devcouncil.codeintel.sync import get_sync_coordinator
|
|
331
|
+
from devcouncil.indexing.graph.build import refresh_map_for_paths
|
|
332
|
+
|
|
333
|
+
coordinator = get_sync_coordinator(
|
|
334
|
+
root,
|
|
335
|
+
debounce_seconds=0.8,
|
|
336
|
+
sync_callback=lambda paths: refresh_map_for_paths(root, paths, liveness=liveness),
|
|
337
|
+
)
|
|
338
|
+
state = coordinator.start()
|
|
339
|
+
status_console.print(
|
|
340
|
+
f"[cyan]Watching {root} with {state.backend or 'reconciliation'} "
|
|
341
|
+
f"(state={state.state}, debounce 0.8s). Ctrl-C to stop.[/cyan]"
|
|
342
|
+
)
|
|
343
|
+
try:
|
|
344
|
+
while True:
|
|
345
|
+
time.sleep(0.8)
|
|
346
|
+
before = coordinator.status().pending
|
|
347
|
+
if before and not coordinator.sync_now():
|
|
348
|
+
failure = coordinator.status().last_error or coordinator.status().degraded_reason
|
|
349
|
+
status_console.print(f"[yellow]Watch refresh failed (ignored): {failure}[/yellow]")
|
|
350
|
+
elif before:
|
|
351
|
+
status_console.print(f"[green]Refreshed map for {len(before)} path(s)[/green]")
|
|
352
|
+
except KeyboardInterrupt:
|
|
353
|
+
status_console.print("Stopped watching.")
|
|
354
|
+
finally:
|
|
355
|
+
coordinator.stop(timeout=2)
|