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,19 @@
|
|
|
1
|
+
"""Native filesystem synchronization for code-intelligence generations."""
|
|
2
|
+
|
|
3
|
+
from devcouncil.codeintel.sync.coordinator import (
|
|
4
|
+
SyncCoordinator,
|
|
5
|
+
SyncState,
|
|
6
|
+
get_sync_coordinator,
|
|
7
|
+
stop_all_coordinators,
|
|
8
|
+
)
|
|
9
|
+
from devcouncil.codeintel.sync.incremental import sync_affected_paths
|
|
10
|
+
from devcouncil.codeintel.sync.scope import IndexScope
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"IndexScope",
|
|
14
|
+
"SyncCoordinator",
|
|
15
|
+
"SyncState",
|
|
16
|
+
"get_sync_coordinator",
|
|
17
|
+
"sync_affected_paths",
|
|
18
|
+
"stop_all_coordinators",
|
|
19
|
+
]
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Debounced native watcher with reconciliation and a single writer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from dataclasses import asdict, dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
from devcouncil.codeintel.service import CodeIntelService, get_codeintel_service
|
|
15
|
+
from devcouncil.codeintel.sync.lease import WriterLease
|
|
16
|
+
from devcouncil.codeintel.sync.scope import IndexScope
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _fsevents_preflight(root: Path) -> bool:
|
|
22
|
+
"""Probe FSEvents out of process because startup failure may abort Python.
|
|
23
|
+
|
|
24
|
+
Retries once: under heavy reconcile/build load the first probe can time out
|
|
25
|
+
even when FSEvents is healthy, which previously forced a permanent Kqueue
|
|
26
|
+
fallback for the process lifetime.
|
|
27
|
+
"""
|
|
28
|
+
script = (
|
|
29
|
+
"import sys,time; "
|
|
30
|
+
"from watchdog.events import FileSystemEventHandler; "
|
|
31
|
+
"from watchdog.observers import Observer; "
|
|
32
|
+
"o=Observer(); o.schedule(FileSystemEventHandler(),sys.argv[1],recursive=True); "
|
|
33
|
+
"o.start(); time.sleep(0.35); "
|
|
34
|
+
"emitters=list(o.emitters); "
|
|
35
|
+
"ok=bool(emitters) and all(e.is_alive() for e in emitters); "
|
|
36
|
+
"o.stop(); o.join(2); raise SystemExit(0 if ok else 3)"
|
|
37
|
+
)
|
|
38
|
+
last_error = ""
|
|
39
|
+
for attempt in range(2):
|
|
40
|
+
try:
|
|
41
|
+
completed = subprocess.run(
|
|
42
|
+
[sys.executable, "-c", script, str(root)],
|
|
43
|
+
capture_output=True,
|
|
44
|
+
timeout=5.0,
|
|
45
|
+
check=False,
|
|
46
|
+
)
|
|
47
|
+
except subprocess.TimeoutExpired:
|
|
48
|
+
last_error = "timeout"
|
|
49
|
+
time.sleep(0.2)
|
|
50
|
+
continue
|
|
51
|
+
except OSError as exc:
|
|
52
|
+
last_error = str(exc)
|
|
53
|
+
break
|
|
54
|
+
if completed.returncode == 0:
|
|
55
|
+
return True
|
|
56
|
+
stderr = getattr(completed, "stderr", None) or b""
|
|
57
|
+
if isinstance(stderr, str):
|
|
58
|
+
stderr_text = stderr.strip()
|
|
59
|
+
else:
|
|
60
|
+
stderr_text = stderr.decode("utf-8", errors="replace").strip()
|
|
61
|
+
last_error = stderr_text or f"exit {completed.returncode}"
|
|
62
|
+
time.sleep(0.2)
|
|
63
|
+
if last_error:
|
|
64
|
+
logger.info("FSEvents preflight failed: %s", last_error)
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class SyncState:
|
|
70
|
+
state: str = "disabled"
|
|
71
|
+
backend: str = ""
|
|
72
|
+
backend_kind: str = ""
|
|
73
|
+
generation: int | None = None
|
|
74
|
+
pending: list[str] = field(default_factory=list)
|
|
75
|
+
last_sync_at: float | None = None
|
|
76
|
+
last_reconcile_at: float | None = None
|
|
77
|
+
degraded_reason: str = ""
|
|
78
|
+
last_error: str = ""
|
|
79
|
+
build_id: str = ""
|
|
80
|
+
build_state: str = "idle"
|
|
81
|
+
build_pid: int | None = None
|
|
82
|
+
build_phase: str = ""
|
|
83
|
+
build_completed: int = 0
|
|
84
|
+
build_total: int = 0
|
|
85
|
+
build_last_progress_at: float | None = None
|
|
86
|
+
generation_before: int | None = None
|
|
87
|
+
generation_after: int | None = None
|
|
88
|
+
compatibility_export: str = "unknown"
|
|
89
|
+
|
|
90
|
+
def as_dict(self) -> dict[str, object]:
|
|
91
|
+
return asdict(self)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SyncCoordinator:
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
service: CodeIntelService,
|
|
98
|
+
*,
|
|
99
|
+
debounce_seconds: float = 0.75,
|
|
100
|
+
reconcile_seconds: float = 60.0,
|
|
101
|
+
sync_callback: Callable[[list[str]], object] | None = None,
|
|
102
|
+
allow_polling_fallback: bool = True,
|
|
103
|
+
):
|
|
104
|
+
self.service = service
|
|
105
|
+
self.root = service.project_root
|
|
106
|
+
self.scope = IndexScope(self.root)
|
|
107
|
+
self.debounce_seconds = max(0.1, min(60.0, debounce_seconds))
|
|
108
|
+
self.reconcile_seconds = max(1.0, reconcile_seconds)
|
|
109
|
+
self.sync_callback = sync_callback or self._default_sync
|
|
110
|
+
self.allow_polling_fallback = allow_polling_fallback
|
|
111
|
+
self._state = SyncState(generation=service.store.current_generation())
|
|
112
|
+
self._pending: set[str] = set()
|
|
113
|
+
self._pending_since: float | None = None
|
|
114
|
+
self._condition = threading.Condition()
|
|
115
|
+
self._stop = threading.Event()
|
|
116
|
+
self._observer: Any = None
|
|
117
|
+
self._worker: threading.Thread | None = None
|
|
118
|
+
self._sync_lock = threading.Lock()
|
|
119
|
+
self._lease = WriterLease(self.root / ".devcouncil" / "codeintel" / "writer.lock")
|
|
120
|
+
|
|
121
|
+
def start(self) -> SyncState:
|
|
122
|
+
if self._worker is not None and self._worker.is_alive():
|
|
123
|
+
return self.status()
|
|
124
|
+
self._stop.clear()
|
|
125
|
+
self._start_observer()
|
|
126
|
+
self._worker = threading.Thread(target=self._run, name="devcouncil-codeintel-sync", daemon=True)
|
|
127
|
+
self._worker.start()
|
|
128
|
+
return self.status()
|
|
129
|
+
|
|
130
|
+
def stop(self, *, timeout: float = 5.0) -> None:
|
|
131
|
+
self._stop.set()
|
|
132
|
+
with self._condition:
|
|
133
|
+
self._condition.notify_all()
|
|
134
|
+
observer = self._observer
|
|
135
|
+
if observer is not None:
|
|
136
|
+
observer.stop()
|
|
137
|
+
observer.join(timeout=timeout)
|
|
138
|
+
self._observer = None
|
|
139
|
+
if self._worker is not None:
|
|
140
|
+
self._worker.join(timeout=timeout)
|
|
141
|
+
if self._worker.is_alive():
|
|
142
|
+
self._state.state = "stopping"
|
|
143
|
+
self._state.degraded_reason = "sync worker did not stop before timeout"
|
|
144
|
+
return
|
|
145
|
+
self._worker = None
|
|
146
|
+
if self._sync_lock.locked():
|
|
147
|
+
self._state.state = "stopping"
|
|
148
|
+
self._state.degraded_reason = "an in-flight sync is still stopping"
|
|
149
|
+
return
|
|
150
|
+
self._state.state = "disabled"
|
|
151
|
+
|
|
152
|
+
def status(self) -> SyncState:
|
|
153
|
+
from devcouncil.codeintel.build_control import read_build_status
|
|
154
|
+
|
|
155
|
+
build = read_build_status(self.root)
|
|
156
|
+
with self._condition:
|
|
157
|
+
if (
|
|
158
|
+
self._worker is not None
|
|
159
|
+
and not self._worker.is_alive()
|
|
160
|
+
and self._stop.is_set()
|
|
161
|
+
):
|
|
162
|
+
self._worker = None
|
|
163
|
+
if self._state.state == "stopping":
|
|
164
|
+
self._state.state = "disabled"
|
|
165
|
+
elif (
|
|
166
|
+
self._worker is None
|
|
167
|
+
and self._stop.is_set()
|
|
168
|
+
and not self._sync_lock.locked()
|
|
169
|
+
and self._state.state == "stopping"
|
|
170
|
+
):
|
|
171
|
+
self._state.state = "disabled"
|
|
172
|
+
self._state.pending = sorted(self._pending)
|
|
173
|
+
self._state.generation = self.service.store.current_generation()
|
|
174
|
+
self._state.build_id = build.build_id
|
|
175
|
+
self._state.build_state = build.state
|
|
176
|
+
self._state.build_pid = build.pid
|
|
177
|
+
self._state.build_phase = build.phase
|
|
178
|
+
self._state.build_completed = build.completed
|
|
179
|
+
self._state.build_total = build.total
|
|
180
|
+
self._state.build_last_progress_at = build.last_progress_at
|
|
181
|
+
self._state.generation_before = build.generation_before
|
|
182
|
+
self._state.generation_after = build.generation_after
|
|
183
|
+
self._state.compatibility_export = build.compatibility_export
|
|
184
|
+
return SyncState(**asdict(self._state))
|
|
185
|
+
|
|
186
|
+
def mark_pending(self, path: str | Path) -> None:
|
|
187
|
+
try:
|
|
188
|
+
rel = self.scope.relative(path)
|
|
189
|
+
except ValueError:
|
|
190
|
+
return
|
|
191
|
+
# Deletions cannot pass an existence-based check, so extension + prefix
|
|
192
|
+
# filtering is enough for paths that were present in the committed index.
|
|
193
|
+
if not self.scope.includes(rel) and not self.service.store.has_indexed_path(rel):
|
|
194
|
+
return
|
|
195
|
+
with self._condition:
|
|
196
|
+
self._pending.add(rel)
|
|
197
|
+
self._pending_since = self._pending_since or time.monotonic()
|
|
198
|
+
self._state.state = "pending"
|
|
199
|
+
self._condition.notify_all()
|
|
200
|
+
|
|
201
|
+
def reconcile(self) -> list[str]:
|
|
202
|
+
indexed = self.service.store.file_metadata()
|
|
203
|
+
current: dict[str, tuple[int, int]] = {}
|
|
204
|
+
for rel in self.scope.files():
|
|
205
|
+
try:
|
|
206
|
+
stat = (self.root / rel).stat()
|
|
207
|
+
except OSError:
|
|
208
|
+
continue
|
|
209
|
+
current[rel] = (stat.st_size, stat.st_mtime_ns)
|
|
210
|
+
changed = {
|
|
211
|
+
rel
|
|
212
|
+
for rel, values in current.items()
|
|
213
|
+
if rel not in indexed or values != indexed[rel][:2]
|
|
214
|
+
}
|
|
215
|
+
changed.update(set(indexed) - set(current))
|
|
216
|
+
if changed:
|
|
217
|
+
# Every item came from IndexScope.files() or the persisted index, so
|
|
218
|
+
# repeating per-path git ignore probes through mark_pending() is both
|
|
219
|
+
# redundant and expensive.
|
|
220
|
+
with self._condition:
|
|
221
|
+
self._pending.update(changed)
|
|
222
|
+
self._pending_since = self._pending_since or time.monotonic()
|
|
223
|
+
self._state.state = "pending"
|
|
224
|
+
self._condition.notify_all()
|
|
225
|
+
self._state.last_reconcile_at = time.time()
|
|
226
|
+
return sorted(changed)
|
|
227
|
+
|
|
228
|
+
def sync_now(self, paths: list[str] | None = None) -> bool:
|
|
229
|
+
from devcouncil.codeintel.build_control import (
|
|
230
|
+
GraphBuildBusy,
|
|
231
|
+
_lease_timeouts,
|
|
232
|
+
graph_build_session,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
with self._sync_lock:
|
|
236
|
+
if paths:
|
|
237
|
+
for path in paths:
|
|
238
|
+
self.mark_pending(path)
|
|
239
|
+
with self._condition:
|
|
240
|
+
batch = sorted(self._pending)
|
|
241
|
+
if not batch:
|
|
242
|
+
return True
|
|
243
|
+
try:
|
|
244
|
+
# Short bounded wait: another watcher/MCP writer often holds the
|
|
245
|
+
# lease briefly. Pending stays queued on timeout so the debounce
|
|
246
|
+
# loop retries instead of clearing work as healthy.
|
|
247
|
+
sync_timeout = _lease_timeouts(self.root)[1]
|
|
248
|
+
with graph_build_session(
|
|
249
|
+
self.root, lease=self._lease, timeout=sync_timeout
|
|
250
|
+
):
|
|
251
|
+
self._state.state = "syncing"
|
|
252
|
+
self.sync_callback(batch)
|
|
253
|
+
with self._condition:
|
|
254
|
+
self._pending.difference_update(batch)
|
|
255
|
+
self._pending_since = None if not self._pending else time.monotonic()
|
|
256
|
+
if self._stop.is_set():
|
|
257
|
+
self._state.state = "stopping"
|
|
258
|
+
else:
|
|
259
|
+
# Drop transient lease-busy reasons so a later successful sync
|
|
260
|
+
# does not stay permanently ``degraded`` / ``read_only``.
|
|
261
|
+
if "writer lease" in (self._state.degraded_reason or "").lower():
|
|
262
|
+
self._state.degraded_reason = ""
|
|
263
|
+
self._state.state = (
|
|
264
|
+
"healthy" if not self._state.degraded_reason else "degraded"
|
|
265
|
+
)
|
|
266
|
+
self._state.last_sync_at = time.time()
|
|
267
|
+
self._state.last_error = ""
|
|
268
|
+
self._state.generation = self.service.store.current_generation()
|
|
269
|
+
return True
|
|
270
|
+
except GraphBuildBusy as exc:
|
|
271
|
+
# Keep pending queued; debounce/reconcile will retry after backoff.
|
|
272
|
+
self._state.state = "pending" if batch or self._pending else "read_only"
|
|
273
|
+
self._state.degraded_reason = str(exc)
|
|
274
|
+
return False
|
|
275
|
+
except Exception as exc:
|
|
276
|
+
logger.exception("code-intelligence sync failed")
|
|
277
|
+
self._state.state = "degraded"
|
|
278
|
+
self._state.last_error = f"{type(exc).__name__}: {exc}"
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
def wait_until_fresh(self, *, timeout: float = 2.0) -> bool:
|
|
282
|
+
deadline = time.monotonic() + max(0.0, timeout)
|
|
283
|
+
while time.monotonic() < deadline:
|
|
284
|
+
if not self.status().pending:
|
|
285
|
+
return True
|
|
286
|
+
self.sync_now()
|
|
287
|
+
if not self.status().pending:
|
|
288
|
+
return True
|
|
289
|
+
time.sleep(0.05)
|
|
290
|
+
return not self.status().pending
|
|
291
|
+
|
|
292
|
+
def _run(self) -> None:
|
|
293
|
+
# Initial reconciliation is intentionally asynchronous. MCP clients must
|
|
294
|
+
# be able to complete initialize/list_tools immediately while the index is
|
|
295
|
+
# brought current in the background.
|
|
296
|
+
try:
|
|
297
|
+
self.reconcile()
|
|
298
|
+
except Exception as exc: # noqa: BLE001
|
|
299
|
+
logger.exception("initial code-intelligence reconciliation failed")
|
|
300
|
+
self._state.state = "degraded"
|
|
301
|
+
self._state.last_error = f"{type(exc).__name__}: {exc}"
|
|
302
|
+
next_reconcile = time.monotonic() + self.reconcile_seconds
|
|
303
|
+
while not self._stop.is_set():
|
|
304
|
+
with self._condition:
|
|
305
|
+
self._condition.wait(timeout=min(self.debounce_seconds, 1.0))
|
|
306
|
+
pending_since = self._pending_since
|
|
307
|
+
now = time.monotonic()
|
|
308
|
+
if pending_since is not None and now - pending_since >= self.debounce_seconds:
|
|
309
|
+
self.sync_now()
|
|
310
|
+
if now >= next_reconcile:
|
|
311
|
+
self.reconcile()
|
|
312
|
+
next_reconcile = now + self.reconcile_seconds
|
|
313
|
+
|
|
314
|
+
def _start_observer(self) -> None:
|
|
315
|
+
try:
|
|
316
|
+
from watchdog.events import FileSystemEventHandler
|
|
317
|
+
from watchdog.observers import Observer
|
|
318
|
+
from watchdog.observers.polling import PollingObserver
|
|
319
|
+
|
|
320
|
+
coordinator = self
|
|
321
|
+
|
|
322
|
+
class Handler(FileSystemEventHandler):
|
|
323
|
+
def on_any_event(self, event): # noqa: ANN001
|
|
324
|
+
if getattr(event, "is_directory", False):
|
|
325
|
+
return
|
|
326
|
+
source = getattr(event, "src_path", "")
|
|
327
|
+
destination = getattr(event, "dest_path", "")
|
|
328
|
+
if source:
|
|
329
|
+
coordinator.mark_pending(source)
|
|
330
|
+
if destination:
|
|
331
|
+
coordinator.mark_pending(destination)
|
|
332
|
+
|
|
333
|
+
candidates: list[tuple[str, Callable[[], Any]]] = [("native", Observer)]
|
|
334
|
+
try:
|
|
335
|
+
from watchdog.observers.kqueue import KqueueObserver
|
|
336
|
+
|
|
337
|
+
if KqueueObserver is not Observer:
|
|
338
|
+
candidates.append(("native-fallback", KqueueObserver))
|
|
339
|
+
except (ImportError, AttributeError):
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
if self.allow_polling_fallback:
|
|
343
|
+
candidates.append((
|
|
344
|
+
"polling",
|
|
345
|
+
lambda: PollingObserver(timeout=max(1.0, self.debounce_seconds)),
|
|
346
|
+
))
|
|
347
|
+
|
|
348
|
+
observer = None
|
|
349
|
+
failures: list[str] = []
|
|
350
|
+
backend = ""
|
|
351
|
+
selected_kind = ""
|
|
352
|
+
for kind, factory in candidates:
|
|
353
|
+
candidate = factory()
|
|
354
|
+
try:
|
|
355
|
+
if candidate.__class__.__name__ == "FSEventsObserver" and not _fsevents_preflight(self.root):
|
|
356
|
+
raise RuntimeError("FSEvents preflight failed in isolated process")
|
|
357
|
+
candidate.schedule(Handler(), str(self.root), recursive=True)
|
|
358
|
+
candidate.start()
|
|
359
|
+
emitters = list(getattr(candidate, "emitters", []))
|
|
360
|
+
if emitters:
|
|
361
|
+
# FSEvents can report startup failure only from its
|
|
362
|
+
# emitter thread, after Observer.start() has returned.
|
|
363
|
+
time.sleep(0.1)
|
|
364
|
+
if not all(emitter.is_alive() for emitter in emitters):
|
|
365
|
+
raise RuntimeError("observer emitter terminated during startup")
|
|
366
|
+
observer = candidate
|
|
367
|
+
backend = candidate.__class__.__name__
|
|
368
|
+
selected_kind = kind
|
|
369
|
+
break
|
|
370
|
+
except Exception as exc:
|
|
371
|
+
failures.append(f"{candidate.__class__.__name__}: {type(exc).__name__}: {exc}")
|
|
372
|
+
try:
|
|
373
|
+
candidate.stop()
|
|
374
|
+
candidate.join(timeout=1.0)
|
|
375
|
+
except Exception:
|
|
376
|
+
logger.debug("failed to stop rejected observer", exc_info=True)
|
|
377
|
+
if observer is None:
|
|
378
|
+
raise RuntimeError("; ".join(failures) or "no observer backend available")
|
|
379
|
+
if failures or selected_kind == "polling":
|
|
380
|
+
self._state.degraded_reason = "; ".join(failures) or "polling fallback selected"
|
|
381
|
+
logger.warning(
|
|
382
|
+
"filesystem observer degraded to %s: %s",
|
|
383
|
+
backend,
|
|
384
|
+
self._state.degraded_reason,
|
|
385
|
+
)
|
|
386
|
+
self._observer = observer
|
|
387
|
+
self._state.backend = backend
|
|
388
|
+
self._state.backend_kind = "polling" if selected_kind == "polling" else "native"
|
|
389
|
+
self._state.state = "degraded" if self._state.degraded_reason else "healthy"
|
|
390
|
+
except Exception as exc:
|
|
391
|
+
self._observer = None
|
|
392
|
+
self._state.state = "degraded"
|
|
393
|
+
self._state.degraded_reason = f"watcher unavailable: {type(exc).__name__}: {exc}"
|
|
394
|
+
|
|
395
|
+
def _default_sync(self, paths: list[str]) -> None:
|
|
396
|
+
# Keep graph watch and ``dev map --watch`` on the same path: incremental
|
|
397
|
+
# graph sync plus a full repo-map rebuild (subsystems/dependents/guides).
|
|
398
|
+
from devcouncil.indexing.graph.build import refresh_map_for_paths
|
|
399
|
+
|
|
400
|
+
refresh_map_for_paths(self.service.project_root, paths, liveness=True)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
_COORDINATORS: dict[Path, SyncCoordinator] = {}
|
|
404
|
+
_COORDINATORS_LOCK = threading.Lock()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def get_sync_coordinator(root: Path, **kwargs: object) -> SyncCoordinator:
|
|
408
|
+
service = get_codeintel_service(root)
|
|
409
|
+
with _COORDINATORS_LOCK:
|
|
410
|
+
coordinator = _COORDINATORS.get(service.project_root)
|
|
411
|
+
if coordinator is None:
|
|
412
|
+
coordinator = SyncCoordinator(service, **kwargs) # type: ignore[arg-type]
|
|
413
|
+
_COORDINATORS[service.project_root] = coordinator
|
|
414
|
+
else:
|
|
415
|
+
for name, requested in kwargs.items():
|
|
416
|
+
current = getattr(coordinator, name)
|
|
417
|
+
if current != requested:
|
|
418
|
+
raise ValueError(
|
|
419
|
+
f"sync coordinator for {service.project_root} already uses "
|
|
420
|
+
f"a different {name}"
|
|
421
|
+
)
|
|
422
|
+
return coordinator
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def stop_all_coordinators() -> None:
|
|
426
|
+
with _COORDINATORS_LOCK:
|
|
427
|
+
coordinators = list(_COORDINATORS.values())
|
|
428
|
+
_COORDINATORS.clear()
|
|
429
|
+
for coordinator in coordinators:
|
|
430
|
+
coordinator.stop()
|