devcouncil 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/README.md +92 -33
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +56 -40
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +964 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +325 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/summary.py +21 -3
  255. package/src/devcouncil/live/tasks.py +12 -3
  256. package/src/devcouncil/live/transcripts.py +69 -2
  257. package/src/devcouncil/llm/cache.py +5 -6
  258. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  259. package/src/devcouncil/llm/provider.py +647 -73
  260. package/src/devcouncil/llm/router.py +271 -46
  261. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  262. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  263. package/src/devcouncil/optimization/skillopt.py +9 -5
  264. package/src/devcouncil/planning/arbiter_service.py +12 -3
  265. package/src/devcouncil/planning/correction_manifest.py +107 -10
  266. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  267. package/src/devcouncil/planning/plan_service.py +5 -2
  268. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  269. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  270. package/src/devcouncil/planning/question_conversion.py +56 -0
  271. package/src/devcouncil/planning/spec_service.py +9 -3
  272. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  273. package/src/devcouncil/repo/gitignore.py +1 -2
  274. package/src/devcouncil/reporting/evidence_export.py +124 -0
  275. package/src/devcouncil/reporting/evidence_html.py +210 -0
  276. package/src/devcouncil/reporting/json_report.py +16 -12
  277. package/src/devcouncil/reporting/markdown_report.py +38 -9
  278. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  279. package/src/devcouncil/reporting/report_builder.py +40 -4
  280. package/src/devcouncil/reporting/task_provenance.py +42 -0
  281. package/src/devcouncil/reporting/verdict.py +75 -0
  282. package/src/devcouncil/skills/library/README.md +1 -0
  283. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  285. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  286. package/src/devcouncil/skills/registry.py +43 -12
  287. package/src/devcouncil/storage/db.py +57 -11
  288. package/src/devcouncil/storage/models.py +6 -0
  289. package/src/devcouncil/storage/native.py +5 -3
  290. package/src/devcouncil/storage/repositories.py +50 -18
  291. package/src/devcouncil/telemetry/context.py +28 -0
  292. package/src/devcouncil/telemetry/cost.py +4 -5
  293. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  294. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  295. package/src/devcouncil/telemetry/stages.py +27 -2
  296. package/src/devcouncil/telemetry/tracker.py +50 -13
  297. package/src/devcouncil/ui/dashboard.py +120 -8
  298. package/src/devcouncil/utils/fsio.py +58 -0
  299. package/src/devcouncil/utils/git_snapshot.py +112 -0
  300. package/src/devcouncil/utils/json_persist.py +53 -0
  301. package/src/devcouncil/utils/proc.py +89 -0
  302. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  303. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  304. package/src/devcouncil/verification/checks/__init__.py +41 -0
  305. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  306. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  307. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  308. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  309. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  310. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  311. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  312. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  313. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  314. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  315. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  316. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  317. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  318. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  319. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  320. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  321. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  322. package/src/devcouncil/verification/checks/wiring.py +216 -0
  323. package/src/devcouncil/verification/claims/__init__.py +23 -0
  324. package/src/devcouncil/verification/claims/checks.py +395 -0
  325. package/src/devcouncil/verification/claims/mapper.py +168 -0
  326. package/src/devcouncil/verification/claims/models.py +39 -0
  327. package/src/devcouncil/verification/claims/transcript.py +92 -0
  328. package/src/devcouncil/verification/claims/verdict.py +88 -0
  329. package/src/devcouncil/verification/command_evidence.py +170 -0
  330. package/src/devcouncil/verification/command_malformation.py +147 -0
  331. package/src/devcouncil/verification/command_runner.py +164 -0
  332. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  333. package/src/devcouncil/verification/diff_coverage.py +151 -0
  334. package/src/devcouncil/verification/difficulty.py +296 -0
  335. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  336. package/src/devcouncil/verification/gap_ids.py +63 -0
  337. package/src/devcouncil/verification/gate_cache.py +194 -0
  338. package/src/devcouncil/verification/gate_selector.py +344 -0
  339. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  340. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  341. package/src/devcouncil/verification/incremental_check.py +241 -0
  342. package/src/devcouncil/verification/next_actions.py +60 -1
  343. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  344. package/src/devcouncil/verification/sandbox.py +38 -11
  345. package/src/devcouncil/verification/stub_detector.py +369 -0
  346. package/src/devcouncil/verification/test_resolver.py +67 -1
  347. package/src/devcouncil/verification/verifier.py +137 -1666
  348. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  349. package/src/devcouncil/verification/verify_setup.py +176 -0
  350. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  351. package/src/semantic_layer/__init__.py +58 -0
  352. package/src/semantic_layer/benchmark.py +75 -0
  353. package/src/semantic_layer/cache.py +290 -0
  354. package/src/semantic_layer/compressor.py +137 -0
  355. package/src/semantic_layer/config.py +75 -0
  356. package/src/semantic_layer/embeddings.py +69 -0
  357. package/src/semantic_layer/llm_backends.py +99 -0
  358. package/src/semantic_layer/pipeline.py +111 -0
  359. package/src/semantic_layer/router.py +128 -0
  360. package/src/semantic_layer/tuner.py +72 -0
  361. package/uv.lock +973 -9
  362. package/src/devcouncil/artifacts/migrations.py +0 -20
  363. package/src/devcouncil/artifacts/schemas.py +0 -23
  364. package/src/devcouncil/artifacts/serializer.py +0 -21
  365. package/src/devcouncil/integrations/gitnexus.py +0 -70
  366. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -1,17 +1,21 @@
1
- from typing import List, Dict, Any, Type, Optional, TypeVar
1
+ from typing import List, Dict, Any, Type, Optional, TypeVar, cast
2
2
  import copy
3
3
  import functools
4
4
  import json
5
5
  import logging
6
6
  import asyncio
7
+ import os
8
+ import re
7
9
  import time
8
10
  from pathlib import Path
9
11
 
12
+ import httpx
10
13
  from pydantic import BaseModel
11
- from devcouncil.llm.provider import Provider, LLMResponse
14
+ from devcouncil.llm.provider import Provider, LLMResponse, ProviderRequestError
12
15
  from devcouncil.llm.cache import LLMCache
13
16
  from devcouncil.telemetry.tracker import TelemetryTracker
14
17
  from devcouncil.telemetry.traces import TraceLogger
18
+ from devcouncil.telemetry.stages import log_step
15
19
 
16
20
  logger = logging.getLogger(__name__)
17
21
  StructuredModel = TypeVar("StructuredModel", bound=BaseModel)
@@ -33,6 +37,32 @@ class StructuredOutputError(RuntimeError):
33
37
  self.model = model
34
38
 
35
39
 
40
+ def _provider_retry_delay(exc: Exception, attempt: int) -> float:
41
+ """Seconds to wait before retrying a failed provider call."""
42
+ if isinstance(exc, ProviderRequestError):
43
+ if exc.retry_after_seconds is not None:
44
+ return float(min(120.0, max(1.0, exc.retry_after_seconds)))
45
+ if exc.status_code == 429:
46
+ # OpenRouter tiers often cap at ~20 RPM; back off generously.
47
+ return float(min(90.0, 15.0 * (2 ** attempt)))
48
+ return min(30.0, float(2 ** attempt))
49
+
50
+
51
+ def _rate_limit_retry_budget() -> int:
52
+ """How many 429 responses to wait out per call (beyond the normal attempts).
53
+
54
+ Default 8: with the 429 backoff above that is ~7 minutes of patience —
55
+ enough to ride out an RPM-window burst, small enough that a hard quota
56
+ (daily cap) still fails the call in bounded time. Override with
57
+ DEVCOUNCIL_RATE_LIMIT_RETRIES; 0 disables the separate budget."""
58
+ raw = os.environ.get("DEVCOUNCIL_RATE_LIMIT_RETRIES", "")
59
+ try:
60
+ value = int(raw)
61
+ except ValueError:
62
+ return 8
63
+ return max(0, value)
64
+
65
+
36
66
  class ModelRouter:
37
67
  # Independent fresh attempts at producing valid structured output before
38
68
  # giving up. Even capable models occasionally emit malformed JSON; a second
@@ -45,6 +75,7 @@ class ModelRouter:
45
75
  provider: Provider,
46
76
  role_config: Dict[str, Dict[str, Any]],
47
77
  project_root: Path = Path("."),
78
+ semantic_adapter: Optional[Any] = None,
48
79
  ):
49
80
  self.provider = provider
50
81
  self.role_config = role_config
@@ -55,6 +86,15 @@ class ModelRouter:
55
86
  # concurrent-write safe.
56
87
  self._cache = LLMCache(self.project_root)
57
88
  self._traces = TraceLogger(self.project_root)
89
+ # Optional semantic cache / routing / compression (config-driven, lazy).
90
+ if semantic_adapter is not None:
91
+ self._semantic = semantic_adapter
92
+ else:
93
+ from devcouncil.llm.semantic_bridge import load_semantic_adapter
94
+
95
+ self._semantic = load_semantic_adapter(project_root)
96
+ if self._semantic is not None:
97
+ self._semantic.warm_up()
58
98
  # Lazily-built providers for roles that override ``models.provider`` with
59
99
  # their own ``provider:`` (e.g. live_reviewer on Ollama while planners run
60
100
  # on OpenRouter). Keyed by normalized provider name; the default provider
@@ -95,34 +135,29 @@ class ModelRouter:
95
135
  )
96
136
  return self._role_providers[normalized]
97
137
 
98
- @staticmethod
99
- def _extract_json(content: str) -> str:
100
- """Best-effort extraction of a JSON document from a model response.
138
+ # Inline reasoning blocks emitted by thinking models when the serving stack does
139
+ # not split them into a separate channel (local runners with a mismatched chat
140
+ # template are the common case). Removed before JSON extraction: the reasoning
141
+ # prose routinely contains JSON-looking examples that would otherwise be picked
142
+ # up instead of the real answer that follows the block.
143
+ _THINK_BLOCK_RE = re.compile(
144
+ r"<(think|thinking|reasoning|thought)>.*?</\1>", re.DOTALL | re.IGNORECASE
145
+ )
146
+ # An UNCLOSED reasoning tag (generation cut off or template quirk): everything
147
+ # from the opener is reasoning; nothing after it to salvage, but text BEFORE a
148
+ # dangling opener (rare) may hold the answer.
149
+ _THINK_OPEN_RE = re.compile(r"<(think|thinking|reasoning|thought)>", re.IGNORECASE)
101
150
 
102
- Handles the common ways a model wraps valid JSON: triple-backtick fences and
103
- surrounding prose ("Here you go: {...} thanks"). Strips fences, returns the
104
- whole thing if it already parses, otherwise scans for the first balanced
105
- object/array (string- and escape-aware so braces inside string values don't
106
- confuse it). Falls back to the de-fenced text so the existing healing path
107
- still produces a meaningful error. A strict superset of plain fence-stripping
108
- — clean/fenced JSON is returned unchanged."""
109
- text = content.strip()
110
- if "```json" in text:
111
- text = text.split("```json", 1)[1].split("```", 1)[0].strip()
112
- elif "```" in text:
113
- text = text.split("```", 1)[1].split("```", 1)[0].strip()
114
- try:
115
- json.loads(text)
116
- return text
117
- except Exception:
118
- pass
119
- for opener, closer in (("{", "}"), ("[", "]")):
120
- start = text.find(opener)
121
- if start == -1:
122
- continue
151
+ @staticmethod
152
+ def _balanced_candidates(text: str, opener: str, closer: str):
153
+ """Yield every balanced top-level ``opener...closer`` span in ``text``,
154
+ string- and escape-aware so braces inside string values don't confuse it."""
155
+ start = text.find(opener)
156
+ while start != -1:
123
157
  depth = 0
124
158
  in_str = False
125
159
  escaped = False
160
+ end = -1
126
161
  for i in range(start, len(text)):
127
162
  ch = text[i]
128
163
  if in_str:
@@ -140,12 +175,56 @@ class ModelRouter:
140
175
  elif ch == closer:
141
176
  depth -= 1
142
177
  if depth == 0:
143
- candidate = text[start:i + 1]
144
- try:
145
- json.loads(candidate)
146
- return candidate
147
- except Exception:
148
- break
178
+ end = i
179
+ break
180
+ if end == -1:
181
+ return
182
+ yield text[start:end + 1]
183
+ start = text.find(opener, end + 1)
184
+
185
+ @classmethod
186
+ def _extract_json(cls, content: str) -> str:
187
+ """Best-effort extraction of a JSON document from a model response.
188
+
189
+ Handles the common ways a model wraps valid JSON: inline ``<think>`` blocks
190
+ (local/thinking models whose serving stack leaves reasoning in the content),
191
+ triple-backtick fences, and surrounding prose ("Here you go: {...} thanks").
192
+ Strips reasoning blocks and fences, returns the whole thing if it already
193
+ parses, otherwise scans EVERY balanced object/array candidate (string- and
194
+ escape-aware) and returns the first that parses — a JSON-looking fragment in
195
+ leading prose no longer masks the real answer that follows it. Falls back to
196
+ the de-fenced text so the existing healing path still produces a meaningful
197
+ error. A strict superset of plain fence-stripping — clean/fenced JSON is
198
+ returned unchanged."""
199
+ text = content.strip()
200
+ # Drop closed reasoning blocks; on a dangling opener keep only what precedes it.
201
+ if "<" in text:
202
+ stripped = cls._THINK_BLOCK_RE.sub("", text).strip()
203
+ dangling = cls._THINK_OPEN_RE.search(stripped)
204
+ if dangling and "</" not in stripped[dangling.start():]:
205
+ before = stripped[:dangling.start()].strip()
206
+ after = stripped[dangling.end():].strip()
207
+ # The answer usually follows the (cut-off) reasoning; prefer whichever
208
+ # side actually contains a JSON-ish payload.
209
+ stripped = after if ("{" in after or "[" in after) else before
210
+ if stripped:
211
+ text = stripped
212
+ if "```json" in text:
213
+ text = text.split("```json", 1)[1].split("```", 1)[0].strip()
214
+ elif "```" in text:
215
+ text = text.split("```", 1)[1].split("```", 1)[0].strip()
216
+ try:
217
+ json.loads(text)
218
+ return text
219
+ except Exception as e:
220
+ logger.debug("Response is not directly parseable JSON, scanning balanced candidates: %s", e)
221
+ for opener, closer in (("{", "}"), ("[", "]")):
222
+ for candidate in cls._balanced_candidates(text, opener, closer):
223
+ try:
224
+ json.loads(candidate)
225
+ return cast(str, candidate)
226
+ except Exception:
227
+ continue
149
228
  return text
150
229
 
151
230
  @staticmethod
@@ -173,15 +252,44 @@ class ModelRouter:
173
252
  temperature: float,
174
253
  run_id: Optional[str],
175
254
  provider: Optional[Provider] = None,
176
- attempts: int = 3,
255
+ attempts: int = 5,
256
+ json_schema: Optional[Dict[str, Any]] = None,
177
257
  ) -> "LLMResponse":
178
258
  """Provider completion with bounded exponential-backoff retry. Used for BOTH the
179
259
  initial call and the healing call so a transient fault in either is retried (and,
180
260
  if still failing, surfaced to the caller's fallback logic) rather than aborting
181
261
  the run. ``provider`` defaults to the router's default provider but may be a
182
- per-role provider for roles that override ``models.provider``."""
262
+ per-role provider for roles that override ``models.provider``. ``json_schema``
263
+ flows through to providers that support grammar-constrained structured output
264
+ (Ollama); others ignore it."""
265
+ if run_id is None:
266
+ # Fall back to the orchestrator-declared run so model_calls.jsonl
267
+ # records stay attributable even when call sites don't thread run_id.
268
+ from devcouncil.telemetry.context import get_current_run_id
269
+
270
+ run_id = get_current_run_id()
183
271
  provider = provider or self.provider
184
- for attempt in range(attempts):
272
+ # Only pass json_schema to providers whose ``complete`` accepts it — duck-typed
273
+ # or third-party Provider implementations may predate the parameter, and a
274
+ # structured-output OPTIMIZATION must never break them.
275
+ extra_kwargs: Dict[str, Any] = {}
276
+ if json_schema is not None:
277
+ import inspect
278
+
279
+ try:
280
+ if "json_schema" in inspect.signature(provider.complete).parameters:
281
+ extra_kwargs["json_schema"] = json_schema
282
+ except (TypeError, ValueError):
283
+ pass
284
+ # Rate limiting (429) gets its OWN, larger budget: it is the provider
285
+ # telling us exactly when to come back (Retry-After), not a fault in the
286
+ # request — counting it against the shared ``attempts`` let a busy
287
+ # endpoint exhaust the budget and kill a run that only needed patience
288
+ # (observed: benchmark tasks dying blocked on limit_rpm mid-run).
289
+ max_rate_limit_retries = _rate_limit_retry_budget()
290
+ attempt = 0
291
+ rate_limit_retries = 0
292
+ while True:
185
293
  try:
186
294
  return await provider.complete(
187
295
  model=model,
@@ -189,16 +297,57 @@ class ModelRouter:
189
297
  temperature=temperature,
190
298
  json_mode=True,
191
299
  run_id=run_id,
300
+ **extra_kwargs,
301
+ )
302
+ except httpx.TimeoutException as exc:
303
+ # A timeout already consumed the provider's ENTIRE request window
304
+ # (600s by default on a local Ollama host). Retrying the identical
305
+ # request almost always times out again — and the structured-output
306
+ # layers above this (healing call + fresh attempts) would multiply
307
+ # the stall until the whole run is killed from outside (observed:
308
+ # benchmark arms burning their full 20-minute budget on 2x 600s
309
+ # timeouts and dying with exit 124 before producing a verdict).
310
+ # Fail fast with an actionable message instead.
311
+ raise ProviderRequestError(
312
+ f"LLM request to model '{model}' timed out after the provider's "
313
+ f"request window ({exc!r}). If this is a local (Ollama) model, "
314
+ "generation is too slow for the configured window: raise "
315
+ "OLLAMA_TIMEOUT, cap generation with OLLAMA_NUM_PREDICT, reduce "
316
+ "thinking with OLLAMA_THINK=low or OLLAMA_THINK=false, or use a "
317
+ "smaller/faster model."
318
+ ) from exc
319
+ except ProviderRequestError as exc:
320
+ if exc.status_code == 429 and rate_limit_retries < max_rate_limit_retries:
321
+ rate_limit_retries += 1
322
+ delay = _provider_retry_delay(exc, rate_limit_retries)
323
+ logger.warning(
324
+ "LLM provider rate-limited (429, retry %d/%d): %r. Retrying in %.0fs...",
325
+ rate_limit_retries, max_rate_limit_retries, exc, delay,
326
+ )
327
+ await asyncio.sleep(delay)
328
+ continue
329
+ attempt += 1
330
+ if attempt >= attempts:
331
+ raise
332
+ delay = _provider_retry_delay(exc, attempt - 1)
333
+ logger.warning(
334
+ "LLM provider request failed (attempt %d/%d): %r. Retrying in %.0fs...",
335
+ attempt, attempts, exc, delay,
192
336
  )
337
+ await asyncio.sleep(delay)
193
338
  except Exception as exc:
194
- if attempt == attempts - 1:
339
+ attempt += 1
340
+ if attempt >= attempts:
195
341
  raise
342
+ delay = _provider_retry_delay(exc, attempt - 1)
343
+ # %r, not %s: common failures (httpx.ReadTimeout, CancelledError)
344
+ # stringify to an EMPTY message, which previously logged the useless
345
+ # "failed (attempt 1/3): ." and made timeouts undiagnosable from logs.
196
346
  logger.warning(
197
- "LLM request failed (attempt %d/%d): %s. Retrying...",
198
- attempt + 1, attempts, exc,
347
+ "LLM request failed (attempt %d/%d): %r. Retrying in %.0fs...",
348
+ attempt, attempts, exc, delay,
199
349
  )
200
- await asyncio.sleep(2 ** attempt)
201
- raise RuntimeError("unreachable") # loop either returns or raises
350
+ await asyncio.sleep(delay)
202
351
 
203
352
  async def complete_structured(
204
353
  self,
@@ -217,9 +366,22 @@ class ModelRouter:
217
366
  model = config["model"]
218
367
  temp = temperature if temperature is not None else config.get("temperature", 0.0)
219
368
  provider = self._provider_for_role(config)
220
-
369
+ role_provider = config.get("provider")
370
+
221
371
  # Deep-copy to avoid mutating the caller's messages list
222
372
  msgs = copy.deepcopy(messages)
373
+
374
+ # Optional long-context compression before schema injection.
375
+ if self._semantic is not None:
376
+ msgs = await self._semantic.maybe_compress_messages_async(msgs)
377
+
378
+ # Optional complexity-based model routing (local Ollama only when enabled).
379
+ if self._semantic is not None:
380
+ model = await self._semantic.maybe_route_model_async(
381
+ msgs,
382
+ configured_model=model,
383
+ role_provider=role_provider,
384
+ )
223
385
 
224
386
  # Add schema instructions to system or user message. Spell out "instance, not
225
387
  # the schema" explicitly: weaker/local models otherwise sometimes echo the schema
@@ -259,12 +421,46 @@ class ModelRouter:
259
421
  # Check cache first
260
422
  response = cache.get(model, msgs, temp, True, provider_fp)
261
423
  cache_hit = response is not None
424
+ semantic_cache_hit = False
425
+
426
+ if not cache_hit and self._semantic is not None:
427
+ response = await self._semantic.lookup_cache_async(msgs, model=model, role=role)
428
+ if response is not None:
429
+ cache_hit = True
430
+ semantic_cache_hit = True
431
+
432
+ # Structured-output schema for providers with grammar-constrained decoding
433
+ # (Ollama's native ``format: <schema>``): the model cannot emit invalid JSON,
434
+ # which on weak/local models eliminates most schema echoes and healing rounds.
435
+ structured_schema = schema.model_json_schema()
262
436
 
263
437
  started = time.monotonic()
264
438
  if not response:
265
- response = await self._complete_with_retry(
266
- model=model, messages=msgs, temperature=temp, run_id=run_id, provider=provider
267
- )
439
+ try:
440
+ response = await self._complete_with_retry(
441
+ model=model, messages=msgs, temperature=temp, run_id=run_id, provider=provider,
442
+ json_schema=structured_schema,
443
+ )
444
+ except ProviderRequestError as exc:
445
+ # A degradable role (caller supplied a fallback) must degrade on a
446
+ # provider failure exactly as it does on unparseable output: the
447
+ # fallback exists to keep the run alive on a flaky/slow model, and a
448
+ # fail-fast timeout (see _complete_with_retry) or exhausted retries
449
+ # is the same class of "this role produced nothing usable".
450
+ if fallback is not None:
451
+ logger.warning(
452
+ "Role '%s' (model '%s') provider request failed (%s); "
453
+ "using a safe fallback so the run can continue.",
454
+ role, model, exc,
455
+ )
456
+ traces.log_event(
457
+ "llm_provider_request_failed_fallback",
458
+ {"role": role, "model": model, "error": str(exc)},
459
+ run_id=run_id,
460
+ summary=f"Provider request failed for {role}; degraded to fallback.",
461
+ )
462
+ return fallback
463
+ raise
268
464
  elapsed = time.monotonic() - started
269
465
 
270
466
  if response is None:
@@ -273,12 +469,26 @@ class ModelRouter:
273
469
  if not cache_hit:
274
470
  tracker.log_usage(model, response.usage, local=provider_local)
275
471
 
472
+ cache_label = "cache_hit" if cache_hit else f"{elapsed:.1f}s"
473
+ if semantic_cache_hit:
474
+ cache_label = "semantic_cache_hit"
475
+
276
476
  # Include latency + cache status: on a slow (e.g. local) model this is what tells
277
477
  # you *which* call dominated a multi-minute planning/verification stage.
278
478
  logger.info(
279
479
  "LLM response: role=%s model=%s tokens=%s %s",
280
480
  role, response.model, response.usage,
281
- "cache_hit" if cache_hit else f"{elapsed:.1f}s",
481
+ cache_label,
482
+ )
483
+ log_step(
484
+ f"llm/{role}: {response.model} {cache_label}",
485
+ project_root=self.project_root,
486
+ run_id=run_id,
487
+ role=role,
488
+ model=response.model,
489
+ latency_s=round(elapsed, 2) if not cache_hit else 0,
490
+ cache_hit=cache_hit,
491
+ semantic_cache_hit=semantic_cache_hit,
282
492
  )
283
493
 
284
494
  try:
@@ -288,6 +498,8 @@ class ModelRouter:
288
498
  result = schema.model_validate(data)
289
499
  if not cache_hit:
290
500
  cache.set(model, msgs, temp, True, response, provider_fp) # cache only validated output
501
+ if self._semantic is not None and not semantic_cache_hit:
502
+ await self._semantic.store_cache_async(msgs, response, model=model, role=role)
291
503
  return result
292
504
  except Exception as e:
293
505
  logger.warning(f"Initial parse failed for {role}, attempting healing: {e}")
@@ -323,6 +535,16 @@ Content:
323
535
  {echo_hint}
324
536
  Please return the corrected JSON object only. No prose.
325
537
  """
538
+ # The healing call must SEE the schema. Previously it got only the error
539
+ # + bad content, so a "Field required" failure asked the model to invent
540
+ # the missing fields blind — on providers without grammar-constrained
541
+ # decoding (OpenRouter/Vertex, e.g. gemini-2.5-flash omitting empty
542
+ # list fields) healing then failed the same way and the whole planning
543
+ # run crashed. Reuse the same schema instruction as the initial call.
544
+ healing_messages = [
545
+ {"role": "system", "content": f"You repair malformed JSON.{instruction}"},
546
+ {"role": "user", "content": healing_prompt},
547
+ ]
326
548
  # The healing completion runs INSIDE this try (with the same retry/backoff as
327
549
  # the initial call). A transient failure here (429/timeout) must be treated as
328
550
  # "healing failed" so it routes into the fresh-attempt/fallback logic below,
@@ -332,16 +554,19 @@ Please return the corrected JSON object only. No prose.
332
554
  # We use a lower temperature for healing
333
555
  healed_response = await self._complete_with_retry(
334
556
  model=model,
335
- messages=[{"role": "user", "content": healing_prompt}],
557
+ messages=healing_messages,
336
558
  temperature=0.0,
337
559
  run_id=run_id,
338
560
  provider=provider,
561
+ json_schema=structured_schema,
339
562
  )
340
563
  tracker.log_usage(healed_response.model, healed_response.usage, local=provider_local)
341
564
  healed_content = self._extract_json(healed_response.content)
342
565
  data = json.loads(healed_content)
343
566
  result = schema.model_validate(data)
344
567
  cache.set(model, msgs, temp, True, healed_response, provider_fp)
568
+ if self._semantic is not None:
569
+ await self._semantic.store_cache_async(msgs, healed_response, model=model, role=role)
345
570
  return result
346
571
  except Exception as final_e:
347
572
  logger.error(f"Healing failed for {role}: {final_e}")