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,577 @@
1
+ """Minimal read-only LSP client (stdio JSON-RPC).
2
+
3
+ Spawns a detected language server from :class:`~devcouncil.indexing.lsp.LspInspector`
4
+ and speaks only ``initialize``, ``textDocument/references``, and
5
+ ``textDocument/definition``. Off by default — enable via ``indexing.lsp_refs``
6
+ config or ``dev map --lsp-refs``. Hard timeouts and clean shutdown; never raises
7
+ to callers (methods return ``None`` / empty on failure).
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ import subprocess
16
+ import threading
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+ from typing import Any, Callable, Iterator
20
+ from urllib.parse import unquote, urlparse
21
+ from urllib.request import url2pathname
22
+
23
+ from devcouncil.indexing.lsp import LspInspector
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ # Default per-request / session budgets. Language servers can be slow to start;
28
+ # keep these tight so map/verify never hang when a server is wedged.
29
+ _DEFAULT_REQUEST_TIMEOUT = 12.0
30
+ _DEFAULT_SHUTDOWN_TIMEOUT = 3.0
31
+ _DEFAULT_INIT_TIMEOUT = 20.0
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class LspLocation:
36
+ """A single LSP location (repo-relative path + 1-based line)."""
37
+
38
+ path: str
39
+ line: int # 1-based
40
+ character: int # 0-based
41
+
42
+
43
+ def _uri_to_rel(project_root: Path, uri: str) -> str | None:
44
+ """Convert a ``file://`` URI to a repo-relative POSIX path, or ``None``."""
45
+ try:
46
+ parsed = urlparse(uri)
47
+ if parsed.scheme != "file":
48
+ return None
49
+ raw = url2pathname(unquote(parsed.path))
50
+ # On Windows url2pathname may yield ``/C:/...``; Path handles it.
51
+ abs_path = Path(raw)
52
+ if os.name == "nt" and raw.startswith("/") and len(raw) > 2 and raw[2] == ":":
53
+ abs_path = Path(raw[1:])
54
+ rel = abs_path.resolve().relative_to(project_root.resolve())
55
+ return rel.as_posix()
56
+ except (OSError, ValueError):
57
+ return None
58
+
59
+
60
+ def _path_to_uri(project_root: Path, rel: str) -> str:
61
+ return (project_root / rel).resolve().as_uri()
62
+
63
+
64
+ def _symbol_position(source: str, line: int, name: str) -> tuple[int, int] | None:
65
+ """Return 0-based ``(line, character)`` for ``name`` on 1-based ``line``."""
66
+ lines = source.splitlines()
67
+ if line < 1 or line > len(lines):
68
+ return None
69
+ text = lines[line - 1]
70
+ col = text.find(name)
71
+ if col < 0:
72
+ # Fall back to first identifier-ish occurrence after ``def``/``class``/``export``.
73
+ col = text.find(name.split(".")[-1]) if "." in name else -1
74
+ if col < 0:
75
+ return None
76
+ return line - 1, col
77
+
78
+
79
+ def _parse_locations(project_root: Path, result: Any) -> list[LspLocation]:
80
+ """Normalize ``Location | Location[] | LocationLink[] | null`` to locations."""
81
+ if result is None:
82
+ return []
83
+ items = result if isinstance(result, list) else [result]
84
+ out: list[LspLocation] = []
85
+ for item in items:
86
+ if not isinstance(item, dict):
87
+ continue
88
+ uri = item.get("uri") or (item.get("targetUri") if "targetUri" in item else None)
89
+ rng = item.get("range") or item.get("targetSelectionRange") or item.get("targetRange")
90
+ if not uri or not isinstance(rng, dict):
91
+ continue
92
+ start = rng.get("start") or {}
93
+ if not isinstance(start, dict):
94
+ continue
95
+ rel = _uri_to_rel(project_root, str(uri))
96
+ if rel is None:
97
+ continue
98
+ line = int(start.get("line", 0)) + 1
99
+ character = int(start.get("character", 0))
100
+ out.append(LspLocation(path=rel, line=line, character=character))
101
+ return out
102
+
103
+
104
+ def language_for_path(path: str) -> str | None:
105
+ return LspInspector._EXTENSIONS.get(Path(path).suffix.lower())
106
+
107
+
108
+ def first_available_command(language: str) -> list[str] | None:
109
+ """First PATH-available command for ``language``, or ``None``."""
110
+ for command in LspInspector._LANGUAGE_SERVERS.get(language, []):
111
+ from shutil import which
112
+
113
+ if which(command[0]) is not None:
114
+ return list(command)
115
+ return None
116
+
117
+
118
+ class LspClient:
119
+ """One stdio language-server session (read-only: initialize / refs / definition)."""
120
+
121
+ def __init__(
122
+ self,
123
+ project_root: Path,
124
+ language: str,
125
+ command: list[str],
126
+ *,
127
+ request_timeout: float = _DEFAULT_REQUEST_TIMEOUT,
128
+ init_timeout: float = _DEFAULT_INIT_TIMEOUT,
129
+ ) -> None:
130
+ self.project_root = project_root.resolve()
131
+ self.language = language
132
+ self.command = list(command)
133
+ self.request_timeout = request_timeout
134
+ self.init_timeout = init_timeout
135
+ self._proc: subprocess.Popen[bytes] | None = None
136
+ self._reader: threading.Thread | None = None
137
+ self._lock = threading.Lock()
138
+ self._next_id = 1
139
+ self._pending: dict[int, dict[str, Any]] = {}
140
+ self._events: dict[int, threading.Event] = {}
141
+ self._alive = False
142
+ self._opened: set[str] = set()
143
+
144
+ def start(self) -> bool:
145
+ """Spawn the server and complete ``initialize``. Returns False on failure."""
146
+ if self._alive:
147
+ return True
148
+ try:
149
+ self._proc = subprocess.Popen(
150
+ self.command,
151
+ stdin=subprocess.PIPE,
152
+ stdout=subprocess.PIPE,
153
+ stderr=subprocess.DEVNULL,
154
+ cwd=str(self.project_root),
155
+ bufsize=0,
156
+ )
157
+ except OSError:
158
+ logger.debug("Failed to spawn LSP %s", self.command, exc_info=True)
159
+ self._proc = None
160
+ return False
161
+ self._reader = threading.Thread(target=self._read_loop, name=f"lsp-{self.language}", daemon=True)
162
+ self._reader.start()
163
+ try:
164
+ init_result = self._request(
165
+ "initialize",
166
+ {
167
+ "processId": os.getpid(),
168
+ "rootUri": self.project_root.as_uri(),
169
+ "rootPath": str(self.project_root),
170
+ "capabilities": {
171
+ "textDocument": {
172
+ "definition": {"linkSupport": True},
173
+ "references": {},
174
+ },
175
+ "workspace": {},
176
+ },
177
+ "initializationOptions": {},
178
+ "trace": "off",
179
+ },
180
+ timeout=self.init_timeout,
181
+ )
182
+ if init_result is None and self._proc is not None and self._proc.poll() is not None:
183
+ self.shutdown()
184
+ return False
185
+ self._notify("initialized", {})
186
+ self._alive = True
187
+ return True
188
+ except Exception:
189
+ logger.debug("LSP initialize failed for %s", self.language, exc_info=True)
190
+ self.shutdown()
191
+ return False
192
+
193
+ def shutdown(self) -> None:
194
+ """Best-effort ``shutdown`` / ``exit`` then terminate the process."""
195
+ proc = self._proc
196
+ self._alive = False
197
+ if proc is None:
198
+ return
199
+ try:
200
+ if proc.poll() is None:
201
+ try:
202
+ self._request("shutdown", None, timeout=_DEFAULT_SHUTDOWN_TIMEOUT)
203
+ except Exception:
204
+ pass
205
+ try:
206
+ self._notify("exit", None)
207
+ except Exception:
208
+ pass
209
+ try:
210
+ proc.wait(timeout=_DEFAULT_SHUTDOWN_TIMEOUT)
211
+ except subprocess.TimeoutExpired:
212
+ proc.kill()
213
+ try:
214
+ proc.wait(timeout=1.0)
215
+ except subprocess.TimeoutExpired:
216
+ pass
217
+ finally:
218
+ self._proc = None
219
+ self._opened.clear()
220
+ with self._lock:
221
+ for ev in self._events.values():
222
+ ev.set()
223
+ self._pending.clear()
224
+ self._events.clear()
225
+
226
+ def __enter__(self) -> LspClient:
227
+ if not self.start():
228
+ raise RuntimeError(f"Failed to start LSP for {self.language}")
229
+ return self
230
+
231
+ def __exit__(self, *exc: object) -> None:
232
+ self.shutdown()
233
+
234
+ def references(
235
+ self,
236
+ rel_path: str,
237
+ line: int,
238
+ character: int,
239
+ *,
240
+ include_declaration: bool = False,
241
+ ) -> list[LspLocation] | None:
242
+ """``textDocument/references`` at 0-based position. ``None`` on failure."""
243
+ if not self._ensure_open(rel_path):
244
+ return None
245
+ result = self._request(
246
+ "textDocument/references",
247
+ {
248
+ "textDocument": {"uri": _path_to_uri(self.project_root, rel_path)},
249
+ "position": {"line": line, "character": character},
250
+ "context": {"includeDeclaration": include_declaration},
251
+ },
252
+ )
253
+ if result is None and not self._alive:
254
+ return None
255
+ return _parse_locations(self.project_root, result)
256
+
257
+ def _ensure_open(self, rel_path: str) -> bool:
258
+ if not self._alive:
259
+ return False
260
+ norm = rel_path.replace("\\", "/")
261
+ if norm in self._opened:
262
+ return True
263
+ abs_path = self.project_root / norm
264
+ try:
265
+ text = abs_path.read_text(encoding="utf-8", errors="replace")
266
+ except OSError:
267
+ return False
268
+ lang_id = {
269
+ "python": "python",
270
+ "typescript": "typescript",
271
+ "javascript": "javascript",
272
+ "go": "go",
273
+ "rust": "rust",
274
+ }.get(self.language, self.language)
275
+ self._notify(
276
+ "textDocument/didOpen",
277
+ {
278
+ "textDocument": {
279
+ "uri": _path_to_uri(self.project_root, norm),
280
+ "languageId": lang_id,
281
+ "version": 1,
282
+ "text": text,
283
+ }
284
+ },
285
+ )
286
+ self._opened.add(norm)
287
+ return True
288
+
289
+ def _read_loop(self) -> None:
290
+ proc = self._proc
291
+ if proc is None or proc.stdout is None:
292
+ return
293
+ stdout = proc.stdout
294
+ try:
295
+ while True:
296
+ headers: dict[str, str] = {}
297
+ while True:
298
+ line = stdout.readline()
299
+ if not line:
300
+ return
301
+ if line in (b"\r\n", b"\n"):
302
+ break
303
+ try:
304
+ key, _, value = line.decode("utf-8", errors="replace").partition(":")
305
+ headers[key.strip().lower()] = value.strip()
306
+ except Exception:
307
+ continue
308
+ length_s = headers.get("content-length")
309
+ if not length_s:
310
+ continue
311
+ try:
312
+ length = int(length_s)
313
+ except ValueError:
314
+ continue
315
+ body = b""
316
+ while len(body) < length:
317
+ chunk = stdout.read(length - len(body))
318
+ if not chunk:
319
+ return
320
+ body += chunk
321
+ try:
322
+ message = json.loads(body.decode("utf-8"))
323
+ except json.JSONDecodeError:
324
+ continue
325
+ if not isinstance(message, dict):
326
+ continue
327
+ msg_id = message.get("id")
328
+ if msg_id is not None and ("result" in message or "error" in message):
329
+ with self._lock:
330
+ self._pending[int(msg_id)] = message
331
+ ev = self._events.get(int(msg_id))
332
+ if ev is not None:
333
+ ev.set()
334
+ # Notifications / server requests are ignored (read-only client).
335
+ except Exception:
336
+ logger.debug("LSP read loop ended for %s", self.language, exc_info=True)
337
+
338
+ def _write(self, payload: dict[str, Any]) -> None:
339
+ proc = self._proc
340
+ if proc is None or proc.stdin is None or proc.poll() is not None:
341
+ raise RuntimeError("LSP process not running")
342
+ raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
343
+ header = f"Content-Length: {len(raw)}\r\n\r\n".encode("ascii")
344
+ proc.stdin.write(header + raw)
345
+ proc.stdin.flush()
346
+
347
+ def _notify(self, method: str, params: Any) -> None:
348
+ msg: dict[str, Any] = {"jsonrpc": "2.0", "method": method}
349
+ if params is not None:
350
+ msg["params"] = params
351
+ with self._lock:
352
+ self._write(msg)
353
+
354
+ def _request(self, method: str, params: Any, *, timeout: float | None = None) -> Any:
355
+ wait = self.request_timeout if timeout is None else timeout
356
+ with self._lock:
357
+ req_id = self._next_id
358
+ self._next_id += 1
359
+ event = threading.Event()
360
+ self._events[req_id] = event
361
+ msg: dict[str, Any] = {"jsonrpc": "2.0", "id": req_id, "method": method}
362
+ if params is not None:
363
+ msg["params"] = params
364
+ try:
365
+ self._write(msg)
366
+ except Exception:
367
+ self._events.pop(req_id, None)
368
+ raise
369
+ if not event.wait(timeout=wait):
370
+ with self._lock:
371
+ self._events.pop(req_id, None)
372
+ self._pending.pop(req_id, None)
373
+ logger.debug("LSP request %s timed out after %.1fs", method, wait)
374
+ return None
375
+ with self._lock:
376
+ self._events.pop(req_id, None)
377
+ response = self._pending.pop(req_id, None)
378
+ if response is None:
379
+ return None
380
+ if "error" in response:
381
+ logger.debug("LSP %s error: %s", method, response.get("error"))
382
+ return None
383
+ return response.get("result")
384
+
385
+
386
+ class LspSessionPool:
387
+ """Lazy per-language :class:`LspClient` pool for one project root."""
388
+
389
+ def __init__(
390
+ self,
391
+ project_root: Path,
392
+ *,
393
+ request_timeout: float = _DEFAULT_REQUEST_TIMEOUT,
394
+ client_factory: Callable[[Path, str, list[str]], LspClient] | None = None,
395
+ ) -> None:
396
+ self.project_root = project_root.resolve()
397
+ self.request_timeout = request_timeout
398
+ self._clients: dict[str, LspClient] = {}
399
+ self._failed: set[str] = set()
400
+ self._factory = client_factory
401
+
402
+ def close(self) -> None:
403
+ for client in list(self._clients.values()):
404
+ client.shutdown()
405
+ self._clients.clear()
406
+ self._failed.clear()
407
+
408
+ def __enter__(self) -> LspSessionPool:
409
+ return self
410
+
411
+ def __exit__(self, *exc: object) -> None:
412
+ self.close()
413
+
414
+ def client_for(self, rel_path: str) -> LspClient | None:
415
+ language = language_for_path(rel_path)
416
+ if not language or language in self._failed:
417
+ return None
418
+ existing = self._clients.get(language)
419
+ if existing is not None:
420
+ return existing if existing._alive else None
421
+ command = first_available_command(language)
422
+ if command is None:
423
+ self._failed.add(language)
424
+ return None
425
+ if self._factory is not None:
426
+ client = self._factory(self.project_root, language, command)
427
+ else:
428
+ client = LspClient(
429
+ self.project_root,
430
+ language,
431
+ command,
432
+ request_timeout=self.request_timeout,
433
+ )
434
+ if not client.start():
435
+ self._failed.add(language)
436
+ return None
437
+ self._clients[language] = client
438
+ return client
439
+
440
+ def confirm_unreferenced(self, rel_path: str, line: int, name: str) -> bool | None:
441
+ """Confirm a token-scan dead symbol has zero external LSP references.
442
+
443
+ Returns:
444
+ ``True`` — LSP confirmed unreferenced (still dead).
445
+ ``False`` — LSP found external references (not dead).
446
+ ``None`` — no server / request failed (keep token-scan result).
447
+ """
448
+ client = self.client_for(rel_path)
449
+ if client is None:
450
+ return None
451
+ abs_path = self.project_root / rel_path
452
+ try:
453
+ source = abs_path.read_text(encoding="utf-8", errors="replace")
454
+ except OSError:
455
+ return None
456
+ pos = _symbol_position(source, line, name)
457
+ if pos is None:
458
+ return None
459
+ locs = client.references(rel_path, pos[0], pos[1], include_declaration=False)
460
+ if locs is None:
461
+ return None
462
+ norm = rel_path.replace("\\", "/")
463
+ for loc in locs:
464
+ if loc.path.replace("\\", "/") != norm:
465
+ return False
466
+ # Same-file hit outside the defining line → a real use.
467
+ if loc.line != line:
468
+ return False
469
+ return True
470
+
471
+ def dependents_of_file(self, rel_path: str) -> list[str] | None:
472
+ """Files that reference any top-level public symbol in ``rel_path``.
473
+
474
+ ``None`` when no server is available or all queries fail.
475
+ """
476
+ client = self.client_for(rel_path)
477
+ if client is None:
478
+ return None
479
+ abs_path = self.project_root / rel_path
480
+ try:
481
+ source = abs_path.read_text(encoding="utf-8", errors="replace")
482
+ except OSError:
483
+ return None
484
+ symbols = list(_iter_public_symbols(rel_path, source))
485
+ if not symbols:
486
+ return []
487
+ norm = rel_path.replace("\\", "/")
488
+ dependents: set[str] = set()
489
+ any_ok = False
490
+ for line, name in symbols:
491
+ pos = _symbol_position(source, line, name)
492
+ if pos is None:
493
+ continue
494
+ locs = client.references(rel_path, pos[0], pos[1], include_declaration=False)
495
+ if locs is None:
496
+ continue
497
+ any_ok = True
498
+ for loc in locs:
499
+ other = loc.path.replace("\\", "/")
500
+ if other != norm:
501
+ dependents.add(other)
502
+ if not any_ok:
503
+ return None
504
+ return sorted(dependents)
505
+
506
+
507
+ def _iter_public_symbols(rel_path: str, source: str) -> Iterator[tuple[int, str]]:
508
+ """Yield ``(1-based line, name)`` for public top-level defs (Python / JS-ish)."""
509
+ suffix = Path(rel_path).suffix.lower()
510
+ if suffix == ".py":
511
+ try:
512
+ import ast
513
+
514
+ tree = ast.parse(source)
515
+ except SyntaxError:
516
+ return
517
+ for node in tree.body:
518
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
519
+ name = node.name
520
+ if name.startswith("_"):
521
+ continue
522
+ yield int(getattr(node, "lineno", 1) or 1), name
523
+ return
524
+ if suffix in {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}:
525
+ import re
526
+
527
+ export_re = re.compile(
528
+ r"(?m)^\s*export\s+(?:async\s+)?(?:function|class|const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)"
529
+ )
530
+ for m in export_re.finditer(source):
531
+ name = m.group(1)
532
+ if name.startswith("_"):
533
+ continue
534
+ line = source[: m.start()].count("\n") + 1
535
+ yield line, name
536
+
537
+
538
+ def lsp_refs_enabled(project_root: Path) -> bool:
539
+ """Read ``indexing.lsp_refs`` from config; default False. Never raises."""
540
+ try:
541
+ from devcouncil.app.config import load_config
542
+
543
+ return bool(load_config(project_root).indexing.lsp_refs)
544
+ except Exception:
545
+ return False
546
+
547
+
548
+ def filter_dead_symbols_with_lsp(
549
+ project_root: Path,
550
+ candidates: list[str],
551
+ *,
552
+ pool: LspSessionPool | None = None,
553
+ ) -> list[str]:
554
+ """Drop token-scan dead symbols that LSP finds referenced.
555
+
556
+ When LSP is unavailable for a candidate, keep it (token-scan stands).
557
+ """
558
+ if not candidates:
559
+ return candidates
560
+ own_pool = pool is None
561
+ session = pool or LspSessionPool(project_root)
562
+ try:
563
+ kept: list[str] = []
564
+ for entry in candidates:
565
+ loc, _, name = entry.partition(" ")
566
+ path, _, line_s = loc.rpartition(":")
567
+ if not path or not name or not line_s.isdigit():
568
+ kept.append(entry)
569
+ continue
570
+ confirmed = session.confirm_unreferenced(path.replace("\\", "/"), int(line_s), name.strip())
571
+ if confirmed is False:
572
+ continue
573
+ kept.append(entry)
574
+ return kept
575
+ finally:
576
+ if own_pool:
577
+ session.close()