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
|
@@ -37,6 +37,27 @@ _CONSOLE_HANDLER_TAG = "devcouncil.console"
|
|
|
37
37
|
_LOG_FORMAT = "%(asctime)s %(levelname)-7s [%(name)s] %(message)s"
|
|
38
38
|
_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
39
39
|
|
|
40
|
+
# Last (console_level, log_path) we announced, so repeat configure_logging()
|
|
41
|
+
# calls in the same process don't spam identical "Logging configured" lines
|
|
42
|
+
# into the DEBUG file (observed: ~16% of a real log was this one line).
|
|
43
|
+
_last_announced_config: Optional[tuple] = None
|
|
44
|
+
|
|
45
|
+
# Messages already emitted via warn_once() in this process.
|
|
46
|
+
_warned_once: set = set()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def warn_once(target_logger: logging.Logger, message: str) -> None:
|
|
50
|
+
"""Emit ``message`` at WARNING at most once per process.
|
|
51
|
+
|
|
52
|
+
For per-task/per-call code paths that detect a PROCESS-level condition (e.g.
|
|
53
|
+
a risky config value): the first occurrence is signal, the 20th identical
|
|
54
|
+
line in one run is log spam that buries real warnings.
|
|
55
|
+
"""
|
|
56
|
+
if message in _warned_once:
|
|
57
|
+
return
|
|
58
|
+
_warned_once.add(message)
|
|
59
|
+
target_logger.warning(message)
|
|
60
|
+
|
|
40
61
|
# Default file location (relative to a project root) for the durable run log.
|
|
41
62
|
LOG_RELATIVE_PATH = Path(".devcouncil") / "logs" / "devcouncil.log"
|
|
42
63
|
|
|
@@ -65,6 +86,21 @@ def _resolve_console_level(verbosity: int, quiet: bool, log_level: Optional[str]
|
|
|
65
86
|
return _level_from_verbosity(verbosity, quiet)
|
|
66
87
|
|
|
67
88
|
|
|
89
|
+
def _resolve_log_path(project_root: Optional[Path]) -> Path:
|
|
90
|
+
"""Resolve the shared DEBUG log file location.
|
|
91
|
+
|
|
92
|
+
``DEVCOUNCIL_LOG_DIR`` overrides everything — used by the test suite so
|
|
93
|
+
fixture noise (fake tasks, deliberate failures) never lands in a real
|
|
94
|
+
project's ``.devcouncil/logs/devcouncil.log``, and available to anyone who
|
|
95
|
+
wants logs outside the repo (CI, read-only checkouts).
|
|
96
|
+
"""
|
|
97
|
+
override = os.environ.get("DEVCOUNCIL_LOG_DIR")
|
|
98
|
+
if override:
|
|
99
|
+
return Path(override) / "devcouncil.log"
|
|
100
|
+
base = Path(project_root) if project_root is not None else Path.cwd()
|
|
101
|
+
return base / LOG_RELATIVE_PATH
|
|
102
|
+
|
|
103
|
+
|
|
68
104
|
def _find_tagged_handler(logger: logging.Logger, tag: str) -> Optional[logging.Handler]:
|
|
69
105
|
for handler in logger.handlers:
|
|
70
106
|
if getattr(handler, "_devcouncil_tag", None) == tag:
|
|
@@ -72,6 +108,35 @@ def _find_tagged_handler(logger: logging.Logger, tag: str) -> Optional[logging.H
|
|
|
72
108
|
return None
|
|
73
109
|
|
|
74
110
|
|
|
111
|
+
class _CurrentStderrHandler(logging.StreamHandler):
|
|
112
|
+
"""A stderr handler that resolves ``sys.stderr`` at EMIT time, not at creation.
|
|
113
|
+
|
|
114
|
+
The handler is installed once per process and reused. A plain
|
|
115
|
+
``logging.StreamHandler()`` snapshots ``sys.stderr`` when constructed; in any
|
|
116
|
+
context that later replaces or closes that stream — test runners, an MCP server
|
|
117
|
+
re-pointing stdio, agents embedding the CLI — every subsequent record then hits a
|
|
118
|
+
dead stream and the logging module prints a ``--- Logging error ---`` report to
|
|
119
|
+
the *current* stderr, polluting agent-facing (e.g. ``--json``) output. Resolving
|
|
120
|
+
the stream per emit keeps console logging bound to whatever stderr is now."""
|
|
121
|
+
|
|
122
|
+
def __init__(self) -> None:
|
|
123
|
+
import sys
|
|
124
|
+
|
|
125
|
+
super().__init__(sys.stderr)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def stream(self):
|
|
129
|
+
import sys
|
|
130
|
+
|
|
131
|
+
return sys.stderr
|
|
132
|
+
|
|
133
|
+
@stream.setter
|
|
134
|
+
def stream(self, value) -> None: # noqa: ARG002 - always current sys.stderr
|
|
135
|
+
# Ignore assignments (StreamHandler.__init__ / setStream): this handler is
|
|
136
|
+
# permanently bound to the CURRENT sys.stderr by design.
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
|
|
75
140
|
def configure_logging(
|
|
76
141
|
project_root: Optional[Path] = None,
|
|
77
142
|
*,
|
|
@@ -97,7 +162,7 @@ def configure_logging(
|
|
|
97
162
|
# --- Console handler (stderr) -------------------------------------------
|
|
98
163
|
console = _find_tagged_handler(root, _CONSOLE_HANDLER_TAG)
|
|
99
164
|
if console is None:
|
|
100
|
-
console =
|
|
165
|
+
console = _CurrentStderrHandler() # emit-time stderr, keeps stdout clean
|
|
101
166
|
console._devcouncil_tag = _CONSOLE_HANDLER_TAG # type: ignore[attr-defined]
|
|
102
167
|
console.setFormatter(formatter)
|
|
103
168
|
root.addHandler(console)
|
|
@@ -106,8 +171,7 @@ def configure_logging(
|
|
|
106
171
|
# --- Rotating file handler (always DEBUG) -------------------------------
|
|
107
172
|
log_path: Optional[Path] = None
|
|
108
173
|
if _find_tagged_handler(root, _FILE_HANDLER_TAG) is None:
|
|
109
|
-
|
|
110
|
-
log_path = base / LOG_RELATIVE_PATH
|
|
174
|
+
log_path = _resolve_log_path(project_root)
|
|
111
175
|
try:
|
|
112
176
|
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
113
177
|
file_handler = RotatingFileHandler(
|
|
@@ -125,8 +189,7 @@ def configure_logging(
|
|
|
125
189
|
# than crashing the command the user actually asked for.
|
|
126
190
|
log_path = None
|
|
127
191
|
else:
|
|
128
|
-
|
|
129
|
-
log_path = base / LOG_RELATIVE_PATH
|
|
192
|
+
log_path = _resolve_log_path(project_root)
|
|
130
193
|
|
|
131
194
|
# Quieten chatty third-party loggers on the console; the file still gets them.
|
|
132
195
|
for noisy in ("httpx", "httpcore", "urllib3", "asyncio"):
|
|
@@ -134,11 +197,15 @@ def configure_logging(
|
|
|
134
197
|
|
|
135
198
|
_install_excepthook()
|
|
136
199
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
200
|
+
global _last_announced_config
|
|
201
|
+
config = (console_level, str(log_path))
|
|
202
|
+
if config != _last_announced_config:
|
|
203
|
+
_last_announced_config = config
|
|
204
|
+
logging.getLogger(__name__).debug(
|
|
205
|
+
"Logging configured (console=%s, file=%s)",
|
|
206
|
+
logging.getLevelName(console_level),
|
|
207
|
+
log_path,
|
|
208
|
+
)
|
|
142
209
|
return log_path
|
|
143
210
|
|
|
144
211
|
|
|
@@ -179,7 +246,7 @@ def set_log_dir(project_root: Path) -> Optional[Path]:
|
|
|
179
246
|
is a cheap no-op. Returns the (re)resolved log path, or ``None`` if it could not be
|
|
180
247
|
created (logging then stays on the previous handler / console).
|
|
181
248
|
"""
|
|
182
|
-
target =
|
|
249
|
+
target = _resolve_log_path(project_root)
|
|
183
250
|
root = logging.getLogger()
|
|
184
251
|
existing = _find_tagged_handler(root, _FILE_HANDLER_TAG)
|
|
185
252
|
if existing is not None:
|
|
@@ -31,6 +31,13 @@ openai/gpt-5.5:
|
|
|
31
31
|
google/gemini-2.5-flash:
|
|
32
32
|
prompt_per_1k: 0.0003
|
|
33
33
|
completion_per_1k: 0.0025
|
|
34
|
+
nvidia/nemotron-3-ultra-550b-a55b:free:
|
|
35
|
+
prompt_per_1k: 0.0
|
|
36
|
+
completion_per_1k: 0.0
|
|
37
|
+
# OpenRouter (Alibaba) Qwen3 Max — default planning/review model.
|
|
38
|
+
qwen/qwen3-max:
|
|
39
|
+
prompt_per_1k: 0.00078
|
|
40
|
+
completion_per_1k: 0.0039
|
|
34
41
|
# Local Ollama models run on-device and are free. Bare tags (no ``ollama/``
|
|
35
42
|
# prefix) are listed here so the cost reporter shows $0; the cost.py prefix
|
|
36
43
|
# guard covers any other ``ollama/`` or ``ollama:`` model id.
|
|
@@ -39,6 +39,13 @@ def _safe_trace(
|
|
|
39
39
|
"""Mirror a stage/step boundary to the JSONL trace, swallowing any error."""
|
|
40
40
|
if project_root is None:
|
|
41
41
|
return
|
|
42
|
+
# Never MATERIALIZE a project root that doesn't exist: TraceLogger mkdirs
|
|
43
|
+
# .devcouncil/ under the given path, so tracing under a typo'd/absent
|
|
44
|
+
# --project-root would silently create directories there — and break
|
|
45
|
+
# commands that first check the root's existence (observed: `lsp inspect`
|
|
46
|
+
# reporting a missing root as valid because the stage trace created it).
|
|
47
|
+
if not Path(project_root).exists():
|
|
48
|
+
return
|
|
42
49
|
try:
|
|
43
50
|
from devcouncil.telemetry.traces import TraceLogger
|
|
44
51
|
|
|
@@ -89,11 +96,29 @@ def log_stage(
|
|
|
89
96
|
yield
|
|
90
97
|
except BaseException as exc:
|
|
91
98
|
elapsed = time.monotonic() - start
|
|
92
|
-
|
|
99
|
+
# Control-flow exceptions are NOT stage failures: typer.Exit(1)/SystemExit
|
|
100
|
+
# is how a CLI command reports a legitimate non-zero verdict (e.g. `verify
|
|
101
|
+
# --json` on a blocked task), and logging it as "✖ failed" both misleads
|
|
102
|
+
# and — because the log line prints after the command's JSON — corrupts
|
|
103
|
+
# machine-readable output for any consumer parsing stdout+stderr.
|
|
104
|
+
control_flow = isinstance(exc, (SystemExit, KeyboardInterrupt, GeneratorExit))
|
|
105
|
+
if not control_flow:
|
|
106
|
+
# typer/click CLI exits, matched STRUCTURALLY rather than by isinstance:
|
|
107
|
+
# typer can vendor click (typer._click.exceptions.Exit), which does not
|
|
108
|
+
# subclass the top-level click.exceptions.Exit, so an import-based
|
|
109
|
+
# isinstance check silently misses typer.Exit.
|
|
110
|
+
cls = type(exc)
|
|
111
|
+
control_flow = cls.__name__ in {"Exit", "Abort"} and "click" in (cls.__module__ or "")
|
|
112
|
+
if control_flow:
|
|
113
|
+
logger.debug("· %s exited via control flow (%.2fs): %r", name, elapsed, exc)
|
|
114
|
+
raise
|
|
115
|
+
# %r, not %s: common failures (httpx.ReadTimeout, CancelledError) stringify
|
|
116
|
+
# to an EMPTY message, which logs a useless "failed (0.05s): ."
|
|
117
|
+
logger.error("✖ %s failed (%.2fs): %r", name, elapsed, exc)
|
|
93
118
|
_safe_trace(
|
|
94
119
|
project_root,
|
|
95
120
|
"stage_failed",
|
|
96
|
-
{"stage": name, "error":
|
|
121
|
+
{"stage": name, "error": repr(exc), "elapsed_s": round(elapsed, 3), **context},
|
|
97
122
|
run_id=run_id,
|
|
98
123
|
task_id=task_id,
|
|
99
124
|
summary=f"Stage failed: {name}",
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import
|
|
1
|
+
import logging
|
|
2
2
|
from pathlib import Path
|
|
3
|
-
from typing import Dict, Any
|
|
3
|
+
from typing import Dict, Any, cast
|
|
4
4
|
|
|
5
5
|
from devcouncil.telemetry.pricing import pricing_for_model
|
|
6
|
+
from devcouncil.utils.json_persist import read_json, write_json
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
6
10
|
|
|
7
11
|
class TelemetryTracker:
|
|
8
12
|
def __init__(self, project_root: Path):
|
|
13
|
+
self.project_root = project_root
|
|
9
14
|
self.log_file = project_root / ".devcouncil" / "logs" / "telemetry.json"
|
|
10
15
|
# log_usage() reloads the ledger immediately before saving (for concurrent-write
|
|
11
16
|
# safety), so any value read here would always be overwritten before use. Start
|
|
@@ -16,16 +21,41 @@ class TelemetryTracker:
|
|
|
16
21
|
def _load(self) -> Dict[str, Any]:
|
|
17
22
|
if self.log_file.exists():
|
|
18
23
|
try:
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
return cast(Dict[str, Any], read_json(self.log_file))
|
|
25
|
+
except Exception as e:
|
|
26
|
+
# A corrupt ledger silently resets accumulated cost stats on the next save.
|
|
27
|
+
logger.warning("Failed to load telemetry ledger %s, starting fresh: %s", self.log_file, e)
|
|
23
28
|
return {"total_cost": 0.0, "total_prompt_tokens": 0, "total_completion_tokens": 0, "models": {}}
|
|
24
29
|
|
|
25
30
|
def _save(self):
|
|
26
31
|
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
write_json(self.log_file, self.stats)
|
|
33
|
+
|
|
34
|
+
def _warn_if_budget_crossed(self, previous_total: float, new_total: float) -> None:
|
|
35
|
+
"""Warn (once, at the crossing) when cumulative spend passes the configured budget.
|
|
36
|
+
|
|
37
|
+
WARN-ONLY by design — this never raises and never blocks a run. The budget is
|
|
38
|
+
``telemetry.cost_budget_usd`` in .devcouncil/config.yaml (``dev cost budget --set X``);
|
|
39
|
+
a missing/invalid config or an unset budget silently disables the check. Warning at
|
|
40
|
+
the crossing (previous < budget <= new) rather than on every over-budget call keeps
|
|
41
|
+
one model-call-per-warning noise out of long runs.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
from devcouncil.app.config import load_config
|
|
45
|
+
|
|
46
|
+
budget = load_config(self.project_root).telemetry.cost_budget_usd
|
|
47
|
+
except Exception:
|
|
48
|
+
return
|
|
49
|
+
if budget is None or budget <= 0:
|
|
50
|
+
return
|
|
51
|
+
if previous_total < budget <= new_total:
|
|
52
|
+
logger.warning(
|
|
53
|
+
"Cost budget crossed: cumulative model spend $%.4f now exceeds the "
|
|
54
|
+
"configured budget of $%.2f (telemetry.cost_budget_usd). Warn-only — "
|
|
55
|
+
"runs continue. Review with 'dev cost budget' or 'dev cost show'.",
|
|
56
|
+
new_total,
|
|
57
|
+
budget,
|
|
58
|
+
)
|
|
29
59
|
|
|
30
60
|
def log_usage(self, model: str, usage: Dict[str, int], *, local: bool = False):
|
|
31
61
|
# Re-read the ledger immediately before mutating so the whole load->mutate->save
|
|
@@ -35,6 +65,7 @@ class TelemetryTracker:
|
|
|
35
65
|
# parallelized SkillOpt _evaluate) each load the same baseline and clobber each
|
|
36
66
|
# other's entries on save. Reloading here makes the last writer additive, not lossy.
|
|
37
67
|
self.stats = self._load()
|
|
68
|
+
previous_total = float(self.stats.get("total_cost", 0.0) or 0.0)
|
|
38
69
|
prompt_tokens = usage.get("prompt_tokens", 0)
|
|
39
70
|
completion_tokens = usage.get("completion_tokens", 0)
|
|
40
71
|
|
|
@@ -49,15 +80,21 @@ class TelemetryTracker:
|
|
|
49
80
|
completion_tokens / 1000.0
|
|
50
81
|
) * rates["completion_per_1k"]
|
|
51
82
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
self.stats["
|
|
83
|
+
# .get with defaults: a hand-edited or partially-written telemetry.json must
|
|
84
|
+
# not crash usage logging — missing keys just restart their counters.
|
|
85
|
+
self.stats["total_cost"] = float(self.stats.get("total_cost", 0.0) or 0.0) + cost
|
|
86
|
+
self.stats["total_prompt_tokens"] = int(self.stats.get("total_prompt_tokens", 0) or 0) + prompt_tokens
|
|
87
|
+
self.stats["total_completion_tokens"] = (
|
|
88
|
+
int(self.stats.get("total_completion_tokens", 0) or 0) + completion_tokens
|
|
89
|
+
)
|
|
55
90
|
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
models = self.stats.setdefault("models", {})
|
|
92
|
+
if model not in models:
|
|
93
|
+
models[model] = {"cost": 0.0, "prompt_tokens": 0, "completion_tokens": 0}
|
|
58
94
|
|
|
59
95
|
self.stats["models"][model]["cost"] += cost
|
|
60
96
|
self.stats["models"][model]["prompt_tokens"] += prompt_tokens
|
|
61
97
|
self.stats["models"][model]["completion_tokens"] += completion_tokens
|
|
62
98
|
|
|
63
99
|
self._save()
|
|
100
|
+
self._warn_if_budget_crossed(previous_total, float(self.stats.get("total_cost", 0.0) or 0.0))
|
|
@@ -2,18 +2,21 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import secrets
|
|
5
|
+
import socketserver
|
|
5
6
|
import time
|
|
6
7
|
from importlib import resources
|
|
7
8
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
8
9
|
from pathlib import Path
|
|
9
|
-
from typing import TYPE_CHECKING
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
10
11
|
from urllib.parse import urlparse
|
|
11
12
|
|
|
12
13
|
from devcouncil.app.project_status import compute_phase
|
|
13
14
|
from devcouncil.integrations.actions import apply_integration_target
|
|
14
15
|
from devcouncil.integrations.check import build_integration_check_report, integration_status_summary
|
|
15
16
|
from devcouncil.storage.db import get_db
|
|
16
|
-
from devcouncil.
|
|
17
|
+
from devcouncil.utils.json_persist import read_json
|
|
18
|
+
from devcouncil.storage.repositories import ArtifactGraphRepository, GapRepository, StateRepository
|
|
19
|
+
from devcouncil.execution.stop_gate_history import read_events as read_stop_gate_events
|
|
17
20
|
from devcouncil.telemetry.traces import TraceEvent, read_trace_events_since
|
|
18
21
|
|
|
19
22
|
if TYPE_CHECKING:
|
|
@@ -38,7 +41,7 @@ def _load_run_manifest(manifest_path: Path) -> dict | None:
|
|
|
38
41
|
if cached is not None and cached[0] == mtime:
|
|
39
42
|
return dict(cached[1])
|
|
40
43
|
try:
|
|
41
|
-
manifest =
|
|
44
|
+
manifest = read_json(manifest_path) or {}
|
|
42
45
|
except (json.JSONDecodeError, OSError):
|
|
43
46
|
return None
|
|
44
47
|
_RUN_MANIFEST_CACHE[key] = (mtime, manifest)
|
|
@@ -132,6 +135,9 @@ def _invalidate_artifact_graph(project_root: Path) -> None:
|
|
|
132
135
|
_TRACE_EVENTS_LIMIT = 50
|
|
133
136
|
_TRACE_EVENTS_CACHE: dict[str, tuple[int, list[TraceEvent]]] = {}
|
|
134
137
|
|
|
138
|
+
_DASHBOARD_GAPS_CAP = 50
|
|
139
|
+
_EMPTY_GAPS_SUMMARY = {"total": 0, "blocking": 0, "items": []}
|
|
140
|
+
|
|
135
141
|
|
|
136
142
|
def _recent_trace_events_cached(project_root: Path) -> list[TraceEvent]:
|
|
137
143
|
key = str(project_root)
|
|
@@ -146,6 +152,28 @@ def _recent_trace_events_cached(project_root: Path) -> list[TraceEvent]:
|
|
|
146
152
|
return buffer
|
|
147
153
|
|
|
148
154
|
|
|
155
|
+
def _dashboard_gaps_summary(session) -> dict:
|
|
156
|
+
all_gaps = GapRepository(session).get_all()
|
|
157
|
+
blocking = [gap for gap in all_gaps if gap.blocking]
|
|
158
|
+
non_blocking = [gap for gap in all_gaps if not gap.blocking]
|
|
159
|
+
items: list[dict[str, Any]] = []
|
|
160
|
+
for gap in blocking + non_blocking:
|
|
161
|
+
if len(items) >= _DASHBOARD_GAPS_CAP:
|
|
162
|
+
break
|
|
163
|
+
description = gap.description
|
|
164
|
+
if len(description) > 200:
|
|
165
|
+
description = description[:197] + "..."
|
|
166
|
+
items.append(
|
|
167
|
+
{
|
|
168
|
+
"blocking": gap.blocking,
|
|
169
|
+
"task_id": gap.task_id or "",
|
|
170
|
+
"gap_type": gap.gap_type,
|
|
171
|
+
"description": description,
|
|
172
|
+
}
|
|
173
|
+
)
|
|
174
|
+
return {"total": len(all_gaps), "blocking": len(blocking), "items": items}
|
|
175
|
+
|
|
176
|
+
|
|
149
177
|
def logo_svg() -> str:
|
|
150
178
|
return resources.files("devcouncil.assets").joinpath(LEGACY_LOGO_ASSET).read_text(encoding="utf-8")
|
|
151
179
|
|
|
@@ -165,6 +193,8 @@ def dashboard_payload(project_root: Path) -> dict:
|
|
|
165
193
|
"events": [],
|
|
166
194
|
"integrations": _integration_summary_cached(project_root),
|
|
167
195
|
"recent_runs": recent_run_artifacts(project_root),
|
|
196
|
+
"stop_gate_events": read_stop_gate_events(project_root, limit=30),
|
|
197
|
+
"gaps": dict(_EMPTY_GAPS_SUMMARY),
|
|
168
198
|
}
|
|
169
199
|
with db.get_session() as session:
|
|
170
200
|
graph = _artifact_graph_cached(project_root, session)
|
|
@@ -179,6 +209,8 @@ def dashboard_payload(project_root: Path) -> dict:
|
|
|
179
209
|
"events": [event.model_dump(by_alias=True) for event in _recent_trace_events_cached(project_root)],
|
|
180
210
|
"integrations": _integration_summary_cached(project_root),
|
|
181
211
|
"recent_runs": recent_run_artifacts(project_root),
|
|
212
|
+
"stop_gate_events": read_stop_gate_events(project_root, limit=30),
|
|
213
|
+
"gaps": _dashboard_gaps_summary(session),
|
|
182
214
|
}
|
|
183
215
|
|
|
184
216
|
|
|
@@ -287,6 +319,21 @@ def dashboard_html(token: str = "") -> str:
|
|
|
287
319
|
<tbody id="runs"></tbody>
|
|
288
320
|
</table>
|
|
289
321
|
</section>
|
|
322
|
+
<section style="grid-column: 1 / -1;">
|
|
323
|
+
<h2>Verification Gaps</h2>
|
|
324
|
+
<p id="gaps-summary" style="font-size: 13px; margin: 0 0 12px;"></p>
|
|
325
|
+
<table>
|
|
326
|
+
<thead><tr><th>Blocking</th><th>Task</th><th>Type</th><th>Description</th></tr></thead>
|
|
327
|
+
<tbody id="gaps"></tbody>
|
|
328
|
+
</table>
|
|
329
|
+
</section>
|
|
330
|
+
<section style="grid-column: 1 / -1;">
|
|
331
|
+
<h2>Stop Gate History</h2>
|
|
332
|
+
<table>
|
|
333
|
+
<thead><tr><th>When</th><th>Session</th><th>Decision</th><th>Task</th><th>Claim</th></tr></thead>
|
|
334
|
+
<tbody id="stop-gate"></tbody>
|
|
335
|
+
</table>
|
|
336
|
+
</section>
|
|
290
337
|
<section style="grid-column: 1 / -1;"><h2>Recent Trace Events</h2><pre id="events"></pre></section>
|
|
291
338
|
</main>
|
|
292
339
|
<script>
|
|
@@ -382,6 +429,37 @@ def dashboard_html(token: str = "") -> str:
|
|
|
382
429
|
}}));
|
|
383
430
|
|
|
384
431
|
document.getElementById('events').textContent = JSON.stringify(data.events || [], null, 2);
|
|
432
|
+
|
|
433
|
+
const gapsSummary = data.gaps || {{}};
|
|
434
|
+
document.getElementById('gaps-summary').textContent =
|
|
435
|
+
`${{gapsSummary.blocking || 0}} blocking / ${{gapsSummary.total || 0}} total` +
|
|
436
|
+
((gapsSummary.total || 0) > (gapsSummary.items || []).length
|
|
437
|
+
? ` (showing first ${{(gapsSummary.items || []).length}})`
|
|
438
|
+
: '');
|
|
439
|
+
|
|
440
|
+
const gapsBody = document.getElementById('gaps');
|
|
441
|
+
gapsBody.replaceChildren(...(gapsSummary.items || []).map(gap => {{
|
|
442
|
+
const row = document.createElement('tr');
|
|
443
|
+
row.appendChild(setText(document.createElement('td'), gap.blocking ? 'yes' : 'no'));
|
|
444
|
+
row.appendChild(setText(document.createElement('td'), gap.task_id || ''));
|
|
445
|
+
row.appendChild(setText(document.createElement('td'), gap.gap_type || ''));
|
|
446
|
+
const desc = (gap.description || '').slice(0, 120);
|
|
447
|
+
row.appendChild(setText(document.createElement('td'), desc));
|
|
448
|
+
return row;
|
|
449
|
+
}}));
|
|
450
|
+
|
|
451
|
+
const stopGateBody = document.getElementById('stop-gate');
|
|
452
|
+
stopGateBody.replaceChildren(...(data.stop_gate_events || []).map(ev => {{
|
|
453
|
+
const row = document.createElement('tr');
|
|
454
|
+
const when = ev.ts ? new Date(ev.ts * 1000).toLocaleString() : '';
|
|
455
|
+
row.appendChild(setText(document.createElement('td'), when));
|
|
456
|
+
row.appendChild(setText(document.createElement('td'), ev.session_id || ''));
|
|
457
|
+
row.appendChild(setText(document.createElement('td'), ev.decision || ''));
|
|
458
|
+
row.appendChild(setText(document.createElement('td'), ev.task_id || ''));
|
|
459
|
+
const claim = (ev.claim || '').slice(0, 120);
|
|
460
|
+
row.appendChild(setText(document.createElement('td'), claim));
|
|
461
|
+
return row;
|
|
462
|
+
}}));
|
|
385
463
|
}}
|
|
386
464
|
|
|
387
465
|
document.addEventListener('click', event => {{
|
|
@@ -402,12 +480,46 @@ def dashboard_html(token: str = "") -> str:
|
|
|
402
480
|
</html>"""
|
|
403
481
|
|
|
404
482
|
|
|
405
|
-
|
|
483
|
+
if TYPE_CHECKING:
|
|
484
|
+
_MixinBase = ThreadingHTTPServer
|
|
485
|
+
else:
|
|
486
|
+
_MixinBase = object
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
class _LoopbackBindMixin(_MixinBase):
|
|
490
|
+
# HTTPServer.server_bind resolves socket.getfqdn(), which can hang for
|
|
491
|
+
# tens of seconds on hosts with slow reverse DNS (seen on macOS CI
|
|
492
|
+
# runners). The dashboard serves loopback only, so skip the lookup.
|
|
493
|
+
def server_bind(self) -> None:
|
|
494
|
+
socketserver.TCPServer.server_bind(self)
|
|
495
|
+
host, port = self.server_address[:2]
|
|
496
|
+
self.server_name = str(host)
|
|
497
|
+
self.server_port = int(port)
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class DashboardHTTPServer(_LoopbackBindMixin, ThreadingHTTPServer):
|
|
501
|
+
allow_reuse_address = True
|
|
502
|
+
daemon_threads = True
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def run_dashboard(
|
|
506
|
+
project_root: Path,
|
|
507
|
+
host: str = "127.0.0.1",
|
|
508
|
+
port: int = 8765,
|
|
509
|
+
*,
|
|
510
|
+
server_factory: type[ThreadingHTTPServer] | None = None,
|
|
511
|
+
) -> None:
|
|
406
512
|
dashboard_token = secrets.token_urlsafe(24)
|
|
407
513
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
514
|
+
if server_factory is None:
|
|
515
|
+
# Derive the default at call time from the module-global
|
|
516
|
+
# ThreadingHTTPServer so tests that monkeypatch it still take effect;
|
|
517
|
+
# real use gets the loopback bind that skips the getfqdn hang.
|
|
518
|
+
server_factory = type(
|
|
519
|
+
"DashboardHTTPServer",
|
|
520
|
+
(_LoopbackBindMixin, ThreadingHTTPServer),
|
|
521
|
+
{"allow_reuse_address": True, "daemon_threads": True},
|
|
522
|
+
)
|
|
411
523
|
|
|
412
524
|
class Handler(BaseHTTPRequestHandler):
|
|
413
525
|
def do_GET(self): # noqa: N802
|
|
@@ -483,5 +595,5 @@ def run_dashboard(project_root: Path, host: str = "127.0.0.1", port: int = 8765)
|
|
|
483
595
|
def log_message(self, format, *args): # noqa: A002
|
|
484
596
|
return
|
|
485
597
|
|
|
486
|
-
server =
|
|
598
|
+
server = server_factory((host, port), Handler)
|
|
487
599
|
server.serve_forever()
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Atomic file-write helpers.
|
|
2
|
+
|
|
3
|
+
State files (checkpoints, handoff manifests, semantic indexes, OKF bundles)
|
|
4
|
+
must never be left half-written by a crash or SIGKILL mid-write. These helpers
|
|
5
|
+
write to a temp file in the same directory and ``os.replace`` it into place,
|
|
6
|
+
which is atomic on POSIX and Windows (same volume).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Union
|
|
16
|
+
|
|
17
|
+
PathLike = Union[str, Path]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def atomic_write_text(path: PathLike, data: str, *, encoding: str = "utf-8") -> None:
|
|
21
|
+
"""Atomically replace ``path`` with ``data``. Parent dir must exist."""
|
|
22
|
+
target = Path(path)
|
|
23
|
+
fd, tmp_name = tempfile.mkstemp(dir=str(target.parent), prefix=f".{target.name}.", suffix=".tmp")
|
|
24
|
+
try:
|
|
25
|
+
with os.fdopen(fd, "w", encoding=encoding, newline="") as handle:
|
|
26
|
+
handle.write(data)
|
|
27
|
+
handle.flush()
|
|
28
|
+
os.fsync(handle.fileno())
|
|
29
|
+
os.replace(tmp_name, target)
|
|
30
|
+
except BaseException:
|
|
31
|
+
try:
|
|
32
|
+
os.unlink(tmp_name)
|
|
33
|
+
except OSError:
|
|
34
|
+
pass
|
|
35
|
+
raise
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def atomic_write_bytes(path: PathLike, data: bytes) -> None:
|
|
39
|
+
"""Atomically replace ``path`` with binary ``data``. Parent dir must exist."""
|
|
40
|
+
target = Path(path)
|
|
41
|
+
fd, tmp_name = tempfile.mkstemp(dir=str(target.parent), prefix=f".{target.name}.", suffix=".tmp")
|
|
42
|
+
try:
|
|
43
|
+
with os.fdopen(fd, "wb") as handle:
|
|
44
|
+
handle.write(data)
|
|
45
|
+
handle.flush()
|
|
46
|
+
os.fsync(handle.fileno())
|
|
47
|
+
os.replace(tmp_name, target)
|
|
48
|
+
except BaseException:
|
|
49
|
+
try:
|
|
50
|
+
os.unlink(tmp_name)
|
|
51
|
+
except OSError:
|
|
52
|
+
pass
|
|
53
|
+
raise
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def atomic_write_json(path: PathLike, payload: Any, *, indent: int = 2, sort_keys: bool = False) -> None:
|
|
57
|
+
"""Serialize ``payload`` to JSON and atomically write it to ``path``."""
|
|
58
|
+
atomic_write_text(path, json.dumps(payload, indent=indent, sort_keys=sort_keys, default=str) + "\n")
|