devcouncil 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. package/README.md +46 -30
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +49 -30
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +960 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +303 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/tasks.py +12 -3
  255. package/src/devcouncil/live/transcripts.py +69 -2
  256. package/src/devcouncil/llm/cache.py +5 -6
  257. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  258. package/src/devcouncil/llm/provider.py +647 -73
  259. package/src/devcouncil/llm/router.py +271 -46
  260. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  261. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  262. package/src/devcouncil/optimization/skillopt.py +9 -5
  263. package/src/devcouncil/planning/arbiter_service.py +12 -3
  264. package/src/devcouncil/planning/correction_manifest.py +107 -10
  265. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  266. package/src/devcouncil/planning/plan_service.py +5 -2
  267. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  268. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  269. package/src/devcouncil/planning/question_conversion.py +56 -0
  270. package/src/devcouncil/planning/spec_service.py +9 -3
  271. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  272. package/src/devcouncil/repo/gitignore.py +1 -2
  273. package/src/devcouncil/reporting/evidence_export.py +124 -0
  274. package/src/devcouncil/reporting/evidence_html.py +210 -0
  275. package/src/devcouncil/reporting/json_report.py +16 -12
  276. package/src/devcouncil/reporting/markdown_report.py +38 -9
  277. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  278. package/src/devcouncil/reporting/report_builder.py +40 -4
  279. package/src/devcouncil/reporting/task_provenance.py +42 -0
  280. package/src/devcouncil/reporting/verdict.py +75 -0
  281. package/src/devcouncil/skills/library/README.md +1 -0
  282. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  283. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  285. package/src/devcouncil/skills/registry.py +43 -12
  286. package/src/devcouncil/storage/db.py +57 -11
  287. package/src/devcouncil/storage/models.py +6 -0
  288. package/src/devcouncil/storage/native.py +5 -3
  289. package/src/devcouncil/storage/repositories.py +50 -18
  290. package/src/devcouncil/telemetry/context.py +28 -0
  291. package/src/devcouncil/telemetry/cost.py +4 -5
  292. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  293. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  294. package/src/devcouncil/telemetry/stages.py +27 -2
  295. package/src/devcouncil/telemetry/tracker.py +50 -13
  296. package/src/devcouncil/ui/dashboard.py +120 -8
  297. package/src/devcouncil/utils/fsio.py +58 -0
  298. package/src/devcouncil/utils/git_snapshot.py +112 -0
  299. package/src/devcouncil/utils/json_persist.py +53 -0
  300. package/src/devcouncil/utils/proc.py +89 -0
  301. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  302. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  303. package/src/devcouncil/verification/checks/__init__.py +41 -0
  304. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  305. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  306. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  307. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  308. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  309. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  310. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  311. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  312. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  313. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  314. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  315. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  316. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  317. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  318. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  319. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  320. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  321. package/src/devcouncil/verification/checks/wiring.py +216 -0
  322. package/src/devcouncil/verification/claims/__init__.py +23 -0
  323. package/src/devcouncil/verification/claims/checks.py +395 -0
  324. package/src/devcouncil/verification/claims/mapper.py +168 -0
  325. package/src/devcouncil/verification/claims/models.py +39 -0
  326. package/src/devcouncil/verification/claims/transcript.py +92 -0
  327. package/src/devcouncil/verification/claims/verdict.py +88 -0
  328. package/src/devcouncil/verification/command_evidence.py +170 -0
  329. package/src/devcouncil/verification/command_malformation.py +147 -0
  330. package/src/devcouncil/verification/command_runner.py +164 -0
  331. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  332. package/src/devcouncil/verification/diff_coverage.py +151 -0
  333. package/src/devcouncil/verification/difficulty.py +296 -0
  334. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  335. package/src/devcouncil/verification/gap_ids.py +63 -0
  336. package/src/devcouncil/verification/gate_cache.py +194 -0
  337. package/src/devcouncil/verification/gate_selector.py +344 -0
  338. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  339. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  340. package/src/devcouncil/verification/incremental_check.py +241 -0
  341. package/src/devcouncil/verification/next_actions.py +60 -1
  342. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  343. package/src/devcouncil/verification/sandbox.py +38 -11
  344. package/src/devcouncil/verification/stub_detector.py +369 -0
  345. package/src/devcouncil/verification/test_resolver.py +67 -1
  346. package/src/devcouncil/verification/verifier.py +137 -1666
  347. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  348. package/src/devcouncil/verification/verify_setup.py +176 -0
  349. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  350. package/src/semantic_layer/__init__.py +58 -0
  351. package/src/semantic_layer/benchmark.py +75 -0
  352. package/src/semantic_layer/cache.py +290 -0
  353. package/src/semantic_layer/compressor.py +137 -0
  354. package/src/semantic_layer/config.py +75 -0
  355. package/src/semantic_layer/embeddings.py +69 -0
  356. package/src/semantic_layer/llm_backends.py +99 -0
  357. package/src/semantic_layer/pipeline.py +111 -0
  358. package/src/semantic_layer/router.py +128 -0
  359. package/src/semantic_layer/tuner.py +72 -0
  360. package/uv.lock +973 -9
  361. package/src/devcouncil/artifacts/migrations.py +0 -20
  362. package/src/devcouncil/artifacts/schemas.py +0 -23
  363. package/src/devcouncil/artifacts/serializer.py +0 -21
  364. package/src/devcouncil/integrations/gitnexus.py +0 -70
  365. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -0,0 +1,303 @@
1
+ """Shared helpers for MCP tool handlers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import logging
7
+ import subprocess
8
+ import sys
9
+ from collections.abc import Awaitable, Callable
10
+ from pathlib import Path
11
+
12
+ from mcp.types import TextContent
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ _CLI_TIMEOUT_SECONDS = 120
17
+ CLI_TIMEOUT_SECONDS = _CLI_TIMEOUT_SECONDS
18
+ _GIT_APPLY_TIMEOUT_SECONDS = 120
19
+ _CLI_OUTPUT_LIMIT = 20_000
20
+
21
+
22
+ def read_log_file(path: str | None) -> str:
23
+ """Best-effort read of a persisted stdout/stderr log."""
24
+ if not path:
25
+ return ""
26
+ try:
27
+ return Path(path).read_text(encoding="utf-8", errors="replace")
28
+ except OSError:
29
+ return ""
30
+
31
+
32
+ def optional_string_list_argument(arguments: dict, name: str) -> tuple[list[str], list[TextContent] | None]:
33
+ value = arguments.get(name)
34
+ if value is None:
35
+ return [], None
36
+ if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
37
+ return [], error_text(f"{name} must be a string array", code="invalid_arguments", argument=name)
38
+ return value, None
39
+
40
+
41
+ def json_text(payload: dict[str, object]) -> list[TextContent]:
42
+ return [TextContent(type="text", text=json.dumps(payload, indent=2))]
43
+
44
+
45
+ def error_text(message: str, *, code: str = "error", **details: object) -> list[TextContent]:
46
+ return json_text({"ok": False, "error": message, "code": code, **details})
47
+
48
+
49
+ def annotate_stale(contents: list[TextContent], coordinator: object) -> list[TextContent]:
50
+ """Attach top-level stale/sync metadata when a freshness wait timed out."""
51
+ text = contents[0].text if contents else ""
52
+ try:
53
+ payload = json.loads(text)
54
+ except json.JSONDecodeError:
55
+ return contents
56
+ if not isinstance(payload, dict):
57
+ return contents
58
+ status = coordinator.status() # type: ignore[attr-defined]
59
+ payload["stale"] = True
60
+ payload["fresh"] = False
61
+ existing_sync = payload.get("sync")
62
+ sync_base = existing_sync if isinstance(existing_sync, dict) else {}
63
+ payload["sync"] = {
64
+ **sync_base,
65
+ "pending": list(getattr(status, "pending", []) or []),
66
+ "state": getattr(status, "state", "pending"),
67
+ "fresh": False,
68
+ }
69
+ return json_text(payload)
70
+
71
+
72
+ async def with_codeintel_freshness(
73
+ root: Path,
74
+ produce: Callable[[], Awaitable[list[TextContent]]],
75
+ *,
76
+ timeout: float = 2.0,
77
+ ) -> list[TextContent]:
78
+ """Await pending sync, run ``produce``, and annotate stale responses."""
79
+ import asyncio
80
+
81
+ from devcouncil.codeintel.sync import get_sync_coordinator
82
+
83
+ coordinator = get_sync_coordinator(root)
84
+ fresh = await asyncio.to_thread(coordinator.wait_until_fresh, timeout=timeout)
85
+ contents = await produce()
86
+ if not fresh and contents:
87
+ return annotate_stale(contents, coordinator)
88
+ return contents
89
+
90
+
91
+ def normalize_arguments(arguments: object) -> dict:
92
+ return arguments if isinstance(arguments, dict) else {}
93
+
94
+
95
+ def int_argument(arguments: dict, name: str, default: int, *, minimum: int, maximum: int) -> int:
96
+ value = arguments.get(name, default)
97
+ if not isinstance(value, int) or isinstance(value, bool):
98
+ value = default
99
+ return max(minimum, min(value, maximum))
100
+
101
+
102
+ def optional_string_argument(arguments: dict, name: str) -> str | None:
103
+ value = arguments.get(name)
104
+ if value is None:
105
+ return None
106
+ return value if isinstance(value, str) else ""
107
+
108
+
109
+ def optional_bool_argument(arguments: dict, name: str) -> tuple[bool | None, list[TextContent] | None]:
110
+ """Return ``(value, None)`` when absent/valid, or ``(None, error)`` when malformed.
111
+
112
+ Absent → ``(None, None)`` so callers can apply their own default.
113
+ """
114
+ if name not in arguments or arguments.get(name) is None:
115
+ return None, None
116
+ value = arguments.get(name)
117
+ if not isinstance(value, bool):
118
+ return None, error_text(f"{name} must be a boolean", code="invalid_arguments", argument=name)
119
+ return value, None
120
+
121
+
122
+ def required_string_argument(arguments: dict, name: str) -> tuple[str | None, list[TextContent] | None]:
123
+ value = arguments.get(name)
124
+ if value is None or value == "":
125
+ return None, error_text(f"Missing {name}", code="missing_argument", argument=name)
126
+ if not isinstance(value, str):
127
+ return None, error_text(f"{name} must be a string", code="invalid_arguments", argument=name)
128
+ return value, None
129
+
130
+
131
+ def truncate_text(value: str | bytes | None, limit: int = _CLI_OUTPUT_LIMIT) -> tuple[str, bool]:
132
+ if value is None:
133
+ return "", False
134
+ if isinstance(value, bytes):
135
+ value = value.decode("utf-8", errors="replace")
136
+ if len(value) <= limit:
137
+ return value, False
138
+ marker = f"\n...[truncated to {limit} characters]"
139
+ return value[:limit] + marker, True
140
+
141
+
142
+ def is_git_repo(root: Path) -> bool:
143
+ try:
144
+ from devcouncil.utils.proc import run_git
145
+
146
+ result = run_git(["rev-parse", "--is-inside-work-tree"], cwd=root)
147
+ return result.returncode == 0 and result.stdout.strip() == "true"
148
+ except Exception as exc:
149
+ logger.debug("git work-tree check failed for %s: %s", root, exc)
150
+ return False
151
+
152
+
153
+ def within_root(root: Path, rel_or_abs: str) -> Path | None:
154
+ raw = rel_or_abs.strip().strip('"').replace("\\", "/")
155
+ try:
156
+ candidate = Path(raw)
157
+ resolved = candidate.resolve() if candidate.is_absolute() else (root / raw).resolve()
158
+ resolved.relative_to(root.resolve())
159
+ return resolved
160
+ except (OSError, ValueError):
161
+ return None
162
+
163
+
164
+ def diff_target_paths(unified_diff: str) -> list[str]:
165
+ targets: list[str] = []
166
+ seen: set[str] = set()
167
+
168
+ def _clean(token: str) -> str | None:
169
+ token = token.strip()
170
+ if len(token) >= 2 and token.startswith('"') and token.endswith('"'):
171
+ try:
172
+ token = token[1:-1].encode("utf-8").decode("unicode_escape")
173
+ except Exception:
174
+ token = token[1:-1]
175
+ if not token or token == "/dev/null":
176
+ return None
177
+ if token[:2] in ("a/", "b/"):
178
+ token = token[2:]
179
+ return token or None
180
+
181
+ def _add(path: str | None) -> None:
182
+ if path and path not in seen:
183
+ seen.add(path)
184
+ targets.append(path)
185
+
186
+ for line in unified_diff.splitlines():
187
+ if line.startswith("--- ") or line.startswith("+++ "):
188
+ _add(_clean(line[4:]))
189
+ elif line.startswith(("rename from ", "rename to ", "copy from ", "copy to ")):
190
+ _add(_clean(line.split(" ", 2)[2]))
191
+ elif line.startswith("diff --git "):
192
+ parts = line[len("diff --git "):].split()
193
+ if len(parts) == 2:
194
+ _add(_clean(parts[0]))
195
+ _add(_clean(parts[1]))
196
+ return targets
197
+
198
+
199
+ def lease_ttl_seconds(root: Path) -> int:
200
+ try:
201
+ from devcouncil.app.config import load_config
202
+
203
+ return max(0, int(load_config(root).execution.lease_ttl_seconds))
204
+ except Exception as exc:
205
+ logger.debug("Could not load lease_ttl_seconds for %s: %s", root, exc)
206
+ return 1800
207
+
208
+
209
+ def allowed_next_tools(status: str, has_blocking_gaps: bool) -> list[str]:
210
+ if status == "verified":
211
+ return ["devcouncil_release_task"]
212
+ if status in {"done", "cancelled"}:
213
+ return []
214
+ if status in {"running", "blocked"} or has_blocking_gaps:
215
+ return [
216
+ "devcouncil_read_file",
217
+ "devcouncil_get_evidence",
218
+ "devcouncil_get_diff",
219
+ "devcouncil_run_command",
220
+ "devcouncil_apply_patch",
221
+ "devcouncil_write_file",
222
+ "devcouncil_update_task_scope",
223
+ "devcouncil_verify_task",
224
+ ]
225
+ return [
226
+ "devcouncil_checkout_task",
227
+ "devcouncil_read_file",
228
+ "devcouncil_get_diff",
229
+ ]
230
+
231
+
232
+ def parse_cli_json(result: dict[str, object]) -> tuple[dict | None, list[TextContent] | None]:
233
+ """Parse JSON stdout from a CLI subprocess, even when exit code is non-zero."""
234
+ stdout = str(result.get("stdout") or "").strip()
235
+ if stdout:
236
+ try:
237
+ return json.loads(stdout), None
238
+ except json.JSONDecodeError:
239
+ pass
240
+ if not result.get("ok"):
241
+ stderr = str(result.get("stderr") or "CLI command failed")
242
+ return None, error_text(stderr, code="cli_failed")
243
+ return None, error_text("CLI command returned invalid JSON", code="cli_parse_error")
244
+
245
+
246
+ def run_cli_command(args: list[str], root: Path) -> dict[str, object]:
247
+ command = [sys.executable, "-m", "devcouncil", *args, "--project-root", str(root)]
248
+ try:
249
+ result = subprocess.run(
250
+ command,
251
+ cwd=root,
252
+ capture_output=True,
253
+ text=True,
254
+ encoding="utf-8",
255
+ errors="replace",
256
+ timeout=_CLI_TIMEOUT_SECONDS,
257
+ )
258
+ stdout, stdout_truncated = truncate_text(result.stdout)
259
+ stderr, stderr_truncated = truncate_text(result.stderr)
260
+ return {
261
+ "ok": result.returncode == 0,
262
+ "returncode": result.returncode,
263
+ "stdout": stdout,
264
+ "stderr": stderr,
265
+ "stdout_truncated": stdout_truncated,
266
+ "stderr_truncated": stderr_truncated,
267
+ "timed_out": False,
268
+ }
269
+ except subprocess.TimeoutExpired as exc:
270
+ stdout, stdout_truncated = truncate_text(exc.output)
271
+ stderr, stderr_truncated = truncate_text(exc.stderr)
272
+ return {
273
+ "ok": False,
274
+ "returncode": None,
275
+ "stdout": stdout,
276
+ "stderr": stderr,
277
+ "stdout_truncated": stdout_truncated,
278
+ "stderr_truncated": stderr_truncated,
279
+ "timed_out": True,
280
+ "timeout_seconds": _CLI_TIMEOUT_SECONDS,
281
+ }
282
+
283
+
284
+ GIT_APPLY_TIMEOUT_SECONDS = _GIT_APPLY_TIMEOUT_SECONDS
285
+
286
+
287
+ def is_secret_path(root: Path, rel_or_abs: str) -> bool:
288
+ """True when a path matches a protected secret/credential glob."""
289
+ from devcouncil.execution.policy_engine import SECRET_PATH_PATTERNS
290
+ import fnmatch as _fnmatch
291
+
292
+ normalized = rel_or_abs.strip().strip('"').replace("\\", "/")
293
+ try:
294
+ candidate = Path(normalized)
295
+ if candidate.is_absolute():
296
+ resolved = candidate.resolve()
297
+ try:
298
+ normalized = resolved.relative_to(root.resolve()).as_posix()
299
+ except ValueError:
300
+ normalized = resolved.as_posix()
301
+ except OSError:
302
+ pass
303
+ return any(_fnmatch.fnmatch(normalized, pattern) for pattern in SECRET_PATH_PATTERNS)
@@ -0,0 +1,152 @@
1
+ """Integration doctor and optional companion setup helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from devcouncil.executors.agent_registry import (
12
+ VALID_INPUT_MODES,
13
+ load_agent_profiles,
14
+ load_cli_agent_specs,
15
+ )
16
+ from devcouncil.integrations.clients.common import _config_path, _load_raw_config, _save_raw_config
17
+ from devcouncil.utils.fsio import atomic_write_text
18
+
19
+ console = Console()
20
+
21
+
22
+ def build_integrations_doctor_table(root: Path) -> Table:
23
+ """Build the integration doctor table (PATH probes + config presence)."""
24
+ table = Table(title="DevCouncil Integration Doctor")
25
+ table.add_column("Integration", style="cyan", no_wrap=True)
26
+ table.add_column("Status")
27
+ table.add_column("Notes", overflow="fold")
28
+
29
+ checks = [
30
+ ("Agent Flow", "agent-flow-app", "Optional live/replay visualizer for trace JSONL."),
31
+ ("code-review-graph", "code-review-graph", "Optional structural graph context adapter."),
32
+ ("Claude Code", "claude", "Optional MCP client and native hook runtime for pre-tool-use enforcement."),
33
+ ("Codex CLI", "codex", "Optional MCP client, headless executor companion, and native hook runtime."),
34
+ ("Gemini CLI (deprecated)", "gemini", "Deprecated — migrate to Google Antigravity CLI (dev integrate antigravity --apply)."),
35
+ ("Cursor", "cursor-agent", "Optional MCP client, agent/cursor-agent executor, and native hooks."),
36
+ ("Grok Build", "grok", "Optional MCP client, grok headless executor, and native hooks."),
37
+ ("OpenCode", "opencode", "Optional MCP client and headless coding-agent executor."),
38
+ ("Google Antigravity CLI", "agy", "Optional Antigravity CLI companion and headless coding-agent executor."),
39
+ ("Warp / Oz", "oz", "Optional Warp/Oz CLI companion and agent executor."),
40
+ ("Aider", "aider", "Optional headless executor via `dev run --executor aider` (no MCP)."),
41
+ ]
42
+ for label, executable, notes in checks:
43
+ found = shutil.which(executable)
44
+ table.add_row(label, "[green]OK[/green]" if found else "[yellow]Missing[/yellow]", found or notes)
45
+
46
+ profiles = load_agent_profiles(root)
47
+ for name, spec in load_cli_agent_specs(root).items():
48
+ if spec.built_in:
49
+ continue
50
+ found = shutil.which(spec.executable)
51
+ mode_ok = spec.input_mode in VALID_INPUT_MODES
52
+ profile_ok = spec.default_profile in profiles
53
+ status = "[green]OK[/green]" if found and mode_ok and profile_ok else "[red]Invalid[/red]"
54
+ if not found:
55
+ status = "[yellow]Missing[/yellow]"
56
+ details = found or f"{spec.executable} not found on PATH"
57
+ if not mode_ok:
58
+ details = f"invalid input_mode={spec.input_mode}"
59
+ if not profile_ok:
60
+ details = f"{details}; missing profile={spec.default_profile}"
61
+ table.add_row(f"CLI agent: {name}", status, details)
62
+
63
+ config = _config_path(root)
64
+ table.add_row(
65
+ "DevCouncil config",
66
+ "[green]OK[/green]" if config.exists() else "[red]Missing[/red]",
67
+ str(config) if config.exists() else "Run dev init first.",
68
+ )
69
+ return table
70
+
71
+
72
+ def apply_agent_flow_setup(root: Path) -> Path:
73
+ """Record Agent Flow trace integration in config and write docs stub."""
74
+ trace_path = root / ".devcouncil" / "logs" / "traces.jsonl"
75
+ config = _load_raw_config(root)
76
+ integrations = config.setdefault("integrations", {})
77
+ integrations["agent_flow"] = {
78
+ "enabled": True,
79
+ "trace_path": str(trace_path),
80
+ "mode": "jsonl",
81
+ }
82
+ _save_raw_config(root, config)
83
+ docs_dir = root / ".devcouncil" / "integrations"
84
+ docs_dir.mkdir(parents=True, exist_ok=True)
85
+ doc_path = docs_dir / "agent-flow.md"
86
+ doc_path.write_text(
87
+ "\n".join([
88
+ "# Agent Flow",
89
+ "",
90
+ f"DevCouncil writes trace events to `{trace_path}`.",
91
+ "",
92
+ "Local replay:",
93
+ "",
94
+ "```bash",
95
+ "dev trace tail --follow",
96
+ "```",
97
+ "",
98
+ "External visualizers can watch the JSONL file directly. DevCouncil does not modify global editor or Claude Code settings from this setup command.",
99
+ "",
100
+ ]),
101
+ encoding="utf-8",
102
+ )
103
+ return trace_path
104
+
105
+
106
+ def apply_code_review_graph_setup(root: Path) -> tuple[str | None, Path, bool]:
107
+ """Record code-review-graph integration, ensure ignore file, and write docs stub."""
108
+ executable = shutil.which("code-review-graph")
109
+ ignore_path = root / ".code-review-graphignore"
110
+ created = False
111
+ if not ignore_path.exists():
112
+ # Guarded by exists(): a torn write here would never be repaired, so write atomically.
113
+ atomic_write_text(
114
+ ignore_path,
115
+ "\n".join([
116
+ ".devcouncil/**",
117
+ ".git/**",
118
+ ".venv/**",
119
+ "dist/**",
120
+ "node_modules/**",
121
+ "",
122
+ ]),
123
+ )
124
+ created = True
125
+
126
+ config = _load_raw_config(root)
127
+ integrations = config.setdefault("integrations", {})
128
+ integrations["code_review_graph"] = {
129
+ "enabled": True,
130
+ "command": "code-review-graph",
131
+ "optional": True,
132
+ }
133
+ _save_raw_config(root, config)
134
+ docs_dir = root / ".devcouncil" / "integrations"
135
+ docs_dir.mkdir(parents=True, exist_ok=True)
136
+ (docs_dir / "code-review-graph.md").write_text(
137
+ "\n".join([
138
+ "# code-review-graph",
139
+ "",
140
+ "Install and build the graph outside DevCouncil:",
141
+ "",
142
+ "```bash",
143
+ "pipx install code-review-graph",
144
+ "code-review-graph build",
145
+ "```",
146
+ "",
147
+ "DevCouncil uses this as an optional context adapter for mapping, prompts, verification traces, and MCP graph context.",
148
+ "",
149
+ ]),
150
+ encoding="utf-8",
151
+ )
152
+ return executable, ignore_path, created
@@ -208,7 +208,11 @@ def _clone_git(url: str) -> FetchedBundle:
208
208
  ["git", "clone", "--depth", "1", url, str(target)],
209
209
  capture_output=True,
210
210
  text=True,
211
+ timeout=600, # network clone; generous, but a stalled remote must not hang fetch
211
212
  )
213
+ except subprocess.TimeoutExpired as exc:
214
+ shutil.rmtree(parent, ignore_errors=True)
215
+ raise RuntimeError(f"git clone of {url!r} timed out after 600s") from exc
212
216
  except OSError as exc: # pragma: no cover - git present but unexecutable
213
217
  shutil.rmtree(parent, ignore_errors=True)
214
218
  raise RuntimeError(f"git clone failed to start: {exc}") from exc
@@ -0,0 +1,38 @@
1
+ """Goal-driven knowledge source selection shared by CLI and MCP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+
8
+ def select_knowledge_payload(root: Path, goal: str) -> dict[str, object]:
9
+ """Return MCP/CLI-compatible knowledge selection for ``goal``."""
10
+ from devcouncil.knowledge.resource_discovery import knowledge_settings
11
+ from devcouncil.knowledge.sources import render_knowledge_preamble, select_knowledge_sources
12
+
13
+ try:
14
+ directory, design_always = knowledge_settings(root)
15
+ if directory is None:
16
+ sources = []
17
+ else:
18
+ sources = select_knowledge_sources(
19
+ goal, root, directory=directory, design_always=design_always
20
+ )
21
+ preamble = render_knowledge_preamble(sources)
22
+ return {
23
+ "ok": True,
24
+ "goal": goal,
25
+ "sources": [
26
+ {"name": source.name, "kind": source.kind, "description": source.description}
27
+ for source in sources
28
+ ],
29
+ "preamble": preamble,
30
+ }
31
+ except Exception as exc:
32
+ return {
33
+ "ok": True,
34
+ "goal": goal,
35
+ "sources": [],
36
+ "preamble": "",
37
+ "note": f"knowledge unavailable: {exc}",
38
+ }
@@ -22,6 +22,7 @@ from typing import Any
22
22
  from pydantic import BaseModel, Field
23
23
 
24
24
  from devcouncil.knowledge.frontmatter import build_frontmatter_markdown, split_frontmatter
25
+ from devcouncil.utils.fsio import atomic_write_text
25
26
 
26
27
  # Markdown inline links: [text](target). We only resolve relative, non-anchor, non-URL
27
28
  # targets into intra-bundle edges; external resources live in the `resource` field.
@@ -179,7 +180,7 @@ def write_bundle(bundle: OKFBundle, bundle_dir: Path) -> list[Path]:
179
180
  continue
180
181
  target = bundle_dir / doc.rel_path
181
182
  target.parent.mkdir(parents=True, exist_ok=True)
182
- target.write_text(doc.to_markdown(), encoding="utf-8")
183
+ atomic_write_text(target, doc.to_markdown())
183
184
  written.append(target)
184
185
  return written
185
186
 
@@ -0,0 +1,40 @@
1
+ """Knowledge source discovery for MCP resources and OKF selection."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from urllib.parse import quote
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def knowledge_source_uri(kind: str, name: str) -> str:
13
+ """Stable, parseable resource URI for one ingested knowledge source."""
14
+ return f"devcouncil://knowledge/{kind}/{quote(name, safe='')}"
15
+
16
+
17
+ def knowledge_settings(root: Path) -> tuple[str | None, bool]:
18
+ """Resolve (directory, design_always) for knowledge exposure from project config."""
19
+ try:
20
+ from devcouncil.app.config import load_config
21
+
22
+ cfg = load_config(root).knowledge
23
+ return (None if not cfg.enabled else cfg.directory), cfg.design_always
24
+ except Exception as exc:
25
+ logger.debug("Could not load knowledge settings for %s: %s", root, exc)
26
+ return ".devcouncil/knowledge", True
27
+
28
+
29
+ def discover_knowledge_sources(root: Path) -> list:
30
+ """Best-effort enumeration of ingested OKF/design knowledge for the project."""
31
+ try:
32
+ from devcouncil.knowledge.sources import discover_knowledge_sources as _discover
33
+
34
+ directory, design_always = knowledge_settings(root)
35
+ if directory is None:
36
+ return []
37
+ return _discover(root, directory=directory, design_always=design_always)
38
+ except Exception as exc:
39
+ logger.debug("Knowledge source discovery failed for %s: %s", root, exc)
40
+ return []