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
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: devcouncil
3
+ title: DevCouncil Integration for Claude Code
4
+ description: Operate inside a DevCouncil-managed repo — use MCP tools and slash commands for status, scope, policy-gated writes, and evidence-first verification instead of guessing project state.
5
+ triggers:
6
+ keywords: [devcouncil, dev council, devcouncil mcp, mcp devcouncil, dev integrate]
7
+ markers: [.devcouncil/config.yaml]
8
+ ---
9
+
10
+ # DevCouncil Integration for Claude Code
11
+
12
+ This repository is managed by **DevCouncil**: a planning, execution, and verification
13
+ layer for coding agents. Evidence — not model confidence — decides when work is done.
14
+
15
+ Use DevCouncil's MCP tools and `dev` CLI for project state, task scope, and verification.
16
+ Do not guess at task status, file scope, or whether tests actually prove the diff.
17
+
18
+ ## Navigate before you edit
19
+
20
+ 1. Open `.devcouncil/repo_map.json` for subsystem entry points, critical files, and
21
+ cross-subsystem handoff paths. Regenerate with `dev map` after large refactors.
22
+ 2. Read `AGENTS.md` / `CLAUDE.md` for workspace conventions.
23
+ 3. Check status: `devcouncil_status` (MCP) or `/devcouncil:status` (slash command) or
24
+ `dev status` (CLI).
25
+
26
+ ## MCP tools vs CLI vs slash commands
27
+
28
+ | Need | Prefer |
29
+ |---|---|
30
+ | Task loop (checkout, write, verify, release) | MCP tools (`mcp__devcouncil__devcouncil_*`) |
31
+ | Quick human-readable status / reports | Slash commands (`/devcouncil:status`, `/devcouncil:report`) |
32
+ | Scripting, CI, headless runs | `dev` CLI (`dev verify`, `dev go`, `dev e2e`) |
33
+ | Read-only inspection (gaps, provenance, diff) | MCP read tools before re-running verify |
34
+
35
+ MCP tool names in Claude Code are prefixed: `mcp__devcouncil__devcouncil_<name>`.
36
+
37
+ ## Slash commands (Claude Code)
38
+
39
+ Install with `dev integrate claude --apply`. Commands live under `/devcouncil:*`:
40
+
41
+ | Command | Purpose |
42
+ |---|---|
43
+ | `/devcouncil:status` | Phase, tasks, blocking gaps |
44
+ | `/devcouncil:next` | Pick up the next unblocked task via MCP |
45
+ | `/devcouncil:verify [TASK-ID]` | Run verification, report gaps |
46
+ | `/devcouncil:repair [TASK-ID]` | Repair blocking gaps |
47
+ | `/devcouncil:plan <goal>` | Plan a goal into requirements/tasks |
48
+ | `/devcouncil:review [TASK-ID]` | Live-review critique cards |
49
+ | `/devcouncil:report` | Full coverage report |
50
+ | `/devcouncil:map [goal]` | Refresh/read the repo map |
51
+ | `/devcouncil:wiki [topic]` | Consult the codebase wiki |
52
+ | `/devcouncil:supervise [RUN-ID]` | Review a recorded agent run |
53
+
54
+ ## Key MCP tools (by role)
55
+
56
+ **Orientation:** `devcouncil_status`, `devcouncil_report`, `devcouncil_integration_status`,
57
+ `devcouncil_wiki_page`, `devcouncil_graph_context`
58
+
59
+ **Task loop:** `devcouncil_next_task`, `devcouncil_checkout_task`, `devcouncil_get_task`,
60
+ `devcouncil_get_prompt`, `devcouncil_release_task`, `devcouncil_renew_lease`
61
+
62
+ **Policy-gated writes:** `devcouncil_write_file`, `devcouncil_apply_patch`,
63
+ `devcouncil_policy_check_write` (preflight), `devcouncil_run_command`,
64
+ `devcouncil_record_command`
65
+
66
+ **Read-only inspection:** `devcouncil_read_file`, `devcouncil_get_diff`,
67
+ `devcouncil_get_gaps`, `devcouncil_get_next_actions`, `devcouncil_get_evidence`,
68
+ `devcouncil_get_task_provenance`
69
+
70
+ **Verification:** `devcouncil_verify_task` (requires an active lease)
71
+
72
+ **Live review:** `devcouncil_live_review`, `devcouncil_live_cards`,
73
+ `devcouncil_live_repair_prompt`
74
+
75
+ ## Subagents
76
+
77
+ When delegated, use the bundled subagents:
78
+
79
+ - **devcouncil-implementer** — checkout → scoped edits → verify → release
80
+ - **devcouncil-verifier** — read-only verification and gap reporting
81
+ - **devcouncil-reviewer** — policy-aware diff review via live critique cards
82
+
83
+ ## Rules of engagement
84
+
85
+ - **Scope:** edit only files declared in the task's planned scope. Out-of-scope or
86
+ protected paths are rejected by the write gate.
87
+ - **Evidence:** run tests and call `devcouncil_verify_task` before claiming done.
88
+ - **Leases:** one agent owns a task at a time; checkout before writes, release when verified.
89
+ - **Repairs:** when gaps exist, read `devcouncil_get_next_actions` and act on each
90
+ typed action — do not declare success while blocking gaps remain.
91
+
92
+ For the full autonomous loop, follow the **devcouncil-hero-loop** skill. For verifier
93
+ gates and the next-actions contract, follow **devcouncil-verification**.
@@ -11,6 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  import fnmatch
13
13
  import functools
14
+ import logging
14
15
  import os
15
16
  import re
16
17
  from pathlib import Path
@@ -19,6 +20,8 @@ from pydantic import BaseModel, Field
19
20
 
20
21
  from devcouncil.knowledge.frontmatter import build_frontmatter_markdown, split_frontmatter
21
22
 
23
+ logger = logging.getLogger(__name__)
24
+
22
25
  LIBRARY_DIR = Path(__file__).resolve().parent / "library"
23
26
 
24
27
 
@@ -50,6 +53,7 @@ _MAX_WALK_FILES = 20_000
50
53
  class SkillTriggers(BaseModel):
51
54
  keywords: list[str] = Field(default_factory=list)
52
55
  globs: list[str] = Field(default_factory=list)
56
+ markers: list[str] = Field(default_factory=list)
53
57
 
54
58
 
55
59
  class Skill(BaseModel):
@@ -61,7 +65,12 @@ class Skill(BaseModel):
61
65
  body: str = ""
62
66
  source_path: Path | None = None
63
67
 
64
- def matches(self, goal: str, repo_files_present: "set[str] | None" = None) -> bool:
68
+ def matches(
69
+ self,
70
+ goal: str,
71
+ repo_files_present: "set[str] | None" = None,
72
+ project_root: Path | None = None,
73
+ ) -> bool:
65
74
  """True if this skill applies to the given goal text / repo file basenames."""
66
75
  if self.always:
67
76
  return True
@@ -73,9 +82,18 @@ class Skill(BaseModel):
73
82
  pat = pattern.lower()
74
83
  if any(fnmatch.fnmatch(name, pat) for name in repo_files_present):
75
84
  return True
85
+ if project_root is not None:
86
+ for marker in self.triggers.markers:
87
+ if (project_root / marker).exists():
88
+ return True
76
89
  return False
77
90
 
78
- def relevance_score(self, goal: str, repo_files_present: "set[str] | None" = None) -> int:
91
+ def relevance_score(
92
+ self,
93
+ goal: str,
94
+ repo_files_present: "set[str] | None" = None,
95
+ project_root: Path | None = None,
96
+ ) -> int:
79
97
  """How strongly this skill applies — used to rank which skills ride inline before
80
98
  the size budget truncates. Goal-text keyword hits weigh more than file-glob
81
99
  presence; always-on skills sort first regardless."""
@@ -88,6 +106,8 @@ class Skill(BaseModel):
88
106
  1 for pattern in self.triggers.globs
89
107
  if any(fnmatch.fnmatch(name, pattern.lower()) for name in repo_files_present)
90
108
  )
109
+ if project_root is not None:
110
+ score += sum(1 for marker in self.triggers.markers if (project_root / marker).exists())
91
111
  return score
92
112
 
93
113
  def to_skill_md(self) -> str:
@@ -113,6 +133,7 @@ def _skill_from_meta(path: Path, meta: dict, body: str) -> Skill:
113
133
  triggers=SkillTriggers(
114
134
  keywords=list(triggers.get("keywords") or []),
115
135
  globs=list(triggers.get("globs") or []),
136
+ markers=list(triggers.get("markers") or []),
116
137
  ),
117
138
  body=body.strip(),
118
139
  source_path=path,
@@ -240,7 +261,9 @@ def load_skills(
240
261
  # selection metadata when it doesn't declare its own — otherwise
241
262
  # scaffolding a skill silently strips its `always`/triggers and the
242
263
  # skill stops being selected (selection would return nothing).
243
- has_own_triggers = bool(skill.triggers.keywords or skill.triggers.globs)
264
+ has_own_triggers = bool(
265
+ skill.triggers.keywords or skill.triggers.globs or skill.triggers.markers
266
+ )
244
267
  skill = skill.model_copy(update={
245
268
  "always": skill.always or base.always,
246
269
  "triggers": skill.triggers if has_own_triggers else base.triggers,
@@ -256,8 +279,8 @@ def load_skills(
256
279
  try:
257
280
  from devcouncil.app.config import load_config
258
281
  knowledge_dir = load_config(project_root).knowledge.directory
259
- except Exception:
260
- pass
282
+ except Exception as e:
283
+ logger.debug("Failed to load knowledge directory from config, using default: %s", e)
261
284
  for skill in load_okf_skills(project_root, directory=knowledge_dir):
262
285
  # Only fill gaps: library + repo-local skills win on name conflict.
263
286
  by_name.setdefault(skill.name, skill)
@@ -342,7 +365,7 @@ def select_skills(
342
365
  # Rank by relevance so the most applicable domain skill survives the inline budget on a
343
366
  # polyglot repo; always-on skills keep their leading position; ties break by name.
344
367
  scored = [
345
- (s, score) for s in skills if (score := s.relevance_score(goal, repo_files)) > 0
368
+ (s, score) for s in skills if (score := s.relevance_score(goal, repo_files, project_root)) > 0
346
369
  ]
347
370
  scored.sort(key=lambda item: (not item[0].always, -item[1], item[0].name))
348
371
  return [skill for skill, _ in scored]
@@ -389,15 +412,23 @@ def scaffold_skills(project_root: Path, skills: list[Skill]) -> list[Path]:
389
412
  """
390
413
  written: list[Path] = []
391
414
  skills_root = project_root / ".claude" / "skills"
392
- proot = project_root.resolve()
415
+ dev_skills_root = project_root / ".devcouncil" / "skills"
393
416
  for skill in skills:
394
- # Don't re-materialize a skill that already lives inside this repo.
417
+ # Don't re-materialize a skill that already lives in a repo-local skill dir.
418
+ # Packaged library files inside a monorepo (e.g. src/.../skills/library/) still
419
+ # scaffold to .claude/skills/ — only skip when the source IS the destination.
395
420
  if skill.source_path is not None:
396
- try:
397
- skill.source_path.resolve().relative_to(proot)
421
+ src = skill.source_path.resolve()
422
+ skip = False
423
+ for base in (skills_root, dev_skills_root):
424
+ try:
425
+ src.relative_to(base.resolve())
426
+ skip = True
427
+ break
428
+ except ValueError:
429
+ continue
430
+ if skip:
398
431
  continue
399
- except ValueError:
400
- pass
401
432
  target = skills_root / skill.name / "SKILL.md"
402
433
  content = skill.to_skill_md()
403
434
  if target.exists() and target.read_text(encoding="utf-8") == content:
@@ -1,3 +1,4 @@
1
+ import logging
1
2
  from collections import defaultdict
2
3
  from contextlib import contextmanager
3
4
  from datetime import datetime, timezone
@@ -10,6 +11,8 @@ from typing import Optional
10
11
 
11
12
  from devcouncil.storage.models import SchemaVersionModel
12
13
 
14
+ logger = logging.getLogger(__name__)
15
+
13
16
 
14
17
  # v3: per-task indexes on the task/audit tables (task_id, lease status).
15
18
  # v4: machine-routable gap columns (file, line, suggested_command,
@@ -17,7 +20,15 @@ from devcouncil.storage.models import SchemaVersionModel
17
20
  # v5: partial unique index enforcing one ACTIVE lease per task (single-writer).
18
21
  # v6: gap column expected_verification_method (repair loop tells an executor-remediable
19
22
  # "incomplete" from a manual/llm one across a reload).
20
- SCHEMA_VERSION = 6
23
+ # v7: task columns agent_appended_expected_tests_json / agent_appended_allowed_commands_json
24
+ # (agent-negotiated scope extensions). These are NOT NULL with a scalar default, which
25
+ # the column migration now adds as ``ADD COLUMN ... NOT NULL DEFAULT <literal>`` —
26
+ # previously non-nullable additions were skipped entirely, so every SELECT on an
27
+ # existing database crashed with "no such column".
28
+ # v8: optional task.priority column (high/medium/low planner hint).
29
+ # v9: task column agent_appended_planned_files_json (lease-gated modify-op scope
30
+ # expansion so unwired_file / orphan_diff repairs can append the intended caller).
31
+ SCHEMA_VERSION = 9
21
32
 
22
33
 
23
34
  # Module-level caches keyed by *resolved* paths so distinct project roots stay
@@ -42,8 +53,9 @@ def reset_db_cache() -> None:
42
53
  for db in _db_instances.values():
43
54
  try:
44
55
  db.engine.dispose()
45
- except Exception:
46
- pass
56
+ except Exception as e:
57
+ # A failed dispose can leave the SQLite file locked for later rebuilds.
58
+ logger.debug("Failed to dispose cached engine for %s: %s", db.db_path, e)
47
59
  _db_instances.clear()
48
60
  _dedup_done.clear()
49
61
 
@@ -126,13 +138,38 @@ class Database:
126
138
  )
127
139
  _dedup_done.add(key)
128
140
 
141
+ @staticmethod
142
+ def _scalar_default_literal(column) -> Optional[str]:
143
+ """SQL literal for a column's scalar Python-side default, or None.
144
+
145
+ Only simple scalars (str/bool/int/float) are rendered — anything callable,
146
+ context-sensitive, or exotic returns None so the caller can skip the column
147
+ rather than emit wrong DDL."""
148
+ default = getattr(column, "default", None)
149
+ if default is None or getattr(default, "is_callable", False):
150
+ return None
151
+ arg = getattr(default, "arg", None)
152
+ if isinstance(arg, bool):
153
+ return "1" if arg else "0"
154
+ if isinstance(arg, (int, float)):
155
+ return str(arg)
156
+ if isinstance(arg, str):
157
+ escaped = arg.replace("'", "''")
158
+ return f"'{escaped}'"
159
+ return None
160
+
129
161
  def _create_missing_columns(self):
130
162
  # create_all never alters an existing table, so columns added to a model
131
163
  # later (e.g. the v4 gap routing columns) are missing on databases created by
132
164
  # an older version — and a SELECT of the model would then fail. Add any
133
- # missing column as a nullable ADD COLUMN (the only shape SQLite can add in
134
- # place without a table rewrite); non-nullable additions are intentionally
135
- # skipped because existing rows could not satisfy them.
165
+ # missing column in place via ADD COLUMN: nullable columns as-is, and
166
+ # NON-nullable columns with a scalar default as
167
+ # ``NOT NULL DEFAULT <literal>`` (the shape SQLite accepts without a table
168
+ # rewrite — existing rows take the default). Previously non-nullable
169
+ # additions were skipped entirely, so a model gaining a required column
170
+ # (v7's agent_appended_*_json) crashed every SELECT on an existing database
171
+ # with "no such column". Non-nullable columns WITHOUT a scalar default are
172
+ # still skipped: existing rows could not satisfy them.
136
173
  inspector = inspect(self.engine)
137
174
  existing_tables = set(inspector.get_table_names())
138
175
  for table in SQLModel.metadata.sorted_tables:
@@ -140,13 +177,22 @@ class Database:
140
177
  continue
141
178
  existing_cols = {col["name"] for col in inspector.get_columns(table.name)}
142
179
  for column in table.columns:
143
- if column.name in existing_cols or not column.nullable:
180
+ if column.name in existing_cols:
144
181
  continue
145
182
  ddl_type = column.type.compile(self.engine.dialect)
146
- stmt = text(f'ALTER TABLE "{table.name}" ADD COLUMN "{column.name}" {ddl_type}')
183
+ if column.nullable:
184
+ ddl = f'ALTER TABLE "{table.name}" ADD COLUMN "{column.name}" {ddl_type}'
185
+ else:
186
+ literal = self._scalar_default_literal(column)
187
+ if literal is None:
188
+ continue
189
+ ddl = (
190
+ f'ALTER TABLE "{table.name}" ADD COLUMN "{column.name}" '
191
+ f"{ddl_type} NOT NULL DEFAULT {literal}"
192
+ )
147
193
  try:
148
194
  with self.engine.begin() as conn:
149
- conn.execute(stmt)
195
+ conn.execute(text(ddl))
150
196
  except OperationalError as exc:
151
197
  if "duplicate column name" not in str(exc).lower():
152
198
  raise
@@ -194,8 +240,8 @@ def get_db(project_root: Path = Path(".")) -> Optional[Database]:
194
240
  return cached
195
241
  try:
196
242
  cached.engine.dispose()
197
- except Exception:
198
- pass
243
+ except Exception as e:
244
+ logger.debug("Failed to dispose stale cached engine for %s: %s", db_path, e)
199
245
 
200
246
  db = Database(db_path)
201
247
  db.ensure_schema_version()
@@ -38,8 +38,14 @@ class TaskModel(SQLModel, table=True):
38
38
  acceptance_criterion_ids_json: str = Field(default="[]")
39
39
  planned_files_json: str = Field(default="[]")
40
40
  expected_tests_json: str = Field(default="[]")
41
+ agent_appended_expected_tests_json: str = Field(default="[]")
41
42
  allowed_commands_json: str = Field(default="[]")
43
+ agent_appended_allowed_commands_json: str = Field(default="[]")
44
+ agent_appended_planned_files_json: str = Field(default="[]")
42
45
  forbidden_changes_json: str = Field(default="[]")
46
+ # Manual/planner difficulty override ("easy"/"normal"/"hard"); None = estimator.
47
+ difficulty: Optional[str] = Field(default=None)
48
+ priority: Optional[str] = Field(default=None)
43
49
  status: str = "planned"
44
50
 
45
51
  class EvidenceModel(SQLModel, table=True):
@@ -12,6 +12,8 @@ from pydantic import BaseModel
12
12
  from sqlalchemy.exc import IntegrityError
13
13
  from sqlmodel import Session, col, select
14
14
 
15
+ from devcouncil.utils.json_persist import dump_json
16
+
15
17
  from devcouncil.storage.models import (
16
18
  AgentHandoffModel,
17
19
  CorrectionManifestModel,
@@ -386,7 +388,7 @@ class SemanticDiffRepository:
386
388
  task_id=task_id,
387
389
  before_snapshot_path=before_snapshot_path,
388
390
  after_snapshot_path=after_snapshot_path,
389
- classifications_json=json.dumps(classifications),
391
+ classifications_json=dump_json(classifications),
390
392
  summary=summary,
391
393
  created_at=_utc_now(),
392
394
  )
@@ -518,8 +520,8 @@ class VerificationRunRepository:
518
520
  id=rid,
519
521
  task_id=task_id,
520
522
  sandbox=sandbox,
521
- environment_json=json.dumps(environment),
522
- commands_json=json.dumps(commands),
523
+ environment_json=dump_json(environment),
524
+ commands_json=dump_json(commands),
523
525
  status=status,
524
526
  started_at=_utc_now(),
525
527
  finished_at=finished_at,
@@ -1,6 +1,7 @@
1
1
  from sqlmodel import Session, col, delete, select
2
2
  from typing import List, Optional, Any
3
3
  import json
4
+ from devcouncil.utils.json_persist import dump_json
4
5
  from devcouncil.storage.models import (
5
6
  RequirementModel,
6
7
  AssumptionModel,
@@ -45,7 +46,7 @@ class RequirementRepository:
45
46
  description=req.description,
46
47
  priority=req.priority,
47
48
  source=req.source,
48
- acceptance_criteria_json=json.dumps([ac.model_dump() for ac in req.acceptance_criteria])
49
+ acceptance_criteria_json=dump_json([ac.model_dump() for ac in req.acceptance_criteria])
49
50
  )
50
51
  self.session.merge(model)
51
52
  self.session.commit()
@@ -80,7 +81,7 @@ class AssumptionRepository:
80
81
  impact=assumption.impact,
81
82
  reversible=assumption.reversible,
82
83
  requires_user_confirmation=assumption.requires_user_confirmation,
83
- linked_requirement_ids_json=json.dumps(assumption.linked_requirement_ids),
84
+ linked_requirement_ids_json=dump_json(assumption.linked_requirement_ids),
84
85
  status=assumption.status,
85
86
  )
86
87
  self.session.merge(model)
@@ -105,8 +106,19 @@ class TaskRepository:
105
106
  "acceptance_criterion_ids": json.loads(m.acceptance_criterion_ids_json),
106
107
  "planned_files": pf_list,
107
108
  "expected_tests": json.loads(m.expected_tests_json),
109
+ "agent_appended_expected_tests": json.loads(
110
+ getattr(m, "agent_appended_expected_tests_json", None) or "[]"
111
+ ),
108
112
  "allowed_commands": json.loads(m.allowed_commands_json),
113
+ "agent_appended_allowed_commands": json.loads(
114
+ getattr(m, "agent_appended_allowed_commands_json", None) or "[]"
115
+ ),
116
+ "agent_appended_planned_files": json.loads(
117
+ getattr(m, "agent_appended_planned_files_json", None) or "[]"
118
+ ),
109
119
  "forbidden_changes": json.loads(m.forbidden_changes_json),
120
+ "difficulty": getattr(m, "difficulty", None),
121
+ "priority": getattr(m, "priority", None),
110
122
  "status": m.status,
111
123
  })
112
124
 
@@ -124,8 +136,19 @@ class TaskRepository:
124
136
  "acceptance_criterion_ids": json.loads(m.acceptance_criterion_ids_json),
125
137
  "planned_files": pf_list,
126
138
  "expected_tests": json.loads(m.expected_tests_json),
139
+ "agent_appended_expected_tests": json.loads(
140
+ getattr(m, "agent_appended_expected_tests_json", None) or "[]"
141
+ ),
127
142
  "allowed_commands": json.loads(m.allowed_commands_json),
143
+ "agent_appended_allowed_commands": json.loads(
144
+ getattr(m, "agent_appended_allowed_commands_json", None) or "[]"
145
+ ),
146
+ "agent_appended_planned_files": json.loads(
147
+ getattr(m, "agent_appended_planned_files_json", None) or "[]"
148
+ ),
128
149
  "forbidden_changes": json.loads(m.forbidden_changes_json),
150
+ "difficulty": getattr(m, "difficulty", None),
151
+ "priority": getattr(m, "priority", None),
129
152
  "status": m.status,
130
153
  }))
131
154
  return results
@@ -135,12 +158,17 @@ class TaskRepository:
135
158
  id=task.id,
136
159
  title=task.title,
137
160
  description=task.description,
138
- requirement_ids_json=json.dumps(task.requirement_ids),
139
- acceptance_criterion_ids_json=json.dumps(task.acceptance_criterion_ids),
140
- planned_files_json=json.dumps([pf.model_dump() for pf in task.planned_files]),
141
- expected_tests_json=json.dumps(task.expected_tests),
142
- allowed_commands_json=json.dumps(task.allowed_commands),
143
- forbidden_changes_json=json.dumps(task.forbidden_changes),
161
+ requirement_ids_json=dump_json(task.requirement_ids),
162
+ acceptance_criterion_ids_json=dump_json(task.acceptance_criterion_ids),
163
+ planned_files_json=dump_json([pf.model_dump() for pf in task.planned_files]),
164
+ expected_tests_json=dump_json(task.expected_tests),
165
+ agent_appended_expected_tests_json=dump_json(task.agent_appended_expected_tests),
166
+ allowed_commands_json=dump_json(task.allowed_commands),
167
+ agent_appended_allowed_commands_json=dump_json(task.agent_appended_allowed_commands),
168
+ agent_appended_planned_files_json=dump_json(task.agent_appended_planned_files),
169
+ forbidden_changes_json=dump_json(task.forbidden_changes),
170
+ difficulty=task.difficulty,
171
+ priority=task.priority,
144
172
  status=task.status
145
173
  )
146
174
  self.session.merge(model)
@@ -196,7 +224,7 @@ class GapRepository:
196
224
  requirement_id=gap.requirement_id,
197
225
  task_id=gap.task_id,
198
226
  description=gap.description,
199
- evidence_json=json.dumps(gap.evidence),
227
+ evidence_json=dump_json(gap.evidence),
200
228
  recommended_fix=gap.recommended_fix,
201
229
  blocking=gap.blocking,
202
230
  file=gap.file,
@@ -360,7 +388,7 @@ class PlanningStateRepository:
360
388
  description=req.description,
361
389
  priority=req.priority,
362
390
  source=req.source,
363
- acceptance_criteria_json=json.dumps([ac.model_dump() for ac in req.acceptance_criteria]),
391
+ acceptance_criteria_json=dump_json([ac.model_dump() for ac in req.acceptance_criteria]),
364
392
  ))
365
393
 
366
394
  for assumption in assumptions:
@@ -371,7 +399,7 @@ class PlanningStateRepository:
371
399
  impact=assumption.impact,
372
400
  reversible=assumption.reversible,
373
401
  requires_user_confirmation=assumption.requires_user_confirmation,
374
- linked_requirement_ids_json=json.dumps(assumption.linked_requirement_ids),
402
+ linked_requirement_ids_json=dump_json(assumption.linked_requirement_ids),
375
403
  status=assumption.status,
376
404
  ))
377
405
 
@@ -380,12 +408,16 @@ class PlanningStateRepository:
380
408
  id=task.id,
381
409
  title=task.title,
382
410
  description=task.description,
383
- requirement_ids_json=json.dumps(task.requirement_ids),
384
- acceptance_criterion_ids_json=json.dumps(task.acceptance_criterion_ids),
385
- planned_files_json=json.dumps([pf.model_dump() for pf in task.planned_files]),
386
- expected_tests_json=json.dumps(task.expected_tests),
387
- allowed_commands_json=json.dumps(task.allowed_commands),
388
- forbidden_changes_json=json.dumps(task.forbidden_changes),
411
+ requirement_ids_json=dump_json(task.requirement_ids),
412
+ acceptance_criterion_ids_json=dump_json(task.acceptance_criterion_ids),
413
+ planned_files_json=dump_json([pf.model_dump() for pf in task.planned_files]),
414
+ expected_tests_json=dump_json(task.expected_tests),
415
+ agent_appended_expected_tests_json=dump_json(task.agent_appended_expected_tests),
416
+ allowed_commands_json=dump_json(task.allowed_commands),
417
+ agent_appended_allowed_commands_json=dump_json(task.agent_appended_allowed_commands),
418
+ agent_appended_planned_files_json=dump_json(task.agent_appended_planned_files),
419
+ forbidden_changes_json=dump_json(task.forbidden_changes),
420
+ difficulty=task.difficulty,
389
421
  status=task.status,
390
422
  ))
391
423
 
@@ -455,7 +487,7 @@ class StateRepository:
455
487
  state = ProjectStateModel(
456
488
  id="singleton",
457
489
  current_phase=state,
458
- history_json=json.dumps(history or []),
490
+ history_json=dump_json(history or []),
459
491
  )
460
492
  self.session.merge(state)
461
493
  self.session.commit()
@@ -0,0 +1,28 @@
1
+ """Process-wide run attribution for telemetry.
2
+
3
+ Every model_calls.jsonl record has a ``run_id`` field, but historically it was
4
+ None on 100% of real records: the LLM router accepts ``run_id`` per call, and
5
+ none of the planning/verification services threaded it through. Rather than
6
+ touching every call site (and every future one), the orchestrator declares the
7
+ current run here and the router falls back to it when a caller doesn't pass one
8
+ explicitly.
9
+
10
+ A ``ContextVar`` (not a bare global) so concurrent runs in one process — e.g.
11
+ an MCP server planning two projects — attribute calls to their own run.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from contextvars import ContextVar
17
+ from typing import Optional
18
+
19
+ _current_run_id: ContextVar[Optional[str]] = ContextVar("devcouncil_run_id", default=None)
20
+
21
+
22
+ def set_current_run_id(run_id: Optional[str]) -> None:
23
+ """Declare the run all subsequent (context-local) model calls belong to."""
24
+ _current_run_id.set(run_id)
25
+
26
+
27
+ def get_current_run_id() -> Optional[str]:
28
+ return _current_run_id.get()
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import logging
3
+ import os
3
4
  from pathlib import Path
4
5
  from typing import Any, Dict, List
5
6
 
@@ -45,6 +46,9 @@ class CostEstimator:
45
46
 
46
47
 
47
48
  def _model_calls_file(project_root: Path) -> Path:
49
+ override = os.environ.get("DEVCOUNCIL_LOG_DIR")
50
+ if override:
51
+ return Path(override) / "model_calls.jsonl"
48
52
  return project_root / ".devcouncil" / "logs" / "model_calls.jsonl"
49
53
 
50
54
 
@@ -133,8 +137,3 @@ def group_cost(project_root: Path) -> Dict[str, Any]:
133
137
  "by_task": _group(records, "task_id"),
134
138
  "by_run": _group(records, "run_id"),
135
139
  }
136
-
137
-
138
- def cost_by_task(project_root: Path) -> Dict[str, Dict[str, Any]]:
139
- """Convenience accessor for the per-task cost breakdown (used by ``dev status``)."""
140
- return group_cost(project_root)["by_task"]