devcouncil 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (366) hide show
  1. package/README.md +92 -33
  2. package/package.json +6 -2
  3. package/packages/codeintel-grammars/hatch_build.py +43 -0
  4. package/packages/codeintel-grammars/pyproject.toml +16 -0
  5. package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
  6. package/pyproject.toml +99 -4
  7. package/src/devcouncil/app/config.py +512 -20
  8. package/src/devcouncil/app/events.py +4 -23
  9. package/src/devcouncil/app/orchestrator.py +5 -0
  10. package/src/devcouncil/app/run_context.py +3 -3
  11. package/src/devcouncil/assets/__init__.py +4 -1
  12. package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
  13. package/src/devcouncil/campaign/__init__.py +71 -0
  14. package/src/devcouncil/campaign/bloom.py +137 -0
  15. package/src/devcouncil/campaign/dashboard.py +123 -0
  16. package/src/devcouncil/campaign/mailbox.py +305 -0
  17. package/src/devcouncil/campaign/notify.py +91 -0
  18. package/src/devcouncil/campaign/orchestrator.py +592 -0
  19. package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
  20. package/src/devcouncil/campaign/prompts/director.md +21 -0
  21. package/src/devcouncil/campaign/prompts/protocol.md +46 -0
  22. package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
  23. package/src/devcouncil/campaign/prompts/worker.md +24 -0
  24. package/src/devcouncil/campaign/roles.py +202 -0
  25. package/src/devcouncil/campaign/watcher.py +153 -0
  26. package/src/devcouncil/cli/commands/agents.py +24 -17
  27. package/src/devcouncil/cli/commands/artifacts.py +36 -27
  28. package/src/devcouncil/cli/commands/ast.py +12 -3
  29. package/src/devcouncil/cli/commands/baseline.py +21 -12
  30. package/src/devcouncil/cli/commands/boot.py +218 -0
  31. package/src/devcouncil/cli/commands/campaign.py +302 -0
  32. package/src/devcouncil/cli/commands/check.py +225 -12
  33. package/src/devcouncil/cli/commands/config.py +221 -74
  34. package/src/devcouncil/cli/commands/cost.py +137 -28
  35. package/src/devcouncil/cli/commands/dashboard.py +12 -4
  36. package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
  37. package/src/devcouncil/cli/commands/design.py +27 -17
  38. package/src/devcouncil/cli/commands/doctor.py +790 -8
  39. package/src/devcouncil/cli/commands/evidence.py +41 -20
  40. package/src/devcouncil/cli/commands/export.py +73 -0
  41. package/src/devcouncil/cli/commands/gaps.py +175 -0
  42. package/src/devcouncil/cli/commands/gated_write.py +76 -0
  43. package/src/devcouncil/cli/commands/go.py +220 -68
  44. package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
  45. package/src/devcouncil/cli/commands/handoff.py +45 -34
  46. package/src/devcouncil/cli/commands/hook.py +630 -85
  47. package/src/devcouncil/cli/commands/init.py +89 -30
  48. package/src/devcouncil/cli/commands/integrate.py +296 -1385
  49. package/src/devcouncil/cli/commands/lease.py +120 -0
  50. package/src/devcouncil/cli/commands/logs.py +12 -5
  51. package/src/devcouncil/cli/commands/lsp.py +40 -5
  52. package/src/devcouncil/cli/commands/map.py +317 -74
  53. package/src/devcouncil/cli/commands/mcp_server.py +12 -2
  54. package/src/devcouncil/cli/commands/okf.py +44 -6
  55. package/src/devcouncil/cli/commands/plan.py +184 -69
  56. package/src/devcouncil/cli/commands/prompt.py +26 -17
  57. package/src/devcouncil/cli/commands/provenance.py +79 -0
  58. package/src/devcouncil/cli/commands/repair.py +60 -49
  59. package/src/devcouncil/cli/commands/report.py +148 -40
  60. package/src/devcouncil/cli/commands/requirements.py +104 -0
  61. package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
  62. package/src/devcouncil/cli/commands/rollback.py +46 -35
  63. package/src/devcouncil/cli/commands/run.py +173 -8
  64. package/src/devcouncil/cli/commands/runs.py +298 -68
  65. package/src/devcouncil/cli/commands/scaffold.py +33 -12
  66. package/src/devcouncil/cli/commands/semantic.py +29 -14
  67. package/src/devcouncil/cli/commands/setup.py +103 -93
  68. package/src/devcouncil/cli/commands/shell.py +51 -42
  69. package/src/devcouncil/cli/commands/show.py +56 -42
  70. package/src/devcouncil/cli/commands/skills.py +29 -20
  71. package/src/devcouncil/cli/commands/status.py +80 -67
  72. package/src/devcouncil/cli/commands/task_gate.py +295 -0
  73. package/src/devcouncil/cli/commands/tasks.py +248 -19
  74. package/src/devcouncil/cli/commands/trace.py +14 -8
  75. package/src/devcouncil/cli/commands/verify.py +33 -8
  76. package/src/devcouncil/cli/commands/version.py +14 -6
  77. package/src/devcouncil/cli/commands/watch.py +56 -40
  78. package/src/devcouncil/cli/commands/watch_fs.py +30 -19
  79. package/src/devcouncil/cli/commands/wiki.py +278 -0
  80. package/src/devcouncil/cli/main.py +58 -1
  81. package/src/devcouncil/codeintel/__init__.py +16 -0
  82. package/src/devcouncil/codeintel/build_control.py +429 -0
  83. package/src/devcouncil/codeintel/build_worker.py +78 -0
  84. package/src/devcouncil/codeintel/debug/__init__.py +17 -0
  85. package/src/devcouncil/codeintel/debug/broker.py +114 -0
  86. package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
  87. package/src/devcouncil/codeintel/debug/consent.py +36 -0
  88. package/src/devcouncil/codeintel/debug/discovery.py +132 -0
  89. package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
  90. package/src/devcouncil/codeintel/debug/protocol.py +259 -0
  91. package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
  92. package/src/devcouncil/codeintel/debug/session.py +238 -0
  93. package/src/devcouncil/codeintel/debug/tracing.py +201 -0
  94. package/src/devcouncil/codeintel/languages/__init__.py +17 -0
  95. package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
  96. package/src/devcouncil/codeintel/languages/registry.py +149 -0
  97. package/src/devcouncil/codeintel/languages/workers.py +245 -0
  98. package/src/devcouncil/codeintel/query/__init__.py +5 -0
  99. package/src/devcouncil/codeintel/query/engine.py +289 -0
  100. package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
  101. package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
  102. package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
  103. package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
  104. package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
  105. package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
  106. package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
  107. package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
  108. package/src/devcouncil/codeintel/service.py +104 -0
  109. package/src/devcouncil/codeintel/store/__init__.py +15 -0
  110. package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
  111. package/src/devcouncil/codeintel/sync/__init__.py +19 -0
  112. package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
  113. package/src/devcouncil/codeintel/sync/incremental.py +484 -0
  114. package/src/devcouncil/codeintel/sync/lease.py +96 -0
  115. package/src/devcouncil/codeintel/sync/scope.py +98 -0
  116. package/src/devcouncil/council/__init__.py +4 -0
  117. package/src/devcouncil/council/prompts/__init__.py +4 -0
  118. package/src/devcouncil/domain/checkpoint_refs.py +17 -0
  119. package/src/devcouncil/domain/evidence.py +1 -0
  120. package/src/devcouncil/domain/gap.py +10 -0
  121. package/src/devcouncil/domain/requirement.py +5 -1
  122. package/src/devcouncil/domain/task.py +43 -2
  123. package/src/devcouncil/execution/checkpoints.py +25 -31
  124. package/src/devcouncil/execution/context_builder.py +15 -44
  125. package/src/devcouncil/execution/fs_watcher.py +64 -0
  126. package/src/devcouncil/execution/gated_write.py +203 -0
  127. package/src/devcouncil/execution/handoff.py +2 -1
  128. package/src/devcouncil/execution/hook_policy.py +19 -5
  129. package/src/devcouncil/execution/lease_ops.py +177 -0
  130. package/src/devcouncil/execution/lease_validation.py +71 -0
  131. package/src/devcouncil/execution/patch.py +3 -0
  132. package/src/devcouncil/execution/permissions.py +1 -0
  133. package/src/devcouncil/execution/policy_engine.py +205 -10
  134. package/src/devcouncil/execution/prompt_builder.py +278 -33
  135. package/src/devcouncil/execution/run_trace.py +356 -0
  136. package/src/devcouncil/execution/shell_session.py +46 -5
  137. package/src/devcouncil/execution/stop_gate.py +746 -0
  138. package/src/devcouncil/execution/stop_gate_history.py +113 -0
  139. package/src/devcouncil/execution/stop_gate_state.py +54 -0
  140. package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
  141. package/src/devcouncil/execution/task_gate_ops.py +590 -0
  142. package/src/devcouncil/execution/task_runner.py +19 -0
  143. package/src/devcouncil/executors/advisor_tool.py +315 -0
  144. package/src/devcouncil/executors/agent_registry.py +125 -17
  145. package/src/devcouncil/executors/claude_sdk.py +376 -0
  146. package/src/devcouncil/executors/coding_cli.py +724 -25
  147. package/src/devcouncil/executors/mini_swe.py +50 -8
  148. package/src/devcouncil/executors/native/agent.py +224 -19
  149. package/src/devcouncil/executors/openhands.py +50 -8
  150. package/src/devcouncil/executors/transient_retry.py +99 -0
  151. package/src/devcouncil/gating/checks/clean_git.py +5 -2
  152. package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
  153. package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
  154. package/src/devcouncil/gating/policy.py +46 -2
  155. package/src/devcouncil/indexing/ast_matcher.py +41 -4
  156. package/src/devcouncil/indexing/graph/__init__.py +78 -0
  157. package/src/devcouncil/indexing/graph/api_routes.py +522 -0
  158. package/src/devcouncil/indexing/graph/build.py +862 -0
  159. package/src/devcouncil/indexing/graph/cache.py +329 -0
  160. package/src/devcouncil/indexing/graph/communities.py +28 -0
  161. package/src/devcouncil/indexing/graph/cypher.py +107 -0
  162. package/src/devcouncil/indexing/graph/embeddings.py +194 -0
  163. package/src/devcouncil/indexing/graph/export.py +381 -0
  164. package/src/devcouncil/indexing/graph/export_links.py +81 -0
  165. package/src/devcouncil/indexing/graph/extract_python.py +307 -0
  166. package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
  167. package/src/devcouncil/indexing/graph/intel.py +668 -0
  168. package/src/devcouncil/indexing/graph/liveness.py +992 -0
  169. package/src/devcouncil/indexing/graph/okf_export.py +65 -0
  170. package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
  171. package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
  172. package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
  173. package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
  174. package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
  175. package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
  176. package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
  177. package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
  178. package/src/devcouncil/indexing/graph/query.py +302 -0
  179. package/src/devcouncil/indexing/graph/resolve.py +1020 -0
  180. package/src/devcouncil/indexing/graph/schema.py +103 -0
  181. package/src/devcouncil/indexing/graph_index.py +20 -29
  182. package/src/devcouncil/indexing/lsp.py +57 -25
  183. package/src/devcouncil/indexing/lsp_client.py +577 -0
  184. package/src/devcouncil/indexing/map_artifacts.py +355 -0
  185. package/src/devcouncil/indexing/map_refresh.py +141 -0
  186. package/src/devcouncil/indexing/repo_mapper.py +1509 -138
  187. package/src/devcouncil/indexing/semantic_index.py +12 -6
  188. package/src/devcouncil/indexing/subsystem_map.py +163 -0
  189. package/src/devcouncil/indexing/ts_imports.py +343 -0
  190. package/src/devcouncil/indexing/viz.py +964 -0
  191. package/src/devcouncil/indexing/walk.py +52 -0
  192. package/src/devcouncil/indexing/wiring.py +1776 -0
  193. package/src/devcouncil/integrations/actions.py +27 -4
  194. package/src/devcouncil/integrations/check.py +211 -16
  195. package/src/devcouncil/integrations/claude_assets.py +209 -12
  196. package/src/devcouncil/integrations/clients/__init__.py +1 -0
  197. package/src/devcouncil/integrations/clients/aider.py +52 -0
  198. package/src/devcouncil/integrations/clients/antigravity.py +87 -0
  199. package/src/devcouncil/integrations/clients/claude.py +339 -0
  200. package/src/devcouncil/integrations/clients/codex.py +39 -0
  201. package/src/devcouncil/integrations/clients/common.py +332 -0
  202. package/src/devcouncil/integrations/clients/cursor.py +164 -0
  203. package/src/devcouncil/integrations/clients/gemini.py +49 -0
  204. package/src/devcouncil/integrations/clients/grok.py +105 -0
  205. package/src/devcouncil/integrations/clients/hooks.py +500 -0
  206. package/src/devcouncil/integrations/clients/opencode.py +96 -0
  207. package/src/devcouncil/integrations/clients/warp.py +75 -0
  208. package/src/devcouncil/integrations/code_review_graph.py +2 -2
  209. package/src/devcouncil/integrations/github.py +73 -7
  210. package/src/devcouncil/integrations/integration_cli.py +197 -0
  211. package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
  212. package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
  213. package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
  214. package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
  215. package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
  216. package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
  217. package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
  218. package/src/devcouncil/integrations/mcp/handlers/git.py +281 -0
  219. package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
  220. package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
  221. package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -0
  222. package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
  223. package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
  224. package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
  225. package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
  226. package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
  227. package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
  228. package/src/devcouncil/integrations/mcp/handlers/provenance.py +87 -0
  229. package/src/devcouncil/integrations/mcp/handlers/read.py +103 -0
  230. package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
  231. package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
  232. package/src/devcouncil/integrations/mcp/handlers/runs.py +69 -0
  233. package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
  234. package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
  235. package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
  236. package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -0
  237. package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
  238. package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
  239. package/src/devcouncil/integrations/mcp/handlers/wiki.py +53 -0
  240. package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
  241. package/src/devcouncil/integrations/mcp/server.py +265 -2391
  242. package/src/devcouncil/integrations/mcp/util.py +325 -0
  243. package/src/devcouncil/integrations/setup.py +152 -0
  244. package/src/devcouncil/knowledge/fetch.py +4 -0
  245. package/src/devcouncil/knowledge/knowledge_select.py +38 -0
  246. package/src/devcouncil/knowledge/okf.py +2 -1
  247. package/src/devcouncil/knowledge/resource_discovery.py +40 -0
  248. package/src/devcouncil/knowledge/wiki.py +643 -0
  249. package/src/devcouncil/knowledge/wiki_read.py +87 -0
  250. package/src/devcouncil/live/cards.py +7 -7
  251. package/src/devcouncil/live/models.py +4 -1
  252. package/src/devcouncil/live/reviewer.py +90 -11
  253. package/src/devcouncil/live/signals.py +4 -2
  254. package/src/devcouncil/live/summary.py +21 -3
  255. package/src/devcouncil/live/tasks.py +12 -3
  256. package/src/devcouncil/live/transcripts.py +69 -2
  257. package/src/devcouncil/llm/cache.py +5 -6
  258. package/src/devcouncil/llm/model_defaults.yaml +10 -10
  259. package/src/devcouncil/llm/provider.py +647 -73
  260. package/src/devcouncil/llm/router.py +271 -46
  261. package/src/devcouncil/llm/semantic_bridge.py +614 -0
  262. package/src/devcouncil/optimization/gepa_agent.py +6 -4
  263. package/src/devcouncil/optimization/skillopt.py +9 -5
  264. package/src/devcouncil/planning/arbiter_service.py +12 -3
  265. package/src/devcouncil/planning/correction_manifest.py +107 -10
  266. package/src/devcouncil/planning/plan_difficulty.py +69 -0
  267. package/src/devcouncil/planning/plan_service.py +5 -2
  268. package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
  269. package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
  270. package/src/devcouncil/planning/question_conversion.py +56 -0
  271. package/src/devcouncil/planning/spec_service.py +9 -3
  272. package/src/devcouncil/repo/ci_scaffold.py +197 -1
  273. package/src/devcouncil/repo/gitignore.py +1 -2
  274. package/src/devcouncil/reporting/evidence_export.py +124 -0
  275. package/src/devcouncil/reporting/evidence_html.py +210 -0
  276. package/src/devcouncil/reporting/json_report.py +16 -12
  277. package/src/devcouncil/reporting/markdown_report.py +38 -9
  278. package/src/devcouncil/reporting/mcp_resources.py +142 -0
  279. package/src/devcouncil/reporting/report_builder.py +40 -4
  280. package/src/devcouncil/reporting/task_provenance.py +42 -0
  281. package/src/devcouncil/reporting/verdict.py +75 -0
  282. package/src/devcouncil/skills/library/README.md +1 -0
  283. package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
  284. package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
  285. package/src/devcouncil/skills/library/devcouncil.md +93 -0
  286. package/src/devcouncil/skills/registry.py +43 -12
  287. package/src/devcouncil/storage/db.py +57 -11
  288. package/src/devcouncil/storage/models.py +6 -0
  289. package/src/devcouncil/storage/native.py +5 -3
  290. package/src/devcouncil/storage/repositories.py +50 -18
  291. package/src/devcouncil/telemetry/context.py +28 -0
  292. package/src/devcouncil/telemetry/cost.py +4 -5
  293. package/src/devcouncil/telemetry/logging_setup.py +78 -11
  294. package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
  295. package/src/devcouncil/telemetry/stages.py +27 -2
  296. package/src/devcouncil/telemetry/tracker.py +50 -13
  297. package/src/devcouncil/ui/dashboard.py +120 -8
  298. package/src/devcouncil/utils/fsio.py +58 -0
  299. package/src/devcouncil/utils/git_snapshot.py +112 -0
  300. package/src/devcouncil/utils/json_persist.py +53 -0
  301. package/src/devcouncil/utils/proc.py +89 -0
  302. package/src/devcouncil/verification/acceptance_compiler.py +36 -13
  303. package/src/devcouncil/verification/ad_hoc_check.py +95 -3
  304. package/src/devcouncil/verification/checks/__init__.py +41 -0
  305. package/src/devcouncil/verification/checks/acceptance.py +39 -0
  306. package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
  307. package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
  308. package/src/devcouncil/verification/checks/command_evidence.py +148 -0
  309. package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
  310. package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
  311. package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
  312. package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
  313. package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
  314. package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
  315. package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
  316. package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
  317. package/src/devcouncil/verification/checks/planned_files.py +98 -0
  318. package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
  319. package/src/devcouncil/verification/checks/stale_map.py +80 -0
  320. package/src/devcouncil/verification/checks/stub_scan.py +71 -0
  321. package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
  322. package/src/devcouncil/verification/checks/wiring.py +216 -0
  323. package/src/devcouncil/verification/claims/__init__.py +23 -0
  324. package/src/devcouncil/verification/claims/checks.py +395 -0
  325. package/src/devcouncil/verification/claims/mapper.py +168 -0
  326. package/src/devcouncil/verification/claims/models.py +39 -0
  327. package/src/devcouncil/verification/claims/transcript.py +92 -0
  328. package/src/devcouncil/verification/claims/verdict.py +88 -0
  329. package/src/devcouncil/verification/command_evidence.py +170 -0
  330. package/src/devcouncil/verification/command_malformation.py +147 -0
  331. package/src/devcouncil/verification/command_runner.py +164 -0
  332. package/src/devcouncil/verification/coverage_measurement.py +292 -0
  333. package/src/devcouncil/verification/diff_coverage.py +151 -0
  334. package/src/devcouncil/verification/difficulty.py +296 -0
  335. package/src/devcouncil/verification/effort_heuristics.py +178 -0
  336. package/src/devcouncil/verification/gap_ids.py +63 -0
  337. package/src/devcouncil/verification/gate_cache.py +194 -0
  338. package/src/devcouncil/verification/gate_selector.py +344 -0
  339. package/src/devcouncil/verification/git_diff_fallback.py +272 -0
  340. package/src/devcouncil/verification/implementation_reviewer.py +13 -0
  341. package/src/devcouncil/verification/incremental_check.py +241 -0
  342. package/src/devcouncil/verification/next_actions.py +60 -1
  343. package/src/devcouncil/verification/rigor_analytics.py +130 -0
  344. package/src/devcouncil/verification/sandbox.py +38 -11
  345. package/src/devcouncil/verification/stub_detector.py +369 -0
  346. package/src/devcouncil/verification/test_resolver.py +67 -1
  347. package/src/devcouncil/verification/verifier.py +137 -1666
  348. package/src/devcouncil/verification/verify_orchestration.py +610 -0
  349. package/src/devcouncil/verification/verify_setup.py +176 -0
  350. package/src/devcouncil/verification/wiki_refresh.py +208 -0
  351. package/src/semantic_layer/__init__.py +58 -0
  352. package/src/semantic_layer/benchmark.py +75 -0
  353. package/src/semantic_layer/cache.py +290 -0
  354. package/src/semantic_layer/compressor.py +137 -0
  355. package/src/semantic_layer/config.py +75 -0
  356. package/src/semantic_layer/embeddings.py +69 -0
  357. package/src/semantic_layer/llm_backends.py +99 -0
  358. package/src/semantic_layer/pipeline.py +111 -0
  359. package/src/semantic_layer/router.py +128 -0
  360. package/src/semantic_layer/tuner.py +72 -0
  361. package/uv.lock +973 -9
  362. package/src/devcouncil/artifacts/migrations.py +0 -20
  363. package/src/devcouncil/artifacts/schemas.py +0 -23
  364. package/src/devcouncil/artifacts/serializer.py +0 -21
  365. package/src/devcouncil/integrations/gitnexus.py +0 -70
  366. package/src/devcouncil/integrations/graphify.py +0 -34
@@ -0,0 +1,484 @@
1
+ """Affected-file graph replacement without global static re-resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import asdict
7
+ from pathlib import Path
8
+
9
+ from devcouncil.codeintel.resolution import enrich_semantic_edges
10
+ from devcouncil.codeintel.service import CodeIntelService
11
+ from devcouncil.codeintel.build_control import (
12
+ graph_build_session,
13
+ record_inline_build_status,
14
+ run_isolated_full_build,
15
+ )
16
+ from devcouncil.indexing.graph.build import (
17
+ _CODE_SUFFIXES,
18
+ _code_files,
19
+ _files_fingerprint,
20
+ _git_head,
21
+ content_fingerprint,
22
+ extract_all,
23
+ extract_paths,
24
+ write_code_graph,
25
+ )
26
+ from devcouncil.indexing.graph.liveness import (
27
+ build_liveness_shard,
28
+ dynamic_import_index_from_shards,
29
+ extraction_from_liveness_shard,
30
+ file_liveness_from_shards,
31
+ legacy_dead_strings,
32
+ project_call_edges_to_files,
33
+ symbol_reachability_dead,
34
+ token_dead_from_shards,
35
+ )
36
+ from devcouncil.indexing.graph.resolve import (
37
+ build_file_and_symbol_nodes,
38
+ contains_and_defines_edges,
39
+ decorator_edges,
40
+ import_graph_edges,
41
+ inherit_edges,
42
+ named_import_edges,
43
+ resolve_calls,
44
+ resolve_import_edges,
45
+ )
46
+ from devcouncil.indexing.graph.schema import CodeGraph, NodeKind
47
+ from devcouncil.indexing.map_refresh import refresh_repo_map_from_graph
48
+ from devcouncil.indexing.repo_mapper import RepoMapper
49
+
50
+ logger = logging.getLogger(__name__)
51
+ # Kept as a patch point for existing correctness tests; this is the shard-based
52
+ # implementation, not the repository-scanning legacy helper.
53
+ _token_scan_dead = token_dead_from_shards
54
+
55
+
56
+ def sync_affected_paths(
57
+ service: CodeIntelService,
58
+ paths: list[str],
59
+ *,
60
+ liveness: bool = True,
61
+ ) -> CodeGraph:
62
+ """Replace affected rows and dependent resolution as one committed generation."""
63
+ with graph_build_session(service.project_root):
64
+ return _sync_affected_paths_locked(service, paths, liveness=liveness)
65
+
66
+
67
+ def _sync_affected_paths_locked(
68
+ service: CodeIntelService,
69
+ paths: list[str],
70
+ *,
71
+ liveness: bool,
72
+ ) -> CodeGraph:
73
+ root = service.project_root
74
+ current = service.load()
75
+ if current is None:
76
+ result = run_isolated_full_build(
77
+ root,
78
+ changed_paths={_normal(path) for path in paths},
79
+ liveness=liveness,
80
+ )
81
+ return result.graph
82
+ previous_communities = {
83
+ (node.id, node.path, node.line, node.kind.value): node.community
84
+ for node in current.nodes
85
+ }
86
+
87
+ mapper = RepoMapper(root)
88
+ files = mapper.get_git_files()
89
+ code_files = _code_files(files)
90
+ changed = {_normal(path) for path in paths}
91
+ affected = _affected_closure(current, changed, service)
92
+ selected = extract_paths(root, affected)
93
+ shards = service.store.analysis_shards()
94
+ if not shards:
95
+ # One-time migration path for v1 stores. Subsequent edits use persisted
96
+ # shards and never scan unchanged source files.
97
+ migrated = extract_all(root, files, changed_paths=affected)
98
+ shards = {
99
+ path: build_liveness_shard(root, extraction)
100
+ for path, extraction in migrated.items()
101
+ }
102
+ live_paths = set(code_files)
103
+ stale_shard_paths = set(shards) - live_paths
104
+ shards = {
105
+ path: shard
106
+ for path, shard in shards.items()
107
+ if path in live_paths
108
+ }
109
+ code_changed = {
110
+ path for path in changed
111
+ if path in live_paths or path in shards or Path(path).suffix.lower() in _CODE_SUFFIXES
112
+ }
113
+ if not _incremental_surface_is_stable(code_changed, shards, selected):
114
+ result = run_isolated_full_build(
115
+ root,
116
+ changed_paths=code_changed,
117
+ liveness=liveness,
118
+ )
119
+ refresh_repo_map_from_graph(
120
+ root,
121
+ result.graph,
122
+ changed,
123
+ files,
124
+ mapper=mapper,
125
+ )
126
+ return result.graph
127
+ for path in affected:
128
+ shards.pop(path, None)
129
+ shards.update({
130
+ path: build_liveness_shard(root, extraction)
131
+ for path, extraction in selected.items()
132
+ })
133
+ extractions = {
134
+ path: extraction_from_liveness_shard(shard)
135
+ for path, shard in shards.items()
136
+ }
137
+ replacement_nodes, _ = build_file_and_symbol_nodes(
138
+ selected,
139
+ area_fn=mapper._area_for_file,
140
+ )
141
+
142
+ old_ids = {node.id for node in current.nodes if node.path in affected}
143
+ nodes = [
144
+ node for node in current.nodes
145
+ if node.path not in affected and (not node.path or node.path in live_paths)
146
+ ]
147
+ nodes.extend(replacement_nodes)
148
+ symbol_index = _symbol_index(nodes)
149
+ class_ids = {node.id for node in nodes if node.kind == NodeKind.CLASS}
150
+
151
+ kept_edges = [
152
+ edge for edge in current.edges
153
+ if edge.source not in old_ids
154
+ and edge.source not in affected
155
+ ]
156
+ old_file_edges = {
157
+ (edge.source, edge.target)
158
+ for edge in kept_edges
159
+ if edge.kind == "imports" and "::" not in edge.source and "::" not in edge.target
160
+ }
161
+ resolved_file_edges = {
162
+ edge for edge in resolve_import_edges(selected, files, root=root, mapper=mapper)
163
+ if edge[0] in affected
164
+ }
165
+ file_edges = sorted(old_file_edges | resolved_file_edges)
166
+ # File-to-file imports are rebuilt from the updated adjacency below.
167
+ # Preserve symbol-level named imports for unchanged files: dropping them
168
+ # makes their targets appear newly dead and expands a one-file write into
169
+ # hundreds or thousands of unrelated liveness payloads.
170
+ kept_edges = [
171
+ edge for edge in kept_edges
172
+ if not (
173
+ edge.kind == "imports"
174
+ and "::" not in edge.source
175
+ and "::" not in edge.target
176
+ )
177
+ ]
178
+
179
+ replacement_edges = []
180
+ replacement_edges.extend(contains_and_defines_edges(selected))
181
+ replacement_edges.extend(inherit_edges(selected, symbol_index))
182
+ replacement_edges.extend(decorator_edges(selected, symbol_index))
183
+ replacement_edges.extend(import_graph_edges(file_edges))
184
+ replacement_edges.extend(named_import_edges(selected, symbol_index, file_edges))
185
+ unresolved_refs: list[dict[str, object]] = []
186
+ replacement_edges.extend(
187
+ resolve_calls(
188
+ selected,
189
+ symbol_index,
190
+ file_edges,
191
+ class_ids=class_ids,
192
+ unresolved_out=unresolved_refs,
193
+ )
194
+ )
195
+
196
+ graph = current.model_copy(deep=True)
197
+ graph.nodes = nodes
198
+ graph.edges = kept_edges + replacement_edges
199
+ enrich_budget = 120.0
200
+ try:
201
+ from devcouncil.app.config import load_config
202
+
203
+ enrich_budget = float(load_config(root).indexing.semantic_enrich_timeout_seconds)
204
+ except Exception:
205
+ logger.debug("semantic enrich budget config load failed", exc_info=True)
206
+ enrich_semantic_edges(
207
+ graph,
208
+ root=root,
209
+ paths=affected,
210
+ extractions=extractions,
211
+ budget_seconds=enrich_budget,
212
+ )
213
+ referenced_ids = {
214
+ identifier
215
+ for edge in graph.edges
216
+ for identifier in (edge.source, edge.target)
217
+ }
218
+ graph.nodes = [
219
+ node for node in graph.nodes
220
+ if node.path
221
+ or node.kind not in {NodeKind.EVENT, NodeKind.DYNAMIC}
222
+ or node.id in referenced_ids
223
+ ]
224
+
225
+ if liveness:
226
+ from devcouncil.indexing.wiring import build_dynamic_import_index, entry_roots
227
+
228
+ all_roots = entry_roots(root, files)
229
+ fresh_roots = entry_roots(root, files, production_only=True)
230
+ if any("dynamic_import_keys" not in shard for shard in shards.values()):
231
+ dynamic_index = build_dynamic_import_index(root, files)
232
+ for shard in shards.values():
233
+ shard["dynamic_import_keys"] = []
234
+ for key, referencing_paths in dynamic_index.items():
235
+ for referencing_path in referencing_paths:
236
+ target_shard = shards.get(referencing_path)
237
+ if target_shard is not None:
238
+ raw_keys = target_shard.setdefault("dynamic_import_keys", [])
239
+ if isinstance(raw_keys, list):
240
+ raw_keys.append(key)
241
+ else:
242
+ dynamic_index = dynamic_import_index_from_shards(shards)
243
+ unsharded_files = [path for path in files if path not in shards]
244
+ for key, referencing_paths in build_dynamic_import_index(
245
+ root, unsharded_files
246
+ ).items():
247
+ dynamic_index.setdefault(key, set()).update(referencing_paths)
248
+ # Same call-edge projection as the full build (see assemble_graph) so a
249
+ # Go same-package callee doesn't flip back to unwired on the first
250
+ # incremental refresh after a full map said it was live.
251
+ liveness_file_edges = sorted(
252
+ set(file_edges) | project_call_edges_to_files(graph.edges)
253
+ )
254
+ entry_roots_list, unwired, unreachable, unreliable = file_liveness_from_shards(
255
+ files,
256
+ liveness_file_edges,
257
+ shards,
258
+ root=root,
259
+ entry_roots=all_roots,
260
+ production_entry_roots=fresh_roots,
261
+ dynamic_index=dynamic_index,
262
+ )
263
+ token_dead, _token_index, token_keys = _token_scan_dead(
264
+ graph.nodes, shards
265
+ )
266
+ graph.dead_code = symbol_reachability_dead(
267
+ root,
268
+ files,
269
+ graph.nodes,
270
+ graph.edges,
271
+ extractions,
272
+ entry_roots_list,
273
+ token_dead_keys=token_keys,
274
+ file_edges=file_edges,
275
+ unreachable=[] if unreliable else unreachable,
276
+ dynamic_index=dynamic_index,
277
+ )
278
+ graph.entry_roots = entry_roots_list
279
+ graph.unwired_candidates = unwired
280
+ graph.unreachable_files = unreachable
281
+ graph.meta["liveness_unreachable_unreliable"] = unreliable
282
+ graph.meta["token_dead_count"] = len(token_dead)
283
+ prior_unresolved = [
284
+ ref
285
+ for ref in (current.meta.get("unresolved_references") or [])
286
+ if isinstance(ref, dict) and str(ref.get("path") or "") not in affected
287
+ ]
288
+ merged_unresolved = prior_unresolved + unresolved_refs
289
+ graph.meta["unresolved_references"] = merged_unresolved[:200]
290
+ graph.meta["unresolved_total"] = len(merged_unresolved)
291
+ graph.meta["legacy_dead_symbol_candidates"] = legacy_dead_strings(
292
+ graph.dead_code,
293
+ token_dead,
294
+ cap=None,
295
+ )
296
+
297
+ try:
298
+ from devcouncil.indexing.graph.intel import enrich_graph_intel
299
+
300
+ enrich_graph_intel(graph, root=root)
301
+ except Exception:
302
+ logger.debug("incremental graph-intelligence refresh failed", exc_info=True)
303
+
304
+ graph.generated_head = _git_head(root)
305
+ graph.indexed_hash = _files_fingerprint(files)
306
+ graph.content_fingerprint = content_fingerprint(root, files)
307
+ graph.meta.update({
308
+ "incremental": True,
309
+ "changed_paths": sorted(changed),
310
+ "affected_paths": sorted(affected),
311
+ "affected_fraction": len(affected) / max(1, len(code_files)),
312
+ "resolution_scope": "affected",
313
+ })
314
+ previous_dead = {
315
+ (entry.id, entry.confidence.value, entry.reason)
316
+ for entry in current.dead_code
317
+ }
318
+ next_dead = {
319
+ (entry.id, entry.confidence.value, entry.reason)
320
+ for entry in graph.dead_code
321
+ }
322
+ dead_changed_ids = {
323
+ item[0] for item in previous_dead.symmetric_difference(next_dead)
324
+ }
325
+ liveness_changed_paths = {
326
+ node.path for node in graph.nodes
327
+ if node.id in dead_changed_ids and node.path
328
+ } | {
329
+ entry.path for entry in current.dead_code
330
+ if entry.id in dead_changed_ids and entry.path
331
+ }
332
+ community_changed_paths = {
333
+ node.path
334
+ for node in graph.nodes
335
+ if node.path
336
+ and previous_communities.get(
337
+ (node.id, node.path, node.line, node.kind.value)
338
+ ) != node.community
339
+ }
340
+ # Pathless semantic channel/bridge nodes must be replaced as a group; SQLite
341
+ # otherwise copies them from the prior generation as "unaffected".
342
+ persistence_paths = (
343
+ affected
344
+ | liveness_changed_paths
345
+ | community_changed_paths
346
+ | stale_shard_paths
347
+ | {""}
348
+ )
349
+ graph.meta["liveness_changed_paths"] = sorted(liveness_changed_paths)
350
+ graph.meta["community_changed_paths"] = sorted(community_changed_paths)
351
+ from devcouncil.indexing.graph.build import CompatibilityGraphTooLarge
352
+
353
+ generation_before = service.store.current_generation()
354
+ compatibility_reason = ""
355
+ try:
356
+ write_code_graph(
357
+ root,
358
+ graph,
359
+ changed_paths=persistence_paths,
360
+ analysis_shards=shards,
361
+ )
362
+ except CompatibilityGraphTooLarge as exc:
363
+ compatibility_reason = str(exc)
364
+ logger.warning("graph committed but compatibility export was skipped: %s", exc)
365
+ generation_after = service.store.current_generation()
366
+ record_inline_build_status(
367
+ root,
368
+ state="degraded" if compatibility_reason else "complete",
369
+ mode="incremental",
370
+ phase="complete",
371
+ generation_before=generation_before,
372
+ generation_after=generation_after,
373
+ reason=compatibility_reason,
374
+ compatibility_export="degraded" if compatibility_reason else "healthy",
375
+ )
376
+ refresh_repo_map_from_graph(
377
+ root,
378
+ graph,
379
+ affected,
380
+ files,
381
+ mapper=mapper,
382
+ )
383
+ return service.load() or graph
384
+
385
+
386
+ def _resolution_surface(extraction) -> dict[str, object]: # noqa: ANN001
387
+ """Fields whose changes can alter resolution outside the edited file."""
388
+ return {
389
+ "language": extraction.language,
390
+ "imports": list(extraction.imports),
391
+ "import_details": [asdict(value) for value in extraction.import_details],
392
+ "symbols": [
393
+ {
394
+ key: value
395
+ for key, value in asdict(symbol).items()
396
+ if key not in {"line", "end_line"}
397
+ }
398
+ for symbol in extraction.symbols
399
+ ],
400
+ "all_exports": list(extraction.all_exports),
401
+ "reexports": list(extraction.reexports),
402
+ }
403
+
404
+
405
+ def _incremental_surface_is_stable(
406
+ changed: set[str],
407
+ shards: dict[str, dict[str, object]],
408
+ selected: dict,
409
+ ) -> bool:
410
+ """Only existing files with an unchanged external resolution surface are safe."""
411
+ for path in changed:
412
+ old_shard = shards.get(path)
413
+ new_extraction = selected.get(path)
414
+ if old_shard is None or new_extraction is None:
415
+ return False
416
+ old_extraction = extraction_from_liveness_shard(old_shard)
417
+ if _resolution_surface(old_extraction) != _resolution_surface(new_extraction):
418
+ return False
419
+ return True
420
+
421
+
422
+ def _affected_closure(
423
+ graph: CodeGraph,
424
+ changed: set[str],
425
+ service: CodeIntelService,
426
+ ) -> set[str]:
427
+ reverse: dict[str, set[str]] = {}
428
+ for edge in graph.edges:
429
+ if edge.kind == "imports" and "::" not in edge.source and "::" not in edge.target:
430
+ reverse.setdefault(edge.target, set()).add(edge.source)
431
+ affected = set(changed)
432
+ queue = list(changed)
433
+ while queue:
434
+ target = queue.pop()
435
+ for importer in reverse.get(target, set()):
436
+ if importer not in affected:
437
+ affected.add(importer)
438
+ queue.append(importer)
439
+
440
+ changed_names = {
441
+ node.name for node in graph.nodes
442
+ if node.path in changed
443
+ and node.name
444
+ and node.kind not in {
445
+ NodeKind.FILE,
446
+ NodeKind.DYNAMIC,
447
+ NodeKind.EVENT,
448
+ NodeKind.ROUTE,
449
+ NodeKind.STATE,
450
+ NodeKind.PROVIDER,
451
+ }
452
+ }
453
+ affected.update(
454
+ str(row["path"])
455
+ for row in service.store.unresolved_references()
456
+ if row.get("path") and str(row.get("name") or "") in changed_names
457
+ )
458
+ changed_semantic_kinds = {
459
+ node.kind
460
+ for node in graph.nodes
461
+ if node.path in changed
462
+ and node.kind in {NodeKind.ROUTE, NodeKind.EVENT, NodeKind.PROVIDER}
463
+ }
464
+ if changed_semantic_kinds:
465
+ affected.update(
466
+ node.path for node in graph.nodes
467
+ if node.path and node.kind in changed_semantic_kinds
468
+ )
469
+ return affected
470
+
471
+
472
+ def _symbol_index(nodes) -> dict[str, str]: # noqa: ANN001
473
+ index: dict[str, str] = {}
474
+ for node in nodes:
475
+ if node.kind == NodeKind.FILE or not node.path:
476
+ continue
477
+ qualname = str(node.extras.get("qualname") or node.id.split("::", 1)[-1].split("#", 1)[0])
478
+ index[f"{node.path}::{qualname}"] = node.id
479
+ return index
480
+
481
+
482
+ def _normal(path: str) -> str:
483
+ normalized = path.replace("\\", "/")
484
+ return normalized[2:] if normalized.startswith("./") else normalized
@@ -0,0 +1,96 @@
1
+ """Portable best-effort cross-process writer lease."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import time
7
+ from pathlib import Path
8
+ from typing import BinaryIO, Callable
9
+
10
+
11
+ class WriterLease:
12
+ def __init__(self, path: Path):
13
+ self.path = path
14
+ self._handle: BinaryIO | None = None
15
+
16
+ def acquire(self) -> bool:
17
+ self.path.parent.mkdir(parents=True, exist_ok=True)
18
+ handle = self.path.open("a+b")
19
+ try:
20
+ if os.name == "nt":
21
+ import msvcrt
22
+
23
+ handle.seek(0)
24
+ if handle.tell() == 0:
25
+ handle.write(b"\0")
26
+ handle.flush()
27
+ handle.seek(0)
28
+ msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1) # type: ignore[attr-defined]
29
+ else:
30
+ import fcntl
31
+
32
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
33
+ except (OSError, BlockingIOError):
34
+ handle.close()
35
+ return False
36
+ self._handle = handle
37
+ return True
38
+
39
+ def acquire_with_retry(
40
+ self,
41
+ *,
42
+ timeout: float = 30.0,
43
+ initial_delay: float = 0.05,
44
+ max_delay: float = 1.0,
45
+ sleep: Callable[[float], None] | None = None,
46
+ ) -> bool:
47
+ """Acquire the lease, retrying with exponential backoff until ``timeout``.
48
+
49
+ Concurrent watchers / MCP writers commonly hold ``writer.lock`` for a short
50
+ window. Callers that would otherwise fail closed on the first busy probe
51
+ should use this so pending work drains instead of oscillating into
52
+ ``read_only`` / lean-map fallbacks.
53
+ """
54
+ sleeper = sleep or time.sleep
55
+ if self.acquire():
56
+ return True
57
+ deadline = time.monotonic() + max(0.0, timeout)
58
+ delay = max(0.0, initial_delay)
59
+ max_delay = max(delay, max_delay)
60
+ while True:
61
+ remaining = deadline - time.monotonic()
62
+ if remaining <= 0:
63
+ return False
64
+ sleeper(min(delay, remaining, max_delay))
65
+ if self.acquire():
66
+ return True
67
+ if delay <= 0:
68
+ delay = max(initial_delay, 0.05)
69
+ else:
70
+ delay = min(max_delay, delay * 2)
71
+
72
+ def release(self) -> None:
73
+ handle = self._handle
74
+ if handle is None:
75
+ return
76
+ try:
77
+ if os.name == "nt":
78
+ import msvcrt
79
+
80
+ handle.seek(0)
81
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1) # type: ignore[attr-defined]
82
+ else:
83
+ import fcntl
84
+
85
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
86
+ finally:
87
+ handle.close()
88
+ self._handle = None
89
+
90
+ def __enter__(self) -> "WriterLease":
91
+ if not self.acquire():
92
+ raise BlockingIOError("another code-intelligence writer owns the project lease")
93
+ return self
94
+
95
+ def __exit__(self, *_args: object) -> None:
96
+ self.release()
@@ -0,0 +1,98 @@
1
+ """One index/watch scope policy backed by Git ignore semantics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from pathlib import Path
7
+
8
+ from devcouncil.codeintel.languages import detect_language
9
+ from devcouncil.indexing.wiring import is_vendored_path
10
+
11
+ _IGNORED_PREFIXES = (
12
+ ".git/",
13
+ ".devcouncil/",
14
+ "node_modules/",
15
+ "vendor/",
16
+ ".venv/",
17
+ "venv/",
18
+ "dist/",
19
+ "build/",
20
+ "target/",
21
+ "__pycache__/",
22
+ ".pytest_cache/",
23
+ ".mypy_cache/",
24
+ ".ruff_cache/",
25
+ )
26
+
27
+
28
+ class IndexScope:
29
+ def __init__(self, root: Path):
30
+ self.root = root.expanduser().resolve()
31
+
32
+ def includes(self, path: str | Path) -> bool:
33
+ try:
34
+ rel = self.relative(path)
35
+ except ValueError:
36
+ return False
37
+ if not self._language_and_prefix_included(rel):
38
+ return False
39
+ return not self._git_ignored(rel)
40
+
41
+ @staticmethod
42
+ def _language_and_prefix_included(rel: str) -> bool:
43
+ if any(rel == prefix.rstrip("/") or rel.startswith(prefix) for prefix in _IGNORED_PREFIXES):
44
+ return False
45
+ # Match graph ingestion: nested ``vendor/`` / ``.min.js`` must stay out of
46
+ # watch/reconcile scope or they remain perpetually "changed".
47
+ if is_vendored_path(rel):
48
+ return False
49
+ return detect_language(rel) is not None
50
+
51
+ def relative(self, path: str | Path) -> str:
52
+ candidate = Path(path)
53
+ if not candidate.is_absolute():
54
+ candidate = self.root / candidate
55
+ return candidate.resolve(strict=False).relative_to(self.root).as_posix()
56
+
57
+ def files(self) -> list[str]:
58
+ try:
59
+ proc = subprocess.run(
60
+ ["git", "ls-files", "-co", "--exclude-standard", "-z"],
61
+ cwd=self.root,
62
+ check=False,
63
+ capture_output=True,
64
+ timeout=10,
65
+ )
66
+ if proc.returncode == 0:
67
+ values = proc.stdout.decode("utf-8", errors="replace").split("\0")
68
+ # git ls-files --exclude-standard has already applied repository
69
+ # ignore rules. Calling check-ignore again for every returned path
70
+ # made MCP startup O(files) subprocesses.
71
+ return sorted({
72
+ value
73
+ for value in values
74
+ if value and self._language_and_prefix_included(value)
75
+ })
76
+ except (OSError, subprocess.SubprocessError):
77
+ pass
78
+ return sorted(
79
+ path.relative_to(self.root).as_posix()
80
+ for path in self.root.rglob("*")
81
+ if path.is_file() and self.includes(path)
82
+ )
83
+
84
+ def _git_ignored(self, rel: str) -> bool:
85
+ # Fast exclusions cover high-volume directories. Git is the authority for
86
+ # project-specific rules and correctly handles nested .gitignore files.
87
+ try:
88
+ result = subprocess.run(
89
+ ["git", "check-ignore", "-q", "--", rel],
90
+ cwd=self.root,
91
+ check=False,
92
+ stdout=subprocess.DEVNULL,
93
+ stderr=subprocess.DEVNULL,
94
+ timeout=2,
95
+ )
96
+ return result.returncode == 0
97
+ except (OSError, subprocess.SubprocessError):
98
+ return False
@@ -0,0 +1,4 @@
1
+ """Council prompt package.
2
+
3
+ devcouncil: allow-unwired — package marker for prompt templates under prompts/.
4
+ """
@@ -0,0 +1,4 @@
1
+ """Council prompt templates.
2
+
3
+ devcouncil: allow-unwired — markdown templates loaded as package resources.
4
+ """