devcouncil 0.3.0 → 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,614 @@
1
+ """Optional semantic-layer integration for DevCouncil's LLM path.
2
+
3
+ Wraps ``semantic_layer`` cache, router, and compressor components behind a lazy
4
+ adapter that degrades gracefully when optional deps are not installed.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import atexit
11
+ import logging
12
+ import random
13
+ from pathlib import Path
14
+ from typing import TYPE_CHECKING, Any
15
+
16
+ from devcouncil.llm.provider import LLMResponse
17
+
18
+ if TYPE_CHECKING:
19
+ from devcouncil.app.config import DevCouncilConfig, SemanticLayerConfig as SemanticLayerSettings
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ _ADAPTER_BY_ROOT: dict[Path, SemanticLayerAdapter] = {}
24
+ _SHUTDOWN_REGISTERED = False
25
+
26
+
27
+ def prompt_text_from_messages(messages: list[dict[str, str]]) -> str:
28
+ """Stable text key for semantic cache lookup from chat messages."""
29
+ parts: list[str] = []
30
+ for msg in messages:
31
+ role = msg.get("role", "user")
32
+ content = (msg.get("content") or "").strip()
33
+ if content:
34
+ parts.append(f"{role}:{content}")
35
+ return "\n".join(parts)
36
+
37
+
38
+ def semantic_deps_available() -> bool:
39
+ """True when the lightweight semantic-layer runtime deps are importable."""
40
+ try:
41
+ import faiss # noqa: F401
42
+ import numpy # noqa: F401
43
+ except ImportError:
44
+ return False
45
+ return True
46
+
47
+
48
+ def semantic_embedding_deps_available() -> bool:
49
+ """True when sentence-transformers is importable for embedding."""
50
+ try:
51
+ import sentence_transformers # noqa: F401
52
+ except ImportError:
53
+ return False
54
+ return True
55
+
56
+
57
+ def _register_shutdown_once() -> None:
58
+ global _SHUTDOWN_REGISTERED
59
+ if _SHUTDOWN_REGISTERED:
60
+ return
61
+ atexit.register(_persist_all_adapters)
62
+ _SHUTDOWN_REGISTERED = True
63
+
64
+
65
+ def _persist_all_adapters() -> None:
66
+ for adapter in list(_ADAPTER_BY_ROOT.values()):
67
+ try:
68
+ adapter.persist_cache()
69
+ except Exception as exc:
70
+ logger.debug("Semantic cache shutdown persist failed: %s", exc)
71
+
72
+
73
+ def reset_semantic_adapters_for_tests() -> None:
74
+ """Clear the per-project adapter singleton — tests only."""
75
+ _ADAPTER_BY_ROOT.clear()
76
+
77
+
78
+ class SemanticLayerAdapter:
79
+ """Lazy facade over semantic_layer components, driven by DevCouncil config."""
80
+
81
+ def __init__(self, settings: SemanticLayerSettings, project_root: Path = Path(".")) -> None:
82
+ self.settings = settings
83
+ self.project_root = project_root
84
+ self._initialized = False
85
+ self._init_failed = False
86
+ self._cache: Any = None
87
+ self._router: Any = None
88
+ self._compressor: Any = None
89
+ self._embedder: Any = None
90
+
91
+ @property
92
+ def active(self) -> bool:
93
+ return self.settings.enabled and not self._init_failed
94
+
95
+ @property
96
+ def cache_enabled(self) -> bool:
97
+ return self.active and self.settings.cache.enabled
98
+
99
+ @property
100
+ def router_enabled(self) -> bool:
101
+ return self.active and self.settings.router.enabled
102
+
103
+ @property
104
+ def compressor_enabled(self) -> bool:
105
+ return self.active and self.settings.compressor.enabled
106
+
107
+ def cache_dir(self) -> Path:
108
+ return self.project_root / ".devcouncil" / "cache" / "semantic"
109
+
110
+ def cache_base_path(self) -> Path:
111
+ return self.cache_dir() / self.settings.cache.namespace
112
+
113
+ def _ensure_initialized(self) -> bool:
114
+ if self._initialized:
115
+ return not self._init_failed
116
+ self._initialized = True
117
+ if not self.settings.enabled:
118
+ self._init_failed = True
119
+ return False
120
+ if not semantic_deps_available():
121
+ logger.info(
122
+ "Semantic layer enabled in config but optional deps are missing "
123
+ "(install with: uv sync --group semantic). Falling back to standard LLM path."
124
+ )
125
+ self._init_failed = True
126
+ return False
127
+ try:
128
+ from semantic_layer.cache import SemanticCache
129
+ from semantic_layer.compressor import SemanticCompressor
130
+ from semantic_layer.config import (
131
+ CacheConfig,
132
+ CompressorConfig,
133
+ EmbeddingConfig,
134
+ RouterConfig,
135
+ )
136
+ from semantic_layer.embeddings import EmbeddingService
137
+ from semantic_layer.router import SemanticRouter
138
+
139
+ emb = self.settings.embedding
140
+ embedding_cfg = EmbeddingConfig(
141
+ model_name=emb.model_name,
142
+ dimension=emb.dimension,
143
+ device=emb.device, # type: ignore[arg-type]
144
+ batch_size=emb.batch_size,
145
+ normalize=emb.normalize,
146
+ )
147
+ self._embedder = EmbeddingService.get_instance(embedding_cfg)
148
+
149
+ cache_cfg = self.settings.cache
150
+ self._cache = SemanticCache(
151
+ CacheConfig(
152
+ backend=cache_cfg.backend, # type: ignore[arg-type]
153
+ similarity_threshold=cache_cfg.similarity_threshold,
154
+ ood_threshold=cache_cfg.ood_threshold,
155
+ margin_threshold=cache_cfg.margin_threshold,
156
+ ttl_seconds=cache_cfg.ttl_seconds,
157
+ max_entries=cache_cfg.max_entries,
158
+ namespace=cache_cfg.namespace,
159
+ exploration_rate=cache_cfg.exploration_rate,
160
+ ),
161
+ self._embedder,
162
+ embedding_cfg.dimension,
163
+ )
164
+ self._load_persisted_cache()
165
+
166
+ router_cfg = self.settings.router
167
+ self._router = SemanticRouter(
168
+ RouterConfig(
169
+ complexity_threshold=router_cfg.complexity_threshold,
170
+ small_model=router_cfg.small_model or "qwen2.5:1.5b",
171
+ large_model=router_cfg.large_model or "llama3.1:8b",
172
+ ),
173
+ self._embedder,
174
+ )
175
+
176
+ comp_cfg = self.settings.compressor
177
+ self._compressor = SemanticCompressor(
178
+ CompressorConfig(
179
+ token_budget=comp_cfg.token_budget,
180
+ top_k=comp_cfg.top_k,
181
+ chunk_token_size=comp_cfg.chunk_token_size,
182
+ chunk_overlap=comp_cfg.chunk_overlap,
183
+ min_chunk_score=comp_cfg.min_chunk_score,
184
+ mmr_lambda=comp_cfg.mmr_lambda,
185
+ ),
186
+ self._embedder,
187
+ )
188
+ if self.cache_enabled:
189
+ _register_shutdown_once()
190
+ return True
191
+ except Exception as exc:
192
+ logger.warning("Semantic layer initialization failed; disabling: %s", exc)
193
+ self._init_failed = True
194
+ return False
195
+
196
+ def _load_persisted_cache(self) -> None:
197
+ if self._cache is None:
198
+ return
199
+ base = self.cache_base_path()
200
+ faiss_path = Path(f"{base}.faiss")
201
+ json_path = Path(f"{base}.json")
202
+ if not faiss_path.is_file() or not json_path.is_file():
203
+ return
204
+ try:
205
+ self._cache.load(str(base))
206
+ logger.info(
207
+ "Loaded semantic cache from %s (%d entries)",
208
+ base,
209
+ len(self._cache._entries),
210
+ )
211
+ except Exception as exc:
212
+ logger.warning("Could not load persisted semantic cache from %s: %s", base, exc)
213
+
214
+ def persist_cache(self) -> None:
215
+ """Write the in-memory FAISS index and metadata to disk."""
216
+ if not self.cache_enabled or self._cache is None:
217
+ return
218
+ base = self.cache_base_path()
219
+ try:
220
+ base.parent.mkdir(parents=True, exist_ok=True)
221
+ self._cache.persist(str(base))
222
+ logger.debug("Persisted semantic cache to %s (%d entries)", base, len(self._cache._entries))
223
+ except Exception as exc:
224
+ logger.warning("Semantic cache persist failed: %s", exc)
225
+
226
+ def warm_up(self) -> bool:
227
+ """Preload the embedding model so the first LLM call is not blocked."""
228
+ if not self.settings.enabled:
229
+ return False
230
+ if not self._ensure_initialized() or self._embedder is None:
231
+ return False
232
+ try:
233
+ self._embedder.embed_one("warmup")
234
+ logger.debug("Semantic embedding model warmed up (%s)", self.settings.embedding.model_name)
235
+ return True
236
+ except Exception as exc:
237
+ logger.warning("Semantic embedding warm-up failed: %s", exc)
238
+ return False
239
+
240
+ async def _embed_one_async(self, text: str) -> Any:
241
+ assert self._embedder is not None
242
+ return await asyncio.to_thread(self._embedder.embed_one, text)
243
+
244
+ def _intent_for_prompt(self, prompt: str, query_vec: Any, *, model: str, role: str) -> str:
245
+ route = self._router.route(prompt, query_vec) if self._router else None
246
+ intent = f"{role}:{model}"
247
+ if route is not None:
248
+ intent = f"{role}:{model}:{route.intent}"
249
+ return intent
250
+
251
+ def lookup_cache(
252
+ self,
253
+ messages: list[dict[str, str]],
254
+ *,
255
+ model: str,
256
+ role: str,
257
+ ) -> LLMResponse | None:
258
+ """Return a cached LLM response for a semantically similar prompt, if any."""
259
+ if not self.cache_enabled or not self._ensure_initialized():
260
+ return None
261
+ assert self._cache is not None and self._embedder is not None
262
+
263
+ prompt = prompt_text_from_messages(messages)
264
+ if not prompt.strip():
265
+ return None
266
+
267
+ try:
268
+ query_vec = self._embedder.embed_one(prompt)
269
+ return self._lookup_with_vector(prompt, query_vec, model=model, role=role)
270
+ except Exception as exc:
271
+ logger.debug("Semantic cache lookup failed: %s", exc)
272
+ return None
273
+
274
+ async def lookup_cache_async(
275
+ self,
276
+ messages: list[dict[str, str]],
277
+ *,
278
+ model: str,
279
+ role: str,
280
+ ) -> LLMResponse | None:
281
+ """Async semantic cache lookup; embedding runs in a worker thread."""
282
+ if not self.cache_enabled or not self._ensure_initialized():
283
+ return None
284
+ assert self._cache is not None and self._embedder is not None
285
+
286
+ prompt = prompt_text_from_messages(messages)
287
+ if not prompt.strip():
288
+ return None
289
+
290
+ try:
291
+ query_vec = await self._embed_one_async(prompt)
292
+ return self._lookup_with_vector(prompt, query_vec, model=model, role=role)
293
+ except Exception as exc:
294
+ logger.debug("Semantic cache lookup failed: %s", exc)
295
+ return None
296
+
297
+ def _lookup_with_vector(
298
+ self,
299
+ prompt: str,
300
+ query_vec: Any,
301
+ *,
302
+ model: str,
303
+ role: str,
304
+ ) -> LLMResponse | None:
305
+ assert self._cache is not None
306
+ intent = self._intent_for_prompt(prompt, query_vec, model=model, role=role)
307
+ force_miss = random.random() < self.settings.cache.exploration_rate
308
+ result = self._cache.lookup(prompt, query_vec, intent=intent, force_miss=force_miss)
309
+ if not result.hit or not result.response:
310
+ return None
311
+
312
+ logger.info(
313
+ "Semantic cache HIT role=%s model=%s similarity=%.3f",
314
+ role,
315
+ model,
316
+ result.similarity,
317
+ )
318
+ return LLMResponse(
319
+ content=result.response,
320
+ model=model,
321
+ usage={"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0},
322
+ raw_response={
323
+ "semantic_cache": True,
324
+ "similarity": result.similarity,
325
+ "entry_id": result.entry_id,
326
+ },
327
+ )
328
+
329
+ def store_cache(
330
+ self,
331
+ messages: list[dict[str, str]],
332
+ response: LLMResponse,
333
+ *,
334
+ model: str,
335
+ role: str,
336
+ ) -> None:
337
+ """Persist a validated LLM response in the semantic cache."""
338
+ if not self.cache_enabled or not self._ensure_initialized():
339
+ return
340
+ assert self._cache is not None and self._embedder is not None
341
+
342
+ prompt = prompt_text_from_messages(messages)
343
+ if not prompt.strip() or not (response.content or "").strip():
344
+ return
345
+
346
+ try:
347
+ query_vec = self._embedder.embed_one(prompt)
348
+ intent = self._intent_for_prompt(prompt, query_vec, model=model, role=role)
349
+ self._cache.put(prompt, response.content, query_vec, intent=intent)
350
+ except Exception as exc:
351
+ logger.debug("Semantic cache store failed: %s", exc)
352
+
353
+ async def store_cache_async(
354
+ self,
355
+ messages: list[dict[str, str]],
356
+ response: LLMResponse,
357
+ *,
358
+ model: str,
359
+ role: str,
360
+ ) -> None:
361
+ """Async semantic cache store; embedding runs in a worker thread."""
362
+ if not self.cache_enabled or not self._ensure_initialized():
363
+ return
364
+ assert self._cache is not None and self._embedder is not None
365
+
366
+ prompt = prompt_text_from_messages(messages)
367
+ if not prompt.strip() or not (response.content or "").strip():
368
+ return
369
+
370
+ try:
371
+ query_vec = await self._embed_one_async(prompt)
372
+ intent = self._intent_for_prompt(prompt, query_vec, model=model, role=role)
373
+ self._cache.put(prompt, response.content, query_vec, intent=intent)
374
+ except Exception as exc:
375
+ logger.debug("Semantic cache store failed: %s", exc)
376
+
377
+ async def maybe_route_model_async(
378
+ self,
379
+ messages: list[dict[str, str]],
380
+ *,
381
+ configured_model: str,
382
+ role_provider: str | None,
383
+ ) -> str:
384
+ """Async model routing; embedding runs in a worker thread."""
385
+ if not self.router_enabled or not self._ensure_initialized():
386
+ return configured_model
387
+ assert self._router is not None and self._embedder is not None
388
+
389
+ if (role_provider or "").strip().lower() not in {"ollama", "ollama-local", "ollama_local"}:
390
+ return configured_model
391
+
392
+ prompt = prompt_text_from_messages(messages)
393
+ if not prompt.strip():
394
+ return configured_model
395
+
396
+ try:
397
+ query_vec = await self._embed_one_async(prompt)
398
+ return self._route_with_vector(prompt, query_vec, configured_model=configured_model)
399
+ except Exception as exc:
400
+ logger.debug("Semantic routing failed: %s", exc)
401
+ return configured_model
402
+
403
+ def _route_with_vector(self, prompt: str, query_vec: Any, *, configured_model: str) -> str:
404
+ assert self._router is not None
405
+ decision = self._router.route(prompt, query_vec)
406
+ logger.debug(
407
+ "Semantic route role=%s complexity=%.3f tier=%s model=%s",
408
+ configured_model,
409
+ decision.complexity_score,
410
+ decision.tier.value,
411
+ decision.model_name,
412
+ )
413
+ return str(decision.model_name)
414
+
415
+ async def maybe_compress_messages_async(self, messages: list[dict[str, str]]) -> list[dict[str, str]]:
416
+ """Async message compression; embedding runs in a worker thread."""
417
+ if not self.compressor_enabled or not self._ensure_initialized():
418
+ return messages
419
+ assert self._compressor is not None and self._embedder is not None
420
+
421
+ prepared = self._compression_inputs(messages)
422
+ if prepared is None:
423
+ return messages
424
+ query, context_docs = prepared
425
+
426
+ try:
427
+ query_vec = await self._embed_one_async(query)
428
+ return self._compress_with_vector(messages, query, context_docs, query_vec)
429
+ except Exception as exc:
430
+ logger.debug("Semantic compression failed: %s", exc)
431
+ return messages
432
+
433
+ def _compression_inputs(self, messages: list[dict[str, str]]) -> tuple[str, list[str]] | None:
434
+ min_chars = self.settings.compressor.min_chars
435
+ total_chars = sum(len(m.get("content") or "") for m in messages)
436
+ if total_chars < min_chars:
437
+ return None
438
+
439
+ user_messages = [m for m in messages if m.get("role") == "user" and (m.get("content") or "").strip()]
440
+ if not user_messages:
441
+ return None
442
+
443
+ query = user_messages[-1]["content"]
444
+ context_docs = [
445
+ m["content"]
446
+ for m in messages
447
+ if m is not user_messages[-1] and (m.get("content") or "").strip()
448
+ ]
449
+ if not context_docs:
450
+ blob = query
451
+ if len(blob) <= min_chars:
452
+ return None
453
+ split_at = max(500, len(blob) - 2000)
454
+ context_docs = [blob[:split_at]]
455
+ query = blob[split_at:].strip() or blob[-2000:]
456
+ return query, context_docs
457
+
458
+ def _compress_with_vector(
459
+ self,
460
+ messages: list[dict[str, str]],
461
+ query: str,
462
+ context_docs: list[str],
463
+ query_vec: Any,
464
+ ) -> list[dict[str, str]]:
465
+ assert self._compressor is not None
466
+ compressed = self._compressor.compress(query, context_docs, query_vec)
467
+ if not compressed.text.strip():
468
+ return messages
469
+
470
+ user_messages = [m for m in messages if m.get("role") == "user" and (m.get("content") or "").strip()]
471
+ compressed_messages: list[dict[str, str]] = []
472
+ replaced = False
473
+ for msg in messages:
474
+ if msg.get("role") == "user" and not replaced and msg is user_messages[-1]:
475
+ compressed_messages.append(
476
+ {
477
+ "role": "user",
478
+ "content": f"Context:\n{compressed.text}\n\nQuestion: {query}",
479
+ }
480
+ )
481
+ replaced = True
482
+ elif msg.get("role") != "user" or msg is not user_messages[-1]:
483
+ if msg.get("role") != "user":
484
+ compressed_messages.append(dict(msg))
485
+ if not replaced:
486
+ compressed_messages.append(
487
+ {"role": "user", "content": f"Context:\n{compressed.text}\n\nQuestion: {query}"}
488
+ )
489
+ logger.info(
490
+ "Semantic compression applied: %d -> %d chunks, ~%d tokens",
491
+ compressed.chunks_total,
492
+ compressed.chunks_used,
493
+ compressed.estimated_tokens,
494
+ )
495
+ return compressed_messages
496
+
497
+
498
+ def check_semantic_layer(
499
+ project_root: Path,
500
+ config: DevCouncilConfig | None = None,
501
+ ) -> list[tuple[str, str, str]]:
502
+ """Doctor rows for semantic-layer readiness (never raises)."""
503
+ ok = "[green]OK[/green]"
504
+ warn = "[yellow]WARN[/yellow]"
505
+ info = "[cyan]INFO[/cyan]"
506
+ rows: list[tuple[str, str, str]] = []
507
+
508
+ try:
509
+ from devcouncil.app.config import load_config
510
+
511
+ cfg = config if config is not None else load_config(project_root)
512
+ except Exception as exc:
513
+ rows.append(("Semantic layer", info, f"Config unavailable; skipping semantic checks ({exc})."))
514
+ return rows
515
+
516
+ sem = cfg.semantic_layer
517
+ if not sem.enabled:
518
+ rows.append(
519
+ (
520
+ "Semantic layer",
521
+ info,
522
+ "Disabled. Enable with: dev config set semantic_layer.enabled true "
523
+ "(requires: uv sync --group semantic).",
524
+ )
525
+ )
526
+ return rows
527
+
528
+ if not semantic_deps_available():
529
+ rows.append(
530
+ (
531
+ "Semantic deps",
532
+ warn,
533
+ "Missing faiss/numpy. Install with: uv sync --group semantic.",
534
+ )
535
+ )
536
+ return rows
537
+
538
+ rows.append(("Semantic layer", ok, "Enabled in config."))
539
+
540
+ if not semantic_embedding_deps_available():
541
+ rows.append(
542
+ (
543
+ "Semantic embedder",
544
+ warn,
545
+ "Missing sentence-transformers. Install with: uv sync --group semantic.",
546
+ )
547
+ )
548
+ else:
549
+ rows.append(
550
+ (
551
+ "Semantic embedder",
552
+ ok,
553
+ f"sentence-transformers available; model={sem.embedding.model_name}.",
554
+ )
555
+ )
556
+
557
+ cache_dir = project_root / ".devcouncil" / "cache" / "semantic"
558
+ try:
559
+ cache_dir.mkdir(parents=True, exist_ok=True)
560
+ probe = cache_dir / ".write_probe"
561
+ probe.write_text("ok", encoding="utf-8")
562
+ probe.unlink(missing_ok=True)
563
+ persisted = cache_dir / f"{sem.cache.namespace}.faiss"
564
+ detail = f"Writable at {cache_dir}."
565
+ if persisted.is_file():
566
+ detail += f" Persisted index present ({sem.cache.namespace})."
567
+ rows.append(("Semantic cache dir", ok, detail))
568
+ except Exception as exc:
569
+ rows.append(
570
+ (
571
+ "Semantic cache dir",
572
+ warn,
573
+ f"Not writable at {cache_dir}: {exc}.",
574
+ )
575
+ )
576
+
577
+ adapter = _ADAPTER_BY_ROOT.get(project_root.resolve())
578
+ if adapter is None:
579
+ adapter = SemanticLayerAdapter(sem, project_root)
580
+ if adapter.warm_up():
581
+ rows.append(("Semantic warm-up", ok, "Embedding model loaded successfully."))
582
+ else:
583
+ rows.append(
584
+ (
585
+ "Semantic warm-up",
586
+ warn,
587
+ f"Could not load embedding model {sem.embedding.model_name!r}. "
588
+ "First LLM call will retry or fall back to the standard path.",
589
+ )
590
+ )
591
+
592
+ return rows
593
+
594
+
595
+ def load_semantic_adapter(project_root: Path) -> SemanticLayerAdapter | None:
596
+ """Build a semantic adapter from project config, or None when disabled/unavailable."""
597
+ try:
598
+ from devcouncil.app.config import load_config
599
+
600
+ cfg = load_config(project_root)
601
+ if not cfg.semantic_layer.enabled:
602
+ return None
603
+ key = project_root.resolve()
604
+ existing = _ADAPTER_BY_ROOT.get(key)
605
+ if existing is not None:
606
+ return existing
607
+ adapter = SemanticLayerAdapter(cfg.semantic_layer, project_root)
608
+ _ADAPTER_BY_ROOT[key] = adapter
609
+ return adapter
610
+ except FileNotFoundError:
611
+ return None
612
+ except Exception as exc:
613
+ logger.debug("Could not load semantic layer config: %s", exc)
614
+ return None
@@ -9,13 +9,15 @@ from datetime import datetime, timezone
9
9
  from pathlib import Path
10
10
  from typing import Any, Callable
11
11
 
12
- import yaml # type: ignore[import-untyped]
12
+ import yaml
13
13
 
14
14
  from devcouncil.executors.agent_registry import (
15
15
  load_agent_profiles,
16
16
  load_cli_agent_specs,
17
17
  normalize_agent_name,
18
18
  )
19
+ from devcouncil.utils.fsio import atomic_write_text
20
+ from devcouncil.utils.json_persist import read_json, write_json
19
21
 
20
22
 
21
23
  DEFAULT_OBJECTIVE = (
@@ -156,7 +158,7 @@ def load_agent_eval_dataset(path: Path) -> list[dict[str, Any]]:
156
158
  if line.strip()
157
159
  ]
158
160
  else:
159
- raw = json.loads(evals_path.read_text(encoding="utf-8"))
161
+ raw = read_json(evals_path)
160
162
  if isinstance(raw, dict):
161
163
  if isinstance(raw.get("examples"), list):
162
164
  examples = raw["examples"]
@@ -304,7 +306,7 @@ def _default_artifact_path(project_root: Path, agent: str, profile_name: str) ->
304
306
 
305
307
  def _write_result_artifact(path: Path, payload: dict[str, Any]) -> None:
306
308
  path.parent.mkdir(parents=True, exist_ok=True)
307
- path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
309
+ write_json(path, payload)
308
310
 
309
311
 
310
312
  def _apply_profile_preamble(project_root: Path, profile_name: str, best_preamble: str) -> None:
@@ -315,4 +317,4 @@ def _apply_profile_preamble(project_root: Path, profile_name: str, best_preamble
315
317
  profile = profiles.setdefault(profile_name, {})
316
318
  profile["prompt_preamble"] = best_preamble
317
319
  config_path.parent.mkdir(parents=True, exist_ok=True)
318
- config_path.write_text(yaml.safe_dump(raw_config, sort_keys=False), encoding="utf-8")
320
+ atomic_write_text(config_path, yaml.safe_dump(raw_config, sort_keys=False))
@@ -36,13 +36,14 @@ from collections.abc import Awaitable, Callable
36
36
  from dataclasses import asdict, dataclass, field
37
37
  from datetime import datetime, timezone
38
38
  from pathlib import Path
39
- from typing import Any, Literal
39
+ from typing import Any, Literal, cast
40
40
 
41
41
  from pydantic import BaseModel, Field
42
42
 
43
43
  # Reuse the small list-coercion helper from the agent-profile optimizer so both
44
44
  # optimizers share one eval-dataset field contract (required_terms, forbidden_terms…).
45
45
  from devcouncil.optimization.gepa_agent import _string_list
46
+ from devcouncil.utils.json_persist import write_json
46
47
 
47
48
  logger = logging.getLogger(__name__)
48
49
 
@@ -593,7 +594,7 @@ def make_llm_rollout(
593
594
  result = await router.complete_structured(
594
595
  role, messages, _AgentAnswer, fallback=_AgentAnswer(answer="")
595
596
  )
596
- return result.answer
597
+ return str(result.answer)
597
598
 
598
599
  return rollout
599
600
 
@@ -632,8 +633,11 @@ def make_llm_optimizer(
632
633
  {"role": "system", "content": system},
633
634
  {"role": "user", "content": user},
634
635
  ]
635
- return await router.complete_structured(
636
- role, messages, SkillEditProposal, fallback=SkillEditProposal()
636
+ return cast(
637
+ SkillEditProposal,
638
+ await router.complete_structured(
639
+ role, messages, SkillEditProposal, fallback=SkillEditProposal()
640
+ ),
637
641
  )
638
642
 
639
643
  return optimizer
@@ -670,4 +674,4 @@ def write_result_artifact(path: Path, result: SkillOptResult, *, objective: str,
670
674
  "applied": result.applied,
671
675
  "created_at": datetime.now(timezone.utc).isoformat(),
672
676
  }
673
- path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
677
+ write_json(path, payload)