devcouncil 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -30
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +49 -30
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +960 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +303 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"""In-process Claude executor backed by the Claude Agent SDK.
|
|
2
|
+
|
|
3
|
+
Unlike :class:`CodingCliExecutor`, which shells out to ``claude -p`` and can only enforce
|
|
4
|
+
scope *after* the subprocess has already written to disk, this executor arbitrates every
|
|
5
|
+
tool call live through the SDK's ``can_use_tool`` callback. The callback is backed by the
|
|
6
|
+
same :class:`HookPolicy` the shell write-gate uses, so an out-of-scope write or command is
|
|
7
|
+
denied *before it happens* — real containment that, unlike the shell PreToolUse gate, does
|
|
8
|
+
not fail-closed on an interactive session (the decision is evaluated against the run's own
|
|
9
|
+
task, not a globally-resolved lease).
|
|
10
|
+
|
|
11
|
+
The Claude Agent SDK is an OPTIONAL dependency: it is imported lazily so nothing else in
|
|
12
|
+
DevCouncil requires it, and a missing install produces an actionable error rather than an
|
|
13
|
+
import-time crash. Wire it up with ``pip install claude-agent-sdk``.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import importlib
|
|
19
|
+
import logging
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Coroutine, List, Optional, TypeVar
|
|
22
|
+
|
|
23
|
+
from devcouncil.domain.requirement import Requirement
|
|
24
|
+
from devcouncil.domain.task import Task
|
|
25
|
+
from devcouncil.execution.executor import ExecutionResult, Executor
|
|
26
|
+
from devcouncil.execution.hook_policy import HookDecision, HookPolicy
|
|
27
|
+
from devcouncil.execution.prompt_builder import PromptBuilder
|
|
28
|
+
from devcouncil.executors.transient_retry import transient_error_in_text
|
|
29
|
+
from devcouncil.telemetry.stages import log_step
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_T = TypeVar("_T")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ClaudeSdkExecutor(Executor):
|
|
37
|
+
"""Run a DevCouncil task through the Claude Agent SDK with lease-aware gating."""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
project_root: Path | str,
|
|
42
|
+
*,
|
|
43
|
+
active_task: Task | None = None,
|
|
44
|
+
model: str | None = None,
|
|
45
|
+
advisor_model: str | None = None,
|
|
46
|
+
permission_mode: str = "default",
|
|
47
|
+
env: dict[str, str] | None = None,
|
|
48
|
+
):
|
|
49
|
+
# NOTE: default (not acceptEdits). acceptEdits auto-approves file edits, which would
|
|
50
|
+
# skip the can_use_tool callback entirely and defeat the lease gate; "default" routes
|
|
51
|
+
# every write/command through our callback so scope is actually enforced.
|
|
52
|
+
self.project_root = Path(project_root)
|
|
53
|
+
# The task whose scope authorizes writes/commands. Passed explicitly so the gate is
|
|
54
|
+
# evaluated against THIS run's task rather than a globally-resolved lease (which is
|
|
55
|
+
# why this can enforce interactively where the shell PreToolUse gate cannot).
|
|
56
|
+
self.active_task = active_task
|
|
57
|
+
self.model = model
|
|
58
|
+
self.advisor_model = (advisor_model or "").strip() or None
|
|
59
|
+
self.permission_mode = permission_mode
|
|
60
|
+
# Extra environment for the SDK's underlying Claude Code process. Lets a run
|
|
61
|
+
# target an alternative Anthropic-compatible endpoint (ANTHROPIC_BASE_URL /
|
|
62
|
+
# ANTHROPIC_AUTH_TOKEN, e.g. a local proxy) — same knob the subprocess
|
|
63
|
+
# executor exposes via per-profile ``env`` overrides.
|
|
64
|
+
self.env = dict(env) if env else {}
|
|
65
|
+
self.policy = HookPolicy(project_root=self.project_root)
|
|
66
|
+
self.last_agent_session_id: str | None = None
|
|
67
|
+
# (tool_name, reason) for every call the gate denied — surfaced in the result.
|
|
68
|
+
self.denials: list[tuple[str, str]] = []
|
|
69
|
+
|
|
70
|
+
# -- policy ---------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def permission_decision(self, tool_name: str, tool_input: dict[str, Any]) -> HookDecision:
|
|
73
|
+
"""Lease-aware allow/deny for a single tool call, reusing DevCouncil's HookPolicy.
|
|
74
|
+
|
|
75
|
+
Pure and side-effect free (bar reading repo policy) so it is unit-testable on its
|
|
76
|
+
own: the same call the SDK would make, evaluated against this run's task."""
|
|
77
|
+
call_data = {"tool_name": tool_name, "tool_input": tool_input or {}}
|
|
78
|
+
return self.policy.evaluate(call_data, self.active_task)
|
|
79
|
+
|
|
80
|
+
# -- SDK loading ----------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _load_sdk():
|
|
84
|
+
try:
|
|
85
|
+
return importlib.import_module("claude_agent_sdk")
|
|
86
|
+
except ModuleNotFoundError as exc:
|
|
87
|
+
raise RuntimeError(
|
|
88
|
+
"The Claude Agent SDK is not installed. Install it with "
|
|
89
|
+
"`pip install claude-agent-sdk` to use the claude-sdk executor."
|
|
90
|
+
) from exc
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _allow(sdk, tool_input: dict[str, Any]):
|
|
94
|
+
"""Build the SDK's 'allow' permission result, preferring its typed class."""
|
|
95
|
+
allow_cls = getattr(sdk, "PermissionResultAllow", None)
|
|
96
|
+
if allow_cls is not None:
|
|
97
|
+
try:
|
|
98
|
+
return allow_cls(updated_input=tool_input)
|
|
99
|
+
except TypeError:
|
|
100
|
+
return allow_cls()
|
|
101
|
+
# Documented plain-object contract as a fallback.
|
|
102
|
+
return {"behavior": "allow", "updatedInput": tool_input}
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def _deny(sdk, reason: str):
|
|
106
|
+
deny_cls = getattr(sdk, "PermissionResultDeny", None)
|
|
107
|
+
if deny_cls is not None:
|
|
108
|
+
try:
|
|
109
|
+
return deny_cls(message=reason)
|
|
110
|
+
except TypeError:
|
|
111
|
+
return deny_cls(reason)
|
|
112
|
+
return {"behavior": "deny", "message": reason}
|
|
113
|
+
|
|
114
|
+
# -- run ------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
def run_task(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
|
|
117
|
+
# A task can be passed at construction (for the gate) or here; the run argument wins.
|
|
118
|
+
if task is not None:
|
|
119
|
+
self.active_task = task
|
|
120
|
+
log_step(
|
|
121
|
+
f"executor/claude-sdk: starting task {task.id}",
|
|
122
|
+
project_root=self.project_root,
|
|
123
|
+
task_id=task.id,
|
|
124
|
+
)
|
|
125
|
+
try:
|
|
126
|
+
result = self._run_coroutine_from_sync(self._run_async(task, requirements))
|
|
127
|
+
log_step(
|
|
128
|
+
f"executor/claude-sdk: finished task {task.id}",
|
|
129
|
+
project_root=self.project_root,
|
|
130
|
+
task_id=task.id,
|
|
131
|
+
success=result.success,
|
|
132
|
+
)
|
|
133
|
+
return result
|
|
134
|
+
except RuntimeError as exc:
|
|
135
|
+
# Missing SDK or a nested event loop — report cleanly, never crash the caller.
|
|
136
|
+
logger.error("claude-sdk executor could not run %s: %s", getattr(task, "id", "?"), exc)
|
|
137
|
+
log_step(
|
|
138
|
+
f"executor/claude-sdk: finished task {getattr(task, 'id', '?')}",
|
|
139
|
+
project_root=self.project_root,
|
|
140
|
+
task_id=getattr(task, "id", None),
|
|
141
|
+
success=False,
|
|
142
|
+
error=str(exc),
|
|
143
|
+
)
|
|
144
|
+
return ExecutionResult(success=False, message=str(exc))
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def _run_coroutine_from_sync(coro: Coroutine[Any, Any, _T]) -> _T:
|
|
148
|
+
"""Run an async coroutine from sync code, including nested-loop contexts.
|
|
149
|
+
|
|
150
|
+
``asyncio.run()`` raises when a loop is already running (pytest-asyncio,
|
|
151
|
+
Jupyter). With no running loop we use ``asyncio.run()``; otherwise we run
|
|
152
|
+
the coroutine on a fresh loop in a worker thread.
|
|
153
|
+
"""
|
|
154
|
+
import asyncio
|
|
155
|
+
import concurrent.futures
|
|
156
|
+
|
|
157
|
+
try:
|
|
158
|
+
asyncio.get_running_loop()
|
|
159
|
+
except RuntimeError:
|
|
160
|
+
return asyncio.run(coro)
|
|
161
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
|
162
|
+
return executor.submit(asyncio.run, coro).result()
|
|
163
|
+
|
|
164
|
+
async def _run_async(self, task: Task, requirements: List[Requirement]) -> ExecutionResult:
|
|
165
|
+
sdk = self._load_sdk()
|
|
166
|
+
prompt = PromptBuilder(self.project_root).build_task_prompt(task, requirements)
|
|
167
|
+
# Parity with the subprocess executor: a repair run must carry the correction
|
|
168
|
+
# manifest so the model knows what the prior attempt got wrong.
|
|
169
|
+
from devcouncil.planning.correction_manifest import repair_prompt_prefix
|
|
170
|
+
|
|
171
|
+
prefix = repair_prompt_prefix(self.project_root, task.id)
|
|
172
|
+
if prefix:
|
|
173
|
+
prompt = f"{prefix}{prompt}"
|
|
174
|
+
|
|
175
|
+
# Claude-only advisor: resolve attach, build options (old SDKs may drop
|
|
176
|
+
# extra_args), then steer only when the advisor flag actually landed.
|
|
177
|
+
advisor_extra = self._resolved_advisor_extra_args()
|
|
178
|
+
|
|
179
|
+
async def can_use_tool(tool_name: str, tool_input: dict[str, Any], *args: Any, **kwargs: Any):
|
|
180
|
+
decision = self.permission_decision(tool_name, tool_input or {})
|
|
181
|
+
if decision.allowed:
|
|
182
|
+
return self._allow(sdk, tool_input or {})
|
|
183
|
+
self.denials.append((tool_name, decision.reason))
|
|
184
|
+
logger.info("claude-sdk gate denied %s: %s", tool_name, decision.reason)
|
|
185
|
+
return self._deny(sdk, decision.reason)
|
|
186
|
+
|
|
187
|
+
options = self._build_options(sdk, can_use_tool, advisor_extra=advisor_extra)
|
|
188
|
+
if self._options_have_advisor(options, advisor_extra):
|
|
189
|
+
from devcouncil.executors.advisor_tool import (
|
|
190
|
+
advisor_steering_text,
|
|
191
|
+
advisor_user_cost_trim,
|
|
192
|
+
warn_advisor_preflight,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
for warning in warn_advisor_preflight(
|
|
196
|
+
env=self.env or None,
|
|
197
|
+
main_model=self.model,
|
|
198
|
+
advisor_model=self.advisor_model,
|
|
199
|
+
):
|
|
200
|
+
logger.warning("%s", warning)
|
|
201
|
+
nudge = advisor_steering_text(repair=bool(prefix))
|
|
202
|
+
cost_trim = advisor_user_cost_trim()
|
|
203
|
+
prompt = f"{nudge}\n\n{cost_trim}\n\n{prompt}"
|
|
204
|
+
try:
|
|
205
|
+
return await self._run_async_once(sdk, prompt, options, task)
|
|
206
|
+
except Exception as exc: # a mid-run SDK failure is a failed execution, not a crash
|
|
207
|
+
logger.exception(
|
|
208
|
+
"claude-sdk executor raised mid-run for %s: %s",
|
|
209
|
+
getattr(task, "id", "?"),
|
|
210
|
+
exc,
|
|
211
|
+
)
|
|
212
|
+
reason = transient_error_in_text(str(exc))
|
|
213
|
+
if reason is not None:
|
|
214
|
+
import time
|
|
215
|
+
|
|
216
|
+
logger.warning(
|
|
217
|
+
"claude-sdk transient failure for %s (%s); retrying once in 5s",
|
|
218
|
+
getattr(task, "id", "?"),
|
|
219
|
+
reason,
|
|
220
|
+
)
|
|
221
|
+
time.sleep(5.0)
|
|
222
|
+
try:
|
|
223
|
+
return await self._run_async_once(sdk, prompt, options, task)
|
|
224
|
+
except Exception as retry_exc:
|
|
225
|
+
logger.exception(
|
|
226
|
+
"claude-sdk retry failed for %s: %s",
|
|
227
|
+
getattr(task, "id", "?"),
|
|
228
|
+
retry_exc,
|
|
229
|
+
)
|
|
230
|
+
return ExecutionResult(
|
|
231
|
+
success=False,
|
|
232
|
+
message=f"Claude Agent SDK run failed after retry: {retry_exc}",
|
|
233
|
+
)
|
|
234
|
+
return ExecutionResult(success=False, message=f"Claude Agent SDK run failed: {exc}")
|
|
235
|
+
|
|
236
|
+
async def _run_async_once(self, sdk, prompt: str, options, task: Task) -> ExecutionResult:
|
|
237
|
+
result_text: Optional[str] = None
|
|
238
|
+
is_error = False
|
|
239
|
+
async for message in sdk.query(prompt=prompt, options=options):
|
|
240
|
+
session_id = self._message_session_id(message)
|
|
241
|
+
if session_id:
|
|
242
|
+
self.last_agent_session_id = session_id
|
|
243
|
+
text, error = self._message_result(message)
|
|
244
|
+
if text is not None:
|
|
245
|
+
result_text = text
|
|
246
|
+
if error:
|
|
247
|
+
is_error = True
|
|
248
|
+
if self.denials:
|
|
249
|
+
denied = "; ".join(f"{name} ({reason})" for name, reason in self.denials)
|
|
250
|
+
suffix = f" Gate denied {len(self.denials)} out-of-scope call(s): {denied}."
|
|
251
|
+
else:
|
|
252
|
+
suffix = ""
|
|
253
|
+
message = (result_text or "Claude Agent SDK run finished.") + suffix
|
|
254
|
+
return ExecutionResult(success=not is_error, message=message)
|
|
255
|
+
|
|
256
|
+
def _resolved_advisor_extra_args(self) -> dict[str, str] | None:
|
|
257
|
+
"""Build SDK ``extra_args={"advisor": ...}`` when pairing looks safe."""
|
|
258
|
+
if not self.advisor_model:
|
|
259
|
+
return None
|
|
260
|
+
from devcouncil.executors.advisor_tool import decide_advisor_attach
|
|
261
|
+
|
|
262
|
+
decision, resolved, reason = decide_advisor_attach(
|
|
263
|
+
main_model=self.model,
|
|
264
|
+
advisor_model=self.advisor_model,
|
|
265
|
+
env=self.env,
|
|
266
|
+
)
|
|
267
|
+
if decision != "attach" or not resolved:
|
|
268
|
+
if reason:
|
|
269
|
+
logger.warning(
|
|
270
|
+
"Skipping SDK advisor %s: %s",
|
|
271
|
+
self.advisor_model,
|
|
272
|
+
reason,
|
|
273
|
+
)
|
|
274
|
+
return None
|
|
275
|
+
return {"advisor": resolved}
|
|
276
|
+
|
|
277
|
+
@staticmethod
|
|
278
|
+
def _options_have_advisor(options: Any, advisor_extra: dict[str, str] | None) -> bool:
|
|
279
|
+
"""True when advisor was requested and survived into the SDK options object."""
|
|
280
|
+
if not advisor_extra:
|
|
281
|
+
return False
|
|
282
|
+
if isinstance(options, dict):
|
|
283
|
+
extra = options.get("extra_args") or {}
|
|
284
|
+
else:
|
|
285
|
+
extra = getattr(options, "extra_args", None) or {}
|
|
286
|
+
if not isinstance(extra, dict):
|
|
287
|
+
return False
|
|
288
|
+
return bool(extra.get("advisor"))
|
|
289
|
+
|
|
290
|
+
def _build_options(self, sdk, can_use_tool, *, advisor_extra: dict[str, str] | None = None):
|
|
291
|
+
options_cls = getattr(sdk, "ClaudeAgentOptions", None)
|
|
292
|
+
kwargs: dict[str, Any] = {
|
|
293
|
+
"cwd": str(self.project_root),
|
|
294
|
+
"permission_mode": self.permission_mode,
|
|
295
|
+
"can_use_tool": can_use_tool,
|
|
296
|
+
}
|
|
297
|
+
if self.model:
|
|
298
|
+
kwargs["model"] = self.model
|
|
299
|
+
if self.env:
|
|
300
|
+
kwargs["env"] = dict(self.env)
|
|
301
|
+
if advisor_extra:
|
|
302
|
+
kwargs["extra_args"] = dict(advisor_extra)
|
|
303
|
+
if options_cls is None:
|
|
304
|
+
return kwargs
|
|
305
|
+
# Only pass kwargs the SDK's options object actually accepts, so a version skew in
|
|
306
|
+
# field names degrades gracefully instead of raising TypeError.
|
|
307
|
+
try:
|
|
308
|
+
return options_cls(**kwargs)
|
|
309
|
+
except TypeError:
|
|
310
|
+
# Progressively narrower kwarg sets: an SDK too old for ``env`` / ``extra_args``
|
|
311
|
+
# still gets cwd/permission_mode/model rather than losing all options.
|
|
312
|
+
obj = None
|
|
313
|
+
dropped_extra = bool(advisor_extra)
|
|
314
|
+
for keys in (
|
|
315
|
+
{"cwd", "permission_mode", "model", "env", "extra_args"},
|
|
316
|
+
{"cwd", "permission_mode", "model", "env"},
|
|
317
|
+
{"cwd", "permission_mode", "model"},
|
|
318
|
+
):
|
|
319
|
+
safe = {k: v for k, v in kwargs.items() if k in keys}
|
|
320
|
+
try:
|
|
321
|
+
obj = options_cls(**safe)
|
|
322
|
+
if dropped_extra and "extra_args" not in safe:
|
|
323
|
+
logger.warning(
|
|
324
|
+
"claude-agent-sdk ClaudeAgentOptions rejected extra_args; "
|
|
325
|
+
"advisor_model=%r will not be applied. Upgrade claude-agent-sdk.",
|
|
326
|
+
self.advisor_model,
|
|
327
|
+
)
|
|
328
|
+
break
|
|
329
|
+
except TypeError:
|
|
330
|
+
continue
|
|
331
|
+
if obj is None:
|
|
332
|
+
obj = options_cls()
|
|
333
|
+
if dropped_extra:
|
|
334
|
+
logger.warning(
|
|
335
|
+
"claude-agent-sdk ClaudeAgentOptions could not accept advisor "
|
|
336
|
+
"extra_args; advisor_model=%r dropped.",
|
|
337
|
+
self.advisor_model,
|
|
338
|
+
)
|
|
339
|
+
if hasattr(obj, "can_use_tool"):
|
|
340
|
+
obj.can_use_tool = can_use_tool
|
|
341
|
+
return obj
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def _message_session_id(message: Any) -> str | None:
|
|
345
|
+
for getter in (lambda: getattr(message, "session_id", None),
|
|
346
|
+
lambda: message.get("session_id") if isinstance(message, dict) else None,
|
|
347
|
+
lambda: (getattr(message, "data", {}) or {}).get("session_id")):
|
|
348
|
+
try:
|
|
349
|
+
value = getter()
|
|
350
|
+
except Exception:
|
|
351
|
+
value = None
|
|
352
|
+
if value:
|
|
353
|
+
return str(value)
|
|
354
|
+
return None
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def _message_result(message: Any) -> tuple[str | None, bool]:
|
|
358
|
+
"""Extract final result text and error flag from a terminal result message."""
|
|
359
|
+
mtype = getattr(message, "type", None)
|
|
360
|
+
if mtype is None and isinstance(message, dict):
|
|
361
|
+
mtype = message.get("type")
|
|
362
|
+
subtype = getattr(message, "subtype", None)
|
|
363
|
+
if subtype is None and isinstance(message, dict):
|
|
364
|
+
subtype = message.get("subtype")
|
|
365
|
+
is_result = mtype == "result" or type(message).__name__ == "ResultMessage"
|
|
366
|
+
if not is_result:
|
|
367
|
+
return None, False
|
|
368
|
+
result = getattr(message, "result", None)
|
|
369
|
+
if result is None and isinstance(message, dict):
|
|
370
|
+
result = message.get("result")
|
|
371
|
+
is_error = getattr(message, "is_error", None)
|
|
372
|
+
if is_error is None and isinstance(message, dict):
|
|
373
|
+
is_error = message.get("is_error")
|
|
374
|
+
if is_error is None:
|
|
375
|
+
is_error = subtype not in (None, "success")
|
|
376
|
+
return (str(result) if result is not None else None), bool(is_error)
|