devcouncil 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +46 -30
  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 +49 -30
  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 +960 -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 +99 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -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 +101 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +74 -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 +84 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -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 +60 -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 +303 -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/tasks.py +12 -3
  255. package/src/devcouncil/live/transcripts.py +69 -2
  256. package/src/devcouncil/llm/cache.py +5 -6
  257. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  258. package/src/devcouncil/llm/provider.py +647 -73
  259. package/src/devcouncil/llm/router.py +271 -46
  260. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  261. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  262. package/src/devcouncil/optimization/skillopt.py +9 -5
  263. package/src/devcouncil/planning/arbiter_service.py +12 -3
  264. package/src/devcouncil/planning/correction_manifest.py +107 -10
  265. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  266. package/src/devcouncil/planning/plan_service.py +5 -2
  267. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  268. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  269. package/src/devcouncil/planning/question_conversion.py +56 -0
  270. package/src/devcouncil/planning/spec_service.py +9 -3
  271. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  272. package/src/devcouncil/repo/gitignore.py +1 -2
  273. package/src/devcouncil/reporting/evidence_export.py +124 -0
  274. package/src/devcouncil/reporting/evidence_html.py +210 -0
  275. package/src/devcouncil/reporting/json_report.py +16 -12
  276. package/src/devcouncil/reporting/markdown_report.py +38 -9
  277. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  278. package/src/devcouncil/reporting/report_builder.py +40 -4
  279. package/src/devcouncil/reporting/task_provenance.py +42 -0
  280. package/src/devcouncil/reporting/verdict.py +75 -0
  281. package/src/devcouncil/skills/library/README.md +1 -0
  282. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  283. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  285. package/src/devcouncil/skills/registry.py +43 -12
  286. package/src/devcouncil/storage/db.py +57 -11
  287. package/src/devcouncil/storage/models.py +6 -0
  288. package/src/devcouncil/storage/native.py +5 -3
  289. package/src/devcouncil/storage/repositories.py +50 -18
  290. package/src/devcouncil/telemetry/context.py +28 -0
  291. package/src/devcouncil/telemetry/cost.py +4 -5
  292. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  293. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  294. package/src/devcouncil/telemetry/stages.py +27 -2
  295. package/src/devcouncil/telemetry/tracker.py +50 -13
  296. package/src/devcouncil/ui/dashboard.py +120 -8
  297. package/src/devcouncil/utils/fsio.py +58 -0
  298. package/src/devcouncil/utils/git_snapshot.py +112 -0
  299. package/src/devcouncil/utils/json_persist.py +53 -0
  300. package/src/devcouncil/utils/proc.py +89 -0
  301. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  302. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  303. package/src/devcouncil/verification/checks/__init__.py +41 -0
  304. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  305. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  306. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  307. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  308. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  309. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  310. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  311. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  312. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  313. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  314. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  315. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  316. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  317. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  318. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  319. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  320. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  321. package/src/devcouncil/verification/checks/wiring.py +216 -0
  322. package/src/devcouncil/verification/claims/__init__.py +23 -0
  323. package/src/devcouncil/verification/claims/checks.py +395 -0
  324. package/src/devcouncil/verification/claims/mapper.py +168 -0
  325. package/src/devcouncil/verification/claims/models.py +39 -0
  326. package/src/devcouncil/verification/claims/transcript.py +92 -0
  327. package/src/devcouncil/verification/claims/verdict.py +88 -0
  328. package/src/devcouncil/verification/command_evidence.py +170 -0
  329. package/src/devcouncil/verification/command_malformation.py +147 -0
  330. package/src/devcouncil/verification/command_runner.py +164 -0
  331. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  332. package/src/devcouncil/verification/diff_coverage.py +151 -0
  333. package/src/devcouncil/verification/difficulty.py +296 -0
  334. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  335. package/src/devcouncil/verification/gap_ids.py +63 -0
  336. package/src/devcouncil/verification/gate_cache.py +194 -0
  337. package/src/devcouncil/verification/gate_selector.py +344 -0
  338. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  339. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  340. package/src/devcouncil/verification/incremental_check.py +241 -0
  341. package/src/devcouncil/verification/next_actions.py +60 -1
  342. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  343. package/src/devcouncil/verification/sandbox.py +38 -11
  344. package/src/devcouncil/verification/stub_detector.py +369 -0
  345. package/src/devcouncil/verification/test_resolver.py +67 -1
  346. package/src/devcouncil/verification/verifier.py +137 -1666
  347. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  348. package/src/devcouncil/verification/verify_setup.py +176 -0
  349. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  350. package/src/semantic_layer/__init__.py +58 -0
  351. package/src/semantic_layer/benchmark.py +75 -0
  352. package/src/semantic_layer/cache.py +290 -0
  353. package/src/semantic_layer/compressor.py +137 -0
  354. package/src/semantic_layer/config.py +75 -0
  355. package/src/semantic_layer/embeddings.py +69 -0
  356. package/src/semantic_layer/llm_backends.py +99 -0
  357. package/src/semantic_layer/pipeline.py +111 -0
  358. package/src/semantic_layer/router.py +128 -0
  359. package/src/semantic_layer/tuner.py +72 -0
  360. package/uv.lock +973 -9
  361. package/src/devcouncil/artifacts/migrations.py +0 -20
  362. package/src/devcouncil/artifacts/schemas.py +0 -23
  363. package/src/devcouncil/artifacts/serializer.py +0 -21
  364. package/src/devcouncil/integrations/gitnexus.py +0 -70
  365. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -10,9 +10,11 @@ from the stated intent and runs them as evidence.
10
10
  from __future__ import annotations
11
11
 
12
12
  import asyncio
13
- import json
13
+ from devcouncil.utils.json_persist import dump_json
14
14
  import logging
15
- import subprocess
15
+ import os
16
+ import time
17
+ from datetime import datetime
16
18
  from pathlib import Path
17
19
 
18
20
  import typer
@@ -27,8 +29,14 @@ from devcouncil.integrations.github_intent import resolve_goal_intent
27
29
  from devcouncil.llm.provider import ProviderRequestError, create_provider, validate_model_provider
28
30
  from devcouncil.llm.router import ModelRouter, StructuredOutputError
29
31
  from devcouncil.verification.ad_hoc_check import AdHocCheckResult, run_working_tree_check
32
+ from devcouncil.verification.gate_cache import GateResultCache
33
+ from devcouncil.verification.incremental_check import (
34
+ run_incremental_gates,
35
+ selected_gate_specs,
36
+ )
30
37
  from devcouncil.verification.implementation_reviewer import ImplementationReviewer
31
38
  from devcouncil.verification.verifier import Verifier
39
+ from devcouncil.telemetry.stages import log_stage, log_step
32
40
 
33
41
  console = Console()
34
42
  logger = logging.getLogger(__name__)
@@ -37,12 +45,9 @@ logger = logging.getLogger(__name__)
37
45
  def _diff(root: Path, base: str | None) -> str:
38
46
  if not base:
39
47
  return Verifier(root).get_diff()
40
- try:
41
- return subprocess.check_output(
42
- ["git", "diff", base, "--"], cwd=root, text=True, encoding="utf-8", errors="replace"
43
- )
44
- except Exception:
45
- return ""
48
+ from devcouncil.utils.proc import git_output
49
+
50
+ return git_output(["diff", base, "--"], cwd=root, default="")
46
51
 
47
52
 
48
53
  def check(
@@ -50,8 +55,11 @@ def check(
50
55
  base: str | None = typer.Option(None, "--base", help="Diff against this git ref instead of the uncommitted working tree."),
51
56
  test_commands: list[str] | None = typer.Option(None, "--test", "-t", help="A verification command proving the change works (repeatable). Switches to the deterministic evidence gate."),
52
57
  verify: bool = typer.Option(False, "--verify", help="Run the deterministic evidence gate (orphan-diff, acceptance evidence, diff↔coverage, next actions) instead of the LLM audit. No provider keys needed."),
58
+ watch: bool = typer.Option(False, "--watch", help="Keep watching the project tree and re-run the deterministic evidence gate on every change (implies --verify). Prints one compact verdict line per run; Ctrl-C to stop."),
59
+ list_gates: bool = typer.Option(False, "--list-gates", help="Preview which incremental stack gates would run for the current working-tree changes (no execution)."),
53
60
  enforce_coverage: bool = typer.Option(False, "--enforce-coverage", help="Evidence gate: block when the tests do not exercise the changed lines."),
54
61
  min_coverage: float = typer.Option(0.0, "--min-coverage", help="Evidence gate: minimum fraction of changed lines that must be exercised (implies --enforce-coverage)."),
62
+ persist: bool = typer.Option(False, "--persist", help="Evidence gate: persist gaps/evidence for dev report (typical in CI)."),
55
63
  json_format: bool = typer.Option(False, "--json", help="Machine-readable output."),
56
64
  project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
57
65
  ):
@@ -59,8 +67,66 @@ def check(
59
67
  root = project_root.expanduser().resolve()
60
68
  from devcouncil.telemetry.logging_setup import set_log_dir
61
69
  set_log_dir(root)
70
+ logger.info(
71
+ "dev check: verify=%s goal=%s base=%s watch=%s list_gates=%s",
72
+ verify, bool(goal), base or "working-tree", watch, list_gates,
73
+ )
62
74
  initialize_project(root, quiet=True)
63
75
 
76
+ with log_stage("check", project_root=root, verify=verify):
77
+ if list_gates:
78
+ _list_gates(root, test_commands, json_format)
79
+ return
80
+ _run_check_body(
81
+ root, goal, base, test_commands, verify, enforce_coverage,
82
+ min_coverage, json_format, watch, persist,
83
+ )
84
+
85
+
86
+ def _list_gates(root: Path, test_commands: list[str] | None, json_format: bool) -> None:
87
+ """Dry-run: show incremental gates for the current working-tree diff."""
88
+ from devcouncil.verification.incremental_check import (
89
+ _default_changed_files,
90
+ _default_commands,
91
+ )
92
+
93
+ changed = _default_changed_files(root)
94
+ cmds = _default_commands(root)
95
+ if test_commands:
96
+ cmds = {**cmds, "test": list(cmds.get("test") or []) + list(test_commands)}
97
+ gates = selected_gate_specs(root, changed, commands=cmds, narrow=True)
98
+ payload = {
99
+ "changed_files": changed,
100
+ "gates": [
101
+ {
102
+ "name": g.name,
103
+ "kind": g.kind,
104
+ "command": list(g.command),
105
+ "narrowed": g.narrowed,
106
+ }
107
+ for g in gates
108
+ ],
109
+ }
110
+ if json_format:
111
+ typer.echo(dump_json(payload, indent=2))
112
+ return
113
+ if not changed:
114
+ console.print("[dim]No working-tree changes — no gates selected.[/dim]")
115
+ return
116
+ if not gates:
117
+ console.print(f"[dim]{len(changed)} changed file(s), no matching stack gates.[/dim]")
118
+ return
119
+ console.print(f"[bold]{len(gates)}[/bold] gate(s) for {len(changed)} changed file(s):")
120
+ for g in gates:
121
+ narrow = " [narrowed]" if g.narrowed else ""
122
+ console.print(f" • {g.kind}/{g.name}{narrow}: {' '.join(g.command)}")
123
+
124
+
125
+ def _run_check_body(
126
+ root, goal, base, test_commands, verify, enforce_coverage, min_coverage, json_format, watch, persist,
127
+ ):
128
+ log_step("check/1: resolving diff scope", project_root=root, trace=True)
129
+
64
130
  # A --goal of "#142" or a GitHub issue/PR URL is a reference, not a spec —
65
131
  # expand it into the issue/PR title + body so acceptance checks and the review
66
132
  # are grounded in the real intent (same behavior as `dev go`).
@@ -70,6 +136,20 @@ def check(
70
136
  console.print(f"[dim]{intent_note}[/dim]")
71
137
  goal = expanded_goal
72
138
 
139
+ # Watch mode: run the deterministic gate, then re-run it whenever the project tree
140
+ # changes, printing one compact verdict line per run. Implies --verify — looping the
141
+ # LLM audit would burn provider tokens on every save.
142
+ if watch:
143
+ if json_format:
144
+ raise typer.BadParameter("--watch is interactive; it cannot be combined with --json.")
145
+ _watch_gate(
146
+ root, goal,
147
+ test_commands=list(test_commands or []),
148
+ enforce_coverage=enforce_coverage,
149
+ min_coverage=min_coverage,
150
+ )
151
+ return
152
+
73
153
  # Evidence-gate mode: deterministic verification of the working-tree diff against an
74
154
  # inline requirement (--goal), with the diff↔coverage gate and the typed next-actions
75
155
  # contract. Provider-key-free — this is the lite path that lets you taste the gate.
@@ -77,16 +157,19 @@ def check(
77
157
  result = run_working_tree_check(
78
158
  root,
79
159
  goal,
160
+ base=base,
80
161
  test_commands=list(test_commands or []),
81
162
  enforce_coverage=enforce_coverage,
82
163
  min_ratio=min_coverage,
164
+ persist=persist,
83
165
  )
84
166
  if json_format:
85
- typer.echo(json.dumps(result.to_dict(), indent=2))
167
+ typer.echo(dump_json(result.to_dict(), indent=2))
86
168
  else:
87
169
  _render_gate(result)
88
170
  raise typer.Exit(code=0 if result.passed else 1)
89
171
 
172
+ log_step("check/2: running LLM audit", project_root=root)
90
173
  verifier = Verifier(root)
91
174
 
92
175
  logger.info("dev check (LLM audit): base=%s goal=%s", base or "working-tree", "set" if goal else "none")
@@ -94,7 +177,7 @@ def check(
94
177
  if not diff.strip():
95
178
  logger.info("dev check: clean working tree; nothing to audit")
96
179
  msg = "No changes to check (clean working tree)."
97
- typer.echo(json.dumps({"ok": True, "message": msg}) if json_format else msg)
180
+ typer.echo(dump_json({"ok": True, "message": msg}) if json_format else msg)
98
181
  return
99
182
 
100
183
  changed_files = verifier.get_changed_files()
@@ -136,7 +219,7 @@ def check(
136
219
  logger.info("dev check audit complete: %d secret finding(s), %d review finding(s)", len(secret_gaps), len(findings))
137
220
 
138
221
  if json_format:
139
- typer.echo(json.dumps({
222
+ typer.echo(dump_json({
140
223
  "ok": not secret_gaps,
141
224
  "changed_files": changed_files,
142
225
  "secret_findings": [g.model_dump() for g in secret_gaps],
@@ -180,12 +263,15 @@ def check(
180
263
  "[dim]Tip: `dev check --verify --test \"<cmd>\"` runs the deterministic evidence gate "
181
264
  "(no provider keys).[/dim]"
182
265
  )
266
+ log_step("check/complete", project_root=root, trace=True)
183
267
 
184
268
 
185
269
  def _render_gate(result: AdHocCheckResult) -> None:
186
270
  """Render the deterministic evidence-gate result for humans."""
187
271
  if result.reason == "no_changes":
188
- console.print("[yellow]No working-tree changes to verify. Make a change first, then re-run.[/yellow]")
272
+ console.print(
273
+ "[yellow]No changes to verify. Make a change or pass --base for PR-scoped diff.[/yellow]"
274
+ )
189
275
  return
190
276
 
191
277
  console.print(f"[bold]Checking:[/bold] {result.requirement}")
@@ -218,3 +304,130 @@ def _render_gate(result: AdHocCheckResult) -> None:
218
304
  console.print("\n[green]Verified with non-blocking signals only.[/green]")
219
305
  else:
220
306
  console.print("\n[red]Not verified: blocking gaps must be resolved.[/red]")
307
+
308
+
309
+ # --watch plumbing. execution/fs_watcher.FilesystemWatcher was considered and rejected:
310
+ # it exists to ATTRIBUTE changes to a leased task (needs a task_id; records file-change
311
+ # events and orphan-diff gaps in the DB through the policy engine), not to signal
312
+ # "something changed". A 2s mtime scan plus a short settle debounce is the genuinely
313
+ # simpler fit here — no watchdog dependency, identical behavior on every platform.
314
+ _WATCH_IGNORED_DIRS = frozenset({
315
+ ".git", ".hg", ".svn", ".devcouncil", "__pycache__", ".pytest_cache", ".mypy_cache",
316
+ ".ruff_cache", "node_modules", "dist", "build", "target", ".venv", "venv", ".tox",
317
+ ".idea", ".vscode",
318
+ })
319
+ _WATCH_POLL_SECONDS = 2.0
320
+ _WATCH_DEBOUNCE_SECONDS = 1.5
321
+
322
+
323
+ def _watch_snapshot(root: Path) -> dict:
324
+ """One cheap pass over the tree: ``{path: mtime_ns}`` for non-ignored files."""
325
+ snapshot: dict = {}
326
+ stack = [str(root)]
327
+ while stack:
328
+ directory = stack.pop()
329
+ try:
330
+ with os.scandir(directory) as entries:
331
+ for entry in entries:
332
+ try:
333
+ if entry.is_dir(follow_symlinks=False):
334
+ if entry.name not in _WATCH_IGNORED_DIRS:
335
+ stack.append(entry.path)
336
+ elif entry.is_file(follow_symlinks=False):
337
+ snapshot[entry.path] = entry.stat(follow_symlinks=False).st_mtime_ns
338
+ except OSError:
339
+ continue
340
+ except OSError:
341
+ continue
342
+ return snapshot
343
+
344
+
345
+ def _watch_evidence_once(root: Path, goal, test_commands, enforce_coverage, min_coverage, stamp) -> None:
346
+ """Full deterministic evidence gate — the fallback when no stack gate applies."""
347
+ try:
348
+ result = run_working_tree_check(
349
+ root, goal,
350
+ test_commands=test_commands,
351
+ enforce_coverage=enforce_coverage,
352
+ min_ratio=min_coverage,
353
+ )
354
+ except Exception as exc: # keep watching through transient failures (git hiccups, ...)
355
+ logger.warning("dev check --watch: run failed: %s", exc)
356
+ console.print(f"[dim]{stamp}[/dim] [red]ERROR[/red] — {str(exc)[:120]}")
357
+ return
358
+ verdict = "[green]PASS[/green]" if result.passed else "[red]FAIL[/red]"
359
+ blocking = len([g for g in result.gaps if g.blocking])
360
+ detail = f"{len(result.gaps)} gap(s)" + (f", {blocking} blocking" if blocking else "")
361
+ if result.reason == "no_changes":
362
+ detail = "no working-tree changes"
363
+ console.print(f"[dim]{stamp}[/dim] {verdict} — {detail} [dim](evidence gate)[/dim]")
364
+
365
+
366
+ def _watch_gate_once(root: Path, goal, test_commands, enforce_coverage, min_coverage, cache) -> None:
367
+ """Run the incremental gate once and print a compact one-line verdict.
368
+
369
+ Only the gates whose inputs actually changed are executed; the rest are served from
370
+ the content-hash cache, so iterative edits stay sub-second. When no stack-relevant
371
+ gate is configured for the change, fall back to the full evidence gate so the user
372
+ still gets orphan/secret/coverage feedback."""
373
+ stamp = datetime.now().strftime("%H:%M:%S")
374
+ try:
375
+ result = run_incremental_gates(
376
+ root, extra_test_commands=test_commands, cache=cache,
377
+ )
378
+ except Exception as exc: # keep watching through transient failures (git hiccups, ...)
379
+ logger.warning("dev check --watch: incremental run failed: %s", exc)
380
+ console.print(f"[dim]{stamp}[/dim] [red]ERROR[/red] — {str(exc)[:120]}")
381
+ return
382
+
383
+ if result.no_changes:
384
+ console.print(f"[dim]{stamp}[/dim] [dim]— no working-tree changes[/dim]")
385
+ return
386
+ if result.no_gates:
387
+ # No configured command targets the changed stack(s); give the full picture.
388
+ _watch_evidence_once(root, goal, test_commands, enforce_coverage, min_coverage, stamp)
389
+ return
390
+
391
+ verdict = "[green]PASS[/green]" if result.passed else "[red]FAIL[/red]"
392
+ ran, cached = len(result.ran), len(result.cached)
393
+ detail = f"{ran} gate(s) run, {cached} cached"
394
+ failing = [o for o in result.outcomes if not o.passed]
395
+ if failing:
396
+ detail += " — failed: " + ", ".join(o.kind for o in failing[:4])
397
+ ms = result.duration_s * 1000
398
+ console.print(f"[dim]{stamp}[/dim] {verdict} — {detail} [dim]({ms:.0f}ms)[/dim]")
399
+ if result.narrowed:
400
+ logger.info("dev check --watch: narrowed — full verify recommended before commit")
401
+ console.print(
402
+ "[dim]narrowed — full verify recommended before commit[/dim]"
403
+ )
404
+
405
+
406
+ def _watch_gate(root: Path, goal, *, test_commands, enforce_coverage, min_coverage) -> None:
407
+ """Initial gate run, then a poll/debounce loop re-running it until Ctrl-C.
408
+
409
+ A single :class:`GateResultCache` is shared across iterations so a gate whose inputs
410
+ did not change between saves is skipped rather than re-run."""
411
+ console.print(
412
+ f"[dim]Watching {root} — re-running only the gates affected by each change "
413
+ f"(poll {_WATCH_POLL_SECONDS:.0f}s, debounce {_WATCH_DEBOUNCE_SECONDS:.1f}s). "
414
+ "Ctrl-C to stop. Run without --watch for full findings.[/dim]"
415
+ )
416
+ cache = GateResultCache(root)
417
+ try:
418
+ _watch_gate_once(root, goal, test_commands, enforce_coverage, min_coverage, cache)
419
+ baseline = _watch_snapshot(root)
420
+ while True:
421
+ time.sleep(_WATCH_POLL_SECONDS)
422
+ current = _watch_snapshot(root)
423
+ if current == baseline:
424
+ continue
425
+ # Debounce: let an editor save-storm settle before burning a run.
426
+ time.sleep(_WATCH_DEBOUNCE_SECONDS)
427
+ _watch_gate_once(root, goal, test_commands, enforce_coverage, min_coverage, cache)
428
+ # Re-baseline AFTER the run so artifacts the gate itself writes (coverage
429
+ # data, logs outside .devcouncil/) can never re-trigger it. Trade-off:
430
+ # edits made while the gate was running are absorbed — save again.
431
+ baseline = _watch_snapshot(root)
432
+ except KeyboardInterrupt:
433
+ console.print("\n[dim]Watch stopped.[/dim]")
@@ -1,4 +1,6 @@
1
+ import logging
1
2
  import typer
3
+ from typing import cast
2
4
  import yaml
3
5
  from pathlib import Path
4
6
  from rich.console import Console
@@ -10,9 +12,143 @@ from devcouncil.llm.provider import (
10
12
  build_role_model_config,
11
13
  validate_model_provider,
12
14
  )
15
+ from devcouncil.telemetry.stages import log_stage, log_step
13
16
 
14
17
  app = typer.Typer(help="Manage DevCouncil configuration")
15
18
  console = Console()
19
+ logger = logging.getLogger(__name__)
20
+
21
+ _CONFIG_SETTABLE_KEYS = {
22
+ "execution.default_executor": ("execution", "default_executor", str),
23
+ "execution.max_repair_attempts": ("execution", "max_repair_attempts", int),
24
+ "execution.command_timeout": ("execution", "command_timeout", int),
25
+ "execution.verify_on_post_task": ("execution", "verify_on_post_task", bool),
26
+ "execution.stop_gate.mode": ("execution", "stop_gate", "mode", str),
27
+ "execution.stop_gate.check_claims": ("execution", "stop_gate", "check_claims", bool),
28
+ "execution.stop_gate.verify_active_task": ("execution", "stop_gate", "verify_active_task", bool),
29
+ "execution.stop_gate.max_blocks": ("execution", "stop_gate", "max_blocks", int),
30
+ "verification.diff_coverage.enforce": ("verification", "diff_coverage", "enforce", bool),
31
+ "semantic_layer.enabled": ("semantic_layer", "enabled", bool),
32
+ "semantic_layer.cache.enabled": ("semantic_layer", "cache", "enabled", bool),
33
+ "semantic_layer.router.enabled": ("semantic_layer", "router", "enabled", bool),
34
+ "semantic_layer.compressor.enabled": ("semantic_layer", "compressor", "enabled", bool),
35
+ }
36
+
37
+
38
+ def _parse_config_value(raw: str, typ: type) -> object:
39
+ if typ is bool:
40
+ normalized = raw.strip().lower()
41
+ if normalized in {"true", "1", "yes", "on"}:
42
+ return True
43
+ if normalized in {"false", "0", "no", "off"}:
44
+ return False
45
+ raise ValueError(f"Expected boolean (true/false), got {raw!r}")
46
+ if typ is int:
47
+ return int(raw)
48
+ return raw
49
+
50
+
51
+ def _nested_get(raw: dict, path: tuple[str, ...]) -> object | None:
52
+ current: object = raw
53
+ for key in path:
54
+ if not isinstance(current, dict):
55
+ return None
56
+ current = current.get(key)
57
+ return current
58
+
59
+
60
+ def _nested_set(raw: dict, path: tuple[str, ...], value: object) -> None:
61
+ current = raw
62
+ for key in path[:-1]:
63
+ if key not in current or not isinstance(current[key], dict):
64
+ current[key] = {}
65
+ current = current[key]
66
+ current[path[-1]] = value
67
+
68
+
69
+ @app.command("show")
70
+ def show_config(
71
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
72
+ ):
73
+ """Display key DevCouncil settings."""
74
+ root = project_root.expanduser().resolve()
75
+ from devcouncil.telemetry.logging_setup import set_log_dir
76
+ set_log_dir(root)
77
+
78
+ with log_stage("config", project_root=root, subcommand="show"):
79
+ try:
80
+ cfg = load_config(root)
81
+ except FileNotFoundError as e:
82
+ console.print(f"[red]{e}[/red]")
83
+ raise typer.Exit(code=1) from e
84
+
85
+ console.print("[bold]DevCouncil Settings[/bold]")
86
+ console.print(f" [cyan]execution.default_executor[/cyan]: {cfg.execution.default_executor}")
87
+ console.print(f" [cyan]execution.max_repair_attempts[/cyan]: {cfg.execution.max_repair_attempts}")
88
+ console.print(f" [cyan]execution.command_timeout[/cyan]: {cfg.execution.command_timeout}")
89
+ console.print(f" [cyan]execution.verify_on_post_task[/cyan]: {cfg.execution.verify_on_post_task} [dim](deprecated alias)[/dim]")
90
+ sg = cfg.execution.stop_gate
91
+ console.print(f" [cyan]execution.stop_gate.mode[/cyan]: {sg.mode}")
92
+ console.print(f" [cyan]execution.stop_gate.check_claims[/cyan]: {sg.check_claims}")
93
+ console.print(f" [cyan]execution.stop_gate.verify_active_task[/cyan]: {sg.verify_active_task}")
94
+ console.print(f" [cyan]execution.stop_gate.max_blocks[/cyan]: {sg.max_blocks}")
95
+ console.print(f" [cyan]verification.diff_coverage.enforce[/cyan]: {cfg.verification.diff_coverage.enforce}")
96
+ console.print(f" [cyan]verification.rigor.enabled[/cyan]: {cfg.verification.rigor.enabled}")
97
+ console.print(f" [cyan]verification.rigor.stub_detection[/cyan]: {cfg.verification.rigor.stub_detection}")
98
+ console.print(f" [cyan]gates.block_orphan_diffs[/cyan]: {cfg.gates.block_orphan_diffs}")
99
+ console.print(f" [cyan]semantic_layer.enabled[/cyan]: {cfg.semantic_layer.enabled}")
100
+ if cfg.semantic_layer.enabled:
101
+ console.print(f" [cyan]semantic_layer.cache.enabled[/cyan]: {cfg.semantic_layer.cache.enabled}")
102
+ console.print(f" [cyan]semantic_layer.router.enabled[/cyan]: {cfg.semantic_layer.router.enabled}")
103
+ console.print(f" [cyan]semantic_layer.compressor.enabled[/cyan]: {cfg.semantic_layer.compressor.enabled}")
104
+ console.print(
105
+ f" [cyan]semantic_layer.embedding.model_name[/cyan]: {cfg.semantic_layer.embedding.model_name}"
106
+ )
107
+ console.print(
108
+ " [dim]Enable: dev config set semantic_layer.enabled true "
109
+ "(requires uv sync --group semantic). "
110
+ "Check: dev doctor.[/dim]"
111
+ )
112
+
113
+
114
+ @app.command("set")
115
+ def set_config(
116
+ key: str = typer.Argument(..., help="Dotted config key (e.g. execution.command_timeout)."),
117
+ value: str = typer.Argument(..., help="New value."),
118
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
119
+ ):
120
+ """Set a common DevCouncil config key."""
121
+ root = project_root.expanduser().resolve()
122
+ from devcouncil.telemetry.logging_setup import set_log_dir
123
+ set_log_dir(root)
124
+
125
+ mapping = _CONFIG_SETTABLE_KEYS.get(key)
126
+ if mapping is None:
127
+ supported = ", ".join(sorted(_CONFIG_SETTABLE_KEYS))
128
+ console.print(f"[red]Unsupported key {key!r}. Supported: {supported}[/red]")
129
+ raise typer.Exit(code=2)
130
+
131
+ config_path = root / ".devcouncil" / "config.yaml"
132
+ if not config_path.exists():
133
+ console.print(f"[red]Config not found at {config_path}[/red]")
134
+ raise typer.Exit(code=1)
135
+
136
+ typ = cast(type, mapping[-1])
137
+ path = cast(tuple[str, ...], mapping[:-1])
138
+ try:
139
+ parsed = _parse_config_value(value, typ)
140
+ except ValueError as exc:
141
+ console.print(f"[red]{exc}[/red]")
142
+ raise typer.Exit(code=2) from exc
143
+
144
+ with log_stage("config", project_root=root, subcommand="set"):
145
+ with open(config_path) as f:
146
+ raw_config = yaml.safe_load(f) or {}
147
+ _nested_set(raw_config, path, parsed)
148
+ with open(config_path, "w") as f:
149
+ yaml.dump(raw_config, f, default_flow_style=False)
150
+ console.print(f"[green]Updated {key} = {parsed!r}[/green]")
151
+
16
152
 
17
153
  @app.command("models")
18
154
  def models(
@@ -28,88 +164,99 @@ def models(
28
164
  ):
29
165
  """View or edit model role configuration."""
30
166
  root = project_root.expanduser().resolve()
31
- try:
32
- load_config(root)
33
- except FileNotFoundError as e:
34
- console.print(f"[red]{e}[/red]")
35
- return
167
+ from devcouncil.telemetry.logging_setup import set_log_dir
168
+ set_log_dir(root)
169
+ logger.info("dev config models: role=%s provider=%s", role, provider)
36
170
 
37
- config_path = root / ".devcouncil" / "config.yaml"
38
-
39
- with open(config_path) as f:
40
- raw_config = yaml.safe_load(f) or {}
171
+ with log_stage("config", project_root=root, subcommand="models"):
172
+ log_step("config/1: loading configuration", project_root=root, trace=True)
173
+ try:
174
+ load_config(root)
175
+ except FileNotFoundError as e:
176
+ console.print(f"[red]{e}[/red]")
177
+ return
41
178
 
42
- try:
43
- role_models = parse_role_model_overrides(role_model)
44
- except ValueError as e:
45
- console.print(f"[red]{e}[/red]")
46
- raise typer.Exit(code=2) from e
179
+ config_path = root / ".devcouncil" / "config.yaml"
180
+
181
+ with open(config_path) as f:
182
+ raw_config = yaml.safe_load(f) or {}
47
183
 
48
- if provider:
49
184
  try:
50
- normalized_provider = validate_model_provider(provider)
185
+ role_models = parse_role_model_overrides(role_model)
51
186
  except ValueError as e:
52
187
  console.print(f"[red]{e}[/red]")
53
188
  raise typer.Exit(code=2) from e
54
- raw_config.setdefault("models", {})
55
- previous = raw_config["models"].get("provider", "openrouter")
56
- raw_config["models"]["provider"] = normalized_provider
57
- updated_role_defaults = apply_provider_default_role_models(raw_config, previous, normalized_provider)
58
- with open(config_path, "w") as f:
59
- yaml.dump(raw_config, f, default_flow_style=False)
60
- if previous == normalized_provider:
61
- console.print(f"[green]Model provider remains '{normalized_provider}'.[/green]")
62
- else:
63
- console.print(f"[green]Updated model provider from '{previous}' to '{normalized_provider}'.[/green]")
64
- if updated_role_defaults:
65
- console.print(f"[green]Updated default role models for '{normalized_provider}'.[/green]")
66
- if not model and not role_models:
189
+
190
+ if provider:
191
+ try:
192
+ normalized_provider = validate_model_provider(provider)
193
+ except ValueError as e:
194
+ console.print(f"[red]{e}[/red]")
195
+ raise typer.Exit(code=2) from e
196
+ raw_config.setdefault("models", {})
197
+ previous = raw_config["models"].get("provider", "openrouter")
198
+ raw_config["models"]["provider"] = normalized_provider
199
+ updated_role_defaults = apply_provider_default_role_models(raw_config, previous, normalized_provider)
200
+ with open(config_path, "w") as f:
201
+ yaml.dump(raw_config, f, default_flow_style=False)
202
+ if previous == normalized_provider:
203
+ console.print(f"[green]Model provider remains '{normalized_provider}'.[/green]")
204
+ else:
205
+ console.print(f"[green]Updated model provider from '{previous}' to '{normalized_provider}'.[/green]")
206
+ if updated_role_defaults:
207
+ console.print(f"[green]Updated default role models for '{normalized_provider}'.[/green]")
208
+ if not model and not role_models:
209
+ log_step("config/complete", project_root=root, trace=True)
210
+ return
211
+
212
+ if not role and (model or role_models):
213
+ raw_config.setdefault("models", {})
214
+ configured_provider = validate_model_provider(raw_config["models"].get("provider", "openrouter"))
215
+ raw_config["models"]["roles"] = build_role_model_config(
216
+ configured_provider,
217
+ model=model,
218
+ role_models=role_models,
219
+ )
220
+ with open(config_path, "w") as f:
221
+ yaml.dump(raw_config, f, default_flow_style=False)
222
+ if model:
223
+ console.print(f"[green]Updated all model roles to use '{model}'.[/green]")
224
+ for selected_role, selected_model in role_models.items():
225
+ console.print(f"[green]Updated '{selected_role}' to use model '{selected_model}'.[/green]")
226
+ log_step("config/complete", project_root=root, trace=True)
227
+ return
228
+
229
+ if not role:
230
+ console.print("[bold]Model Configuration[/bold]")
231
+ configured_provider = raw_config.get("models", {}).get("provider", "openrouter")
232
+ supported = ", ".join(SUPPORTED_MODEL_PROVIDERS)
233
+ console.print(f" [cyan]provider[/cyan]: {configured_provider} (supported: {supported})")
234
+ for r, m in raw_config.get("models", {}).get("roles", {}).items():
235
+ console.print(f" [cyan]{r}[/cyan]: {m.get('model')}")
236
+ log_step("config/complete", project_root=root, trace=True)
67
237
  return
68
238
 
69
- if not role and (model or role_models):
70
- raw_config.setdefault("models", {})
71
- configured_provider = validate_model_provider(raw_config["models"].get("provider", "openrouter"))
72
- raw_config["models"]["roles"] = build_role_model_config(
73
- configured_provider,
74
- model=model,
75
- role_models=role_models,
76
- )
239
+ if not model:
240
+ m = raw_config.get("models", {}).get("roles", {}).get(role)
241
+ if m:
242
+ console.print(f"[cyan]{role}[/cyan]: {m.get('model')}")
243
+ else:
244
+ console.print(f"[red]Role '{role}' not found.[/red]")
245
+ log_step("config/complete", project_root=root, trace=True)
246
+ return
247
+
248
+ if "models" not in raw_config:
249
+ raw_config["models"] = {"roles": {}}
250
+ if "roles" not in raw_config["models"]:
251
+ raw_config["models"]["roles"] = {}
252
+
253
+ if role not in raw_config["models"]["roles"]:
254
+ raw_config["models"]["roles"][role] = {}
255
+
256
+ raw_config["models"]["roles"][role]["model"] = model
257
+
77
258
  with open(config_path, "w") as f:
78
259
  yaml.dump(raw_config, f, default_flow_style=False)
79
- if model:
80
- console.print(f"[green]Updated all model roles to use '{model}'.[/green]")
81
- for selected_role, selected_model in role_models.items():
82
- console.print(f"[green]Updated '{selected_role}' to use model '{selected_model}'.[/green]")
83
- return
84
-
85
- if not role:
86
- console.print("[bold]Model Configuration[/bold]")
87
- configured_provider = raw_config.get("models", {}).get("provider", "openrouter")
88
- supported = ", ".join(SUPPORTED_MODEL_PROVIDERS)
89
- console.print(f" [cyan]provider[/cyan]: {configured_provider} (supported: {supported})")
90
- for r, m in raw_config.get("models", {}).get("roles", {}).items():
91
- console.print(f" [cyan]{r}[/cyan]: {m.get('model')}")
92
- return
93
-
94
- if not model:
95
- m = raw_config.get("models", {}).get("roles", {}).get(role)
96
- if m:
97
- console.print(f"[cyan]{role}[/cyan]: {m.get('model')}")
98
- else:
99
- console.print(f"[red]Role '{role}' not found.[/red]")
100
- return
101
-
102
- if "models" not in raw_config:
103
- raw_config["models"] = {"roles": {}}
104
- if "roles" not in raw_config["models"]:
105
- raw_config["models"]["roles"] = {}
106
-
107
- if role not in raw_config["models"]["roles"]:
108
- raw_config["models"]["roles"][role] = {}
109
-
110
- raw_config["models"]["roles"][role]["model"] = model
111
-
112
- with open(config_path, "w") as f:
113
- yaml.dump(raw_config, f, default_flow_style=False)
114
-
115
- console.print(f"[green]Updated '{role}' to use model '{model}'[/green]")
260
+
261
+ console.print(f"[green]Updated '{role}' to use model '{model}'[/green]")
262
+ log_step("config/complete", project_root=root, trace=True)