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
@@ -0,0 +1,120 @@
1
+ """`dev checkout`, `dev release`, and `dev lease` — task lease CLI service layer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+
8
+ import typer
9
+
10
+ from devcouncil.execution.lease_ops import (
11
+ checkout_task_payload,
12
+ list_leases_payload,
13
+ release_task_payload,
14
+ renew_lease_payload,
15
+ )
16
+ from devcouncil.telemetry.logging_setup import set_log_dir
17
+ from devcouncil.telemetry.stages import log_stage, log_step
18
+ from devcouncil.utils.json_persist import dump_json
19
+
20
+ lease_app = typer.Typer(help="List and renew task leases.")
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def checkout(
25
+ task_id: str = typer.Argument(..., help="Task ID to acquire."),
26
+ client_id: str = typer.Option(..., "--client-id", help="Stable client id for the lease owner."),
27
+ agent: str | None = typer.Option(None, "--agent", help="Optional agent profile name."),
28
+ force: bool = typer.Option(False, "--force", help="Reclaim a stale lease."),
29
+ json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
30
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
31
+ ) -> None:
32
+ """Acquire a task lease and return scope for gated write tools."""
33
+ root = project_root.expanduser().resolve()
34
+ set_log_dir(root)
35
+ logger.info("dev checkout: task=%s client=%s", task_id, client_id)
36
+
37
+ with log_stage("checkout", project_root=root, task_id=task_id):
38
+ payload = checkout_task_payload(root, task_id=task_id, client_id=client_id, agent=agent, force=force)
39
+ if json_format:
40
+ typer.echo(dump_json(payload, indent=2))
41
+ log_step("checkout/complete", project_root=root, task_id=task_id, trace=True)
42
+ if not payload.get("ok"):
43
+ raise typer.Exit(code=1)
44
+ return
45
+ if not payload.get("ok"):
46
+ typer.echo(payload.get("error", "checkout failed"))
47
+ raise typer.Exit(code=1)
48
+ typer.echo(f"Checked out {task_id} (expires {payload.get('expires_at')})")
49
+ log_step("checkout/complete", project_root=root, task_id=task_id, trace=True)
50
+
51
+
52
+ def release(
53
+ task_id: str = typer.Argument(..., help="Task ID to release."),
54
+ lease_token: str = typer.Option(..., "--lease-token", help="Lease token from checkout."),
55
+ json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
56
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
57
+ ) -> None:
58
+ """Release a held task lease."""
59
+ root = project_root.expanduser().resolve()
60
+ set_log_dir(root)
61
+ logger.info("dev release: task=%s", task_id)
62
+
63
+ with log_stage("release", project_root=root, task_id=task_id):
64
+ payload = release_task_payload(root, task_id=task_id, lease_token=lease_token)
65
+ if json_format:
66
+ typer.echo(dump_json(payload, indent=2))
67
+ log_step("release/complete", project_root=root, task_id=task_id, trace=True)
68
+ if not payload.get("ok"):
69
+ raise typer.Exit(code=1)
70
+ return
71
+ if not payload.get("ok"):
72
+ typer.echo(payload.get("error", "release failed"))
73
+ raise typer.Exit(code=1)
74
+ typer.echo(f"Released {task_id}")
75
+ log_step("release/complete", project_root=root, task_id=task_id, trace=True)
76
+
77
+
78
+ @lease_app.command("list")
79
+ def lease_list(
80
+ active_only: bool = typer.Option(True, "--active-only/--all", help="Only active leases."),
81
+ json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
82
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
83
+ ) -> None:
84
+ """List task leases for fleet supervision."""
85
+ root = project_root.expanduser().resolve()
86
+ set_log_dir(root)
87
+ payload = list_leases_payload(root, active_only=active_only)
88
+ if json_format:
89
+ typer.echo(dump_json(payload, indent=2))
90
+ if not payload.get("ok"):
91
+ raise typer.Exit(code=1)
92
+ return
93
+ if not payload.get("ok"):
94
+ typer.echo(payload.get("error", "lease list failed"))
95
+ raise typer.Exit(code=1)
96
+ for item in payload.get("leases") or []:
97
+ typer.echo(f"{item['task_id']} {item['owner']} expires={item['expires_at']}")
98
+
99
+
100
+ @lease_app.command("renew")
101
+ def lease_renew(
102
+ task_id: str = typer.Argument(..., help="Task ID."),
103
+ lease_token: str = typer.Option(..., "--lease-token", help="Lease token to renew."),
104
+ ttl_seconds: int | None = typer.Option(None, "--ttl-seconds", help="New TTL in seconds."),
105
+ json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
106
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
107
+ ) -> None:
108
+ """Extend a held task lease's TTL."""
109
+ root = project_root.expanduser().resolve()
110
+ set_log_dir(root)
111
+ payload = renew_lease_payload(root, task_id=task_id, lease_token=lease_token, ttl_seconds=ttl_seconds)
112
+ if json_format:
113
+ typer.echo(dump_json(payload, indent=2))
114
+ if not payload.get("ok"):
115
+ raise typer.Exit(code=1)
116
+ return
117
+ if not payload.get("ok"):
118
+ typer.echo(payload.get("error", "renew failed"))
119
+ raise typer.Exit(code=1)
120
+ typer.echo(f"Renewed {task_id} until {payload.get('expires_at')}")
@@ -5,6 +5,13 @@ breaks. This command surfaces the always-on shared log
5
5
  (``.devcouncil/logs/devcouncil.log``) and the per-run logs
6
6
  (``.devcouncil/runs/<run_id>/run.log``) without the user needing to remember
7
7
  paths or hand-roll ``tail``/``grep``.
8
+
9
+ DELIBERATELY UNINSTRUMENTED: this command's subject *is* the log. Emitting log
10
+ records (``logger.info`` / ``log_stage`` / ``log_step``) from here mutates what
11
+ the user asked to inspect — ``tail`` displaces the trailing lines it was asked
12
+ to show with its own records, and ``path`` creates the very file it is supposed
13
+ to report as "not created yet". An introspection command must not modify what
14
+ it introspects, so no stage instrumentation belongs in this file.
8
15
  """
9
16
 
10
17
  import time
@@ -14,14 +21,16 @@ from typing import Optional
14
21
  import typer
15
22
  from rich.console import Console
16
23
 
17
- from devcouncil.telemetry.logging_setup import LOG_RELATIVE_PATH
24
+ from devcouncil.telemetry.logging_setup import _resolve_log_path
18
25
 
19
26
  app = typer.Typer(help="View DevCouncil runtime logs (shared and per-run).")
20
27
  console = Console()
21
28
 
22
29
 
23
30
  def _shared_log(root: Path) -> Path:
24
- return root / LOG_RELATIVE_PATH
31
+ # Honors DEVCOUNCIL_LOG_DIR so `dev logs` inspects the same file the
32
+ # handlers actually write to.
33
+ return _resolve_log_path(root)
25
34
 
26
35
 
27
36
  def _runs_dir(root: Path) -> Path:
@@ -32,7 +41,6 @@ def _print_tail(path: Path, limit: int, grep: Optional[str]) -> None:
32
41
  if not path.exists():
33
42
  console.print(f"[yellow]No log at {path}. Run a command first (or pass --project-root).[/yellow]")
34
43
  raise typer.Exit(code=0)
35
- # Read only the tail rather than the whole (potentially large, rotated) file.
36
44
  lines = path.read_text(encoding="utf-8", errors="replace").splitlines()
37
45
  if grep:
38
46
  lines = [line for line in lines if grep.lower() in line.lower()]
@@ -56,11 +64,10 @@ def tail(
56
64
  if not follow:
57
65
  return
58
66
 
59
- # Follow mode: poll for appended bytes (the file is append-only between rotations).
60
67
  console.print(f"[dim]— following {path} (Ctrl-C to stop) —[/dim]")
61
68
  try:
62
69
  with open(path, "r", encoding="utf-8", errors="replace") as handle:
63
- handle.seek(0, 2) # jump to EOF; we already printed the tail
70
+ handle.seek(0, 2)
64
71
  while True:
65
72
  line = handle.readline()
66
73
  if line:
@@ -1,20 +1,55 @@
1
- import json
1
+ from devcouncil.utils.json_persist import dump_json
2
+ import logging
2
3
  from pathlib import Path
3
4
 
4
5
  import typer
5
6
 
6
7
  from devcouncil.indexing.lsp import LspInspector
8
+ from devcouncil.telemetry.stages import log_stage, log_step
7
9
 
8
- app = typer.Typer(help="Inspect optional LSP integration readiness.")
10
+ app = typer.Typer(
11
+ help=(
12
+ "Inspect optional LSP integration readiness. "
13
+ "Default is detection-only (PATH check). When indexing.lsp_refs is enabled, "
14
+ "reports mode: client for the live references/definition client."
15
+ ),
16
+ )
17
+ logger = logging.getLogger(__name__)
9
18
 
10
19
 
11
20
  @app.command("inspect")
12
21
  def inspect(
13
22
  project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
23
+ json_format: bool = typer.Option(False, "--json", help="Emit {mode, servers_detected, note} JSON."),
14
24
  ):
15
- """Print detected language servers and initialize payloads."""
25
+ """Print detected language servers; mode is detection-only or client."""
16
26
  root = project_root.expanduser().resolve()
27
+ # Existence check FIRST: set_log_dir/log_stage create .devcouncil/ under the
28
+ # root, which would materialize a missing/typo'd path and turn the "does not
29
+ # exist" report into a false success.
17
30
  if not root.exists():
18
- typer.echo(json.dumps({"languages": [], "servers": [], "initialize_requests": {}, "error": f"{root} does not exist"}, indent=2))
31
+ typer.echo(dump_json({"languages": [], "servers": [], "initialize_requests": {}, "error": f"{root} does not exist"}, indent=2))
19
32
  raise typer.Exit(code=1)
20
- typer.echo(LspInspector(root).summary_json())
33
+ from devcouncil.telemetry.logging_setup import set_log_dir
34
+ set_log_dir(root)
35
+ logger.info("dev lsp inspect")
36
+ with log_stage("lsp", project_root=root):
37
+ log_step("lsp/1: inspecting language servers", project_root=root, trace=True)
38
+ client_enabled = False
39
+ try:
40
+ from devcouncil.indexing.lsp_client import lsp_refs_enabled
41
+
42
+ client_enabled = lsp_refs_enabled(root)
43
+ except Exception:
44
+ client_enabled = False
45
+ summary = LspInspector(root).summary(client_enabled=client_enabled)
46
+ if json_format:
47
+ compact = {
48
+ "mode": summary.get("mode", "detection-only"),
49
+ "servers_detected": summary.get("servers_detected", summary.get("detected_servers", [])),
50
+ "note": summary.get("note", LspInspector._DETECTION_ONLY_NOTE),
51
+ }
52
+ typer.echo(dump_json(compact, indent=2))
53
+ else:
54
+ typer.echo(dump_json(summary, indent=2))
55
+ log_step("lsp/complete", project_root=root, trace=True)
@@ -1,88 +1,63 @@
1
- import json
1
+ import logging
2
2
  from pathlib import Path
3
3
 
4
4
  import typer
5
5
  from rich.console import Console
6
6
 
7
- from devcouncil.cli.commands.init import initialize_project
7
+ from devcouncil.indexing.map_artifacts import (
8
+ _important_surfaces as _important_surfaces,
9
+ _wiki_index_rel as _wiki_index_rel,
10
+ generate_map_artifacts as generate_map_artifacts,
11
+ write_agent_guides,
12
+ )
8
13
  from devcouncil.indexing.repo_mapper import RepoMap, RepoMapper
9
14
  from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
10
15
  from devcouncil.storage.db import get_db
16
+ from devcouncil.telemetry.stages import log_stage, log_step
17
+ from devcouncil.utils.json_persist import dump_json
18
+
19
+ # Back-compat aliases for tests / external importers.
20
+ _write_agent_guides = write_agent_guides
11
21
 
12
22
  console = Console()
13
23
  status_console = Console(stderr=True)
14
-
15
- AGENT_GUIDE_MARKER = "<!-- Managed by dev map: keep this file in sync with .devcouncil/repo_map.json. -->"
16
-
17
-
18
- def _important_surfaces(repo_map: RepoMap) -> list[str]:
19
- """Derive the 'important surfaces' list from the computed map, so the guide points
20
- at THIS repo's real subsystems instead of hardcoded DevCouncil paths."""
21
- lines: list[str] = []
22
- for index, subsystem in enumerate(repo_map.subsystems[:6], start=1):
23
- lines.append(f"{index}. `{subsystem.area}/` — {subsystem.summary}")
24
- if not lines:
25
- for index, path in enumerate(repo_map.important_files[:6], start=1):
26
- lines.append(f"{index}. `{path}`")
27
- return lines or ["1. See `.devcouncil/repo_map.json` for the file index."]
28
-
29
-
30
- def _agent_guide_text(repo_map_path: Path, repo_root: Path, repo_map: RepoMap) -> str:
31
- return "\n".join(
32
- [
33
- AGENT_GUIDE_MARKER,
34
- "",
35
- "# Agent Workspace Guide",
36
- "",
37
- "Use `.devcouncil/repo_map.json` as the primary file index for this workspace.",
38
- f"Repo map: `{repo_map_path.relative_to(repo_root).as_posix() if repo_map_path.is_relative_to(repo_root) else repo_map_path}`",
39
- "",
40
- "Workflow for agents:",
41
- "1. Open `.devcouncil/repo_map.json` before guessing at file locations.",
42
- "2. Use the `files` list to resolve module ownership and nearby siblings.",
43
- "3. Use `subsystems` for subsystem-level navigation.",
44
- "4. In `subsystems`, use `entry_points` + `critical_files` for entry points and starting context.",
45
- "5. Use `role_files` in `subsystems` for subsystem role buckets (entry, runtime, policy, adapters, etc.).",
46
- "6. Use `neighbors` and `handoff_paths` in `subsystems` to follow cross-subsystem flow.",
47
- "7. Run `dev map` again after large refactors to refresh the map.",
48
- "",
49
- "Important surfaces:",
50
- *_important_surfaces(repo_map),
51
- "",
52
- "If the map and source disagree, trust the source and regenerate the map.",
53
- ]
54
- )
24
+ logger = logging.getLogger(__name__)
55
25
 
56
26
 
57
- def _write_agent_guides(repo_root: Path, repo_map_path: Path, repo_map: RepoMap) -> None:
58
- for filename in ("AGENTS.md", "CLAUDE.md"):
59
- path = repo_root / filename
60
- if path.exists():
61
- existing = path.read_text(encoding="utf-8")
62
- if AGENT_GUIDE_MARKER not in existing:
63
- continue
64
- path.write_text(_agent_guide_text(repo_map_path, repo_root, repo_map) + "\n", encoding="utf-8")
27
+ def _liveness_summary(repo_map: RepoMap) -> str | None:
28
+ unreliable = bool(getattr(repo_map, "liveness_unreachable_unreliable", False))
29
+ if not (
30
+ repo_map.entry_roots
31
+ or repo_map.unwired_candidates
32
+ or repo_map.unreachable_files
33
+ or repo_map.dead_symbol_candidates
34
+ or unreliable
35
+ ):
36
+ return None
37
+ if not repo_map.entry_roots or unreliable:
38
+ import sys
65
39
 
66
-
67
- def generate_map_artifacts(root: Path, output: Path, goal: str = "", *, scan_dependencies: bool = False) -> RepoMap:
68
- """Build the repo map and write repo_map.json + agent guides (no LLM, no re-init).
69
-
70
- Assumes ``.devcouncil/`` already exists. Shared by the ``dev map`` command and
71
- by project initialization so a freshly set-up repo is immediately navigable.
72
- ``scan_dependencies`` is opt-in (off for init and default mapping) because it can
73
- shell out to dependency auditors.
74
- """
75
- repo_map = RepoMapper(root).map_repo(goal, scan_dependencies=scan_dependencies)
76
- graph_context = CodeReviewGraphAdapter(root).get_context()
77
- output = output if output.is_absolute() else root / output
78
- output.parent.mkdir(parents=True, exist_ok=True)
79
- output.write_text(repo_map.model_dump_json(indent=2), encoding="utf-8")
80
- _write_agent_guides(root, output, repo_map)
81
- if graph_context.available:
82
- graph_output = output.with_name("code_review_graph_context.json")
83
- graph_output.write_text(graph_context.model_dump_json(indent=2), encoding="utf-8")
84
- status_console.print(f"[green]Wrote code-review-graph context to {graph_output}[/green]")
85
- return repo_map
40
+ print(
41
+ "warning: unreachable_files low-confidence "
42
+ "(liveness_unreachable_unreliable); prefer unwired_candidates / "
43
+ "dead_symbol_candidates and `dev graph dead --confidence extracted`",
44
+ file=sys.stderr,
45
+ )
46
+ samples = repo_map.unwired_candidates[:3] + repo_map.dead_symbol_candidates[:2]
47
+ sample_txt = (", " + ", ".join(samples)) if samples else ""
48
+ unreachable_txt = (
49
+ "omitted (unreliable)"
50
+ if unreliable
51
+ else str(len(repo_map.unreachable_files))
52
+ )
53
+ return (
54
+ f"liveness: {len(repo_map.entry_roots)} entry roots, "
55
+ f"{len(repo_map.unwired_candidates)} unwired, "
56
+ f"{unreachable_txt} unreachable, "
57
+ f"{len(repo_map.dead_symbol_candidates)} dead symbols"
58
+ f"{sample_txt}"
59
+ " (prefer unwired + extracted dead; map clears on any non-test importer)"
60
+ )
86
61
 
87
62
 
88
63
  def map_repo(
@@ -99,14 +74,282 @@ def map_repo(
99
74
  "--scan-deps",
100
75
  help="Run available dependency auditors (pip-audit/npm audit/osv-scanner) and record dependency_risks in the map. Off by default.",
101
76
  ),
77
+ liveness: bool = typer.Option(
78
+ True,
79
+ "--liveness/--no-liveness",
80
+ help="Compute entry_roots / unwired / unreachable / dead_symbol candidate lists (on by default).",
81
+ ),
82
+ lsp_refs: bool = typer.Option(
83
+ False,
84
+ "--lsp-refs/--no-lsp-refs",
85
+ help=(
86
+ "Confirm dead-symbol candidates via live LSP references when a language "
87
+ "server is on PATH. Also set indexing.lsp_refs in config.yaml. Off by default."
88
+ ),
89
+ ),
90
+ refresh_wiki: bool = typer.Option(
91
+ True,
92
+ "--wiki/--no-wiki",
93
+ help="After mapping, refresh stale codebase-wiki page skeletons when a wiki exists (no LLM calls).",
94
+ ),
95
+ watch: bool = typer.Option(
96
+ False,
97
+ "--watch",
98
+ help="After the initial map, watch the tree and incrementally refresh on code edits (Ctrl-C to stop).",
99
+ ),
100
+ if_stale: bool = typer.Option(
101
+ False,
102
+ "--if-stale",
103
+ help="Fingerprint-check first and exit 0 without rebuilding when the on-disk map is still fresh.",
104
+ ),
105
+ pdg: bool = typer.Option(
106
+ False,
107
+ "--pdg/--no-pdg",
108
+ help="Build opt-in PDG/CFG/taint layer after map (Python-only, intra-procedural).",
109
+ ),
102
110
  ):
103
111
  """Build the deterministic repository map without calling an LLM."""
104
112
  root = project_root.expanduser().resolve()
113
+ # Reject missing roots before set_log_dir / initialize_project mkdir(parents=True)
114
+ # silently creates an empty project and maps zero files with exit 0.
115
+ if not root.is_dir():
116
+ status_console.print(f"[red]Project root does not exist: {root}[/red]")
117
+ raise typer.Exit(code=1)
118
+ # `dev map /some/repo` parses the path as the GOAL and maps the CWD repo with
119
+ # exit 0 — surface the likely intent instead of silently mapping the wrong repo.
120
+ if goal and project_root == Path("."):
121
+ goal_path = Path(goal).expanduser()
122
+ if goal_path.is_absolute() and goal_path.is_dir() and goal_path.resolve() != root:
123
+ status_console.print(
124
+ f"[yellow]Goal argument is a directory ({goal}). Did you mean "
125
+ f"`dev map --project-root {goal}`? Mapping {root} with it as goal text.[/yellow]"
126
+ )
127
+ from devcouncil.telemetry.logging_setup import set_log_dir
128
+ set_log_dir(root)
129
+ # CLI flag OR config; flag alone is enough without rewriting config.
130
+ use_lsp = lsp_refs
131
+ if not use_lsp:
132
+ try:
133
+ from devcouncil.app.config import load_config
134
+
135
+ use_lsp = bool(load_config(root).indexing.lsp_refs)
136
+ except Exception:
137
+ use_lsp = False
138
+ logger.info(
139
+ "dev map: goal=%r scan_deps=%s liveness=%s lsp_refs=%s if_stale=%s",
140
+ goal, scan_deps, liveness, use_lsp, if_stale,
141
+ )
142
+ from devcouncil.cli.commands.init import initialize_project
143
+
105
144
  initialize_project(root, quiet=True, with_map=False)
106
145
  if not get_db(root):
107
146
  raise typer.Exit(code=1)
108
147
 
109
148
  output = output if output.is_absolute() else root / output
110
- repo_map = generate_map_artifacts(root, output, goal, scan_dependencies=scan_deps)
111
- typer.echo(json.dumps(repo_map.model_dump(), indent=2))
112
- status_console.print(f"[green]Wrote repository map to {output}[/green]")
149
+ if if_stale and output.is_file():
150
+ try:
151
+ from devcouncil.utils.json_persist import read_json
152
+
153
+ data = read_json(output) or {}
154
+ if isinstance(data, dict) and not RepoMapper(root).map_is_stale(data):
155
+ status_console.print(f"[dim]Map is fresh; skipping rebuild ({output})[/dim]")
156
+ raise typer.Exit(code=0)
157
+ except typer.Exit:
158
+ raise
159
+ except Exception:
160
+ logger.debug("if-stale freshness check failed; rebuilding", exc_info=True)
161
+
162
+ with log_stage("map", project_root=root, scan_deps=scan_deps):
163
+ log_step("map/1: generating repository map", project_root=root, trace=True)
164
+ from devcouncil.codeintel.build_control import GraphBuildBusy
165
+
166
+ try:
167
+ from devcouncil.indexing.map_artifacts import refresh_map_artifacts
168
+
169
+ refresh = refresh_map_artifacts(
170
+ root,
171
+ output,
172
+ goal,
173
+ scan_dependencies=scan_deps,
174
+ liveness=liveness,
175
+ lsp_refs=use_lsp,
176
+ )
177
+ repo_map = refresh.repo_map
178
+ except GraphBuildBusy as exc:
179
+ status_console.print(f"[red]{exc}[/red]")
180
+ raise typer.Exit(code=1) from exc
181
+ if refresh.degraded:
182
+ status_console.print(
183
+ f"[red]Map wrote lean/degraded artifacts: {refresh.reason or refresh.mode}[/red]"
184
+ )
185
+ raise typer.Exit(code=1)
186
+ if refresh.compatibility_export_degraded:
187
+ status_console.print(
188
+ f"[yellow]Compatibility export degraded (canonical SQLite ok): "
189
+ f"{refresh.reason}[/yellow]"
190
+ )
191
+ try:
192
+ typer.echo(dump_json(repo_map.model_dump(), indent=2))
193
+ except BrokenPipeError:
194
+ # Consumer closed stdout early (`dev map | head`). The artifacts are
195
+ # already written — a closed pipe must not turn the stage red.
196
+ import os
197
+ import sys
198
+
199
+ logger.debug("stdout pipe closed while streaming repo map JSON")
200
+ try:
201
+ os.dup2(os.open(os.devnull, os.O_WRONLY), sys.stdout.fileno())
202
+ except OSError:
203
+ pass
204
+ status_console.print(f"[green]Wrote repository map to {output}[/green]")
205
+ graph_out = root / ".devcouncil" / "graph" / "code_graph.json"
206
+ if not graph_out.is_file():
207
+ # The export write failed or an external cleanup removed the JSON
208
+ # while SQLite (canonical) still holds the graph — re-export instead
209
+ # of exiting green without the documented artifact.
210
+ from devcouncil.indexing.graph.build import export_code_graph_json
211
+
212
+ if export_code_graph_json(root) is not None:
213
+ status_console.print(
214
+ f"[yellow]Code graph JSON was missing; re-exported from store to {graph_out}[/yellow]"
215
+ )
216
+ else:
217
+ status_console.print(
218
+ f"[yellow]Code graph JSON missing and store re-export failed ({graph_out}); "
219
+ "run `dev graph doctor`[/yellow]"
220
+ )
221
+ if graph_out.is_file():
222
+ status_console.print(f"[green]Wrote code graph to {graph_out}[/green]")
223
+ if pdg:
224
+ try:
225
+ from devcouncil.indexing.graph.build import (
226
+ build_pdg_for_paths,
227
+ load_code_graph,
228
+ merge_pdg_into_graph,
229
+ write_code_graph,
230
+ )
231
+
232
+ graph = load_code_graph(root)
233
+ if graph is not None:
234
+ layer = build_pdg_for_paths(root, graph)
235
+ shards = merge_pdg_into_graph(graph, layer)
236
+ merged: dict = {}
237
+ try:
238
+ from devcouncil.codeintel import get_codeintel_service
239
+
240
+ merged = dict(get_codeintel_service(root).store.analysis_shards())
241
+ except Exception:
242
+ pass
243
+ for path, payload in shards.items():
244
+ merged.setdefault(path, {}).update(payload)
245
+ write_code_graph(root, graph, analysis_shards=merged)
246
+ stats = (graph.meta.get("pdg") or {}).get("stats") or {}
247
+ status_console.print(
248
+ f"[green]Wrote PDG layer[/green] "
249
+ f"({stats.get('function_count', 0)} functions, "
250
+ f"{stats.get('taint_count', 0)} taint findings)"
251
+ )
252
+ except Exception as exc:
253
+ logger.warning("PDG build after map failed: %s", exc)
254
+ try:
255
+ from devcouncil.app.config import load_config
256
+
257
+ if bool(load_config(root).indexing.write_graph_html):
258
+ from devcouncil.indexing.viz import write_graph_html
259
+
260
+ html_out = write_graph_html(root, open_browser=False)
261
+ status_console.print(f"[green]Wrote graph HTML to {html_out}[/green]")
262
+ except Exception as exc:
263
+ logger.warning("Failed to write graph.html after map: %s", exc)
264
+ summary = _liveness_summary(repo_map)
265
+ if summary:
266
+ status_console.print(f"[cyan]{summary}[/cyan]")
267
+ if refresh_wiki:
268
+ _refresh_wiki_skeletons(root, repo_map)
269
+ log_step("map/complete", project_root=root, trace=True)
270
+ if watch:
271
+ _watch_map(root, liveness=liveness)
272
+
273
+
274
+ def graph_context_cmd(
275
+ files: list[str] = typer.Option([], "--file", help="Changed files to scope blast-radius (repeatable)."),
276
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
277
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
278
+ ) -> None:
279
+ """Return code-review-graph context for the given files."""
280
+ root = project_root.expanduser().resolve()
281
+ context = CodeReviewGraphAdapter(root).get_context(files)
282
+ if json_output:
283
+ typer.echo(context.model_dump_json(indent=2))
284
+ return
285
+ if not context.available:
286
+ console.print("[dim]Code-review graph integration is not available.[/dim]")
287
+ return
288
+ if context.impacted_files:
289
+ console.print("[cyan]Impacted files:[/cyan] " + ", ".join(context.impacted_files[:20]))
290
+ if context.related_tests:
291
+ console.print("[cyan]Related tests:[/cyan] " + ", ".join(context.related_tests[:20]))
292
+
293
+
294
+ def _refresh_wiki_skeletons(root: Path, repo_map: RepoMap) -> None:
295
+ """Keep the codebase wiki in step with the map — seamlessly, but deterministically.
296
+
297
+ Only runs when a wiki was already generated (`dev wiki update` opted the repo in),
298
+ and only rewrites pages whose repo-map slice changed. No model calls here: `dev map`
299
+ must stay fast and offline; run `dev wiki update` for LLM-enriched prose on the
300
+ refreshed pages.
301
+ """
302
+ try:
303
+ from devcouncil.knowledge.wiki import (
304
+ _project_name,
305
+ generate_wiki,
306
+ wiki_dir_for,
307
+ wiki_stale_pages,
308
+ )
309
+
310
+ wiki_dir = wiki_dir_for(root)
311
+ if not (wiki_dir / "index.md").is_file():
312
+ return
313
+ stale = wiki_stale_pages(root, repo_map, wiki_dir)
314
+ if not stale:
315
+ return
316
+ result = generate_wiki(root, repo_map, wiki_dir, project_name=_project_name(root))
317
+ status_console.print(
318
+ f"[green]Refreshed {len(result.changed)} stale wiki page(s)[/green] "
319
+ "(skeleton only — run `dev wiki update` for LLM-enriched prose)."
320
+ )
321
+ except Exception as exc:
322
+ # The wiki is a convenience layer; never let it fail `dev map`.
323
+ logger.warning("Wiki refresh after map failed: %s", exc)
324
+
325
+
326
+ def _watch_map(root: Path, *, liveness: bool = True) -> None:
327
+ """Run the shared code-intelligence coordinator for map compatibility."""
328
+ import time
329
+
330
+ from devcouncil.codeintel.sync import get_sync_coordinator
331
+ from devcouncil.indexing.graph.build import refresh_map_for_paths
332
+
333
+ coordinator = get_sync_coordinator(
334
+ root,
335
+ debounce_seconds=0.8,
336
+ sync_callback=lambda paths: refresh_map_for_paths(root, paths, liveness=liveness),
337
+ )
338
+ state = coordinator.start()
339
+ status_console.print(
340
+ f"[cyan]Watching {root} with {state.backend or 'reconciliation'} "
341
+ f"(state={state.state}, debounce 0.8s). Ctrl-C to stop.[/cyan]"
342
+ )
343
+ try:
344
+ while True:
345
+ time.sleep(0.8)
346
+ before = coordinator.status().pending
347
+ if before and not coordinator.sync_now():
348
+ failure = coordinator.status().last_error or coordinator.status().degraded_reason
349
+ status_console.print(f"[yellow]Watch refresh failed (ignored): {failure}[/yellow]")
350
+ elif before:
351
+ status_console.print(f"[green]Refreshed map for {len(before)} path(s)[/green]")
352
+ except KeyboardInterrupt:
353
+ status_console.print("Stopped watching.")
354
+ finally:
355
+ coordinator.stop(timeout=2)