devcouncil 0.3.1 → 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
@@ -1,10 +1,12 @@
1
1
  import asyncio
2
+ import logging
2
3
 
3
4
  import typer
4
5
 
5
- from devcouncil.integrations.mcp.server import run
6
+ from devcouncil.telemetry.stages import log_stage, log_step
6
7
 
7
8
  app = typer.Typer()
9
+ logger = logging.getLogger(__name__)
8
10
 
9
11
 
10
12
  @app.callback(invoke_without_command=True)
@@ -15,4 +17,12 @@ def mcp_server(ctx: typer.Context):
15
17
  if ctx.invoked_subcommand is not None:
16
18
  return
17
19
 
18
- asyncio.run(run())
20
+ # Deferred import: the MCP SDK (and the server module's large import surface)
21
+ # loads only when this command actually runs, not on every `dev` invocation —
22
+ # main.py imports every command module even for `dev --help`.
23
+ from devcouncil.integrations.mcp.server import run
24
+
25
+ logger.info("dev mcp-server: starting stdio server")
26
+ with log_stage("mcp_server"):
27
+ log_step("mcp_server/1: initializing stdio transport", trace=True)
28
+ asyncio.run(run())
@@ -19,9 +19,12 @@ from devcouncil.cli.commands.init import initialize_project
19
19
  from devcouncil.knowledge.okf import read_bundle, validate_bundle
20
20
  from devcouncil.storage.db import get_db
21
21
  from devcouncil.storage.repositories import ArtifactGraphRepository
22
+ from devcouncil.telemetry.stages import log_stage, log_step
23
+ import logging
22
24
 
23
25
  app = typer.Typer(help="Export, ingest, and validate Open Knowledge Format bundles.")
24
26
  console = Console()
27
+ logger = logging.getLogger(__name__)
25
28
 
26
29
 
27
30
  def _knowledge_okf_dir(root: Path) -> Path:
@@ -30,8 +33,8 @@ def _knowledge_okf_dir(root: Path) -> Path:
30
33
  directory = ".devcouncil/knowledge"
31
34
  try:
32
35
  directory = load_config(root).knowledge.directory
33
- except Exception:
34
- pass
36
+ except Exception as e:
37
+ logger.debug("Failed to load knowledge directory from config, using default: %s", e)
35
38
  return root / directory / "okf"
36
39
 
37
40
 
@@ -46,8 +49,8 @@ def _knowledge_design_md(root: Path) -> Path | None:
46
49
  directory = ".devcouncil/knowledge"
47
50
  try:
48
51
  directory = load_config(root).knowledge.directory
49
- except Exception:
50
- pass
52
+ except Exception as e:
53
+ logger.debug("Failed to load knowledge directory from config, using default: %s", e)
51
54
  candidates = [
52
55
  root / directory / "design" / "design.md",
53
56
  root / ".devcouncil" / "knowledge" / "design" / "design.md",
@@ -75,12 +78,22 @@ def export(
75
78
  ):
76
79
  """Export the DevCouncil artifact graph as an OKF bundle."""
77
80
  root = project_root.expanduser().resolve()
81
+ from devcouncil.telemetry.logging_setup import set_log_dir
82
+ set_log_dir(root)
83
+ logger.info("dev okf export")
78
84
  initialize_project(root, quiet=True)
79
85
  db = get_db(root)
80
86
  if not db:
81
87
  console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
82
88
  raise typer.Exit(code=1)
83
89
 
90
+ with log_stage("okf", project_root=root, subcommand="export"):
91
+ log_step("okf/1: exporting OKF bundle", project_root=root, trace=True)
92
+ _run_okf_export(root, db, output, skills, design)
93
+ log_step("okf/complete", project_root=root, trace=True)
94
+
95
+
96
+ def _run_okf_export(root, db, output, skills, design):
84
97
  out_dir = output.expanduser().resolve()
85
98
  with db.get_session() as session:
86
99
  graph = ArtifactGraphRepository(session).load_graph()
@@ -90,8 +103,8 @@ def export(
90
103
  from devcouncil.app.config import load_config
91
104
 
92
105
  project_name = load_config(root).project.name or project_name
93
- except Exception:
94
- pass
106
+ except Exception as e:
107
+ logger.debug("Failed to load project name from config, using directory name: %s", e)
95
108
 
96
109
  # Load the FULL skill set (packaged library + this repo's own skills) so the export is
97
110
  # complete; goal-driven selection would only emit the few skills that match a goal.
@@ -219,6 +232,31 @@ def validate(
219
232
  raise typer.Exit(code=1)
220
233
 
221
234
 
235
+ @app.command("select")
236
+ def select_knowledge(
237
+ goal: str = typer.Option(..., "--goal", "-g", help="Task goal used to rank ingested knowledge sources."),
238
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
239
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
240
+ ) -> None:
241
+ """Select OKF/design knowledge sources that match a goal."""
242
+ from devcouncil.knowledge.knowledge_select import select_knowledge_payload
243
+
244
+ root = project_root.expanduser().resolve()
245
+ payload = select_knowledge_payload(root, goal)
246
+ if json_output:
247
+ console.print_json(data=payload)
248
+ return
249
+ sources = payload.get("sources")
250
+ if isinstance(sources, list):
251
+ for source in sources:
252
+ if isinstance(source, dict):
253
+ console.print(
254
+ f"- [{source.get('kind')}] {source.get('name')}: {source.get('description')}"
255
+ )
256
+ if payload.get("preamble"):
257
+ console.print("\n" + str(payload["preamble"]))
258
+
259
+
222
260
  @app.command("html")
223
261
  def html(
224
262
  bundle: Path = typer.Argument(..., help="Path to an OKF bundle directory to render."),
@@ -1,8 +1,8 @@
1
1
  import typer
2
2
  import asyncio
3
- import json
4
3
  import datetime
5
4
  import logging
5
+ import sys
6
6
  from typing import Any
7
7
  from rich.console import Console
8
8
  from rich.panel import Panel
@@ -14,13 +14,19 @@ from devcouncil.storage.repositories import (
14
14
  GapRepository,
15
15
  PlanningStateRepository,
16
16
  )
17
- from devcouncil.indexing.repo_mapper import RepoMapper
18
17
  from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
19
18
  from devcouncil.llm.provider import Provider, MockProvider, ProviderRequestError, build_role_model_config, create_provider, validate_model_provider
20
19
  from devcouncil.llm.router import ModelRouter, StructuredOutputError
21
20
  from devcouncil.planning.spec_service import SpecService
22
21
  from devcouncil.planning.prompt_enhancer_service import PromptEnhancerService, save_active_prompt_enhancement
23
22
  from devcouncil.planning.plan_service import PlanService, backfill_acceptance_criteria
23
+ from devcouncil.planning.plan_difficulty import apply_plan_difficulty
24
+ from devcouncil.planning.planned_files_reconcile import (
25
+ expand_scope_with_dependents,
26
+ reconcile_planned_files,
27
+ repo_files_from_map,
28
+ )
29
+ from devcouncil.planning.question_conversion import convert_blocking_questions_to_assumptions
24
30
  from devcouncil.planning.critique_service import CritiqueService
25
31
  from devcouncil.planning.arbiter_service import ArbiterDecision, ArbiterService
26
32
  from devcouncil.gating.policy import GatePolicy
@@ -29,7 +35,8 @@ from devcouncil.app.state_machine import ProjectPhase
29
35
  from devcouncil.app.config import ModelRoleConfig, load_config, get_api_key
30
36
  from devcouncil.cli.commands.init import initialize_project
31
37
  from devcouncil.telemetry.traces import TraceLogger
32
- from devcouncil.telemetry.stages import log_step
38
+ from devcouncil.telemetry.stages import log_stage, log_step
39
+ from devcouncil.utils.json_persist import dump_json
33
40
 
34
41
  app = typer.Typer()
35
42
  console = Console()
@@ -86,6 +93,46 @@ def _ensure_planning_roles(config) -> None:
86
93
  for role in REQUIRED_PLANNING_ROLES:
87
94
  config.models.roles.setdefault(role, fallback.model_copy())
88
95
 
96
+
97
+ def _should_auto_convert_blocking_questions(config) -> bool:
98
+ if not config.planning.auto_convert_blocking_questions_in_noninteractive:
99
+ return False
100
+ return not sys.stdin.isatty()
101
+
102
+
103
+ def _maybe_convert_blocking_questions(
104
+ spec_output,
105
+ config,
106
+ console,
107
+ *,
108
+ orchestrator=None,
109
+ artifact_path: Path | None = None,
110
+ ):
111
+ """In non-interactive mode, turn blocking questions into non-blocking assumptions."""
112
+ if not _should_auto_convert_blocking_questions(config):
113
+ return spec_output
114
+ if not spec_output.blocking_questions:
115
+ return spec_output
116
+
117
+ question_count = len(spec_output.blocking_questions)
118
+ assumptions, cleared = convert_blocking_questions_to_assumptions(
119
+ spec_output.assumptions,
120
+ spec_output.blocking_questions,
121
+ )
122
+ spec_output = spec_output.model_copy(
123
+ update={"assumptions": assumptions, "blocking_questions": cleared},
124
+ )
125
+ console.print(
126
+ f"[dim]Non-interactive mode: converted {question_count} blocking question(s) "
127
+ "to open assumptions (will not block plan approval).[/dim]"
128
+ )
129
+ if orchestrator is not None and orchestrator.current_run is not None:
130
+ orchestrator.save_run_artifact("requirements.json", spec_output.model_dump())
131
+ elif artifact_path is not None:
132
+ artifact_path.write_text(spec_output.model_dump_json(indent=2), encoding="utf-8")
133
+ return spec_output
134
+
135
+
89
136
  async def run_plan_flow(
90
137
  goal: str,
91
138
  requirements_only: bool = False,
@@ -129,7 +176,7 @@ async def run_plan_flow(
129
176
 
130
177
  provider = MockProvider()
131
178
  provider.responses = {
132
- "mock/prompt_enhancer": json.dumps({
179
+ "mock/prompt_enhancer": dump_json({
133
180
  "original_goal": goal,
134
181
  "enhanced_goal": (
135
182
  f"Plan and implement {goal} using the mapped repository's "
@@ -139,26 +186,26 @@ async def run_plan_flow(
139
186
  "debate_focus": ["Compare minimal implementation scope against production-readiness concerns."],
140
187
  "constraints": [f"Do not broaden beyond: {goal}."]
141
188
  }),
142
- "mock/spec_writer": json.dumps({
189
+ "mock/spec_writer": dump_json({
143
190
  "requirements": [{"id": "REQ-001", "title": "Mock Req", "description": "Desc", "priority": "high", "source": "user", "acceptance_criteria": []}],
144
191
  "assumptions": [],
145
192
  "blocking_questions": []
146
193
  }),
147
194
  "mock/planner_a": [
148
- json.dumps({
195
+ dump_json({
149
196
  "id": "PLAN-A", "rationale": "Simple", "tasks": [{"id": "TASK-001", "title": "Mock Task", "description": "Desc", "requirement_ids": ["REQ-001"], "acceptance_criterion_ids": [], "planned_files": [], "expected_tests": [], "allowed_commands": [], "status": "planned"}]
150
197
  }),
151
- json.dumps({"rebuttals": []})
198
+ dump_json({"rebuttals": []})
152
199
  ],
153
200
  "mock/planner_b": [
154
- json.dumps({
201
+ dump_json({
155
202
  "id": "PLAN-B", "rationale": "Robust", "tasks": [{"id": "TASK-001", "title": "Mock Task", "description": "Desc", "requirement_ids": ["REQ-001"], "acceptance_criterion_ids": [], "planned_files": [], "expected_tests": [], "allowed_commands": [], "status": "planned"}]
156
203
  }),
157
- json.dumps({"rebuttals": []})
204
+ dump_json({"rebuttals": []})
158
205
  ],
159
206
  "mock/critic_a": '{"findings": []}',
160
207
  "mock/critic_b": '{"findings": []}',
161
- "mock/arbiter": json.dumps({
208
+ "mock/arbiter": dump_json({
162
209
  "accepted_finding_ids": [], "rejected_finding_ids": [],
163
210
  "final_requirements": [{"id": "REQ-001", "title": "Mock Req", "description": "Desc", "priority": "high", "source": "user", "acceptance_criteria": [{"id": "AC-1", "description": "Test it", "verification_method": "unit_test"}]}],
164
211
  "final_tasks": [{"id": "TASK-001", "title": "Mock Task", "description": "Desc", "requirement_ids": ["REQ-001"], "acceptance_criterion_ids": ["AC-1"], "planned_files": [{"path": "test.py", "reason": "logic", "allowed_change": "modify"}], "expected_tests": [], "allowed_commands": [], "status": "planned"}]
@@ -182,17 +229,37 @@ async def run_plan_flow(
182
229
  plan_service = PlanService(router)
183
230
  critique_service = CritiqueService(router)
184
231
  arbiter_service = ArbiterService(router)
185
- mapper = RepoMapper(root)
232
+ with log_stage("plan", project_root=root, run_id=run_id, goal=goal, quick=quick, dry_run=dry_run):
233
+ return await _run_plan_body(
234
+ root, goal, requirements_only, dry_run, persist, quick,
235
+ orchestrator, run_id, db, config, router,
236
+ prompt_enhancer, spec_service, plan_service, critique_service,
237
+ arbiter_service,
238
+ )
186
239
 
240
+
241
+ async def _run_plan_body(
242
+ root, goal, requirements_only, dry_run, persist, quick,
243
+ orchestrator, run_id, db, config, router,
244
+ prompt_enhancer, spec_service, plan_service, critique_service,
245
+ arbiter_service,
246
+ ):
187
247
  with Progress(
188
248
  SpinnerColumn(),
189
249
  TextColumn("[progress.description]{task.description}"),
190
250
  transient=True,
191
251
  ) as progress:
192
252
  # 1. Repo Map
193
- log_step("plan/1: mapping repository", project_root=root, run_id=run_id)
253
+ log_step("plan/1: mapping repository", project_root=root, run_id=run_id, trace=True)
194
254
  progress.add_task(description="Mapping repository...", total=None)
195
- repo_map = mapper.map_repo(goal)
255
+ from devcouncil.indexing.map_artifacts import refresh_map_artifacts
256
+
257
+ repo_map = refresh_map_artifacts(
258
+ root,
259
+ root / ".devcouncil" / "repo_map.json",
260
+ goal,
261
+ quiet=True,
262
+ ).repo_map
196
263
  repo_map_json = repo_map.model_dump_json(indent=2)
197
264
  # save_run_artifact re-serializes via json.dump, so pass the dict directly
198
265
  # instead of round-tripping the already-serialized JSON back through json.loads.
@@ -203,7 +270,7 @@ async def run_plan_flow(
203
270
  await orchestrator.transition_to(ProjectPhase.REPO_MAPPED)
204
271
 
205
272
  # 2. Codebase-specific prompt enhancement
206
- log_step("plan/2: enhancing prompt for codebase debate", project_root=root, run_id=run_id)
273
+ log_step("plan/2: enhancing prompt for codebase debate", project_root=root, run_id=run_id, trace=True)
207
274
  progress.add_task(description="Enhancing prompt for codebase debate...", total=None)
208
275
  prompt_enhancement = await prompt_enhancer.enhance_prompt(
209
276
  goal,
@@ -234,7 +301,7 @@ async def run_plan_flow(
234
301
  )
235
302
 
236
303
  # 3. Spec / Requirements
237
- log_step("plan/3: generating requirements", project_root=root, run_id=run_id)
304
+ log_step("plan/3: generating requirements", project_root=root, run_id=run_id, trace=True)
238
305
  progress.add_task(description="Generating requirements...", total=None)
239
306
  spec_output = await spec_service.generate_spec(debate_goal, repo_map_json)
240
307
  orchestrator.save_run_artifact("requirements.json", spec_output.model_dump())
@@ -244,7 +311,7 @@ async def run_plan_flow(
244
311
  console.print(Panel(f"Found {len(spec_output.requirements)} requirements.", title="Requirements Generated"))
245
312
  return []
246
313
 
247
- requirements_json = json.dumps([r.model_dump() for r in spec_output.requirements])
314
+ requirements_json = dump_json([r.model_dump() for r in spec_output.requirements])
248
315
 
249
316
  if quick:
250
317
  # Rigor dial: single pragmatic plan, no A/B debate, critique, rebuttal,
@@ -253,7 +320,7 @@ async def run_plan_flow(
253
320
  # adversarial robustness for ~5 fewer model calls — the right setting
254
321
  # for small, well-scoped changes where verification (which still gates
255
322
  # every diff) is the real safety net, not planning debate.
256
- log_step("plan/4: generating single plan (quick mode)", project_root=root, run_id=run_id)
323
+ log_step("plan/4: generating single plan (quick mode)", project_root=root, run_id=run_id, trace=True)
257
324
  progress.add_task(description="Generating single plan (quick mode)...", total=None)
258
325
  plan_a = await plan_service.generate_plan(
259
326
  "planner_a", debate_goal, requirements_json, repo_map_json
@@ -277,7 +344,7 @@ async def run_plan_flow(
277
344
  final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
278
345
  else:
279
346
  # 4. Independent Plans (run concurrently — they don't depend on each other)
280
- log_step("plan/4: generating Plans A (pragmatic) and B (robust)", project_root=root, run_id=run_id)
347
+ log_step("plan/4: generating Plans A (pragmatic) and B (robust)", project_root=root, run_id=run_id, trace=True)
281
348
  progress.add_task(description="Generating Plans A (Pragmatic) and B (Robust)...", total=None)
282
349
  plan_a, plan_b = await asyncio.gather(
283
350
  plan_service.generate_plan("planner_a", debate_goal, requirements_json, repo_map_json),
@@ -293,7 +360,7 @@ async def run_plan_flow(
293
360
  plan_b_json = plan_b.model_dump_json()
294
361
 
295
362
  # 5. Cross-Critique (independent — run concurrently)
296
- log_step("plan/5: cross-critiquing Plans A and B", project_root=root, run_id=run_id)
363
+ log_step("plan/5: cross-critiquing Plans A and B", project_root=root, run_id=run_id, trace=True)
297
364
  progress.add_task(description="Critiquing Plans A and B...", total=None)
298
365
  critique_a, critique_b = await asyncio.gather(
299
366
  critique_service.generate_critique("critic_a", plan_b_json, requirements_json),
@@ -306,7 +373,7 @@ async def run_plan_flow(
306
373
  critique_b_json = critique_b.model_dump_json()
307
374
 
308
375
  # 6. Rebuttals (independent — run concurrently)
309
- log_step("plan/6: generating rebuttals", project_root=root, run_id=run_id)
376
+ log_step("plan/6: generating rebuttals", project_root=root, run_id=run_id, trace=True)
310
377
  progress.add_task(description="Generating rebuttals...", total=None)
311
378
  rebuttal_a, rebuttal_b = await asyncio.gather(
312
379
  critique_service.generate_rebuttal("planner_a", plan_a_json, critique_b_json),
@@ -316,7 +383,7 @@ async def run_plan_flow(
316
383
  orchestrator.save_run_artifact("rebuttal_b.json", rebuttal_b.model_dump())
317
384
 
318
385
  # 7. Arbitration
319
- log_step("plan/7: arbitrating final plan", project_root=root, run_id=run_id)
386
+ log_step("plan/7: arbitrating final plan", project_root=root, run_id=run_id, trace=True)
320
387
  progress.add_task(description="Arbitrating final plan...", total=None)
321
388
  decision = await arbiter_service.arbitrate(
322
389
  debate_goal,
@@ -350,12 +417,37 @@ async def run_plan_flow(
350
417
  f"[dim]Linked {len(backfilled_acs)} unmapped acceptance criterion(s) to owning task(s) "
351
418
  "so every elaborated behavior is verified.[/dim]"
352
419
  )
420
+ # Ground planner-named files in the real repo before they become the scope
421
+ # whitelist: repair a typo'd/renamed modify target to its true path so the
422
+ # agent's correct write isn't reverted for pointing at a path the planner
423
+ # misspelled. Only relaxes/repairs scope — never tightens it.
424
+ final_tasks, planned_file_warnings = reconcile_planned_files(
425
+ final_tasks, repo_files_from_map(repo_map)
426
+ )
427
+ for warning in planned_file_warnings:
428
+ console.print(f"[yellow]{warning}[/yellow]")
429
+
430
+ # The other half: add the real callers (repo-map dependents) of each writable
431
+ # file so a caller the planner omitted isn't reverted mid-run. Only widens scope.
432
+ final_tasks, scope_widen_warnings = expand_scope_with_dependents(
433
+ final_tasks, repo_map.dependents, repo_files_from_map(repo_map)
434
+ )
435
+ for warning in scope_widen_warnings:
436
+ console.print(f"[dim]{warning}[/dim]")
437
+
438
+ final_tasks, difficulty_warnings = apply_plan_difficulty(final_tasks, decision.final_requirements)
439
+ for warning in difficulty_warnings:
440
+ console.print(f"[yellow]{warning}[/yellow]")
353
441
 
354
442
  console.print(f"Final Requirements: [bold]{len(decision.final_requirements)}[/bold]")
355
443
  console.print(f"Final Tasks: [bold]{len(final_tasks)}[/bold]")
356
444
 
445
+ spec_output = _maybe_convert_blocking_questions(
446
+ spec_output, config, console, orchestrator=orchestrator,
447
+ )
448
+
357
449
  # 8. Check Gates
358
- log_step("plan/8: checking plan-approval gates", project_root=root, run_id=run_id)
450
+ log_step("plan/8: checking plan-approval gates", project_root=root, run_id=run_id, trace=True)
359
451
  policy = GatePolicy()
360
452
  result = policy.check_plan_approval(
361
453
  decision.final_requirements,
@@ -384,6 +476,7 @@ async def run_plan_flow(
384
476
  console.print("[green]Plan approved by gates.[/green]")
385
477
  logger.info("Plan approved by gates: %d task(s)", len(final_tasks))
386
478
  await orchestrator.transition_to(ProjectPhase.PLAN_APPROVED)
479
+ log_step("plan/complete: approved", project_root=root, run_id=run_id, trace=True)
387
480
  return [task.id for task in final_tasks]
388
481
  else:
389
482
  if persist:
@@ -394,6 +487,7 @@ async def run_plan_flow(
394
487
  console.print("[yellow]Plan generated but failed gates. See status for gaps.[/yellow]")
395
488
  logger.warning("Plan failed approval gates with %d gap(s)", len(result.gaps))
396
489
  await orchestrator.transition_to(ProjectPhase.AWAITING_USER_DECISIONS)
490
+ log_step("plan/complete: awaiting user decisions", project_root=root, run_id=run_id, trace=True)
397
491
  return []
398
492
 
399
493
  def _latest_run_with_decision(root: Path, run_id: str | None) -> Path | None:
@@ -422,64 +516,81 @@ def approve(
422
516
  from devcouncil.planning.spec_service import SpecOutput
423
517
 
424
518
  root = project_root.expanduser().resolve()
519
+ from devcouncil.telemetry.logging_setup import set_log_dir
520
+ set_log_dir(root)
521
+ logger.info("dev plan approve: run_id=%s force=%s", run_id or "latest", force)
425
522
  db = get_db(root)
426
523
  if not db:
427
524
  console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
428
525
  raise typer.Exit(code=1)
429
526
 
430
- run_dir = _latest_run_with_decision(root, run_id)
431
- if run_dir is None:
432
- console.print("[red]No planning run with a decision was found. Run 'dev plan' first.[/red]")
433
- raise typer.Exit(code=1)
434
-
435
- decision = ArbiterDecision.model_validate_json((run_dir / "decision.json").read_text(encoding="utf-8"))
436
- spec_path = run_dir / "requirements.json"
437
- spec_output = (
438
- SpecOutput.model_validate_json(spec_path.read_text(encoding="utf-8")) if spec_path.exists() else None
439
- )
440
-
441
- findings = []
442
- for name in ("critique_a.json", "critique_b.json"):
443
- critique_path = run_dir / name
444
- if critique_path.exists():
445
- findings.extend(CritiqueOutput.model_validate_json(critique_path.read_text(encoding="utf-8")).findings)
446
- reconciled_findings = _reconcile_findings(findings, decision)
447
- final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
448
- assumptions = spec_output.assumptions if spec_output else []
527
+ with log_stage("approve", project_root=root, run_id=run_id or "latest", force=force):
528
+ log_step("approve/1: loading plan decision", project_root=root, trace=True)
529
+ run_dir = _latest_run_with_decision(root, run_id)
530
+ if run_dir is None:
531
+ console.print("[red]No planning run with a decision was found. Run 'dev plan' first.[/red]")
532
+ raise typer.Exit(code=1)
533
+
534
+ decision = ArbiterDecision.model_validate_json((run_dir / "decision.json").read_text(encoding="utf-8"))
535
+ spec_path = run_dir / "requirements.json"
536
+ spec_output = (
537
+ SpecOutput.model_validate_json(spec_path.read_text(encoding="utf-8")) if spec_path.exists() else None
538
+ )
449
539
 
450
- policy = GatePolicy()
451
- result = policy.check_plan_approval(
452
- decision.final_requirements,
453
- final_tasks,
454
- assumptions=assumptions,
455
- findings=reconciled_findings,
456
- blocking_questions=spec_output.blocking_questions if spec_output else [],
457
- )
458
- if not result.passed and not force:
459
- console.print("[yellow]Plan still fails approval gates:[/yellow]")
460
- for gap in result.gaps:
461
- marker = "[red][BLOCKING][/red] " if gap.blocking else ""
462
- console.print(f" - {marker}{gap.description} (Fix: {gap.recommended_fix})")
463
- console.print("Resolve the gaps and re-run 'dev plan', or use --force to approve anyway.")
464
- raise typer.Exit(code=1)
540
+ config = load_config(root)
541
+ if spec_output is not None:
542
+ spec_output = _maybe_convert_blocking_questions(
543
+ spec_output,
544
+ config,
545
+ console,
546
+ artifact_path=spec_path if spec_path.exists() else None,
547
+ )
465
548
 
466
- with db.get_session() as session:
467
- GapRepository(session).delete_plan_gaps()
468
- PlanningStateRepository(session).replace_active_plan(
549
+ findings = []
550
+ for name in ("critique_a.json", "critique_b.json"):
551
+ critique_path = run_dir / name
552
+ if critique_path.exists():
553
+ findings.extend(CritiqueOutput.model_validate_json(critique_path.read_text(encoding="utf-8")).findings)
554
+ reconciled_findings = _reconcile_findings(findings, decision)
555
+ final_tasks = [task.model_copy(update={"status": "planned"}) for task in decision.final_tasks]
556
+ final_tasks, _ = apply_plan_difficulty(final_tasks, decision.final_requirements)
557
+ assumptions = spec_output.assumptions if spec_output else []
558
+
559
+ policy = GatePolicy()
560
+ log_step("approve/2: checking plan-approval gates", project_root=root)
561
+ result = policy.check_plan_approval(
469
562
  decision.final_requirements,
470
- assumptions,
471
563
  final_tasks,
472
- reconciled_findings,
564
+ assumptions=assumptions,
565
+ findings=reconciled_findings,
566
+ blocking_questions=spec_output.blocking_questions if spec_output else [],
473
567
  )
568
+ if not result.passed and not force:
569
+ console.print("[yellow]Plan still fails approval gates:[/yellow]")
570
+ for gap in result.gaps:
571
+ marker = "[red][BLOCKING][/red] " if gap.blocking else ""
572
+ console.print(f" - {marker}{gap.description} (Fix: {gap.recommended_fix})")
573
+ console.print("Resolve the gaps and re-run 'dev plan', or use --force to approve anyway.")
574
+ raise typer.Exit(code=1)
474
575
 
475
- orchestrator = Orchestrator(root)
476
- try:
477
- asyncio.run(orchestrator.transition_to(ProjectPhase.PLAN_APPROVED))
478
- except ValueError as exc:
479
- console.print(f"[red]Cannot approve from the current project phase: {exc}[/red]")
480
- raise typer.Exit(code=1)
481
- console.print(f"[green]Plan from run {run_dir.name} approved ({len(final_tasks)} tasks).[/green]")
482
- console.print("Use 'dev tasks list' to see the planned tasks and 'dev run TASK-ID' to execute one.")
576
+ with db.get_session() as session:
577
+ GapRepository(session).delete_plan_gaps()
578
+ PlanningStateRepository(session).replace_active_plan(
579
+ decision.final_requirements,
580
+ assumptions,
581
+ final_tasks,
582
+ reconciled_findings,
583
+ )
584
+
585
+ orchestrator = Orchestrator(root)
586
+ try:
587
+ asyncio.run(orchestrator.transition_to(ProjectPhase.PLAN_APPROVED))
588
+ except ValueError as exc:
589
+ console.print(f"[red]Cannot approve from the current project phase: {exc}[/red]")
590
+ raise typer.Exit(code=1)
591
+ console.print(f"[green]Plan from run {run_dir.name} approved ({len(final_tasks)} tasks).[/green]")
592
+ console.print("Use 'dev tasks list' to see the planned tasks and 'dev run TASK-ID' to execute one.")
593
+ log_step("approve/complete", project_root=root, run_id=run_dir.name, trace=True)
483
594
 
484
595
 
485
596
  @app.command()
@@ -504,6 +615,10 @@ def plan(
504
615
  Run the full planning cycle (Repo map -> Spec -> Plan A/B -> Critique -> Arbiter).
505
616
  """
506
617
  should_persist = persist or not dry_run
618
+ root = project_root.expanduser().resolve()
619
+ from devcouncil.telemetry.logging_setup import set_log_dir
620
+ set_log_dir(root)
621
+ logger.info("dev plan: quick=%s dry_run=%s requirements_only=%s", quick, dry_run, requirements_only)
507
622
  try:
508
623
  asyncio.run(run_plan_flow(goal, requirements_only, dry_run, should_persist, project_root, quick=quick))
509
624
  except (ProviderRequestError, StructuredOutputError) as exc:
@@ -1,4 +1,5 @@
1
- import json
1
+ from devcouncil.utils.json_persist import dump_json
2
+ import logging
2
3
  from typing import NoReturn
3
4
  import typer
4
5
  from rich.console import Console
@@ -9,9 +10,11 @@ from devcouncil.storage.repositories import TaskRepository, RequirementRepositor
9
10
  from pathlib import Path
10
11
 
11
12
  from devcouncil.execution.prompt_builder import PromptBuilder
13
+ from devcouncil.telemetry.stages import log_stage, log_step
12
14
 
13
15
  app = typer.Typer()
14
16
  console = Console()
17
+ logger = logging.getLogger(__name__)
15
18
 
16
19
  @app.callback(invoke_without_command=True)
17
20
  def prompt(
@@ -29,33 +32,39 @@ def prompt(
29
32
 
30
33
  def _fail(message: str) -> NoReturn:
31
34
  if json_format:
32
- typer.echo(json.dumps({"ok": False, "task_id": task_id, "error": message}, indent=2))
35
+ typer.echo(dump_json({"ok": False, "task_id": task_id, "error": message}, indent=2))
33
36
  else:
34
37
  console.print(f"[red]{message}[/red]")
35
38
  raise typer.Exit(code=1)
36
39
 
37
40
  root = project_root.expanduser().resolve()
41
+ from devcouncil.telemetry.logging_setup import set_log_dir
42
+ set_log_dir(root)
43
+ logger.info("dev prompt: task=%s pretty=%s", task_id, pretty)
38
44
  initialize_project(root, quiet=True)
39
45
  db = get_db(root)
40
46
  if not db:
41
47
  _fail("DevCouncil state is unavailable in this directory.")
42
48
 
43
- with db.get_session() as session:
44
- task_repo = TaskRepository(session)
45
- req_repo = RequirementRepository(session)
49
+ with log_stage("prompt", project_root=root, task_id=task_id):
50
+ log_step("prompt/1: loading task and building prompt", project_root=root, task_id=task_id, trace=True)
51
+ with db.get_session() as session:
52
+ task_repo = TaskRepository(session)
53
+ req_repo = RequirementRepository(session)
46
54
 
47
- task = task_repo.get_by_id(task_id)
48
- if not task:
49
- _fail(f"Task {task_id} not found.")
55
+ task = task_repo.get_by_id(task_id)
56
+ if not task:
57
+ _fail(f"Task {task_id} not found.")
50
58
 
51
- reqs = req_repo.get_all()
59
+ reqs = req_repo.get_all()
52
60
 
53
- builder = PromptBuilder(root)
54
- task_prompt = builder.build_task_prompt(task, reqs)
61
+ builder = PromptBuilder(root)
62
+ task_prompt = builder.build_task_prompt(task, reqs)
55
63
 
56
- if json_format:
57
- typer.echo(json.dumps({"ok": True, "task_id": task_id, "prompt": task_prompt}, indent=2))
58
- elif pretty:
59
- console.print(Markdown(task_prompt))
60
- else:
61
- typer.echo(task_prompt, nl=not task_prompt.endswith("\n"))
64
+ if json_format:
65
+ typer.echo(dump_json({"ok": True, "task_id": task_id, "prompt": task_prompt}, indent=2))
66
+ elif pretty:
67
+ console.print(Markdown(task_prompt))
68
+ else:
69
+ typer.echo(task_prompt, nl=not task_prompt.endswith("\n"))
70
+ log_step("prompt/complete", project_root=root, task_id=task_id, trace=True)