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.
Files changed (366) hide show
  1. package/README.md +92 -33
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +56 -40
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +964 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +325 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/summary.py +21 -3
  255. package/src/devcouncil/live/tasks.py +12 -3
  256. package/src/devcouncil/live/transcripts.py +69 -2
  257. package/src/devcouncil/llm/cache.py +5 -6
  258. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  259. package/src/devcouncil/llm/provider.py +647 -73
  260. package/src/devcouncil/llm/router.py +271 -46
  261. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  262. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  263. package/src/devcouncil/optimization/skillopt.py +9 -5
  264. package/src/devcouncil/planning/arbiter_service.py +12 -3
  265. package/src/devcouncil/planning/correction_manifest.py +107 -10
  266. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  267. package/src/devcouncil/planning/plan_service.py +5 -2
  268. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  269. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  270. package/src/devcouncil/planning/question_conversion.py +56 -0
  271. package/src/devcouncil/planning/spec_service.py +9 -3
  272. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  273. package/src/devcouncil/repo/gitignore.py +1 -2
  274. package/src/devcouncil/reporting/evidence_export.py +124 -0
  275. package/src/devcouncil/reporting/evidence_html.py +210 -0
  276. package/src/devcouncil/reporting/json_report.py +16 -12
  277. package/src/devcouncil/reporting/markdown_report.py +38 -9
  278. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  279. package/src/devcouncil/reporting/report_builder.py +40 -4
  280. package/src/devcouncil/reporting/task_provenance.py +42 -0
  281. package/src/devcouncil/reporting/verdict.py +75 -0
  282. package/src/devcouncil/skills/library/README.md +1 -0
  283. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  285. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  286. package/src/devcouncil/skills/registry.py +43 -12
  287. package/src/devcouncil/storage/db.py +57 -11
  288. package/src/devcouncil/storage/models.py +6 -0
  289. package/src/devcouncil/storage/native.py +5 -3
  290. package/src/devcouncil/storage/repositories.py +50 -18
  291. package/src/devcouncil/telemetry/context.py +28 -0
  292. package/src/devcouncil/telemetry/cost.py +4 -5
  293. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  294. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  295. package/src/devcouncil/telemetry/stages.py +27 -2
  296. package/src/devcouncil/telemetry/tracker.py +50 -13
  297. package/src/devcouncil/ui/dashboard.py +120 -8
  298. package/src/devcouncil/utils/fsio.py +58 -0
  299. package/src/devcouncil/utils/git_snapshot.py +112 -0
  300. package/src/devcouncil/utils/json_persist.py +53 -0
  301. package/src/devcouncil/utils/proc.py +89 -0
  302. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  303. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  304. package/src/devcouncil/verification/checks/__init__.py +41 -0
  305. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  306. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  307. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  308. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  309. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  310. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  311. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  312. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  313. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  314. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  315. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  316. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  317. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  318. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  319. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  320. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  321. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  322. package/src/devcouncil/verification/checks/wiring.py +216 -0
  323. package/src/devcouncil/verification/claims/__init__.py +23 -0
  324. package/src/devcouncil/verification/claims/checks.py +395 -0
  325. package/src/devcouncil/verification/claims/mapper.py +168 -0
  326. package/src/devcouncil/verification/claims/models.py +39 -0
  327. package/src/devcouncil/verification/claims/transcript.py +92 -0
  328. package/src/devcouncil/verification/claims/verdict.py +88 -0
  329. package/src/devcouncil/verification/command_evidence.py +170 -0
  330. package/src/devcouncil/verification/command_malformation.py +147 -0
  331. package/src/devcouncil/verification/command_runner.py +164 -0
  332. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  333. package/src/devcouncil/verification/diff_coverage.py +151 -0
  334. package/src/devcouncil/verification/difficulty.py +296 -0
  335. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  336. package/src/devcouncil/verification/gap_ids.py +63 -0
  337. package/src/devcouncil/verification/gate_cache.py +194 -0
  338. package/src/devcouncil/verification/gate_selector.py +344 -0
  339. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  340. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  341. package/src/devcouncil/verification/incremental_check.py +241 -0
  342. package/src/devcouncil/verification/next_actions.py +60 -1
  343. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  344. package/src/devcouncil/verification/sandbox.py +38 -11
  345. package/src/devcouncil/verification/stub_detector.py +369 -0
  346. package/src/devcouncil/verification/test_resolver.py +67 -1
  347. package/src/devcouncil/verification/verifier.py +137 -1666
  348. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  349. package/src/devcouncil/verification/verify_setup.py +176 -0
  350. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  351. package/src/semantic_layer/__init__.py +58 -0
  352. package/src/semantic_layer/benchmark.py +75 -0
  353. package/src/semantic_layer/cache.py +290 -0
  354. package/src/semantic_layer/compressor.py +137 -0
  355. package/src/semantic_layer/config.py +75 -0
  356. package/src/semantic_layer/embeddings.py +69 -0
  357. package/src/semantic_layer/llm_backends.py +99 -0
  358. package/src/semantic_layer/pipeline.py +111 -0
  359. package/src/semantic_layer/router.py +128 -0
  360. package/src/semantic_layer/tuner.py +72 -0
  361. package/uv.lock +973 -9
  362. package/src/devcouncil/artifacts/migrations.py +0 -20
  363. package/src/devcouncil/artifacts/schemas.py +0 -23
  364. package/src/devcouncil/artifacts/serializer.py +0 -21
  365. package/src/devcouncil/integrations/gitnexus.py +0 -70
  366. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -1,7 +1,9 @@
1
+ import logging
1
2
  import typer
2
3
  import subprocess
3
4
  import os
4
5
  import shutil
6
+ import sys
5
7
  from pathlib import Path
6
8
  from rich.console import Console
7
9
  from rich.table import Table
@@ -11,13 +13,18 @@ from devcouncil.executors.agent_registry import (
11
13
  CODING_CLI_INTEGRATION_INFO,
12
14
  CODING_CLI_PROBE_ORDER,
13
15
  CODING_CLI_VERSION_COMMANDS,
16
+ DEPRECATED_CODING_CLIS,
17
+ GEMINI_DEPRECATION_MESSAGE,
14
18
  detect_available_coding_cli,
15
19
  resolve_automated_executor,
16
20
  )
17
21
  from devcouncil.llm.provider import SUPPORTED_MODEL_PROVIDERS, validate_model_provider
22
+ from devcouncil.telemetry.stages import log_stage, log_step
18
23
 
19
24
  app = typer.Typer()
25
+
20
26
  console = Console()
27
+ logger = logging.getLogger(__name__)
21
28
 
22
29
 
23
30
  def _probe_ollama(base_url: str) -> tuple[bool, str]:
@@ -94,8 +101,8 @@ def _knowledge_dir(project_root: Path, config=None) -> str:
94
101
  try:
95
102
  cfg = config if config is not None else load_config(project_root)
96
103
  directory = cfg.knowledge.directory
97
- except Exception:
98
- pass
104
+ except Exception as e:
105
+ logger.debug("Failed to load knowledge directory from config, using default: %s", e)
99
106
  return directory
100
107
 
101
108
 
@@ -208,12 +215,663 @@ def check_ingested_knowledge(project_root: Path, config=None) -> list[tuple[str,
208
215
  return rows
209
216
 
210
217
 
218
+ # Explicit map from an area row in docs/project-status.md to the tests/unit/<subsystem>/
219
+ # directory expected to back a "Stable" claim. Deliberately small and in-code (see
220
+ # IMPROVEMENTS.md: doc/status drift check) — extend it as areas graduate to Stable.
221
+ STATUS_DOC_UNIT_TEST_DIRS: tuple[tuple[str, str], ...] = (
222
+ ("CLI & Storage", "storage"),
223
+ ("Artifact Graph", "artifacts"),
224
+ ("Council Debate", "council"),
225
+ ("Diff↔Coverage Gate", "verification"),
226
+ ("Cost & Run Telemetry", "telemetry"),
227
+ ("Security Scanning", "gating"),
228
+ ("Manual Executor", "executors"),
229
+ ("Lite Check (`dev check --verify`)", "execution"),
230
+ ("Next-Actions Contract", "reporting"),
231
+ ("Repo Map & Code Graph", "indexing"),
232
+ ("LSP / AST Indexing", "indexing"),
233
+ ("Live Dashboard", "dashboard"),
234
+ )
235
+
236
+ # Flat tests/unit/test_*.py prefixes that back a subsystem when no per-subsystem dir exists.
237
+ STATUS_DOC_FLAT_TEST_PREFIXES: dict[str, tuple[str, ...]] = {
238
+ "artifacts": ("test_artifact_graph",),
239
+ "council": ("test_orchestrator", "test_state_machine"),
240
+ "verification": ("test_verification", "test_verifier", "test_diff_coverage"),
241
+ "telemetry": ("test_telemetry",),
242
+ "executors": ("test_executors", "test_executor_", "test_claude_sdk_executor"),
243
+ "execution": ("test_execution", "test_go_", "test_cli_check", "test_ad_hoc_check"),
244
+ "reporting": ("test_json_report", "test_export_command", "test_pr_comments"),
245
+ "dashboard": ("test_dashboard",),
246
+ "indexing": (
247
+ "test_indexing",
248
+ "test_repo_mapper",
249
+ "test_repo_map",
250
+ "test_map_",
251
+ "test_graph_",
252
+ "test_graph_html",
253
+ "test_graph_dead",
254
+ "test_graph_cmd",
255
+ "test_graph_intel",
256
+ "test_graph_query",
257
+ "test_graph_incremental",
258
+ "test_graph_schema",
259
+ ),
260
+ }
261
+
262
+
263
+ def _subsystem_has_unit_tests(unit_root: Path, subsystem: str) -> bool:
264
+ """True when tests/unit/<subsystem>/ or mapped flat test files exist."""
265
+ tests_dir = unit_root / subsystem
266
+ try:
267
+ if tests_dir.is_dir() and any(tests_dir.rglob("test_*.py")):
268
+ return True
269
+ if any(unit_root.glob(f"test_{subsystem}*.py")):
270
+ return True
271
+ for prefix in STATUS_DOC_FLAT_TEST_PREFIXES.get(subsystem, ()):
272
+ if any(unit_root.glob(f"{prefix}*.py")):
273
+ return True
274
+ except Exception:
275
+ return False
276
+ return False
277
+
278
+
279
+ def _parse_status_doc_areas(status_doc: Path) -> dict[str, str]:
280
+ """Parse the docs/project-status.md maturity table into {area: status-cell text}.
281
+
282
+ Rows look like ``| **CLI & Storage** | Stable: SQLite + SQLModel, ... |``. The
283
+ header row and the ``| :--- |`` separator row are skipped. Best-effort by design;
284
+ the caller wraps this in try/except so a malformed doc never crashes doctor.
285
+ """
286
+ areas: dict[str, str] = {}
287
+ for line in status_doc.read_text(encoding="utf-8").splitlines():
288
+ stripped = line.strip()
289
+ if not stripped.startswith("|"):
290
+ continue
291
+ cells = [cell.strip() for cell in stripped.strip("|").split("|")]
292
+ if len(cells) < 2:
293
+ continue
294
+ area = cells[0].strip().strip("*").strip()
295
+ if not area or area.lower() == "area" or set(area) <= {":", "-", " "}:
296
+ continue
297
+ areas[area] = cells[1]
298
+ return areas
299
+
300
+
301
+ def check_liveness_reliability(project_root: Path) -> list[tuple[str, str, str]]:
302
+ """Warn when map liveness used empty/unreliable entry roots."""
303
+ ok = "[green]OK[/green]"
304
+ warn = "[yellow]WARN[/yellow]"
305
+ rows: list[tuple[str, str, str]] = []
306
+ try:
307
+ from devcouncil.utils.json_persist import read_json
308
+
309
+ map_path = project_root / ".devcouncil" / "repo_map.json"
310
+ if not map_path.is_file():
311
+ return rows
312
+ data = read_json(map_path)
313
+ payload = data if isinstance(data, dict) else {}
314
+ unreliable = bool(payload.get("liveness_unreachable_unreliable"))
315
+ roots = payload.get("entry_roots") or []
316
+ if unreliable or not roots:
317
+ rows.append((
318
+ "Map liveness",
319
+ warn,
320
+ "Production entry roots are empty or unreachable BFS was skipped. "
321
+ "Add `indexing.entry_roots` in `.devcouncil/config.yaml` (repo-relative "
322
+ "paths) and run `dev map`.",
323
+ ))
324
+ else:
325
+ rows.append((
326
+ "Map liveness",
327
+ ok,
328
+ f"{len(roots)} production entry root(s); unreachable lists are reliable.",
329
+ ))
330
+ except Exception:
331
+ logger.debug("check_liveness_reliability failed", exc_info=True)
332
+ return rows
333
+
334
+
335
+ def _repo_languages(project_root: Path) -> set[str]:
336
+ """Repo languages from repo_map.json, else a bounded filesystem sniff."""
337
+ try:
338
+ from devcouncil.utils.json_persist import read_json
339
+
340
+ map_path = project_root / ".devcouncil" / "repo_map.json"
341
+ if map_path.is_file():
342
+ data = read_json(map_path)
343
+ langs = (data or {}).get("languages") if isinstance(data, dict) else None
344
+ if isinstance(langs, list) and langs:
345
+ return {str(lang) for lang in langs}
346
+ except Exception:
347
+ logger.debug("repo language read from map failed", exc_info=True)
348
+ try:
349
+ from devcouncil.indexing.lsp import LspInspector
350
+
351
+ return set(LspInspector(project_root).detect_languages())
352
+ except Exception:
353
+ logger.debug("repo language detection failed", exc_info=True)
354
+ return set()
355
+
356
+
357
+ def check_grammar_coverage(project_root: Path) -> list[tuple[str, str, str]]:
358
+ """Warn when tree-sitter grammars for the repo's own languages are missing.
359
+
360
+ A missing grammar silently degrades extraction for that language (a
361
+ Python-heavy repo indexed without the Python grammar loses most symbols),
362
+ so surface it with the install action instead of only in graph doctor.
363
+ """
364
+ ok = "[green]OK[/green]"
365
+ warn = "[yellow]WARN[/yellow]"
366
+ rows: list[tuple[str, str, str]] = []
367
+ try:
368
+ from devcouncil.codeintel.languages import grammar_status
369
+
370
+ status = grammar_status()
371
+ # repo_map languages are lowercase; LANGUAGE_SPECS names are display-cased
372
+ # ("Python") — compare casefolded or nothing ever matches.
373
+ repo_langs = {lang.casefold() for lang in _repo_languages(project_root)}
374
+ if not repo_langs:
375
+ return rows
376
+ missing = sorted(
377
+ row["language"]
378
+ for row in status.get("languages", [])
379
+ if str(row.get("language", "")).casefold() in repo_langs
380
+ and row.get("missing_grammars")
381
+ # Python extraction is native stdlib-ast (cache._grammar_identity);
382
+ # a missing python tree-sitter grammar does not degrade indexing.
383
+ and str(row.get("grammar", "")) != "python"
384
+ )
385
+ if missing:
386
+ action = status.get("action") or (
387
+ "Install the platform-matched devcouncil-codeintel-grammars wheel."
388
+ )
389
+ rows.append((
390
+ "Grammar coverage",
391
+ warn,
392
+ f"Missing tree-sitter grammars for repo language(s): {', '.join(missing)}. "
393
+ f"{action}",
394
+ ))
395
+ else:
396
+ rows.append((
397
+ "Grammar coverage",
398
+ ok,
399
+ f"Grammars available for all repo languages ({', '.join(sorted(repo_langs))}).",
400
+ ))
401
+ except Exception:
402
+ logger.debug("check_grammar_coverage failed", exc_info=True)
403
+ return rows
404
+
405
+
406
+ def check_lsp_reference_confirmation(project_root: Path) -> list[tuple[str, str, str]]:
407
+ """Report repo languages with no language server on PATH.
408
+
409
+ Without a server, ``--lsp-refs`` dead-symbol confirmation cannot run for
410
+ that language; WARN when ``indexing.lsp_refs`` is enabled, informational
411
+ otherwise.
412
+ """
413
+ ok = "[green]OK[/green]"
414
+ warn = "[yellow]WARN[/yellow]"
415
+ rows: list[tuple[str, str, str]] = []
416
+ try:
417
+ from devcouncil.indexing.lsp import LspInspector
418
+
419
+ candidates = LspInspector(project_root).server_candidates()
420
+ if not candidates:
421
+ return rows
422
+ available_langs = {c.language for c in candidates if c.available}
423
+ missing = sorted({c.language for c in candidates} - available_langs)
424
+ lsp_refs_enabled = False
425
+ try:
426
+ from devcouncil.app.config import load_config
427
+
428
+ lsp_refs_enabled = bool(load_config(project_root).indexing.lsp_refs)
429
+ except Exception:
430
+ logger.debug("lsp_refs config read failed", exc_info=True)
431
+ if not missing:
432
+ rows.append((
433
+ "LSP servers",
434
+ ok,
435
+ f"Language server on PATH for: {', '.join(sorted(available_langs))}.",
436
+ ))
437
+ elif lsp_refs_enabled:
438
+ rows.append((
439
+ "LSP servers",
440
+ warn,
441
+ "indexing.lsp_refs is enabled but no language server is on PATH for: "
442
+ f"{', '.join(missing)} — dead-symbol confirmation is skipped there.",
443
+ ))
444
+ else:
445
+ rows.append((
446
+ "LSP servers",
447
+ ok,
448
+ f"No server on PATH for: {', '.join(missing)} (detection only; install "
449
+ "one to enable `dev map --lsp-refs` confirmation).",
450
+ ))
451
+ except Exception:
452
+ logger.debug("check_lsp_reference_confirmation failed", exc_info=True)
453
+ return rows
454
+
455
+
456
+ def check_unknown_indexing_keys(project_root: Path) -> list[tuple[str, str, str]]:
457
+ """Warn on unknown ``indexing.*`` keys in config.yaml (typos/removed options)."""
458
+ warn = "[yellow]WARN[/yellow]"
459
+ rows: list[tuple[str, str, str]] = []
460
+ try:
461
+ import yaml
462
+
463
+ cfg_path = project_root / ".devcouncil" / "config.yaml"
464
+ if not cfg_path.is_file():
465
+ return rows
466
+ payload = yaml.safe_load(cfg_path.read_text(encoding="utf-8")) or {}
467
+ indexing = payload.get("indexing")
468
+ if not isinstance(indexing, dict):
469
+ return rows
470
+ from devcouncil.app.config import IndexingConfig
471
+
472
+ unknown = sorted(str(k) for k in indexing if str(k) not in IndexingConfig.model_fields)
473
+ if unknown:
474
+ rows.append((
475
+ "Config keys",
476
+ warn,
477
+ f"Unknown `indexing.*` key(s) ignored: {', '.join(unknown)} — "
478
+ "typo or an option removed in this version.",
479
+ ))
480
+ except Exception:
481
+ logger.debug("check_unknown_indexing_keys failed", exc_info=True)
482
+ return rows
483
+
484
+
485
+ def check_mapping_stack(project_root: Path) -> list[tuple[str, str, str]]:
486
+ """Doctor rows for repo map + code graph freshness (native mapping stack)."""
487
+ rows: list[tuple[str, str, str]] = []
488
+ ok = "[green]OK[/green]"
489
+ warn = "[yellow]WARN[/yellow]"
490
+ graphify_legacy = project_root / ".devcouncil" / "graphify.yaml"
491
+ if graphify_legacy.is_file():
492
+ rows.append((
493
+ "Legacy graphify.yaml",
494
+ warn,
495
+ "``.devcouncil/graphify.yaml`` is deprecated. Move ``corpus:`` settings into "
496
+ "``.devcouncil/config.yaml`` under ``indexing.corpus`` and remove the file.",
497
+ ))
498
+ rows.extend(check_repo_map_freshness(project_root))
499
+ rows.extend(check_liveness_reliability(project_root))
500
+ rows.extend(check_grammar_coverage(project_root))
501
+ rows.extend(check_lsp_reference_confirmation(project_root))
502
+ rows.extend(check_unknown_indexing_keys(project_root))
503
+ graph_path = project_root / ".devcouncil" / "graph" / "code_graph.json"
504
+ if not graph_path.is_file():
505
+ store_has_graph = False
506
+ try:
507
+ from devcouncil.indexing.graph.build import load_code_graph
508
+
509
+ store_has_graph = load_code_graph(project_root) is not None
510
+ except Exception:
511
+ logger.debug("store probe for missing graph JSON failed", exc_info=True)
512
+ if store_has_graph:
513
+ rows.append((
514
+ "Code graph",
515
+ warn,
516
+ "JSON export ``.devcouncil/graph/code_graph.json`` is missing but the "
517
+ "SQLite store has a graph. Run ``dev map`` to re-export it.",
518
+ ))
519
+ else:
520
+ rows.append((
521
+ "Code graph",
522
+ warn,
523
+ "Missing ``.devcouncil/graph/code_graph.json``. Run ``dev map`` or ``dev graph ingest``.",
524
+ ))
525
+ else:
526
+ try:
527
+ from devcouncil.indexing.graph.build import load_code_graph
528
+
529
+ if load_code_graph(project_root) is None:
530
+ rows.append(("Code graph", warn, "Unreadable or empty code graph export."))
531
+ else:
532
+ rows.append(("Code graph", ok, "Present and loadable."))
533
+ except Exception:
534
+ rows.append(("Code graph", warn, "Could not load code graph export."))
535
+ return rows
536
+
537
+
538
+ def check_repo_map_freshness(project_root: Path) -> list[tuple[str, str, str]]:
539
+ """Doctor rows for ``.devcouncil/repo_map.json`` fingerprint freshness."""
540
+ try:
541
+ from devcouncil.indexing.repo_mapper import RepoMapper
542
+ from devcouncil.utils.json_persist import read_json
543
+
544
+ map_path = project_root / ".devcouncil" / "repo_map.json"
545
+ ok = "[green]OK[/green]"
546
+ warn = "[yellow]Stale[/yellow]"
547
+ if not map_path.is_file():
548
+ return [(
549
+ "Repo map",
550
+ warn,
551
+ "Missing `.devcouncil/repo_map.json`. Run `dev map` before verify or checkout.",
552
+ )]
553
+ loaded = read_json(map_path)
554
+ data = loaded if isinstance(loaded, dict) else {}
555
+ mapper = RepoMapper(project_root)
556
+ if mapper.map_is_stale(data):
557
+ head = str(data.get("generated_head") or "(unknown)")[:12]
558
+ current = mapper._git_head()[:12]
559
+ if head == current and head != "(unknown)":
560
+ detail = (
561
+ f"Working-tree content changed since the last map "
562
+ f"(git HEAD {current}). Run `dev map` or rely on auto-refresh "
563
+ f"at checkout/verify."
564
+ )
565
+ else:
566
+ detail = (
567
+ f"Behind current code (map HEAD {head}, repo HEAD {current}). "
568
+ "Run `dev map` or rely on auto-refresh at checkout/verify."
569
+ )
570
+ return [(
571
+ "Repo map",
572
+ warn,
573
+ detail,
574
+ )]
575
+ return [("Repo map", ok, "Fresh — fingerprints match the current repository.")]
576
+ except Exception:
577
+ return []
578
+
579
+
580
+ def check_execution_containment(project_root: Path, config=None) -> list[tuple[str, str, str]]:
581
+ """Doctor rows for YOLO/containment posture (scope gate, write hooks, risky profiles)."""
582
+ try:
583
+ from devcouncil.app.config import load_config
584
+ from devcouncil.executors.agent_registry import load_agent_profiles
585
+
586
+ cfg = config if config is not None else load_config(project_root)
587
+ rows: list[tuple[str, str, str]] = []
588
+ ok = "[green]OK[/green]"
589
+ warn = "[yellow]Risky[/yellow]"
590
+
591
+ if cfg.execution.enforce_file_scope_pre_verify:
592
+ rows.append((
593
+ "Pre-verify scope gate",
594
+ ok,
595
+ "execution.enforce_file_scope_pre_verify is enabled — OOS CLI writes are reverted before verify.",
596
+ ))
597
+ else:
598
+ rows.append((
599
+ "Pre-verify scope gate",
600
+ warn,
601
+ "Off by default. Enable execution.enforce_file_scope_pre_verify for battle-test containment.",
602
+ ))
603
+
604
+ write_gate = bool(getattr(cfg.integrations.claude, "write_gate", False))
605
+ if write_gate:
606
+ rows.append((
607
+ "Claude write-gate",
608
+ ok,
609
+ "integrations.claude.write_gate is enabled. Run `dev integrate hooks --apply` if hooks are missing.",
610
+ ))
611
+ else:
612
+ rows.append((
613
+ "Claude write-gate",
614
+ warn,
615
+ "Disabled. Run `dev integrate claude --apply --write-gate` for PreToolUse containment.",
616
+ ))
617
+
618
+ for name, profile in load_agent_profiles(project_root).items():
619
+ mode = (profile.permission_mode or "").strip().lower()
620
+ if mode == "bypasspermissions":
621
+ rows.append((
622
+ f"Profile {name}",
623
+ warn,
624
+ "permission_mode bypassPermissions bypasses all CLI edit gates.",
625
+ ))
626
+ extra = profile.extra_args or []
627
+ for index, arg in enumerate(extra):
628
+ if arg == "--permission-mode" and index + 1 < len(extra):
629
+ if str(extra[index + 1]).lower() == "bypasspermissions":
630
+ rows.append((
631
+ f"Profile {name}",
632
+ warn,
633
+ "extra_args include --permission-mode bypassPermissions.",
634
+ ))
635
+ return rows
636
+ except Exception:
637
+ return []
638
+
639
+
640
+ def check_local_monitor_sampling(project_root: Path, config=None) -> list[tuple[str, str, str]]:
641
+ """Doctor rows for local-monitor verification safety.
642
+
643
+ Calibration probes (2026-07-03, benchmarks/results/local_monitor_*) showed a
644
+ local monitor with single-shot acceptance checks rubber-stamping real defects,
645
+ while samples>=3 + per-criterion compilation caught 6/6 with zero false passes.
646
+ Auto-resolution picks the safe local settings; these rows surface EXPLICIT
647
+ overrides that disable them — at setup time, where users actually look, rather
648
+ than only as mid-run log warnings. Cloud monitors produce no rows (single-shot
649
+ is their intended default). Never raises.
650
+ """
651
+ try:
652
+ from devcouncil.app.config import load_config, role_runs_on_local_provider
653
+
654
+ cfg = config if config is not None else load_config(project_root)
655
+ rows: list[tuple[str, str, str]] = []
656
+ warn = "[yellow]Risky[/yellow]"
657
+
658
+ local_monitor = role_runs_on_local_provider(cfg, "implementation_reviewer")
659
+ for message in cfg.verification.acceptance_checks.unsafe_override_warnings(local_monitor):
660
+ rows.append(("Local monitor (acceptance checks)", warn, message))
661
+ local_reviewer = role_runs_on_local_provider(cfg, "live_reviewer")
662
+ for message in cfg.verification.reviewer_checks.unsafe_override_warnings(local_reviewer):
663
+ rows.append(("Local reviewer (live review)", warn, message))
664
+
665
+ if not rows and (local_monitor or local_reviewer):
666
+ samples, repairs, per_criterion = cfg.verification.acceptance_checks.resolved(local_monitor)
667
+ votes = cfg.verification.reviewer_checks.resolved(local_reviewer)
668
+ rows.append((
669
+ "Local monitor ensembling",
670
+ "[green]OK[/green]",
671
+ f"Acceptance checks: samples={samples}, repair_attempts={repairs}, "
672
+ f"per_criterion={per_criterion}; reviewer votes={votes}.",
673
+ ))
674
+ return rows
675
+ except Exception:
676
+ return [] # config problems already surface via other doctor rows
677
+
678
+
679
+ def check_coverage_floor(project_root: Path) -> list[tuple[str, str, str]]:
680
+ """Doctor row for ``[tool.coverage.report] fail_under`` in pyproject.toml."""
681
+ ok = "[green]OK[/green]"
682
+ warn = "[yellow]WARN[/yellow]"
683
+ pyproject = project_root / "pyproject.toml"
684
+ if not pyproject.is_file():
685
+ return [("Coverage floor", warn, "No pyproject.toml; cannot verify fail_under.")]
686
+ try:
687
+ import tomllib
688
+
689
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
690
+ fail_under = (
691
+ data.get("tool", {})
692
+ .get("coverage", {})
693
+ .get("report", {})
694
+ .get("fail_under")
695
+ )
696
+ except Exception as exc:
697
+ return [("Coverage floor", warn, f"Could not read pyproject.toml: {exc}.")]
698
+ if fail_under is None:
699
+ return [
700
+ (
701
+ "Coverage floor",
702
+ warn,
703
+ "No [tool.coverage.report] fail_under in pyproject.toml; CI will not enforce a coverage minimum.",
704
+ )
705
+ ]
706
+ return [
707
+ (
708
+ "Coverage floor",
709
+ ok,
710
+ f"fail_under={fail_under} configured in pyproject.toml (enforced by `coverage report`).",
711
+ )
712
+ ]
713
+
714
+
715
+ def _mypy_command(project_root: Path) -> list[str]:
716
+ """Resolve mypy without making a doctor check depend on network access."""
717
+ project_mypy = project_root / ".venv" / "bin" / "mypy"
718
+ if project_mypy.is_file():
719
+ return [str(project_mypy), "src"]
720
+ if shutil.which("uv"):
721
+ return ["uv", "run", "--python", "3.12", "mypy", "src"]
722
+ return [sys.executable, "-m", "mypy", "src"]
723
+
724
+
725
+ def check_mypy_status(project_root: Path) -> list[tuple[str, str, str]]:
726
+ """Doctor row summarizing canonical mypy health for this repository."""
727
+ ok = "[green]OK[/green]"
728
+ warn = "[yellow]WARN[/yellow]"
729
+ pyproject = project_root / "pyproject.toml"
730
+ if not pyproject.is_file():
731
+ return [("mypy green", warn, "No pyproject.toml; skipping mypy probe.")]
732
+ src_root = project_root / "src"
733
+ if not src_root.is_dir():
734
+ return [("mypy green", warn, "No src/ directory; skipping mypy probe.")]
735
+ command = _mypy_command(project_root)
736
+ command_text = subprocess.list2cmdline(command)
737
+ try:
738
+ proc = subprocess.run(
739
+ command,
740
+ cwd=project_root,
741
+ capture_output=True,
742
+ text=True,
743
+ encoding="utf-8",
744
+ errors="replace",
745
+ timeout=120,
746
+ )
747
+ except FileNotFoundError:
748
+ return [("mypy green", warn, f"{command_text} is unavailable; install uv and development dependencies.")]
749
+ except subprocess.TimeoutExpired:
750
+ return [("mypy green", warn, f"{command_text} timed out after 120s.")]
751
+ except Exception as exc:
752
+ return [("mypy green", warn, f"{command_text} probe failed: {exc}.")]
753
+
754
+ output = (proc.stdout or "") + (proc.stderr or "")
755
+ if "No module named mypy" in output or "No module named 'mypy'" in output:
756
+ return [
757
+ (
758
+ "mypy green",
759
+ warn,
760
+ f"{command_text} is unavailable; install uv and development dependencies.",
761
+ )
762
+ ]
763
+ if "INTERNAL ERROR" in output:
764
+ return [
765
+ (
766
+ "mypy green",
767
+ warn,
768
+ f"{command_text} crashed with INTERNAL ERROR; pin or upgrade mypy in dev dependencies.",
769
+ )
770
+ ]
771
+ if proc.returncode == 0:
772
+ return [("mypy green", ok, f"{command_text} passed with zero errors.")]
773
+ error_count = output.count(": error:")
774
+ preview = output.strip().splitlines()[-1] if output.strip() else "mypy failed"
775
+ return [
776
+ (
777
+ "mypy green",
778
+ warn,
779
+ f"{command_text} reported {error_count} error(s). Last line: {preview}",
780
+ )
781
+ ]
782
+
783
+
784
+ def check_status_doc_drift(project_root: Path) -> list[tuple[str, str, str]]:
785
+ """Doctor rows verifying docs/project-status.md "Stable" claims against tests/unit/.
786
+
787
+ For each mapped area (``STATUS_DOC_UNIT_TEST_DIRS``) whose status-table row claims
788
+ Stable, require a non-empty ``tests/unit/<subsystem>/`` directory (at least one
789
+ ``test_*.py``) to back the claim; mismatches become ``WARN`` rows so the status doc
790
+ cannot drift ahead of the test suite unnoticed. A mapped area missing from the doc
791
+ is also reported — that means the in-code mapping went stale.
792
+
793
+ Never raises: a missing status doc yields a neutral ``INFO`` row (most projects that
794
+ embed DevCouncil have no such doc), and any parse failure becomes a ``WARN`` row.
795
+ """
796
+ ok = "[green]OK[/green]"
797
+ warn = "[yellow]WARN[/yellow]"
798
+ info = "[cyan]INFO[/cyan]"
799
+
800
+ status_doc = project_root / "docs" / "project-status.md"
801
+ if not status_doc.is_file():
802
+ return [
803
+ (
804
+ "Status-doc drift",
805
+ info,
806
+ "No docs/project-status.md in this project; skipping status-vs-tests drift check.",
807
+ )
808
+ ]
809
+
810
+ try:
811
+ areas = _parse_status_doc_areas(status_doc)
812
+ except Exception as exc: # never let a malformed status doc crash doctor
813
+ return [("Status-doc drift", warn, f"Could not parse docs/project-status.md: {exc}.")]
814
+
815
+ mismatches: list[str] = []
816
+ verified = 0
817
+ unit_root = project_root / "tests" / "unit"
818
+ for area, subsystem in STATUS_DOC_UNIT_TEST_DIRS:
819
+ status_text = areas.get(area)
820
+ if status_text is None:
821
+ mismatches.append(
822
+ f"'{area}' is in doctor's drift mapping but not in the status table "
823
+ "(update STATUS_DOC_UNIT_TEST_DIRS)"
824
+ )
825
+ continue
826
+ if not status_text.strip().lower().startswith("stable"):
827
+ continue # only Stable rows claim unit-test-backed maturity
828
+ try:
829
+ has_tests = _subsystem_has_unit_tests(unit_root, subsystem)
830
+ except Exception:
831
+ has_tests = False
832
+ if has_tests:
833
+ verified += 1
834
+ else:
835
+ try:
836
+ flat_matches = any(unit_root.glob(f"test_{subsystem}*.py"))
837
+ except Exception:
838
+ flat_matches = False
839
+ hint = (
840
+ f" (flat tests/unit/test_{subsystem}*.py files exist but no per-subsystem dir)"
841
+ if flat_matches
842
+ else ""
843
+ )
844
+ mismatches.append(
845
+ f"'{area}' claims Stable but tests/unit/{subsystem}/ is missing or empty{hint}"
846
+ )
847
+
848
+ if mismatches:
849
+ preview = "; ".join(mismatches[:5])
850
+ extra = "" if len(mismatches) <= 5 else f" (+{len(mismatches) - 5} more)"
851
+ return [
852
+ (
853
+ "Status-doc drift",
854
+ warn,
855
+ f"{len(mismatches)} mismatch(es) between docs/project-status.md and tests/unit/: "
856
+ f"{preview}{extra}.",
857
+ )
858
+ ]
859
+ return [
860
+ (
861
+ "Status-doc drift",
862
+ ok,
863
+ f"{verified} Stable claim(s) in docs/project-status.md backed by non-empty "
864
+ "tests/unit/<subsystem>/ directories.",
865
+ )
866
+ ]
867
+
868
+
211
869
  def _add_logging_row(table, project_root: Path) -> None:
212
870
  """Append a logging-health row: where the durable run log lives and how big it
213
871
  is, so a user chasing a recurring failure knows exactly where to look."""
214
- from devcouncil.telemetry.logging_setup import LOG_RELATIVE_PATH
872
+ from devcouncil.telemetry.logging_setup import _resolve_log_path
215
873
 
216
- log_path = project_root / LOG_RELATIVE_PATH
874
+ log_path = _resolve_log_path(project_root)
217
875
  if log_path.exists():
218
876
  size_kb = log_path.stat().st_size / 1024
219
877
  detail = f"{log_path} ({size_kb:.0f} KB). View: dev logs tail"
@@ -222,6 +880,60 @@ def _add_logging_row(table, project_root: Path) -> None:
222
880
  table.add_row("logging", "[green]OK[/green]", detail)
223
881
 
224
882
 
883
+ def _subsystem_maturity_rows() -> list[tuple[str, str, str]]:
884
+ """Curated maturity tiers aligned with docs/project-status.md."""
885
+ return [
886
+ ("CLI & Storage", "stable", "SQLite + SQLModel; core workflow"),
887
+ ("Artifact Graph / Coverage", "stable", "Coverage engine and reports"),
888
+ ("Council Debate / Planning", "stable", "Multi-agent planning and critique"),
889
+ ("Manual Executor", "stable", "Sidecar mode"),
890
+ ("Diff↔Coverage Gate", "stable", "Signal-first; opt-in blocking"),
891
+ ("Next-Actions / dev check --verify", "stable", "Deterministic evidence gate"),
892
+ ("Ollama provider", "stable", "Offline planning without API keys"),
893
+ ("Engineering Skills", "stable", "dev skills listing/scaffolding"),
894
+ ("Cost & Run Telemetry", "stable", "dev cost show / dev runs"),
895
+ ("Security Scanning", "stable", "Secret redaction and detection"),
896
+ ("Coding CLI Executors", "preview", "Codex, Claude, OpenCode, Antigravity, Warp, Cursor, …; Gemini deprecated"),
897
+ ("Repair Loop (dev go)", "stable", "Bounded self-repair; correction manifest + no-progress"),
898
+ ("LLM repair inference", "preview", "Optional RepairService manifest sharpening"),
899
+ ("MCP Server (Claude hero loop)", "stable", "Certified checkout→verify loop; golden e2e"),
900
+ ("Multi-agent Campaign", "preview", "dev campaign — parallel DAG + Reviewer QC"),
901
+ ("OKF / design.md", "preview", "dev okf / dev design commands"),
902
+ ("CI Scaffolding", "preview", "dev scaffold-ci starter workflow"),
903
+ ("One-command onboarding (dev boot)", "preview", "setup + integrate --apply + go"),
904
+ ("GitHub PR Checks / Comments", "preview", "dev report --github*"),
905
+ ("Repo Map & Code Graph", "stable", "dev map / dev graph; liveness + query/dead/impact/html"),
906
+ ("LSP / AST Indexing", "preview", "dev lsp inspect (detection-only) / dev ast"),
907
+ ("Live Dashboard", "stable", "local-only operator UI; loopback + token-guarded apply"),
908
+ ("Coding CLI Hooks", "preview", "Stop gate on Claude/Codex Stop hooks; assist seeded on integrate"),
909
+ ("Stop gate & claim checks", "preview", "Completion-claim mapper + optional active-task verify"),
910
+ ("Corpus side index", "preview", "dev corpus + optional corpus/doc-ref rigor gates"),
911
+ ("PDG / CFG / taint", "preview", "Opt-in Python PDG; off by default"),
912
+ ("Native Executor", "preview", "Lease-gated writes + shared verify/next-actions loop; sandbox/timeout parity"),
913
+ ]
914
+
915
+
916
+ def _render_maturity_section(table) -> None:
917
+ """Append subsystem maturity rows so preview features are visible upfront."""
918
+ stable = "[green]Stable[/green]"
919
+ preview = "[yellow]Preview[/yellow]"
920
+ experimental = "[magenta]Experimental[/magenta]"
921
+ label_for = {"stable": stable, "preview": preview, "experimental": experimental}
922
+ for area, tier, notes in _subsystem_maturity_rows():
923
+ if tier == "preview":
924
+ notes = f"{notes} — API/output may change."
925
+ table.add_row(area, label_for.get(tier, tier), notes)
926
+
927
+
928
+ def _print_maturity_table() -> None:
929
+ maturity = Table(title="DevCouncil Subsystem Maturity (see docs/project-status.md)")
930
+ maturity.add_column("Area", style="cyan")
931
+ maturity.add_column("Tier", style="magenta")
932
+ maturity.add_column("Notes", style="green")
933
+ _render_maturity_section(maturity)
934
+ console.print(maturity)
935
+
936
+
225
937
  def render_doctor_check(project_root: Path = Path(".")):
226
938
  # Load config once for the whole invocation; the diagnostic checks below reuse
227
939
  # this instead of re-reading config.yaml. None falls back to per-check loading.
@@ -303,6 +1015,18 @@ def render_doctor_check(project_root: Path = Path(".")):
303
1015
  f"Optional. Install {info.label}, then use: {info.notes}.",
304
1016
  )
305
1017
 
1018
+ for name in sorted(DEPRECATED_CODING_CLIS):
1019
+ if name in CODING_CLI_PROBE_ORDER:
1020
+ continue
1021
+ info = CODING_CLI_INTEGRATION_INFO.get(name)
1022
+ if info is None:
1023
+ continue
1024
+ table.add_row(
1025
+ info.label,
1026
+ "[dim]Deprecated[/dim]",
1027
+ GEMINI_DEPRECATION_MESSAGE,
1028
+ )
1029
+
306
1030
  detected = detect_available_coding_cli(project_root)
307
1031
  resolved = resolve_automated_executor(project_root, None)
308
1032
  if detected:
@@ -323,6 +1047,34 @@ def render_doctor_check(project_root: Path = Path(".")):
323
1047
  for component, status, notes in check_ingested_knowledge(project_root, config=config):
324
1048
  table.add_row(component, status, notes)
325
1049
 
1050
+ from devcouncil.llm.semantic_bridge import check_semantic_layer
1051
+
1052
+ for component, status, notes in check_semantic_layer(project_root, config=config):
1053
+ table.add_row(component, status, notes)
1054
+
1055
+ # Status-doc drift: keep docs/project-status.md "Stable" claims honest against the
1056
+ # actual tests/unit/ layout. Placed with the knowledge rows so it also runs on the
1057
+ # ollama / unsupported-provider early-return paths.
1058
+ for component, status, notes in check_status_doc_drift(project_root):
1059
+ table.add_row(component, status, notes)
1060
+
1061
+ for component, status, notes in check_coverage_floor(project_root):
1062
+ table.add_row(component, status, notes)
1063
+
1064
+ for component, status, notes in check_mypy_status(project_root):
1065
+ table.add_row(component, status, notes)
1066
+
1067
+ for component, status, notes in check_mapping_stack(project_root):
1068
+ table.add_row(component, status, notes)
1069
+
1070
+ for component, status, notes in check_execution_containment(project_root, config=config):
1071
+ table.add_row(component, status, notes)
1072
+
1073
+ # Local-monitor verification safety: flag explicit config that disables the
1074
+ # ensembling a local monitor/reviewer needs (see check docstring for the data).
1075
+ for component, status, notes in check_local_monitor_sampling(project_root, config=config):
1076
+ table.add_row(component, status, notes)
1077
+
326
1078
  try:
327
1079
  provider = config.models.provider if config is not None else "openrouter"
328
1080
  except Exception:
@@ -338,6 +1090,7 @@ def render_doctor_check(project_root: Path = Path(".")):
338
1090
  )
339
1091
  _add_logging_row(table, project_root)
340
1092
  console.print(table)
1093
+ _print_maturity_table()
341
1094
  return
342
1095
  if provider == "ollama":
343
1096
  # Use the provider's own resolver so the displayed URL reflects OLLAMA_HOST
@@ -410,11 +1163,14 @@ def render_doctor_check(project_root: Path = Path(".")):
410
1163
  )
411
1164
 
412
1165
  if num_ctx is None:
1166
+ # Only reachable via the explicit OLLAMA_NUM_CTX=0 opt-out: unset now
1167
+ # resolves to the provider's raised DEFAULT_NUM_CTX, never the server default.
413
1168
  table.add_row(
414
1169
  "OLLAMA num_ctx",
415
1170
  "[yellow]WARN[/yellow]",
416
- f"OLLAMA_NUM_CTX not set Ollama's small default (~2048-4096) will "
417
- f"truncate DevCouncil's large planning prompts. Set OLLAMA_NUM_CTX={recommended_ctx}.",
1171
+ f"OLLAMA_NUM_CTX=0 opts into Ollama's small server default (~2048-4096), "
1172
+ f"which will truncate DevCouncil's large planning prompts. Unset it (default "
1173
+ f"{OllamaProvider.DEFAULT_NUM_CTX}) or set OLLAMA_NUM_CTX={recommended_ctx}.",
418
1174
  )
419
1175
  elif num_ctx < min_ctx:
420
1176
  table.add_row(
@@ -424,7 +1180,24 @@ def render_doctor_check(project_root: Path = Path(".")):
424
1180
  f"(~{MAX_PROMPT_CHARS // 4} tokens); recommend >= {recommended_ctx}.",
425
1181
  )
426
1182
  else:
427
- table.add_row("OLLAMA num_ctx", "[green]OK[/green]", f"context window = {num_ctx} tokens.")
1183
+ table.add_row(
1184
+ "OLLAMA num_ctx",
1185
+ "[green]OK[/green]",
1186
+ f"context window = {num_ctx} tokens (auto-grows per request up to "
1187
+ f"{OllamaProvider._resolve_max_num_ctx()} for oversized prompts; cap with OLLAMA_MAX_NUM_CTX).",
1188
+ )
1189
+
1190
+ think = OllamaProvider._resolve_think()
1191
+ if think is None:
1192
+ table.add_row(
1193
+ "OLLAMA think",
1194
+ "[green]OK[/green]",
1195
+ "server default. On thinking models (qwen3/deepseek-r1/...) the reasoning "
1196
+ "channel can dominate review latency; OLLAMA_THINK=false trades some check "
1197
+ "quality for much faster verification calls.",
1198
+ )
1199
+ else:
1200
+ table.add_row("OLLAMA think", "[green]OK[/green]", f"OLLAMA_THINK={'true' if think else 'false'}.")
428
1201
 
429
1202
  # Local model size is bounded by host memory — unified RAM on Apple Silicon,
430
1203
  # VRAM on a discrete-GPU box, system RAM otherwise. Surface a model that will
@@ -442,6 +1215,7 @@ def render_doctor_check(project_root: Path = Path(".")):
442
1215
 
443
1216
  _add_logging_row(table, project_root)
444
1217
  console.print(table)
1218
+ _print_maturity_table()
445
1219
  return
446
1220
  env_var = provider_api_key_env_var(provider)
447
1221
  local_secrets = load_local_secrets(project_root)
@@ -477,6 +1251,7 @@ def render_doctor_check(project_root: Path = Path(".")):
477
1251
 
478
1252
  _add_logging_row(table, project_root)
479
1253
  console.print(table)
1254
+ _print_maturity_table()
480
1255
 
481
1256
 
482
1257
  @app.callback(invoke_without_command=True)
@@ -494,4 +1269,11 @@ def doctor(
494
1269
  if ctx.invoked_subcommand is not None:
495
1270
  return
496
1271
 
497
- render_doctor_check(project_root.expanduser().resolve())
1272
+ root = project_root.expanduser().resolve()
1273
+ from devcouncil.telemetry.logging_setup import set_log_dir
1274
+ set_log_dir(root)
1275
+ logger.info("dev doctor: project_root=%s", root)
1276
+ with log_stage("doctor", project_root=root):
1277
+ log_step("doctor/1: running environment checks", project_root=root, trace=True)
1278
+ render_doctor_check(root)
1279
+ log_step("doctor/complete", project_root=root, trace=True)