devcouncil 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/README.md +92 -33
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +56 -40
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +964 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +325 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/summary.py +21 -3
  255. package/src/devcouncil/live/tasks.py +12 -3
  256. package/src/devcouncil/live/transcripts.py +69 -2
  257. package/src/devcouncil/llm/cache.py +5 -6
  258. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  259. package/src/devcouncil/llm/provider.py +647 -73
  260. package/src/devcouncil/llm/router.py +271 -46
  261. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  262. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  263. package/src/devcouncil/optimization/skillopt.py +9 -5
  264. package/src/devcouncil/planning/arbiter_service.py +12 -3
  265. package/src/devcouncil/planning/correction_manifest.py +107 -10
  266. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  267. package/src/devcouncil/planning/plan_service.py +5 -2
  268. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  269. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  270. package/src/devcouncil/planning/question_conversion.py +56 -0
  271. package/src/devcouncil/planning/spec_service.py +9 -3
  272. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  273. package/src/devcouncil/repo/gitignore.py +1 -2
  274. package/src/devcouncil/reporting/evidence_export.py +124 -0
  275. package/src/devcouncil/reporting/evidence_html.py +210 -0
  276. package/src/devcouncil/reporting/json_report.py +16 -12
  277. package/src/devcouncil/reporting/markdown_report.py +38 -9
  278. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  279. package/src/devcouncil/reporting/report_builder.py +40 -4
  280. package/src/devcouncil/reporting/task_provenance.py +42 -0
  281. package/src/devcouncil/reporting/verdict.py +75 -0
  282. package/src/devcouncil/skills/library/README.md +1 -0
  283. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  285. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  286. package/src/devcouncil/skills/registry.py +43 -12
  287. package/src/devcouncil/storage/db.py +57 -11
  288. package/src/devcouncil/storage/models.py +6 -0
  289. package/src/devcouncil/storage/native.py +5 -3
  290. package/src/devcouncil/storage/repositories.py +50 -18
  291. package/src/devcouncil/telemetry/context.py +28 -0
  292. package/src/devcouncil/telemetry/cost.py +4 -5
  293. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  294. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  295. package/src/devcouncil/telemetry/stages.py +27 -2
  296. package/src/devcouncil/telemetry/tracker.py +50 -13
  297. package/src/devcouncil/ui/dashboard.py +120 -8
  298. package/src/devcouncil/utils/fsio.py +58 -0
  299. package/src/devcouncil/utils/git_snapshot.py +112 -0
  300. package/src/devcouncil/utils/json_persist.py +53 -0
  301. package/src/devcouncil/utils/proc.py +89 -0
  302. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  303. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  304. package/src/devcouncil/verification/checks/__init__.py +41 -0
  305. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  306. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  307. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  308. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  309. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  310. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  311. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  312. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  313. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  314. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  315. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  316. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  317. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  318. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  319. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  320. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  321. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  322. package/src/devcouncil/verification/checks/wiring.py +216 -0
  323. package/src/devcouncil/verification/claims/__init__.py +23 -0
  324. package/src/devcouncil/verification/claims/checks.py +395 -0
  325. package/src/devcouncil/verification/claims/mapper.py +168 -0
  326. package/src/devcouncil/verification/claims/models.py +39 -0
  327. package/src/devcouncil/verification/claims/transcript.py +92 -0
  328. package/src/devcouncil/verification/claims/verdict.py +88 -0
  329. package/src/devcouncil/verification/command_evidence.py +170 -0
  330. package/src/devcouncil/verification/command_malformation.py +147 -0
  331. package/src/devcouncil/verification/command_runner.py +164 -0
  332. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  333. package/src/devcouncil/verification/diff_coverage.py +151 -0
  334. package/src/devcouncil/verification/difficulty.py +296 -0
  335. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  336. package/src/devcouncil/verification/gap_ids.py +63 -0
  337. package/src/devcouncil/verification/gate_cache.py +194 -0
  338. package/src/devcouncil/verification/gate_selector.py +344 -0
  339. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  340. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  341. package/src/devcouncil/verification/incremental_check.py +241 -0
  342. package/src/devcouncil/verification/next_actions.py +60 -1
  343. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  344. package/src/devcouncil/verification/sandbox.py +38 -11
  345. package/src/devcouncil/verification/stub_detector.py +369 -0
  346. package/src/devcouncil/verification/test_resolver.py +67 -1
  347. package/src/devcouncil/verification/verifier.py +137 -1666
  348. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  349. package/src/devcouncil/verification/verify_setup.py +176 -0
  350. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  351. package/src/semantic_layer/__init__.py +58 -0
  352. package/src/semantic_layer/benchmark.py +75 -0
  353. package/src/semantic_layer/cache.py +290 -0
  354. package/src/semantic_layer/compressor.py +137 -0
  355. package/src/semantic_layer/config.py +75 -0
  356. package/src/semantic_layer/embeddings.py +69 -0
  357. package/src/semantic_layer/llm_backends.py +99 -0
  358. package/src/semantic_layer/pipeline.py +111 -0
  359. package/src/semantic_layer/router.py +128 -0
  360. package/src/semantic_layer/tuner.py +72 -0
  361. package/uv.lock +973 -9
  362. package/src/devcouncil/artifacts/migrations.py +0 -20
  363. package/src/devcouncil/artifacts/schemas.py +0 -23
  364. package/src/devcouncil/artifacts/serializer.py +0 -21
  365. package/src/devcouncil/integrations/gitnexus.py +0 -70
  366. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import json
3
+ from devcouncil.utils.json_persist import dump_json
4
4
  import asyncio
5
5
  import logging
6
6
  import time
@@ -30,6 +30,7 @@ from devcouncil.app.config import get_api_key, load_config
30
30
  from devcouncil.llm.provider import create_provider, validate_model_provider
31
31
  from devcouncil.llm.router import ModelRouter
32
32
  from devcouncil.telemetry.traces import TraceLogger
33
+ from devcouncil.telemetry.stages import log_stage, log_step
33
34
 
34
35
  app = typer.Typer(help="Review active coding-agent sessions and emit critique cards.")
35
36
  console = Console()
@@ -44,19 +45,26 @@ def sessions(
44
45
  ):
45
46
  """List coding-agent transcripts DevCouncil can review."""
46
47
  root = project_root.expanduser().resolve()
47
- found = discover_sessions(root, client=client)
48
- if json_format:
49
- typer.echo(json.dumps({"sessions": [item.model_dump() for item in found]}, indent=2))
50
- return
48
+ from devcouncil.telemetry.logging_setup import set_log_dir
49
+ set_log_dir(root)
50
+ logger.info("dev watch sessions: client=%s", client)
51
+ with log_stage("watch", project_root=root, subcommand="sessions", client=client):
52
+ log_step("watch/1: discovering sessions", project_root=root, trace=True)
53
+ found = discover_sessions(root, client=client)
54
+ if json_format:
55
+ typer.echo(dump_json({"sessions": [item.model_dump() for item in found]}, indent=2))
56
+ log_step("watch/complete", project_root=root, count=len(found), trace=True)
57
+ return
51
58
 
52
- table = Table(title="DevCouncil Watch Sessions")
53
- table.add_column("Client", style="cyan")
54
- table.add_column("Session")
55
- table.add_column("Turns", justify="right")
56
- table.add_column("Transcript")
57
- for item in found:
58
- table.add_row(item.client, item.id, str(item.turns), item.transcript_path)
59
- console.print(table)
59
+ table = Table(title="DevCouncil Watch Sessions")
60
+ table.add_column("Client", style="cyan")
61
+ table.add_column("Session")
62
+ table.add_column("Turns", justify="right")
63
+ table.add_column("Transcript")
64
+ for item in found:
65
+ table.add_row(item.client, item.id, str(item.turns), item.transcript_path)
66
+ console.print(table)
67
+ log_step("watch/complete", project_root=root, count=len(found), trace=True)
60
68
 
61
69
 
62
70
  @app.command("review")
@@ -81,7 +89,7 @@ def review(
81
89
  if transcript_path is None:
82
90
  message = "No transcript selected. Use --transcript, --session, or --latest."
83
91
  if json_format:
84
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
92
+ typer.echo(dump_json({"ok": False, "error": message}, indent=2))
85
93
  else:
86
94
  console.print(f"[red]{message}[/red]")
87
95
  raise typer.Exit(code=2)
@@ -89,7 +97,7 @@ def review(
89
97
  if turn is None:
90
98
  message = f"No assistant turn found in {transcript_path}."
91
99
  if json_format:
92
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
100
+ typer.echo(dump_json({"ok": False, "error": message}, indent=2))
93
101
  else:
94
102
  console.print(f"[yellow]{message}[/yellow]")
95
103
  raise typer.Exit(code=1)
@@ -106,7 +114,7 @@ def review(
106
114
  payload["duplicate"] = duplicate
107
115
 
108
116
  if json_format:
109
- typer.echo(json.dumps(payload, indent=2))
117
+ typer.echo(dump_json(payload, indent=2))
110
118
  return
111
119
 
112
120
  _print_card(card)
@@ -129,7 +137,7 @@ def cards(
129
137
  if limit < 1:
130
138
  message = "--limit must be greater than 0."
131
139
  if json_format:
132
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
140
+ typer.echo(dump_json({"ok": False, "error": message}, indent=2))
133
141
  else:
134
142
  console.print(f"[red]{message}[/red]")
135
143
  raise typer.Exit(code=2)
@@ -142,14 +150,14 @@ def cards(
142
150
  )
143
151
  if error:
144
152
  if json_format:
145
- typer.echo(json.dumps({"ok": False, "error": error}, indent=2))
153
+ typer.echo(dump_json({"ok": False, "error": error}, indent=2))
146
154
  else:
147
155
  console.print(f"[red]{error}[/red]")
148
156
  raise typer.Exit(code=2)
149
157
  total = len(found)
150
158
  found = found[:limit]
151
159
  if json_format:
152
- typer.echo(json.dumps({
160
+ typer.echo(dump_json({
153
161
  "cards": [item.model_dump() for item in found],
154
162
  "filters": {
155
163
  "task_id": task_id,
@@ -175,7 +183,7 @@ def status(
175
183
  root = project_root.expanduser().resolve()
176
184
  payload = live_review_summary(root, task_id=task_id)
177
185
  if json_format:
178
- typer.echo(json.dumps(payload, indent=2))
186
+ typer.echo(dump_json(payload, indent=2))
179
187
  return
180
188
  blockers = payload["blocking_cards"]
181
189
 
@@ -202,23 +210,20 @@ def status(
202
210
 
203
211
  if payload["pending_signal_items"]:
204
212
  table = Table(title="Pending Agent Responses")
205
- table.add_column("Client", style="cyan")
213
+ table.add_column("ID", style="cyan")
214
+ table.add_column("Client")
206
215
  table.add_column("Task")
207
- table.add_column("Transcript", overflow="fold")
208
- table.add_column("Review Command", overflow="fold")
209
216
  for signal in payload["pending_signal_items"]:
210
217
  table.add_row(
218
+ signal.get("id") or "",
211
219
  signal.get("client") or "",
212
220
  signal.get("task_id") or "(unscoped)",
213
- signal.get("transcript_path") or "",
214
- signal.get("review_command") or "",
215
221
  )
216
222
  console.print(table)
217
- commands = [signal.get("review_command") for signal in payload["pending_signal_items"] if signal.get("review_command")]
218
- if commands:
219
- console.print("[cyan]Pending review commands:[/cyan]")
220
- for command in commands:
221
- console.print(command)
223
+ console.print(
224
+ "[dim]Sensitive transcript paths and review commands: "
225
+ "`dev watch signals` / `dev watch review`.[/dim]"
226
+ )
222
227
 
223
228
 
224
229
  @app.command("resolve")
@@ -232,7 +237,7 @@ def resolve(
232
237
  if status not in {"resolved", "ignored"}:
233
238
  message = "--status must be resolved or ignored."
234
239
  if json_format:
235
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
240
+ typer.echo(dump_json({"ok": False, "error": message}, indent=2))
236
241
  else:
237
242
  console.print(f"[red]{message}[/red]")
238
243
  raise typer.Exit(code=2)
@@ -241,12 +246,12 @@ def resolve(
241
246
  if card is None:
242
247
  message = f"Critique card {card_id} not found."
243
248
  if json_format:
244
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
249
+ typer.echo(dump_json({"ok": False, "error": message}, indent=2))
245
250
  else:
246
251
  console.print(f"[red]{message}[/red]")
247
252
  raise typer.Exit(code=1)
248
253
  if json_format:
249
- typer.echo(json.dumps({"ok": True, "card": card.model_dump()}, indent=2))
254
+ typer.echo(dump_json({"ok": True, "card": card.model_dump()}, indent=2))
250
255
  _log_card_resolved(root, card)
251
256
  return
252
257
  _log_card_resolved(root, card)
@@ -265,13 +270,13 @@ def repair(
265
270
  if card is None:
266
271
  message = f"Critique card {card_id} not found."
267
272
  if json_format:
268
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
273
+ typer.echo(dump_json({"ok": False, "error": message}, indent=2))
269
274
  else:
270
275
  console.print(f"[red]{message}[/red]")
271
276
  raise typer.Exit(code=1)
272
277
  prompt = build_live_repair_prompt(root, card)
273
278
  if json_format:
274
- typer.echo(json.dumps({"ok": True, "card": card.model_dump(), "prompt": prompt}, indent=2))
279
+ typer.echo(dump_json({"ok": True, "card": card.model_dump(), "prompt": prompt}, indent=2))
275
280
  return
276
281
  typer.echo(prompt)
277
282
 
@@ -296,7 +301,7 @@ def repair_all(
296
301
  resolved_cards = [card for card in cards if card is not None]
297
302
  prompt = build_bulk_live_repair_prompt(root, resolved_cards)
298
303
  if json_format:
299
- typer.echo(json.dumps({
304
+ typer.echo(dump_json({
300
305
  "ok": True,
301
306
  "scope_task_id": summary["scope_task_id"],
302
307
  "cards": [card.model_dump() for card in resolved_cards],
@@ -316,7 +321,7 @@ def signals(
316
321
  root = project_root.expanduser().resolve()
317
322
  found = _filtered_signals(root, client)
318
323
  if json_format:
319
- typer.echo(json.dumps({"signals": [item.model_dump() for item in found]}, indent=2))
324
+ typer.echo(dump_json({"signals": [item.model_dump() for item in found]}, indent=2))
320
325
  return
321
326
  table = Table(title="DevCouncil Watch Signals")
322
327
  table.add_column("Client", style="cyan")
@@ -378,7 +383,7 @@ def pending(
378
383
 
379
384
  logger.info("dev watch pending complete: %d reviewed, %d skipped", len(reviewed), len(skipped))
380
385
  if json_format:
381
- typer.echo(json.dumps({"reviewed": reviewed, "skipped": skipped}, indent=2))
386
+ typer.echo(dump_json({"reviewed": reviewed, "skipped": skipped}, indent=2))
382
387
  return
383
388
  for item in skipped:
384
389
  console.print(f"[yellow]Skipped signal:[/yellow] {item['reason']}")
@@ -441,7 +446,7 @@ def import_transcript(
441
446
  """Normalize a transcript into DevCouncil turn records without reviewing it."""
442
447
  turns = load_turns(transcript.expanduser().resolve(), client=client)
443
448
  if json_format:
444
- typer.echo(json.dumps({"turns": [turn.model_dump() for turn in turns]}, indent=2))
449
+ typer.echo(dump_json({"turns": [turn.model_dump() for turn in turns]}, indent=2))
445
450
  return
446
451
  console.print(f"Loaded {len(turns)} turns from {transcript}")
447
452
 
@@ -492,6 +497,7 @@ def _resolve_transcript(
492
497
  transcript: Path | None = None,
493
498
  session: str | None = None,
494
499
  latest: bool = False,
500
+ task_id: str | None = None,
495
501
  ) -> Path | None:
496
502
  if transcript is not None:
497
503
  path = transcript.expanduser()
@@ -499,6 +505,13 @@ def _resolve_transcript(
499
505
  path = root / path
500
506
  return path.resolve()
501
507
 
508
+ if client.lower() == "claude" and task_id:
509
+ from devcouncil.live.transcripts import claude_transcript_for_task
510
+
511
+ pinned = claude_transcript_for_task(root, task_id)
512
+ if pinned is not None:
513
+ return pinned
514
+
502
515
  selector = "latest" if latest else session
503
516
  if selector is None:
504
517
  return None
@@ -590,5 +603,8 @@ async def _review_turn(turn, root: Path, client: str, use_llm: bool, task_id: st
590
603
  logger.warning("Model-backed live review unavailable; using deterministic card: %s", exc)
591
604
  console.print(f"[yellow]Model-backed review unavailable; using deterministic card: {exc}[/yellow]")
592
605
  return review_turn(turn, root, client=client, task_id=task_id)
593
- card = await LiveReviewService(router).review(turn, root, client=client, use_llm=True)
606
+ # task_id flows through so the LLM review is grounded in the task's recorded
607
+ # verification state (like the deterministic path), instead of judging the
608
+ # agent's prose in a vacuum.
609
+ card = await LiveReviewService(router).review(turn, root, client=client, use_llm=True, task_id=task_id)
594
610
  return card.model_copy(update={"task_id": task_id}) if task_id else card
@@ -1,12 +1,15 @@
1
- import json
1
+ from devcouncil.utils.json_persist import dump_json
2
+ import logging
2
3
  import typer
3
4
  from pathlib import Path
4
5
  from rich.console import Console
5
6
 
6
7
  from devcouncil.cli.commands.init import initialize_project
7
8
  from devcouncil.execution.fs_watcher import FilesystemWatcher
9
+ from devcouncil.telemetry.stages import log_stage, log_step
8
10
 
9
11
  console = Console()
12
+ logger = logging.getLogger(__name__)
10
13
 
11
14
 
12
15
  def watch_fs(
@@ -17,24 +20,32 @@ def watch_fs(
17
20
  json_format: bool = typer.Option(False, "--json"),
18
21
  ):
19
22
  root = project_root.expanduser().resolve()
23
+ from devcouncil.telemetry.logging_setup import set_log_dir
24
+ set_log_dir(root)
25
+ logger.info("dev watch-fs: task=%s once=%s", task_id, once)
20
26
  initialize_project(root, quiet=True)
21
27
 
22
- def _print_event(event: dict) -> None:
23
- status = "allowed" if event["allowed"] else "denied"
24
- console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
28
+ with log_stage("watch_fs", project_root=root, task_id=task_id, once=once):
29
+ log_step("watch_fs/1: starting filesystem watcher", project_root=root, task_id=task_id, trace=True)
25
30
 
26
- watcher = FilesystemWatcher(root, task_id, poll_interval=poll_interval, on_event=_print_event)
27
- if once:
28
- events = watcher.scan_once()
29
- if json_format:
30
- typer.echo(json.dumps({"events": events}, indent=2))
31
- else:
32
- for event in events:
33
- status = "allowed" if event["allowed"] else "denied"
34
- console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
35
- return
36
- console.print(f"[cyan]Watching filesystem for task {task_id}. Ctrl+C to stop.[/cyan]")
37
- try:
38
- watcher.watch()
39
- except KeyboardInterrupt:
40
- console.print("[yellow]Stopped filesystem watcher.[/yellow]")
31
+ def _print_event(event: dict) -> None:
32
+ status = "allowed" if event["allowed"] else "denied"
33
+ console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
34
+
35
+ watcher = FilesystemWatcher(root, task_id, poll_interval=poll_interval, on_event=_print_event)
36
+ if once:
37
+ events = watcher.scan_once()
38
+ if json_format:
39
+ typer.echo(dump_json({"events": events}, indent=2))
40
+ else:
41
+ for event in events:
42
+ status = "allowed" if event["allowed"] else "denied"
43
+ console.print(f"[cyan]{event['path']}[/cyan] {status}: {event['reason']}")
44
+ log_step("watch_fs/complete", project_root=root, task_id=task_id, count=len(events), trace=True)
45
+ return
46
+ console.print(f"[cyan]Watching filesystem for task {task_id}. Ctrl+C to stop.[/cyan]")
47
+ try:
48
+ watcher.watch()
49
+ except KeyboardInterrupt:
50
+ console.print("[yellow]Stopped filesystem watcher.[/yellow]")
51
+ log_step("watch_fs/complete", project_root=root, task_id=task_id, trace=True)
@@ -0,0 +1,278 @@
1
+ """`dev wiki` — generate and maintain the agent-facing codebase wiki.
2
+
3
+ An OpenWiki-style, self-maintaining wiki of this repository, emitted as an OKF bundle
4
+ under ``.devcouncil/knowledge/okf/wiki/`` so pages are automatically injected into
5
+ planning/council/task prompts as knowledge sources (and are exportable/renderable with
6
+ the existing ``dev okf`` tooling).
7
+
8
+ * ``dev wiki update`` (alias ``generate``) — build the wiki, or refresh only the pages
9
+ whose repo-map slice changed. With a configured model, new/stale pages are enriched
10
+ with LLM-written prose by the ``wiki_writer`` role; without one, the deterministic
11
+ skeleton is used.
12
+ * ``dev wiki status`` — show which pages are fresh vs stale against the current map.
13
+ * ``dev wiki install-action`` — write a GitHub Action that keeps the wiki updated by
14
+ opening a PR when the map drifts (the OpenWiki maintenance pattern).
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ from pathlib import Path
21
+
22
+ import typer
23
+ from rich.console import Console
24
+ from rich.table import Table
25
+
26
+ from devcouncil.indexing.repo_mapper import RepoMap
27
+
28
+ app = typer.Typer(help="Generate and maintain the agent-facing codebase wiki (OKF bundle).")
29
+ console = Console()
30
+ logger = logging.getLogger(__name__)
31
+
32
+ _WORKFLOW_PATH = Path(".github") / "workflows" / "devcouncil-wiki-update.yml"
33
+
34
+ _WIKI_WORKFLOW = """\
35
+ # DevCouncil codebase wiki — automatic maintenance.
36
+ #
37
+ # Generated by `dev wiki install-action`. Nightly (and on demand) this rebuilds the
38
+ # repository map, refreshes the stale wiki pages, and opens a PR when anything changed —
39
+ # so the agent-facing documentation tracks the code without anyone remembering to run it.
40
+ #
41
+ # The refresh runs with --no-llm by default (deterministic, no secrets needed). To get
42
+ # LLM-enriched prose in CI, add your provider API key as a repository secret and export
43
+ # it in the "Update wiki" step (e.g. OPENROUTER_API_KEY).
44
+ name: DevCouncil Wiki Update
45
+
46
+ on:
47
+ workflow_dispatch:
48
+ schedule:
49
+ - cron: "0 5 * * *" # nightly (UTC); adjust or remove as needed
50
+
51
+ permissions:
52
+ contents: write
53
+ pull-requests: write
54
+
55
+ jobs:
56
+ update-wiki:
57
+ runs-on: ubuntu-latest
58
+ steps:
59
+ - uses: actions/checkout@v4
60
+ - uses: astral-sh/setup-uv@v7
61
+ - name: Install DevCouncil
62
+ run: |
63
+ uv tool install devcouncil
64
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
65
+ - name: Update wiki
66
+ run: |
67
+ dev map --no-wiki > /dev/null
68
+ dev wiki update --no-llm
69
+ - name: Open PR if the wiki changed
70
+ uses: peter-evans/create-pull-request@v7
71
+ with:
72
+ commit-message: "docs: refresh DevCouncil codebase wiki"
73
+ title: "Refresh DevCouncil codebase wiki"
74
+ body: >
75
+ Automated wiki refresh: the repository map drifted from the last generated
76
+ wiki. Review the page diffs and merge to keep agent-facing docs current.
77
+ branch: devcouncil/wiki-update
78
+ add-paths: |
79
+ .devcouncil/knowledge/okf/wiki/**
80
+ """
81
+
82
+ # .gitignore rules that make the wiki committable in repos that ignore .devcouncil/*
83
+ # (the default DevCouncil layout). Appended by `dev wiki install-action` when needed.
84
+ _GITIGNORE_UNIGNORE = """\
85
+
86
+ # DevCouncil codebase wiki: shared knowledge-as-code, maintained by `dev wiki update`.
87
+ !.devcouncil/knowledge/
88
+ .devcouncil/knowledge/*
89
+ !.devcouncil/knowledge/okf/
90
+ .devcouncil/knowledge/okf/*
91
+ !.devcouncil/knowledge/okf/wiki/
92
+ """
93
+
94
+
95
+ def wiki_dir_for(root: Path) -> Path:
96
+ from devcouncil.knowledge.wiki import wiki_dir_for as _wiki_dir_for
97
+
98
+ return _wiki_dir_for(root)
99
+
100
+
101
+ def _load_repo_map(root: Path, *, remap: bool) -> RepoMap:
102
+ """Load .devcouncil/repo_map.json, (re)generating it when missing or on --remap."""
103
+ map_path = root / ".devcouncil" / "repo_map.json"
104
+ if remap or not map_path.is_file():
105
+ from devcouncil.indexing.map_artifacts import generate_map_artifacts
106
+
107
+ console.print("[dim]Building repository map...[/dim]")
108
+ return generate_map_artifacts(root, map_path)
109
+ return RepoMap.model_validate_json(map_path.read_text(encoding="utf-8"))
110
+
111
+
112
+ def _print_result(result) -> None:
113
+ if result.changed:
114
+ console.print(
115
+ f"[green]Wiki updated:[/green] {len(result.created)} created, "
116
+ f"{len(result.updated)} updated, {len(result.skipped)} unchanged "
117
+ f"([cyan]{result.wiki_dir}[/cyan])."
118
+ )
119
+ else:
120
+ console.print(
121
+ f"[green]Wiki is up to date[/green] ({len(result.skipped)} pages, "
122
+ f"[cyan]{result.wiki_dir}[/cyan])."
123
+ )
124
+ if result.enriched:
125
+ console.print(f"[green]LLM-enriched {len(result.enriched)} page(s).[/green]")
126
+ if result.problems:
127
+ console.print(f"[yellow]{len(result.problems)} OKF validation issue(s):[/yellow]")
128
+ for p in result.problems[:10]:
129
+ console.print(f" - {p}")
130
+
131
+
132
+ def _update(
133
+ project_root: Path,
134
+ *,
135
+ llm: bool,
136
+ force: bool,
137
+ remap: bool,
138
+ ) -> None:
139
+ root = project_root.expanduser().resolve()
140
+ from devcouncil.telemetry.logging_setup import set_log_dir
141
+
142
+ set_log_dir(root)
143
+ logger.info("dev wiki update: llm=%s force=%s remap=%s", llm, force, remap)
144
+ from devcouncil.cli.commands.init import initialize_project
145
+
146
+ initialize_project(root, quiet=True)
147
+
148
+ from devcouncil.knowledge.wiki import refresh_wiki
149
+
150
+ result = refresh_wiki(root, llm=llm, force=force, remap=remap)
151
+ _print_result(result)
152
+
153
+
154
+ @app.command("update")
155
+ def update(
156
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
157
+ llm: bool = typer.Option(True, "--llm/--no-llm", help="Enrich new/stale pages with LLM-written prose (degrades to the skeleton without a configured model)."),
158
+ force: bool = typer.Option(False, "--force", help="Rewrite (and re-enrich) every page, ignoring fingerprints."),
159
+ remap: bool = typer.Option(False, "--remap", help="Rebuild .devcouncil/repo_map.json first."),
160
+ ) -> None:
161
+ """Create the wiki, or refresh only the pages whose repo-map slice changed."""
162
+ _update(project_root, llm=llm, force=force, remap=remap)
163
+
164
+
165
+ @app.command("generate")
166
+ def generate(
167
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
168
+ llm: bool = typer.Option(True, "--llm/--no-llm", help="Enrich pages with LLM-written prose."),
169
+ force: bool = typer.Option(False, "--force", help="Rewrite (and re-enrich) every page."),
170
+ remap: bool = typer.Option(False, "--remap", help="Rebuild .devcouncil/repo_map.json first."),
171
+ ) -> None:
172
+ """Alias of `dev wiki update`."""
173
+ _update(project_root, llm=llm, force=force, remap=remap)
174
+
175
+
176
+ @app.command("read")
177
+ def read(
178
+ page: str | None = typer.Option(None, "--page", help="Fetch one wiki page by relative path."),
179
+ query: str | None = typer.Option(None, "--query", help="Search wiki pages by keyword."),
180
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
181
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
182
+ ) -> None:
183
+ """Read the generated codebase wiki (list, fetch, or search)."""
184
+ from devcouncil.knowledge.wiki_read import read_wiki_page
185
+
186
+ root = project_root.expanduser().resolve()
187
+ payload = read_wiki_page(root, page=page, query=query)
188
+ if json_output:
189
+ console.print_json(data=payload)
190
+ if not payload.get("ok", True):
191
+ raise typer.Exit(code=1)
192
+ return
193
+ if not payload.get("ok", True):
194
+ console.print(f"[red]{payload.get('error')}[/red]")
195
+ raise typer.Exit(code=1)
196
+ if "pages" in payload:
197
+ pages = payload["pages"]
198
+ if isinstance(pages, list):
199
+ for item in pages:
200
+ if isinstance(item, dict):
201
+ console.print(f"- {item.get('page')}: {item.get('title')}")
202
+ return
203
+ console.print(payload.get("body") or "")
204
+
205
+
206
+ @app.command("status")
207
+ def status(
208
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
209
+ json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
210
+ ) -> None:
211
+ """Show which wiki pages are fresh vs stale against the current repository map."""
212
+ root = project_root.expanduser().resolve()
213
+ from devcouncil.telemetry.logging_setup import set_log_dir
214
+
215
+ set_log_dir(root)
216
+
217
+ wiki_dir = wiki_dir_for(root)
218
+ if not (wiki_dir / "index.md").is_file():
219
+ if json_output:
220
+ console.print_json(data={"exists": False, "wiki_dir": str(wiki_dir)})
221
+ else:
222
+ console.print(
223
+ f"[yellow]No wiki found at[/yellow] {wiki_dir}. Run [bold]dev wiki update[/bold] to create it."
224
+ )
225
+ raise typer.Exit(code=1)
226
+
227
+ repo_map = _load_repo_map(root, remap=False)
228
+ from devcouncil.knowledge.wiki import _page_fingerprints, wiki_stale_pages
229
+
230
+ stale = wiki_stale_pages(root, repo_map, wiki_dir)
231
+ total = len(_page_fingerprints(repo_map))
232
+
233
+ if json_output:
234
+ console.print_json(data={
235
+ "exists": True,
236
+ "wiki_dir": str(wiki_dir),
237
+ "pages": total,
238
+ "stale": stale,
239
+ "fresh": total - len(stale),
240
+ })
241
+ return
242
+
243
+ if not stale:
244
+ console.print(f"[green]✓ Wiki is fresh[/green] — {total} page(s) match the current repo map.")
245
+ return
246
+
247
+ table = Table(title=f"Stale wiki pages ({len(stale)}/{total})")
248
+ table.add_column("Page", overflow="fold")
249
+ table.add_column("Reason")
250
+ for rel_path, reason in sorted(stale.items()):
251
+ table.add_row(rel_path, reason)
252
+ console.print(table)
253
+ console.print("Run [bold]dev wiki update[/bold] to refresh them.")
254
+
255
+
256
+ @app.command("install-action")
257
+ def install_action(
258
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
259
+ force: bool = typer.Option(False, "--force", help="Overwrite an existing workflow file."),
260
+ ) -> None:
261
+ """Install a GitHub Action that keeps the wiki updated via automated PRs."""
262
+ root = project_root.expanduser().resolve()
263
+ target = root / _WORKFLOW_PATH
264
+ if target.exists() and not force:
265
+ console.print(f"[yellow]{target} already exists; use --force to overwrite.[/yellow]")
266
+ raise typer.Exit(code=1)
267
+ target.parent.mkdir(parents=True, exist_ok=True)
268
+ target.write_text(_WIKI_WORKFLOW, encoding="utf-8")
269
+ console.print(f"[green]Wrote[/green] {target}. The wiki will refresh nightly via PR.")
270
+
271
+ # The default DevCouncil .gitignore ignores .devcouncil/* — un-ignore the wiki so
272
+ # the workflow's PRs can actually commit it.
273
+ gitignore = root / ".gitignore"
274
+ if gitignore.is_file():
275
+ content = gitignore.read_text(encoding="utf-8")
276
+ if ".devcouncil" in content and "!.devcouncil/knowledge/okf/wiki/" not in content:
277
+ gitignore.write_text(content.rstrip("\n") + "\n" + _GITIGNORE_UNIGNORE, encoding="utf-8")
278
+ console.print("[green]Updated .gitignore[/green] so the wiki is committable.")