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,87 @@
|
|
|
1
|
+
"""Read-only wiki page lookup shared by CLI and MCP."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def wiki_dir_for(root: Path) -> Path:
|
|
10
|
+
from devcouncil.knowledge.wiki import wiki_dir_for as _wiki_dir_for
|
|
11
|
+
|
|
12
|
+
return _wiki_dir_for(root)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _summary(doc: Any) -> dict[str, object]:
|
|
16
|
+
return {
|
|
17
|
+
"page": doc.rel_path,
|
|
18
|
+
"type": doc.type,
|
|
19
|
+
"title": doc.title,
|
|
20
|
+
"description": doc.description,
|
|
21
|
+
"tags": doc.tags,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def read_wiki_page(
|
|
26
|
+
root: Path,
|
|
27
|
+
*,
|
|
28
|
+
page: str | None = None,
|
|
29
|
+
query: str | None = None,
|
|
30
|
+
) -> dict[str, object]:
|
|
31
|
+
"""Return MCP/CLI-compatible payload for wiki listing, page fetch, or search."""
|
|
32
|
+
from devcouncil.knowledge.okf import read_bundle
|
|
33
|
+
|
|
34
|
+
wiki_dir = wiki_dir_for(root)
|
|
35
|
+
if not (wiki_dir / "index.md").is_file():
|
|
36
|
+
return {
|
|
37
|
+
"ok": False,
|
|
38
|
+
"error": "No codebase wiki has been generated. Run `dev wiki update` to create it.",
|
|
39
|
+
"code": "not_found",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
bundle = read_bundle(wiki_dir)
|
|
43
|
+
pages = {
|
|
44
|
+
doc.rel_path: doc
|
|
45
|
+
for doc in bundle.documents
|
|
46
|
+
if doc.rel_path not in ("log.md",)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if page:
|
|
50
|
+
doc = pages.get(page.strip())
|
|
51
|
+
if doc is None:
|
|
52
|
+
return {
|
|
53
|
+
"ok": False,
|
|
54
|
+
"error": f"Wiki page {page!r} not found.",
|
|
55
|
+
"code": "not_found",
|
|
56
|
+
"available": sorted(pages),
|
|
57
|
+
}
|
|
58
|
+
return {"ok": True, **_summary(doc), "body": doc.body, "truncated": False}
|
|
59
|
+
|
|
60
|
+
if query:
|
|
61
|
+
terms = [term for term in query.lower().split() if term]
|
|
62
|
+
scored: list[tuple[int, Any]] = []
|
|
63
|
+
for doc in pages.values():
|
|
64
|
+
haystack = " ".join([doc.title, doc.description, " ".join(doc.tags), doc.rel_path]).lower()
|
|
65
|
+
hits = sum(1 for term in terms if term in haystack)
|
|
66
|
+
if hits:
|
|
67
|
+
scored.append((hits, doc))
|
|
68
|
+
scored.sort(key=lambda item: (-item[0], item[1].rel_path))
|
|
69
|
+
matches = [doc for _, doc in scored[:5]]
|
|
70
|
+
if not matches:
|
|
71
|
+
return {
|
|
72
|
+
"ok": True,
|
|
73
|
+
"query": query,
|
|
74
|
+
"matches": [],
|
|
75
|
+
"available": [_summary(doc) for doc in pages.values()],
|
|
76
|
+
}
|
|
77
|
+
top = matches[0]
|
|
78
|
+
return {
|
|
79
|
+
"ok": True,
|
|
80
|
+
"query": query,
|
|
81
|
+
"matches": [_summary(doc) for doc in matches],
|
|
82
|
+
**_summary(top),
|
|
83
|
+
"body": top.body,
|
|
84
|
+
"truncated": False,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {"ok": True, "pages": [_summary(doc) for doc in pages.values()]}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
|
-
import json
|
|
5
4
|
import re
|
|
6
5
|
from dataclasses import dataclass
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
from devcouncil.live.models import AgentTurn, CardStatus, CritiqueCard, Verdict
|
|
9
|
+
from devcouncil.utils.json_persist import read_model_json, write_model_json
|
|
10
10
|
|
|
11
11
|
RISK_TERMS = (
|
|
12
12
|
"skip tests",
|
|
@@ -239,7 +239,7 @@ def save_card(project_root: Path, card: CritiqueCard) -> Path:
|
|
|
239
239
|
cards_dir = project_root / ".devcouncil" / "live" / "cards"
|
|
240
240
|
cards_dir.mkdir(parents=True, exist_ok=True)
|
|
241
241
|
path = cards_dir / f"{card.id}.json"
|
|
242
|
-
path
|
|
242
|
+
write_model_json(path, card)
|
|
243
243
|
return path
|
|
244
244
|
|
|
245
245
|
|
|
@@ -254,7 +254,7 @@ def load_cards(project_root: Path) -> list[CritiqueCard]:
|
|
|
254
254
|
cards: list[CritiqueCard] = []
|
|
255
255
|
for path in sorted(cards_dir.glob("*.json"), key=lambda item: item.stat().st_mtime, reverse=True):
|
|
256
256
|
try:
|
|
257
|
-
cards.append(
|
|
257
|
+
cards.append(read_model_json(path, CritiqueCard))
|
|
258
258
|
except Exception:
|
|
259
259
|
continue
|
|
260
260
|
return cards
|
|
@@ -305,7 +305,7 @@ def load_card_by_id(project_root: Path, card_id: str) -> CritiqueCard | None:
|
|
|
305
305
|
if not path.exists():
|
|
306
306
|
return None
|
|
307
307
|
try:
|
|
308
|
-
return
|
|
308
|
+
return read_model_json(path, CritiqueCard)
|
|
309
309
|
except Exception:
|
|
310
310
|
return None
|
|
311
311
|
|
|
@@ -319,9 +319,9 @@ def update_card_status(project_root: Path, card_id: str, status: CardStatus) ->
|
|
|
319
319
|
path = cards_dir / f"{card_id}.json"
|
|
320
320
|
if not path.exists():
|
|
321
321
|
return None
|
|
322
|
-
card =
|
|
322
|
+
card = read_model_json(path, CritiqueCard)
|
|
323
323
|
updated = card.model_copy(update={"status": status})
|
|
324
|
-
path
|
|
324
|
+
write_model_json(path, updated)
|
|
325
325
|
return updated
|
|
326
326
|
|
|
327
327
|
|
|
@@ -334,7 +334,7 @@ def unresolved_blocking_cards(
|
|
|
334
334
|
source = cards if cards is not None else load_cards(project_root)
|
|
335
335
|
return [
|
|
336
336
|
card for card in source
|
|
337
|
-
if card.status == "open" and card.
|
|
337
|
+
if card.status == "open" and card.blocks_completion
|
|
338
338
|
and (task_id is None or card.task_id in {None, task_id})
|
|
339
339
|
]
|
|
340
340
|
|
|
@@ -53,10 +53,13 @@ class CritiqueCard(BaseModel):
|
|
|
53
53
|
status: CardStatus = "open"
|
|
54
54
|
created_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
55
55
|
source_path: str | None = None
|
|
56
|
+
# When False the card is advisory (``dev run``/``dev e2e``); only ``dev watch`` cards
|
|
57
|
+
# with blocks_gate=True may block verification.
|
|
58
|
+
blocks_gate: bool = True
|
|
56
59
|
|
|
57
60
|
@property
|
|
58
61
|
def blocks_completion(self) -> bool:
|
|
59
|
-
return self.verdict == "Critical Issues"
|
|
62
|
+
return self.verdict == "Critical Issues" and self.blocks_gate
|
|
60
63
|
|
|
61
64
|
|
|
62
65
|
def session_id_from_path(path: Path) -> str:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import logging
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
@@ -23,12 +24,21 @@ class LiveReviewService:
|
|
|
23
24
|
project_root: Path,
|
|
24
25
|
client: str = "generic",
|
|
25
26
|
use_llm: bool = False,
|
|
27
|
+
task_id: str | None = None,
|
|
26
28
|
) -> CritiqueCard:
|
|
27
|
-
fallback = review_turn(turn, project_root, client=client)
|
|
29
|
+
fallback = review_turn(turn, project_root, client=client, task_id=task_id)
|
|
28
30
|
if not use_llm or self.router is None:
|
|
29
31
|
logger.debug("Live review (deterministic) for %s turn=%s: %s", client, turn.turn_id, fallback.verdict)
|
|
30
32
|
return fallback
|
|
31
33
|
|
|
34
|
+
# Ground the model in the task's RECORDED verification state. Without this
|
|
35
|
+
# the reviewer sees only the agent's prose and reliably hallucinates
|
|
36
|
+
# "provides no evidence of implementation" against code whose acceptance
|
|
37
|
+
# criteria DevCouncil has already proven with executed checks (observed:
|
|
38
|
+
# a benchmark task blocked at 4/4 hidden-test correctness because the
|
|
39
|
+
# ungrounded reviewer critiqued the agent's summary message).
|
|
40
|
+
grounding_block = self._grounding_block(project_root, task_id)
|
|
41
|
+
|
|
32
42
|
prompt = f"""
|
|
33
43
|
You are DevCouncil's live coding-agent reviewer.
|
|
34
44
|
Review the latest assistant response before the developer follows it.
|
|
@@ -43,6 +53,14 @@ Return a critique card with:
|
|
|
43
53
|
Do not praise. Do not review formatting. Focus on correctness, missing requirements, architectural drift,
|
|
44
54
|
unsafe commands, weak evidence, and premature completion claims.
|
|
45
55
|
|
|
56
|
+
Rules for the verdict:
|
|
57
|
+
- The response text is a SUMMARY, not the work itself. Never conclude "no code change /
|
|
58
|
+
no evidence" from the response alone — the recorded verification state below (when
|
|
59
|
+
present) is the authoritative account of the work.
|
|
60
|
+
- "Critical Issues" requires a concrete, checkable problem: a claim contradicted by the
|
|
61
|
+
recorded state, an unsafe command, or a missing requirement — never vagueness, brevity,
|
|
62
|
+
style, or your estimate of the agent's effort.
|
|
63
|
+
{grounding_block}
|
|
46
64
|
Client: {client}
|
|
47
65
|
Session: {turn.session_id}
|
|
48
66
|
Turn: {turn.turn_id}
|
|
@@ -51,30 +69,43 @@ Assistant response:
|
|
|
51
69
|
{turn.content}
|
|
52
70
|
"""
|
|
53
71
|
samples = self._samples(project_root)
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
|
|
73
|
+
async def _one_sample(attempt: int) -> CritiqueCard | None:
|
|
74
|
+
if self.router is None:
|
|
75
|
+
return None
|
|
56
76
|
# Vary temperature so independent samples actually differ (and so the router
|
|
57
77
|
# cache returns distinct generations). Attempt 0 stays deterministic.
|
|
58
78
|
temperature = 0.0 if attempt == 0 else min(0.8, 0.3 + 0.2 * attempt)
|
|
59
79
|
try:
|
|
60
|
-
|
|
80
|
+
return await self.router.complete_structured(
|
|
61
81
|
role=self.role,
|
|
62
82
|
messages=[{"role": "user", "content": prompt}],
|
|
63
83
|
schema=CritiqueCard,
|
|
64
84
|
temperature=temperature,
|
|
65
|
-
)
|
|
85
|
+
)
|
|
66
86
|
except ValueError:
|
|
67
87
|
try:
|
|
68
|
-
|
|
88
|
+
return await self.router.complete_structured(
|
|
69
89
|
role="implementation_reviewer",
|
|
70
90
|
messages=[{"role": "user", "content": prompt}],
|
|
71
91
|
schema=CritiqueCard,
|
|
72
92
|
temperature=temperature,
|
|
73
|
-
)
|
|
93
|
+
)
|
|
74
94
|
except Exception:
|
|
75
|
-
|
|
95
|
+
return None
|
|
76
96
|
except Exception:
|
|
77
|
-
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
# The samples are independent by construction (no sample reads another's
|
|
100
|
+
# output), so gather them concurrently instead of awaiting one at a time.
|
|
101
|
+
# Order is preserved (attempt 0's deterministic card stays first) and each
|
|
102
|
+
# sample keeps its own error handling above; return_exceptions only shields
|
|
103
|
+
# siblings from anything that still escapes, which the filter then drops.
|
|
104
|
+
results = await asyncio.gather(
|
|
105
|
+
*(_one_sample(attempt) for attempt in range(samples)),
|
|
106
|
+
return_exceptions=True,
|
|
107
|
+
)
|
|
108
|
+
cards: list[CritiqueCard] = [card for card in results if isinstance(card, CritiqueCard)]
|
|
78
109
|
|
|
79
110
|
if not cards:
|
|
80
111
|
logger.warning("Live review produced no cards for %s turn=%s; using deterministic fallback", client, turn.turn_id)
|
|
@@ -93,10 +124,58 @@ Assistant response:
|
|
|
93
124
|
"source_path": fallback.source_path,
|
|
94
125
|
})
|
|
95
126
|
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _grounding_block(project_root: Path, task_id: str | None) -> str:
|
|
129
|
+
"""Render the task's recorded verification state for the review prompt.
|
|
130
|
+
|
|
131
|
+
Best-effort: no task id / no DB / unknown task yields an empty block and
|
|
132
|
+
the model reviews ungrounded (the deterministic fallback stays grounded
|
|
133
|
+
via ``review_turn``)."""
|
|
134
|
+
try:
|
|
135
|
+
from devcouncil.live.cards import _load_task_grounding
|
|
136
|
+
|
|
137
|
+
grounding = _load_task_grounding(project_root, task_id)
|
|
138
|
+
except Exception:
|
|
139
|
+
grounding = None
|
|
140
|
+
if grounding is None:
|
|
141
|
+
return ""
|
|
142
|
+
if grounding.is_satisfied:
|
|
143
|
+
corroboration = (
|
|
144
|
+
"This state CORROBORATES a completion claim: do not demand further "
|
|
145
|
+
"evidence and do not raise Critical Issues about proof — the "
|
|
146
|
+
"executable evidence already exists in DevCouncil's records."
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
corroboration = (
|
|
150
|
+
"This state does NOT yet corroborate a completion claim: if the "
|
|
151
|
+
"response claims success, that mismatch is the concern to raise "
|
|
152
|
+
"(cite the numbers above)."
|
|
153
|
+
)
|
|
154
|
+
return f"""
|
|
155
|
+
Recorded verification state for task {grounding.task_id} (authoritative — measured from
|
|
156
|
+
executed checks and the artifact graph, not taken from the agent's words):
|
|
157
|
+
- task status: {grounding.status}
|
|
158
|
+
- blocking gaps: {grounding.blocking_gaps}
|
|
159
|
+
- failing verification commands: {grounding.failing_commands}
|
|
160
|
+
- acceptance criteria with passing evidence: {grounding.acs_passing}/{grounding.acs_total}
|
|
161
|
+
{corroboration}
|
|
162
|
+
"""
|
|
163
|
+
|
|
96
164
|
def _samples(self, project_root: Path) -> int:
|
|
97
165
|
try:
|
|
98
|
-
from devcouncil.app.config import load_config
|
|
99
|
-
|
|
166
|
+
from devcouncil.app.config import load_config, role_runs_on_local_provider
|
|
167
|
+
|
|
168
|
+
cfg = load_config(project_root)
|
|
169
|
+
# Auto-tune by reviewer locality: a cost-free local (Ollama) reviewer votes
|
|
170
|
+
# over 3 independent samples (outvoting a lone mis-calibrated verdict); a
|
|
171
|
+
# paid cloud reviewer stays single-shot. Explicit config always wins —
|
|
172
|
+
# but an explicitly single-shot LOCAL reviewer is flagged, never silent.
|
|
173
|
+
local_reviewer = role_runs_on_local_provider(cfg, "live_reviewer")
|
|
174
|
+
from devcouncil.telemetry.logging_setup import warn_once
|
|
175
|
+
|
|
176
|
+
for warning in cfg.verification.reviewer_checks.unsafe_override_warnings(local_reviewer):
|
|
177
|
+
warn_once(logger, warning) # _samples() runs per review; warn once
|
|
178
|
+
return cfg.verification.reviewer_checks.resolved(local_reviewer)
|
|
100
179
|
except Exception:
|
|
101
180
|
return 1
|
|
102
181
|
|
|
@@ -8,6 +8,8 @@ from typing import Any
|
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, Field
|
|
10
10
|
|
|
11
|
+
from devcouncil.utils.json_persist import read_json, write_model_json
|
|
12
|
+
|
|
11
13
|
|
|
12
14
|
class ReviewSignal(BaseModel):
|
|
13
15
|
client: str = "generic"
|
|
@@ -47,7 +49,7 @@ def write_signal(project_root: Path, client: str, payload: dict[str, Any]) -> Pa
|
|
|
47
49
|
digest = hashlib.sha256(key.encode("utf-8", errors="replace")).hexdigest()[:12]
|
|
48
50
|
path = directory / f"{client.lower()}-{digest}.json"
|
|
49
51
|
signal.path = str(path)
|
|
50
|
-
path
|
|
52
|
+
write_model_json(path, signal)
|
|
51
53
|
return path
|
|
52
54
|
|
|
53
55
|
|
|
@@ -58,7 +60,7 @@ def load_signals(project_root: Path) -> list[ReviewSignal]:
|
|
|
58
60
|
signals: list[ReviewSignal] = []
|
|
59
61
|
for path in sorted(directory.glob("*.json"), key=lambda item: item.stat().st_mtime, reverse=True):
|
|
60
62
|
try:
|
|
61
|
-
raw =
|
|
63
|
+
raw = read_json(path)
|
|
62
64
|
signal = ReviewSignal.model_validate(raw)
|
|
63
65
|
signal.path = str(path)
|
|
64
66
|
signals.append(signal)
|
|
@@ -3,16 +3,25 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from devcouncil.storage.db import get_db
|
|
6
|
+
from devcouncil.storage.native import TaskLeaseRepository
|
|
6
7
|
from devcouncil.storage.repositories import TaskRepository
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def active_task_id(project_root: Path) -> str | None:
|
|
10
|
-
"""Return the single
|
|
11
|
+
"""Return the single active DevCouncil task ID, if one is unambiguous.
|
|
12
|
+
|
|
13
|
+
Prefers a task in ``running`` status. When none are running, falls back to the
|
|
14
|
+
task behind exactly one non-expired active lease (``dev checkout`` path).
|
|
15
|
+
"""
|
|
11
16
|
db = get_db(project_root)
|
|
12
17
|
if not db:
|
|
13
18
|
return None
|
|
14
19
|
with db.get_session() as session:
|
|
15
20
|
running = [task for task in TaskRepository(session).get_all() if task.status == "running"]
|
|
16
|
-
|
|
21
|
+
if len(running) == 1:
|
|
22
|
+
return running[0].id
|
|
23
|
+
leases = TaskLeaseRepository(session).list_leases(active_only=True)
|
|
24
|
+
active = [lease for lease, expired in leases if not expired]
|
|
25
|
+
if len(active) == 1:
|
|
26
|
+
return active[0].task_id
|
|
17
27
|
return None
|
|
18
|
-
return running[0].id
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import shutil
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from typing import Any, Iterable, Literal
|
|
6
7
|
|
|
7
8
|
from devcouncil.live.models import AgentSession, AgentTurn, session_id_from_path
|
|
9
|
+
from devcouncil.utils.json_persist import read_json
|
|
8
10
|
|
|
9
11
|
RoleName = Literal["user", "assistant", "system", "tool", "unknown"]
|
|
10
12
|
KNOWN_ROLES: set[RoleName] = {"user", "assistant", "system", "tool"}
|
|
@@ -86,11 +88,76 @@ def latest_assistant_turn(path: Path, client: str = "generic") -> AgentTurn | No
|
|
|
86
88
|
return result
|
|
87
89
|
|
|
88
90
|
|
|
91
|
+
def claude_project_slug(project_root: Path | str) -> str:
|
|
92
|
+
"""Encode a path the way Claude Code names dirs under ``~/.claude/projects/``."""
|
|
93
|
+
resolved = Path(project_root).expanduser().resolve()
|
|
94
|
+
return resolved.as_posix().replace("/", "-")
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _read_claude_session_id(project_root: Path, task_id: str | None) -> str | None:
|
|
98
|
+
if not task_id:
|
|
99
|
+
return None
|
|
100
|
+
path = project_root / ".devcouncil" / "sessions" / f"{task_id}-claude.json"
|
|
101
|
+
if not path.is_file():
|
|
102
|
+
return None
|
|
103
|
+
try:
|
|
104
|
+
data = read_json(path) or {}
|
|
105
|
+
except (json.JSONDecodeError, OSError):
|
|
106
|
+
return None
|
|
107
|
+
session_id = str(data.get("session_id") or "").strip()
|
|
108
|
+
return session_id or None
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def claude_transcript_for_session(project_root: Path, session_id: str) -> Path | None:
|
|
112
|
+
"""Return the native Claude JSONL for ``session_id`` in this project, if present."""
|
|
113
|
+
if not session_id:
|
|
114
|
+
return None
|
|
115
|
+
slug_dir = CLAUDE_TRANSCRIPT_ROOT / claude_project_slug(project_root)
|
|
116
|
+
direct = slug_dir / f"{session_id}.jsonl"
|
|
117
|
+
if direct.is_file():
|
|
118
|
+
return direct
|
|
119
|
+
for candidate in _claude_transcript_candidates(project_root):
|
|
120
|
+
if candidate.stem == session_id:
|
|
121
|
+
return candidate
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def claude_transcript_for_task(project_root: Path, task_id: str) -> Path | None:
|
|
126
|
+
"""Resolve the Claude transcript DevCouncil pinned for ``task_id``."""
|
|
127
|
+
session_id = _read_claude_session_id(project_root, task_id)
|
|
128
|
+
if session_id:
|
|
129
|
+
path = claude_transcript_for_session(project_root, session_id)
|
|
130
|
+
if path is not None:
|
|
131
|
+
return path
|
|
132
|
+
return None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def mirror_claude_transcript(project_root: Path, session_id: str) -> Path | None:
|
|
136
|
+
"""Copy the native Claude JSONL into ``.devcouncil/live/claude/`` for discovery.
|
|
137
|
+
|
|
138
|
+
Headless ``dev run``/``dev e2e`` pins a session id up front; mirroring the native
|
|
139
|
+
transcript into the project makes live review resilient even when Claude's global
|
|
140
|
+
project slug is slow to appear or ``discover_sessions`` would otherwise race."""
|
|
141
|
+
source = claude_transcript_for_session(project_root, session_id)
|
|
142
|
+
if source is None or not source.is_file():
|
|
143
|
+
return None
|
|
144
|
+
dest_dir = project_root / ".devcouncil" / "live" / "claude"
|
|
145
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
dest = dest_dir / f"{session_id}.jsonl"
|
|
147
|
+
try:
|
|
148
|
+
shutil.copy2(source, dest)
|
|
149
|
+
except OSError:
|
|
150
|
+
return None
|
|
151
|
+
return dest
|
|
152
|
+
|
|
153
|
+
|
|
89
154
|
def _claude_transcript_candidates(project_root: Path) -> list[Path]:
|
|
90
155
|
local_runtime = project_root / ".devcouncil" / "live" / "claude"
|
|
91
156
|
candidates = list(local_runtime.glob("*.jsonl"))
|
|
92
157
|
if CLAUDE_TRANSCRIPT_ROOT.exists():
|
|
93
|
-
|
|
158
|
+
slug_dir = CLAUDE_TRANSCRIPT_ROOT / claude_project_slug(project_root)
|
|
159
|
+
if slug_dir.is_dir():
|
|
160
|
+
candidates.extend(slug_dir.glob("*.jsonl"))
|
|
94
161
|
# Dedup, then sort by path for a deterministic, stat-free order. discover_sessions
|
|
95
162
|
# re-sorts the resulting sessions by mtime; because that sort is stable, giving it a
|
|
96
163
|
# deterministic input keeps the tie-break (equal-mtime sessions) stable across runs —
|
|
@@ -144,7 +211,7 @@ def _role(raw: dict[str, Any]) -> RoleName:
|
|
|
144
211
|
if isinstance(nested, str):
|
|
145
212
|
return nested if nested in KNOWN_ROLES else "unknown"
|
|
146
213
|
record_type = raw.get("type")
|
|
147
|
-
if record_type in KNOWN_ROLES:
|
|
214
|
+
if isinstance(record_type, str) and record_type in KNOWN_ROLES:
|
|
148
215
|
return record_type
|
|
149
216
|
return "unknown"
|
|
150
217
|
|
|
@@ -4,6 +4,7 @@ import logging
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Optional
|
|
6
6
|
from devcouncil.llm.provider import LLMResponse
|
|
7
|
+
from devcouncil.utils.json_persist import read_json, write_json
|
|
7
8
|
|
|
8
9
|
logger = logging.getLogger(__name__)
|
|
9
10
|
|
|
@@ -33,10 +34,9 @@ class LLMCache:
|
|
|
33
34
|
cache_file = self.cache_dir / f"{key}.json"
|
|
34
35
|
if cache_file.exists():
|
|
35
36
|
try:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return LLMResponse(**data)
|
|
37
|
+
data = read_json(cache_file)
|
|
38
|
+
logger.debug("LLM cache HIT model=%s key=%s", model, key[:12])
|
|
39
|
+
return LLMResponse(**data)
|
|
40
40
|
except Exception as e:
|
|
41
41
|
logger.warning("LLM cache read failed for key=%s: %s", key[:12], e)
|
|
42
42
|
logger.debug("LLM cache MISS model=%s key=%s", model, key[:12])
|
|
@@ -45,6 +45,5 @@ class LLMCache:
|
|
|
45
45
|
def set(self, model: str, messages: list, temp: float, json_mode: bool, response: LLMResponse, provider_fingerprint: str = "", cache_key: Optional[str] = None):
|
|
46
46
|
key = cache_key if cache_key is not None else self._get_key(model, messages, temp, json_mode, provider_fingerprint)
|
|
47
47
|
cache_file = self.cache_dir / f"{key}.json"
|
|
48
|
-
|
|
49
|
-
json.dump(response.model_dump(), f)
|
|
48
|
+
write_json(cache_file, response.model_dump())
|
|
50
49
|
logger.debug("LLM cache STORE model=%s key=%s", model, key[:12])
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
openrouter:
|
|
2
|
-
spec_writer:
|
|
3
|
-
prompt_enhancer:
|
|
4
|
-
planner_a:
|
|
5
|
-
planner_b:
|
|
6
|
-
critic_a:
|
|
7
|
-
critic_b:
|
|
8
|
-
arbiter:
|
|
9
|
-
native_agent:
|
|
10
|
-
implementation_reviewer:
|
|
11
|
-
live_reviewer:
|
|
2
|
+
spec_writer: qwen/qwen3-max
|
|
3
|
+
prompt_enhancer: qwen/qwen3-max
|
|
4
|
+
planner_a: qwen/qwen3-max
|
|
5
|
+
planner_b: qwen/qwen3-max
|
|
6
|
+
critic_a: qwen/qwen3-max
|
|
7
|
+
critic_b: qwen/qwen3-max
|
|
8
|
+
arbiter: qwen/qwen3-max
|
|
9
|
+
native_agent: qwen/qwen3-max
|
|
10
|
+
implementation_reviewer: qwen/qwen3-max
|
|
11
|
+
live_reviewer: qwen/qwen3-max
|
|
12
12
|
vertexai:
|
|
13
13
|
spec_writer: google/gemini-2.5-flash
|
|
14
14
|
prompt_enhancer: google/gemini-2.5-flash
|