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,114 @@
|
|
|
1
|
+
"""Loopback JSON broker that keeps CLI debug sessions alive across invocations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import secrets
|
|
9
|
+
import socketserver
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from devcouncil.codeintel.debug.session import get_debug_manager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BrokerServer(socketserver.ThreadingTCPServer):
|
|
17
|
+
allow_reuse_address = False
|
|
18
|
+
daemon_threads = True
|
|
19
|
+
|
|
20
|
+
def __init__(self, root: Path, token: str):
|
|
21
|
+
super().__init__(("127.0.0.1", 0), BrokerHandler)
|
|
22
|
+
self.root = root
|
|
23
|
+
self.token = token
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BrokerHandler(socketserver.StreamRequestHandler):
|
|
27
|
+
def handle(self) -> None:
|
|
28
|
+
try:
|
|
29
|
+
request = json.loads(self.rfile.readline(4 * 1024 * 1024).decode("utf-8"))
|
|
30
|
+
if request.get("token") != self.server.token: # type: ignore[attr-defined]
|
|
31
|
+
raise PermissionError("invalid debug broker token")
|
|
32
|
+
result = dispatch(self.server.root, str(request.get("action", "")), dict(request.get("params") or {})) # type: ignore[attr-defined]
|
|
33
|
+
response = {"ok": True, "result": result}
|
|
34
|
+
except Exception as exc:
|
|
35
|
+
response = {"ok": False, "error": f"{type(exc).__name__}: {exc}"}
|
|
36
|
+
self.wfile.write((json.dumps(response, default=str) + "\n").encode("utf-8"))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def dispatch(root: Path, action: str, params: dict[str, Any]) -> Any:
|
|
40
|
+
manager = get_debug_manager()
|
|
41
|
+
if action == "ping":
|
|
42
|
+
return {"pid": os.getpid(), "sessions": manager.list()}
|
|
43
|
+
if action == "start":
|
|
44
|
+
session = manager.start(
|
|
45
|
+
root,
|
|
46
|
+
[str(value) for value in params["adapter_command"]],
|
|
47
|
+
request=str(params.get("request", "launch")),
|
|
48
|
+
arguments=dict(params.get("arguments") or {}),
|
|
49
|
+
initial_breakpoints={
|
|
50
|
+
str(source): [int(line) for line in lines]
|
|
51
|
+
for source, lines in dict(params.get("initial_breakpoints") or {}).items()
|
|
52
|
+
},
|
|
53
|
+
timeout=float(params.get("timeout", 30.0)),
|
|
54
|
+
)
|
|
55
|
+
return session.as_dict()
|
|
56
|
+
if action == "list":
|
|
57
|
+
return manager.list()
|
|
58
|
+
if action == "breakpoints":
|
|
59
|
+
return manager.set_breakpoints(str(params["session_id"]), str(params["source"]), params.get("lines") or [])
|
|
60
|
+
if action == "control":
|
|
61
|
+
return manager.control(
|
|
62
|
+
str(params["session_id"]),
|
|
63
|
+
str(params["debug_action"]),
|
|
64
|
+
thread_id=int(params["thread_id"]) if params.get("thread_id") is not None else None,
|
|
65
|
+
)
|
|
66
|
+
if action == "inspect":
|
|
67
|
+
return manager.inspect(str(params["session_id"]), str(params["operation"]), dict(params.get("arguments") or {}))
|
|
68
|
+
if action == "evaluate":
|
|
69
|
+
return manager.evaluate(
|
|
70
|
+
str(params["session_id"]),
|
|
71
|
+
str(params["expression"]),
|
|
72
|
+
frame_id=int(params["frame_id"]) if params.get("frame_id") is not None else None,
|
|
73
|
+
allow_side_effects=bool(params.get("allow_side_effects")),
|
|
74
|
+
)
|
|
75
|
+
if action == "capture_stack":
|
|
76
|
+
return manager.capture_stack(str(params["session_id"]), thread_id=int(params["thread_id"]))
|
|
77
|
+
if action == "stop":
|
|
78
|
+
manager.stop(str(params["session_id"]), terminate_debuggee=bool(params.get("terminate_debuggee", True)))
|
|
79
|
+
return {"stopped": params["session_id"]}
|
|
80
|
+
raise ValueError(f"unknown broker action: {action}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def main() -> int:
|
|
84
|
+
parser = argparse.ArgumentParser()
|
|
85
|
+
parser.add_argument("--root", type=Path, required=True)
|
|
86
|
+
ns = parser.parse_args()
|
|
87
|
+
root = ns.root.expanduser().resolve()
|
|
88
|
+
credential = secrets.token_urlsafe(32)
|
|
89
|
+
server = BrokerServer(root, credential)
|
|
90
|
+
state_path = root / ".devcouncil" / "codeintel" / "debug-broker.json"
|
|
91
|
+
state_path.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
state_path.write_text(json.dumps({
|
|
93
|
+
"pid": os.getpid(),
|
|
94
|
+
"host": "127.0.0.1",
|
|
95
|
+
"port": server.server_address[1],
|
|
96
|
+
"token": credential,
|
|
97
|
+
}), encoding="utf-8")
|
|
98
|
+
try:
|
|
99
|
+
state_path.chmod(0o600)
|
|
100
|
+
except OSError:
|
|
101
|
+
pass
|
|
102
|
+
try:
|
|
103
|
+
server.serve_forever(poll_interval=0.25)
|
|
104
|
+
finally:
|
|
105
|
+
server.server_close()
|
|
106
|
+
try:
|
|
107
|
+
state_path.unlink()
|
|
108
|
+
except OSError:
|
|
109
|
+
pass
|
|
110
|
+
return 0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
if __name__ == "__main__":
|
|
114
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Client/startup helper for the persistent local debug broker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import socket
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DebugBrokerClient:
|
|
15
|
+
def __init__(self, root: Path):
|
|
16
|
+
self.root = root.expanduser().resolve()
|
|
17
|
+
self.state_path = self.root / ".devcouncil" / "codeintel" / "debug-broker.json"
|
|
18
|
+
|
|
19
|
+
def ensure_started(self, *, timeout: float = 5.0) -> None:
|
|
20
|
+
try:
|
|
21
|
+
self.call("ping", timeout=0.5)
|
|
22
|
+
return
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
25
|
+
try:
|
|
26
|
+
self.state_path.unlink()
|
|
27
|
+
except OSError:
|
|
28
|
+
pass
|
|
29
|
+
subprocess.Popen(
|
|
30
|
+
[sys.executable, "-m", "devcouncil.codeintel.debug.broker", "--root", str(self.root)],
|
|
31
|
+
cwd=self.root,
|
|
32
|
+
stdin=subprocess.DEVNULL,
|
|
33
|
+
stdout=subprocess.DEVNULL,
|
|
34
|
+
stderr=subprocess.DEVNULL,
|
|
35
|
+
start_new_session=True,
|
|
36
|
+
)
|
|
37
|
+
deadline = time.monotonic() + timeout
|
|
38
|
+
while time.monotonic() < deadline:
|
|
39
|
+
try:
|
|
40
|
+
self.call("ping", timeout=0.5)
|
|
41
|
+
return
|
|
42
|
+
except Exception:
|
|
43
|
+
time.sleep(0.05)
|
|
44
|
+
raise TimeoutError("debug broker did not start")
|
|
45
|
+
|
|
46
|
+
def call(self, action: str, params: dict[str, Any] | None = None, *, timeout: float = 30.0) -> Any:
|
|
47
|
+
state = json.loads(self.state_path.read_text(encoding="utf-8"))
|
|
48
|
+
request = json.dumps({"token": state["token"], "action": action, "params": params or {}}) + "\n"
|
|
49
|
+
with socket.create_connection((str(state["host"]), int(state["port"])), timeout=timeout) as sock:
|
|
50
|
+
sock.sendall(request.encode("utf-8"))
|
|
51
|
+
sock.settimeout(timeout)
|
|
52
|
+
chunks = bytearray()
|
|
53
|
+
while not chunks.endswith(b"\n"):
|
|
54
|
+
chunk = sock.recv(65536)
|
|
55
|
+
if not chunk:
|
|
56
|
+
break
|
|
57
|
+
chunks.extend(chunk)
|
|
58
|
+
response = json.loads(bytes(chunks).decode("utf-8"))
|
|
59
|
+
if not response.get("ok"):
|
|
60
|
+
raise RuntimeError(str(response.get("error") or "debug broker request failed"))
|
|
61
|
+
return response.get("result")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Explicit one-time debugger consent stored in project configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def debug_consent_enabled(root: Path) -> bool:
|
|
11
|
+
try:
|
|
12
|
+
from devcouncil.app.config import load_config
|
|
13
|
+
|
|
14
|
+
return bool(load_config(root).code_intelligence.debug.auto_discover)
|
|
15
|
+
except (FileNotFoundError, ValueError):
|
|
16
|
+
return False
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def set_debug_consent(root: Path, enabled: bool = True) -> Path:
|
|
20
|
+
path = root.expanduser().resolve() / ".devcouncil" / "config.yaml"
|
|
21
|
+
if not path.is_file():
|
|
22
|
+
raise FileNotFoundError(f"Config not found at {path}. Run 'dev init' first.")
|
|
23
|
+
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
24
|
+
codeintel = raw.setdefault("code_intelligence", {})
|
|
25
|
+
debug = codeintel.setdefault("debug", {})
|
|
26
|
+
debug["auto_discover"] = bool(enabled)
|
|
27
|
+
path.write_text(yaml.safe_dump(raw, sort_keys=False), encoding="utf-8")
|
|
28
|
+
return path
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def require_debug_consent(root: Path) -> None:
|
|
32
|
+
if not debug_consent_enabled(root):
|
|
33
|
+
raise PermissionError(
|
|
34
|
+
"Debugger discovery/execution is disabled. Run `dev debug discover --consent` "
|
|
35
|
+
"or set code_intelligence.debug.auto_discover: true."
|
|
36
|
+
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Side-effect-free discovery of installed Debug Adapter Protocol adapters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
import importlib.metadata
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from dataclasses import asdict, dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from devcouncil.codeintel.debug.fingerprint import executable_hash
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class AdapterInfo:
|
|
20
|
+
id: str
|
|
21
|
+
name: str
|
|
22
|
+
command: tuple[str, ...]
|
|
23
|
+
path: str
|
|
24
|
+
executable_hash: str
|
|
25
|
+
version: str = ""
|
|
26
|
+
transport: str = "stdio"
|
|
27
|
+
languages: tuple[str, ...] = ()
|
|
28
|
+
requests: tuple[str, ...] = ("launch", "attach")
|
|
29
|
+
|
|
30
|
+
def as_dict(self) -> dict[str, Any]:
|
|
31
|
+
data = asdict(self)
|
|
32
|
+
data["command"] = list(self.command)
|
|
33
|
+
data["languages"] = list(self.languages)
|
|
34
|
+
data["requests"] = list(self.requests)
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
_EXECUTABLES = (
|
|
39
|
+
("lldb-dap", "LLDB DAP", ("c", "cpp", "objective-c", "swift", "rust"), ("--version",)),
|
|
40
|
+
("codelldb", "CodeLLDB", ("c", "cpp", "objective-c", "swift", "rust"), ("--version",)),
|
|
41
|
+
("dlv", "Delve", ("go",), ("version",)),
|
|
42
|
+
("netcoredbg", ".NET Core Debugger", ("csharp", "vbnet"), ("--version",)),
|
|
43
|
+
("js-debug-adapter", "JavaScript Debugger", ("javascript", "typescript"), ("--version",)),
|
|
44
|
+
("node-debug2", "Node Debug2", ("javascript", "typescript"), ("--version",)),
|
|
45
|
+
("php-debug-adapter", "PHP Debug Adapter", ("php",), ("--version",)),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _command_version(command: tuple[str, ...]) -> str:
|
|
50
|
+
"""Run an adapter's non-mutating version command with a strict bound."""
|
|
51
|
+
try:
|
|
52
|
+
completed = subprocess.run(
|
|
53
|
+
list(command),
|
|
54
|
+
stdin=subprocess.DEVNULL,
|
|
55
|
+
stdout=subprocess.PIPE,
|
|
56
|
+
stderr=subprocess.STDOUT,
|
|
57
|
+
timeout=2.0,
|
|
58
|
+
check=False,
|
|
59
|
+
text=True,
|
|
60
|
+
)
|
|
61
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
62
|
+
return ""
|
|
63
|
+
lines = [line.strip() for line in completed.stdout.splitlines() if line.strip()]
|
|
64
|
+
return lines[0][:256] if lines else ""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def discover_adapters() -> list[AdapterInfo]:
|
|
68
|
+
adapters: list[AdapterInfo] = []
|
|
69
|
+
if importlib.util.find_spec("debugpy") is not None:
|
|
70
|
+
# Preserve the virtual-environment interpreter path. Resolving its
|
|
71
|
+
# symlink can escape the environment that actually contains debugpy.
|
|
72
|
+
path = str(Path(sys.executable).absolute())
|
|
73
|
+
try:
|
|
74
|
+
version = importlib.metadata.version("debugpy")
|
|
75
|
+
except importlib.metadata.PackageNotFoundError:
|
|
76
|
+
version = ""
|
|
77
|
+
adapters.append(AdapterInfo(
|
|
78
|
+
id="debugpy",
|
|
79
|
+
name="Python debugpy",
|
|
80
|
+
command=(path, "-m", "debugpy.adapter"),
|
|
81
|
+
path=path,
|
|
82
|
+
executable_hash=executable_hash(path),
|
|
83
|
+
version=version,
|
|
84
|
+
languages=("python",),
|
|
85
|
+
))
|
|
86
|
+
configured_node = os.environ.get("DEVCOUNCIL_NODE_DEBUG2_PATH")
|
|
87
|
+
node = shutil.which("node")
|
|
88
|
+
if configured_node and node:
|
|
89
|
+
adapter_path = str(Path(configured_node).expanduser().resolve())
|
|
90
|
+
if Path(adapter_path).is_file():
|
|
91
|
+
adapters.append(AdapterInfo(
|
|
92
|
+
id="node-debug2",
|
|
93
|
+
name="Node Debug2",
|
|
94
|
+
command=(str(Path(node).resolve()), adapter_path),
|
|
95
|
+
path=adapter_path,
|
|
96
|
+
executable_hash=executable_hash(adapter_path),
|
|
97
|
+
version=os.environ.get("DEVCOUNCIL_NODE_DEBUG2_VERSION", ""),
|
|
98
|
+
languages=("javascript", "typescript"),
|
|
99
|
+
))
|
|
100
|
+
for executable, name, languages, version_args in _EXECUTABLES:
|
|
101
|
+
resolved = shutil.which(executable)
|
|
102
|
+
if resolved:
|
|
103
|
+
path = str(Path(resolved).resolve())
|
|
104
|
+
adapters.append(AdapterInfo(
|
|
105
|
+
id=executable,
|
|
106
|
+
name=name,
|
|
107
|
+
command=(path,),
|
|
108
|
+
path=path,
|
|
109
|
+
executable_hash=executable_hash(path),
|
|
110
|
+
version=_command_version((path, *version_args)),
|
|
111
|
+
languages=languages,
|
|
112
|
+
))
|
|
113
|
+
return adapters
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def adapter_by_id(adapter_id: str) -> AdapterInfo | None:
|
|
117
|
+
return next((adapter for adapter in discover_adapters() if adapter.id == adapter_id), None)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def adapter_by_command(command: tuple[str, ...] | list[str]) -> AdapterInfo | None:
|
|
121
|
+
if not command:
|
|
122
|
+
return None
|
|
123
|
+
executable = str(Path(command[0]).expanduser().resolve())
|
|
124
|
+
return next(
|
|
125
|
+
(
|
|
126
|
+
adapter
|
|
127
|
+
for adapter in discover_adapters()
|
|
128
|
+
if str(Path(adapter.command[0]).expanduser().resolve()) == executable
|
|
129
|
+
and tuple(command[1:]) == adapter.command[1:]
|
|
130
|
+
),
|
|
131
|
+
None,
|
|
132
|
+
)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Source, build, and executable fingerprints for runtime evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _sha256(data: bytes) -> str:
|
|
11
|
+
return hashlib.sha256(data).hexdigest()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def executable_hash(path: str | Path) -> str:
|
|
15
|
+
try:
|
|
16
|
+
return _sha256(Path(path).resolve().read_bytes())
|
|
17
|
+
except OSError:
|
|
18
|
+
return ""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_GIT_TIMEOUT_SECONDS = 15.0
|
|
22
|
+
_FALLBACK_SKIP_PARTS = frozenset({
|
|
23
|
+
".git", ".devcouncil", "node_modules", "vendor", ".venv", "venv",
|
|
24
|
+
"dist", "build", "target", "__pycache__",
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def source_fingerprint(root: Path) -> str:
|
|
29
|
+
root = root.expanduser().resolve()
|
|
30
|
+
head = b""
|
|
31
|
+
dirty = b""
|
|
32
|
+
try:
|
|
33
|
+
head = subprocess.check_output(
|
|
34
|
+
["git", "rev-parse", "HEAD"],
|
|
35
|
+
cwd=root,
|
|
36
|
+
stderr=subprocess.DEVNULL,
|
|
37
|
+
timeout=_GIT_TIMEOUT_SECONDS,
|
|
38
|
+
)
|
|
39
|
+
dirty = subprocess.check_output(
|
|
40
|
+
["git", "diff", "--no-ext-diff", "--binary", "HEAD"],
|
|
41
|
+
cwd=root,
|
|
42
|
+
stderr=subprocess.DEVNULL,
|
|
43
|
+
timeout=_GIT_TIMEOUT_SECONDS,
|
|
44
|
+
)
|
|
45
|
+
untracked = subprocess.check_output(
|
|
46
|
+
["git", "ls-files", "--others", "--exclude-standard", "-z"],
|
|
47
|
+
cwd=root,
|
|
48
|
+
stderr=subprocess.DEVNULL,
|
|
49
|
+
timeout=_GIT_TIMEOUT_SECONDS,
|
|
50
|
+
)
|
|
51
|
+
for raw_path in sorted(value for value in untracked.split(b"\0") if value):
|
|
52
|
+
path = root / raw_path.decode("utf-8", errors="surrogateescape")
|
|
53
|
+
try:
|
|
54
|
+
dirty += b"\0untracked:" + raw_path + b"\0" + _sha256(path.read_bytes()).encode()
|
|
55
|
+
except OSError:
|
|
56
|
+
continue
|
|
57
|
+
except (OSError, subprocess.SubprocessError):
|
|
58
|
+
for path in sorted(root.rglob("*")):
|
|
59
|
+
if not path.is_file() or _FALLBACK_SKIP_PARTS.intersection(path.parts):
|
|
60
|
+
continue
|
|
61
|
+
try:
|
|
62
|
+
stat = path.stat()
|
|
63
|
+
dirty += f"{path.relative_to(root).as_posix()}\0{stat.st_size}\0{stat.st_mtime_ns}\n".encode()
|
|
64
|
+
except OSError:
|
|
65
|
+
continue
|
|
66
|
+
return _sha256(head + b"\0" + dirty)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def build_fingerprint(root: Path, executable: str | Path = "") -> str:
|
|
70
|
+
parts = [source_fingerprint(root).encode()]
|
|
71
|
+
for rel in (
|
|
72
|
+
".devcouncil/config.yaml",
|
|
73
|
+
"pyproject.toml",
|
|
74
|
+
"package.json",
|
|
75
|
+
"Cargo.toml",
|
|
76
|
+
"go.mod",
|
|
77
|
+
"CMakeLists.txt",
|
|
78
|
+
):
|
|
79
|
+
try:
|
|
80
|
+
parts.append((root / rel).read_bytes())
|
|
81
|
+
except OSError:
|
|
82
|
+
pass
|
|
83
|
+
if executable:
|
|
84
|
+
parts.append(executable_hash(executable).encode())
|
|
85
|
+
return _sha256(b"\0".join(parts))
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""Capability-negotiated Debug Adapter Protocol client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import queue
|
|
7
|
+
import socket
|
|
8
|
+
import subprocess
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, BinaryIO, Sequence, cast
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DAPError(RuntimeError):
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def encode_message(message: dict[str, Any]) -> bytes:
|
|
20
|
+
body = json.dumps(message, separators=(",", ":")).encode("utf-8")
|
|
21
|
+
return f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + body
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_message(stream: BinaryIO) -> dict[str, Any] | None:
|
|
25
|
+
headers: dict[str, str] = {}
|
|
26
|
+
while True:
|
|
27
|
+
line = stream.readline()
|
|
28
|
+
if not line:
|
|
29
|
+
return None
|
|
30
|
+
if line in {b"\r\n", b"\n"}:
|
|
31
|
+
break
|
|
32
|
+
key, _, value = line.decode("ascii", errors="replace").partition(":")
|
|
33
|
+
headers[key.strip().lower()] = value.strip()
|
|
34
|
+
try:
|
|
35
|
+
length = int(headers["content-length"])
|
|
36
|
+
except (KeyError, ValueError) as exc:
|
|
37
|
+
raise DAPError("DAP message is missing a valid Content-Length header") from exc
|
|
38
|
+
body = stream.read(length)
|
|
39
|
+
if len(body) != length:
|
|
40
|
+
raise DAPError("DAP stream ended mid-message")
|
|
41
|
+
parsed = json.loads(body.decode("utf-8"))
|
|
42
|
+
if not isinstance(parsed, dict):
|
|
43
|
+
raise DAPError("DAP message body must be an object")
|
|
44
|
+
return parsed
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class PendingRequest:
|
|
49
|
+
sequence: int
|
|
50
|
+
responses: "queue.Queue[dict[str, Any]]"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class DAPClient:
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
reader: BinaryIO,
|
|
57
|
+
writer: BinaryIO,
|
|
58
|
+
*,
|
|
59
|
+
process: subprocess.Popen[bytes] | None = None,
|
|
60
|
+
sock: socket.socket | None = None,
|
|
61
|
+
):
|
|
62
|
+
self.reader = reader
|
|
63
|
+
self.writer = writer
|
|
64
|
+
self.process = process
|
|
65
|
+
self.sock = sock
|
|
66
|
+
self.capabilities: dict[str, Any] = {}
|
|
67
|
+
self._sequence = 0
|
|
68
|
+
self._write_lock = threading.Lock()
|
|
69
|
+
self._pending: dict[int, queue.Queue[dict[str, Any]]] = {}
|
|
70
|
+
self._events: "queue.Queue[dict[str, Any]]" = queue.Queue()
|
|
71
|
+
self._closed = threading.Event()
|
|
72
|
+
self._reader_thread = threading.Thread(target=self._read_loop, name="devcouncil-dap-reader", daemon=True)
|
|
73
|
+
self._reader_thread.start()
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def start_stdio(cls, command: Sequence[str], *, cwd: str | None = None) -> "DAPClient":
|
|
77
|
+
process = subprocess.Popen(
|
|
78
|
+
list(command),
|
|
79
|
+
cwd=cwd,
|
|
80
|
+
stdin=subprocess.PIPE,
|
|
81
|
+
stdout=subprocess.PIPE,
|
|
82
|
+
stderr=subprocess.DEVNULL,
|
|
83
|
+
)
|
|
84
|
+
if process.stdin is None or process.stdout is None:
|
|
85
|
+
process.kill()
|
|
86
|
+
raise DAPError("debug adapter did not expose stdio pipes")
|
|
87
|
+
return cls(cast(BinaryIO, process.stdout), cast(BinaryIO, process.stdin), process=process)
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def connect_tcp(cls, host: str, port: int, *, timeout: float = 10.0) -> "DAPClient":
|
|
91
|
+
sock = socket.create_connection((host, port), timeout=timeout)
|
|
92
|
+
reader = sock.makefile("rb")
|
|
93
|
+
writer = sock.makefile("wb")
|
|
94
|
+
return cls(reader, writer, sock=sock)
|
|
95
|
+
|
|
96
|
+
def begin_request(self, command: str, arguments: dict[str, Any] | None = None) -> PendingRequest:
|
|
97
|
+
with self._write_lock:
|
|
98
|
+
self._sequence += 1
|
|
99
|
+
sequence = self._sequence
|
|
100
|
+
responses: "queue.Queue[dict[str, Any]]" = queue.Queue(maxsize=1)
|
|
101
|
+
self._pending[sequence] = responses
|
|
102
|
+
message = {
|
|
103
|
+
"seq": sequence,
|
|
104
|
+
"type": "request",
|
|
105
|
+
"command": command,
|
|
106
|
+
"arguments": arguments or {},
|
|
107
|
+
}
|
|
108
|
+
self.writer.write(encode_message(message))
|
|
109
|
+
self.writer.flush()
|
|
110
|
+
return PendingRequest(sequence, responses)
|
|
111
|
+
|
|
112
|
+
def wait_response(self, pending: PendingRequest, *, timeout: float = 15.0) -> dict[str, Any]:
|
|
113
|
+
try:
|
|
114
|
+
response = pending.responses.get(timeout=timeout)
|
|
115
|
+
except queue.Empty as exc:
|
|
116
|
+
self._pending.pop(pending.sequence, None)
|
|
117
|
+
raise TimeoutError(f"DAP request {pending.sequence} timed out") from exc
|
|
118
|
+
if not response.get("success", False):
|
|
119
|
+
message = response.get("message") or response.get("body", {}).get("error", {}).get("format")
|
|
120
|
+
raise DAPError(str(message or "debug adapter rejected request"))
|
|
121
|
+
return dict(response.get("body") or {})
|
|
122
|
+
|
|
123
|
+
def request(self, command: str, arguments: dict[str, Any] | None = None, *, timeout: float = 15.0) -> dict[str, Any]:
|
|
124
|
+
return self.wait_response(self.begin_request(command, arguments), timeout=timeout)
|
|
125
|
+
|
|
126
|
+
def initialize(self, *, client_name: str = "DevCouncil", adapter_id: str = "devcouncil") -> dict[str, Any]:
|
|
127
|
+
self.capabilities = self.request("initialize", {
|
|
128
|
+
"clientID": "devcouncil",
|
|
129
|
+
"clientName": client_name,
|
|
130
|
+
"adapterID": adapter_id,
|
|
131
|
+
"pathFormat": "path",
|
|
132
|
+
"linesStartAt1": True,
|
|
133
|
+
"columnsStartAt1": True,
|
|
134
|
+
"supportsVariableType": True,
|
|
135
|
+
"supportsVariablePaging": True,
|
|
136
|
+
"supportsRunInTerminalRequest": False,
|
|
137
|
+
})
|
|
138
|
+
return self.capabilities
|
|
139
|
+
|
|
140
|
+
def wait_event(
|
|
141
|
+
self,
|
|
142
|
+
event: str | None = None,
|
|
143
|
+
*,
|
|
144
|
+
timeout: float = 15.0,
|
|
145
|
+
fail_on: PendingRequest | None = None,
|
|
146
|
+
) -> dict[str, Any]:
|
|
147
|
+
deadline = time.monotonic() + timeout
|
|
148
|
+
deadline_items: list[dict[str, Any]] = []
|
|
149
|
+
while True:
|
|
150
|
+
if fail_on is not None:
|
|
151
|
+
try:
|
|
152
|
+
response = fail_on.responses.get_nowait()
|
|
153
|
+
except queue.Empty:
|
|
154
|
+
pass
|
|
155
|
+
else:
|
|
156
|
+
fail_on.responses.put(response)
|
|
157
|
+
if not response.get("success", False):
|
|
158
|
+
self.wait_response(fail_on, timeout=0)
|
|
159
|
+
remaining = deadline - time.monotonic()
|
|
160
|
+
if remaining <= 0:
|
|
161
|
+
for item in deadline_items:
|
|
162
|
+
self._events.put(item)
|
|
163
|
+
raise TimeoutError(f"DAP event {event or '*'} timed out")
|
|
164
|
+
try:
|
|
165
|
+
message = self._events.get(timeout=min(0.05, remaining) if fail_on is not None else remaining)
|
|
166
|
+
except queue.Empty as exc:
|
|
167
|
+
if fail_on is not None:
|
|
168
|
+
continue
|
|
169
|
+
for item in deadline_items:
|
|
170
|
+
self._events.put(item)
|
|
171
|
+
raise TimeoutError(f"DAP event {event or '*'} timed out") from exc
|
|
172
|
+
if event is None or message.get("event") == event:
|
|
173
|
+
for item in deadline_items:
|
|
174
|
+
self._events.put(item)
|
|
175
|
+
return message
|
|
176
|
+
deadline_items.append(message)
|
|
177
|
+
|
|
178
|
+
def evaluate(
|
|
179
|
+
self,
|
|
180
|
+
expression: str,
|
|
181
|
+
*,
|
|
182
|
+
frame_id: int | None = None,
|
|
183
|
+
context: str = "repl",
|
|
184
|
+
allow_side_effects: bool = False,
|
|
185
|
+
) -> dict[str, Any]:
|
|
186
|
+
if not allow_side_effects:
|
|
187
|
+
raise PermissionError("DAP evaluate may execute code; set allow_side_effects=True explicitly")
|
|
188
|
+
arguments: dict[str, Any] = {"expression": expression, "context": context}
|
|
189
|
+
if frame_id is not None:
|
|
190
|
+
arguments["frameId"] = frame_id
|
|
191
|
+
return self.request("evaluate", arguments)
|
|
192
|
+
|
|
193
|
+
def close(self) -> None:
|
|
194
|
+
if self._closed.is_set():
|
|
195
|
+
return
|
|
196
|
+
self._closed.set()
|
|
197
|
+
if self.sock is not None:
|
|
198
|
+
try:
|
|
199
|
+
self.sock.shutdown(socket.SHUT_RDWR)
|
|
200
|
+
except OSError:
|
|
201
|
+
pass
|
|
202
|
+
try:
|
|
203
|
+
self.writer.close()
|
|
204
|
+
except OSError:
|
|
205
|
+
pass
|
|
206
|
+
try:
|
|
207
|
+
self.reader.close()
|
|
208
|
+
except OSError:
|
|
209
|
+
pass
|
|
210
|
+
if self.sock is not None:
|
|
211
|
+
self.sock.close()
|
|
212
|
+
if self.process is not None and self.process.poll() is None:
|
|
213
|
+
self.process.terminate()
|
|
214
|
+
try:
|
|
215
|
+
self.process.wait(timeout=3)
|
|
216
|
+
except subprocess.TimeoutExpired:
|
|
217
|
+
self.process.kill()
|
|
218
|
+
|
|
219
|
+
def _read_loop(self) -> None:
|
|
220
|
+
try:
|
|
221
|
+
while not self._closed.is_set():
|
|
222
|
+
message = read_message(self.reader)
|
|
223
|
+
if message is None:
|
|
224
|
+
break
|
|
225
|
+
message_type = message.get("type")
|
|
226
|
+
if message_type == "response":
|
|
227
|
+
request_seq = int(message.get("request_seq", -1))
|
|
228
|
+
responses = self._pending.pop(request_seq, None)
|
|
229
|
+
if responses is not None:
|
|
230
|
+
responses.put(message)
|
|
231
|
+
elif message_type == "event":
|
|
232
|
+
self._events.put(message)
|
|
233
|
+
elif message_type == "request":
|
|
234
|
+
self._respond_unsupported(message)
|
|
235
|
+
except Exception as exc:
|
|
236
|
+
error = {
|
|
237
|
+
"type": "response",
|
|
238
|
+
"success": False,
|
|
239
|
+
"message": f"DAP transport failed: {type(exc).__name__}: {exc}",
|
|
240
|
+
}
|
|
241
|
+
for responses in list(self._pending.values()):
|
|
242
|
+
responses.put(error)
|
|
243
|
+
self._pending.clear()
|
|
244
|
+
finally:
|
|
245
|
+
self._closed.set()
|
|
246
|
+
|
|
247
|
+
def _respond_unsupported(self, request: dict[str, Any]) -> None:
|
|
248
|
+
with self._write_lock:
|
|
249
|
+
self._sequence += 1
|
|
250
|
+
response = {
|
|
251
|
+
"seq": self._sequence,
|
|
252
|
+
"type": "response",
|
|
253
|
+
"request_seq": request.get("seq"),
|
|
254
|
+
"success": False,
|
|
255
|
+
"command": request.get("command"),
|
|
256
|
+
"message": "DevCouncil does not support this reverse request",
|
|
257
|
+
}
|
|
258
|
+
self.writer.write(encode_message(response))
|
|
259
|
+
self.writer.flush()
|