devcouncil 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +46 -30
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +49 -30
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +960 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +303 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/tasks.py +12 -3
  255. package/src/devcouncil/live/transcripts.py +69 -2
  256. package/src/devcouncil/llm/cache.py +5 -6
  257. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  258. package/src/devcouncil/llm/provider.py +647 -73
  259. package/src/devcouncil/llm/router.py +271 -46
  260. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  261. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  262. package/src/devcouncil/optimization/skillopt.py +9 -5
  263. package/src/devcouncil/planning/arbiter_service.py +12 -3
  264. package/src/devcouncil/planning/correction_manifest.py +107 -10
  265. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  266. package/src/devcouncil/planning/plan_service.py +5 -2
  267. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  268. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  269. package/src/devcouncil/planning/question_conversion.py +56 -0
  270. package/src/devcouncil/planning/spec_service.py +9 -3
  271. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  272. package/src/devcouncil/repo/gitignore.py +1 -2
  273. package/src/devcouncil/reporting/evidence_export.py +124 -0
  274. package/src/devcouncil/reporting/evidence_html.py +210 -0
  275. package/src/devcouncil/reporting/json_report.py +16 -12
  276. package/src/devcouncil/reporting/markdown_report.py +38 -9
  277. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  278. package/src/devcouncil/reporting/report_builder.py +40 -4
  279. package/src/devcouncil/reporting/task_provenance.py +42 -0
  280. package/src/devcouncil/reporting/verdict.py +75 -0
  281. package/src/devcouncil/skills/library/README.md +1 -0
  282. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  283. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  285. package/src/devcouncil/skills/registry.py +43 -12
  286. package/src/devcouncil/storage/db.py +57 -11
  287. package/src/devcouncil/storage/models.py +6 -0
  288. package/src/devcouncil/storage/native.py +5 -3
  289. package/src/devcouncil/storage/repositories.py +50 -18
  290. package/src/devcouncil/telemetry/context.py +28 -0
  291. package/src/devcouncil/telemetry/cost.py +4 -5
  292. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  293. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  294. package/src/devcouncil/telemetry/stages.py +27 -2
  295. package/src/devcouncil/telemetry/tracker.py +50 -13
  296. package/src/devcouncil/ui/dashboard.py +120 -8
  297. package/src/devcouncil/utils/fsio.py +58 -0
  298. package/src/devcouncil/utils/git_snapshot.py +112 -0
  299. package/src/devcouncil/utils/json_persist.py +53 -0
  300. package/src/devcouncil/utils/proc.py +89 -0
  301. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  302. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  303. package/src/devcouncil/verification/checks/__init__.py +41 -0
  304. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  305. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  306. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  307. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  308. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  309. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  310. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  311. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  312. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  313. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  314. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  315. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  316. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  317. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  318. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  319. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  320. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  321. package/src/devcouncil/verification/checks/wiring.py +216 -0
  322. package/src/devcouncil/verification/claims/__init__.py +23 -0
  323. package/src/devcouncil/verification/claims/checks.py +395 -0
  324. package/src/devcouncil/verification/claims/mapper.py +168 -0
  325. package/src/devcouncil/verification/claims/models.py +39 -0
  326. package/src/devcouncil/verification/claims/transcript.py +92 -0
  327. package/src/devcouncil/verification/claims/verdict.py +88 -0
  328. package/src/devcouncil/verification/command_evidence.py +170 -0
  329. package/src/devcouncil/verification/command_malformation.py +147 -0
  330. package/src/devcouncil/verification/command_runner.py +164 -0
  331. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  332. package/src/devcouncil/verification/diff_coverage.py +151 -0
  333. package/src/devcouncil/verification/difficulty.py +296 -0
  334. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  335. package/src/devcouncil/verification/gap_ids.py +63 -0
  336. package/src/devcouncil/verification/gate_cache.py +194 -0
  337. package/src/devcouncil/verification/gate_selector.py +344 -0
  338. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  339. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  340. package/src/devcouncil/verification/incremental_check.py +241 -0
  341. package/src/devcouncil/verification/next_actions.py +60 -1
  342. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  343. package/src/devcouncil/verification/sandbox.py +38 -11
  344. package/src/devcouncil/verification/stub_detector.py +369 -0
  345. package/src/devcouncil/verification/test_resolver.py +67 -1
  346. package/src/devcouncil/verification/verifier.py +137 -1666
  347. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  348. package/src/devcouncil/verification/verify_setup.py +176 -0
  349. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  350. package/src/semantic_layer/__init__.py +58 -0
  351. package/src/semantic_layer/benchmark.py +75 -0
  352. package/src/semantic_layer/cache.py +290 -0
  353. package/src/semantic_layer/compressor.py +137 -0
  354. package/src/semantic_layer/config.py +75 -0
  355. package/src/semantic_layer/embeddings.py +69 -0
  356. package/src/semantic_layer/llm_backends.py +99 -0
  357. package/src/semantic_layer/pipeline.py +111 -0
  358. package/src/semantic_layer/router.py +128 -0
  359. package/src/semantic_layer/tuner.py +72 -0
  360. package/uv.lock +973 -9
  361. package/src/devcouncil/artifacts/migrations.py +0 -20
  362. package/src/devcouncil/artifacts/schemas.py +0 -23
  363. package/src/devcouncil/artifacts/serializer.py +0 -21
  364. package/src/devcouncil/integrations/gitnexus.py +0 -70
  365. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -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
 
@@ -232,7 +240,7 @@ def resolve(
232
240
  if status not in {"resolved", "ignored"}:
233
241
  message = "--status must be resolved or ignored."
234
242
  if json_format:
235
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
243
+ typer.echo(dump_json({"ok": False, "error": message}, indent=2))
236
244
  else:
237
245
  console.print(f"[red]{message}[/red]")
238
246
  raise typer.Exit(code=2)
@@ -241,12 +249,12 @@ def resolve(
241
249
  if card is None:
242
250
  message = f"Critique card {card_id} not found."
243
251
  if json_format:
244
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
252
+ typer.echo(dump_json({"ok": False, "error": message}, indent=2))
245
253
  else:
246
254
  console.print(f"[red]{message}[/red]")
247
255
  raise typer.Exit(code=1)
248
256
  if json_format:
249
- typer.echo(json.dumps({"ok": True, "card": card.model_dump()}, indent=2))
257
+ typer.echo(dump_json({"ok": True, "card": card.model_dump()}, indent=2))
250
258
  _log_card_resolved(root, card)
251
259
  return
252
260
  _log_card_resolved(root, card)
@@ -265,13 +273,13 @@ def repair(
265
273
  if card is None:
266
274
  message = f"Critique card {card_id} not found."
267
275
  if json_format:
268
- typer.echo(json.dumps({"ok": False, "error": message}, indent=2))
276
+ typer.echo(dump_json({"ok": False, "error": message}, indent=2))
269
277
  else:
270
278
  console.print(f"[red]{message}[/red]")
271
279
  raise typer.Exit(code=1)
272
280
  prompt = build_live_repair_prompt(root, card)
273
281
  if json_format:
274
- typer.echo(json.dumps({"ok": True, "card": card.model_dump(), "prompt": prompt}, indent=2))
282
+ typer.echo(dump_json({"ok": True, "card": card.model_dump(), "prompt": prompt}, indent=2))
275
283
  return
276
284
  typer.echo(prompt)
277
285
 
@@ -296,7 +304,7 @@ def repair_all(
296
304
  resolved_cards = [card for card in cards if card is not None]
297
305
  prompt = build_bulk_live_repair_prompt(root, resolved_cards)
298
306
  if json_format:
299
- typer.echo(json.dumps({
307
+ typer.echo(dump_json({
300
308
  "ok": True,
301
309
  "scope_task_id": summary["scope_task_id"],
302
310
  "cards": [card.model_dump() for card in resolved_cards],
@@ -316,7 +324,7 @@ def signals(
316
324
  root = project_root.expanduser().resolve()
317
325
  found = _filtered_signals(root, client)
318
326
  if json_format:
319
- typer.echo(json.dumps({"signals": [item.model_dump() for item in found]}, indent=2))
327
+ typer.echo(dump_json({"signals": [item.model_dump() for item in found]}, indent=2))
320
328
  return
321
329
  table = Table(title="DevCouncil Watch Signals")
322
330
  table.add_column("Client", style="cyan")
@@ -378,7 +386,7 @@ def pending(
378
386
 
379
387
  logger.info("dev watch pending complete: %d reviewed, %d skipped", len(reviewed), len(skipped))
380
388
  if json_format:
381
- typer.echo(json.dumps({"reviewed": reviewed, "skipped": skipped}, indent=2))
389
+ typer.echo(dump_json({"reviewed": reviewed, "skipped": skipped}, indent=2))
382
390
  return
383
391
  for item in skipped:
384
392
  console.print(f"[yellow]Skipped signal:[/yellow] {item['reason']}")
@@ -441,7 +449,7 @@ def import_transcript(
441
449
  """Normalize a transcript into DevCouncil turn records without reviewing it."""
442
450
  turns = load_turns(transcript.expanduser().resolve(), client=client)
443
451
  if json_format:
444
- typer.echo(json.dumps({"turns": [turn.model_dump() for turn in turns]}, indent=2))
452
+ typer.echo(dump_json({"turns": [turn.model_dump() for turn in turns]}, indent=2))
445
453
  return
446
454
  console.print(f"Loaded {len(turns)} turns from {transcript}")
447
455
 
@@ -492,6 +500,7 @@ def _resolve_transcript(
492
500
  transcript: Path | None = None,
493
501
  session: str | None = None,
494
502
  latest: bool = False,
503
+ task_id: str | None = None,
495
504
  ) -> Path | None:
496
505
  if transcript is not None:
497
506
  path = transcript.expanduser()
@@ -499,6 +508,13 @@ def _resolve_transcript(
499
508
  path = root / path
500
509
  return path.resolve()
501
510
 
511
+ if client.lower() == "claude" and task_id:
512
+ from devcouncil.live.transcripts import claude_transcript_for_task
513
+
514
+ pinned = claude_transcript_for_task(root, task_id)
515
+ if pinned is not None:
516
+ return pinned
517
+
502
518
  selector = "latest" if latest else session
503
519
  if selector is None:
504
520
  return None
@@ -590,5 +606,8 @@ async def _review_turn(turn, root: Path, client: str, use_llm: bool, task_id: st
590
606
  logger.warning("Model-backed live review unavailable; using deterministic card: %s", exc)
591
607
  console.print(f"[yellow]Model-backed review unavailable; using deterministic card: {exc}[/yellow]")
592
608
  return review_turn(turn, root, client=client, task_id=task_id)
593
- card = await LiveReviewService(router).review(turn, root, client=client, use_llm=True)
609
+ # task_id flows through so the LLM review is grounded in the task's recorded
610
+ # verification state (like the deterministic path), instead of judging the
611
+ # agent's prose in a vacuum.
612
+ card = await LiveReviewService(router).review(turn, root, client=client, use_llm=True, task_id=task_id)
594
613
  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.")