attocode-code-intel 0.1.0__py3-none-any.whl

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 (345) hide show
  1. attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
  2. attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
  3. attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
  4. attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
  5. attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. attocode_intel/GUIDELINES.md +432 -0
  7. attocode_intel/__init__.py +52 -0
  8. attocode_intel/_internal/__init__.py +1 -0
  9. attocode_intel/_internal/errors/__init__.py +126 -0
  10. attocode_intel/_internal/integrations/__init__.py +1 -0
  11. attocode_intel/_internal/integrations/context/__init__.py +1 -0
  12. attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
  13. attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
  14. attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
  15. attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
  16. attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
  17. attocode_intel/_internal/integrations/context/cas.py +466 -0
  18. attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
  19. attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
  20. attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
  21. attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
  22. attocode_intel/_internal/integrations/context/cross_references.py +616 -0
  23. attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
  24. attocode_intel/_internal/integrations/context/embeddings.py +282 -0
  25. attocode_intel/_internal/integrations/context/frecency.py +427 -0
  26. attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
  27. attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
  28. attocode_intel/_internal/integrations/context/hydration.py +90 -0
  29. attocode_intel/_internal/integrations/context/index_repair.py +87 -0
  30. attocode_intel/_internal/integrations/context/index_store.py +586 -0
  31. attocode_intel/_internal/integrations/context/memory_store.py +602 -0
  32. attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
  33. attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
  34. attocode_intel/_internal/integrations/context/query_history.py +417 -0
  35. attocode_intel/_internal/integrations/context/reranker.py +351 -0
  36. attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
  37. attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
  38. attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
  39. attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
  40. attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
  41. attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
  42. attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
  43. attocode_intel/_internal/integrations/context/vector_store.py +795 -0
  44. attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
  45. attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
  46. attocode_intel/_internal/integrations/lsp/client.py +1140 -0
  47. attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
  48. attocode_intel/_internal/integrations/mcp/client.py +253 -0
  49. attocode_intel/_internal/integrations/security/__init__.py +1 -0
  50. attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
  51. attocode_intel/_internal/integrations/security/dataflow.py +522 -0
  52. attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
  53. attocode_intel/_internal/integrations/security/matcher.py +44 -0
  54. attocode_intel/_internal/integrations/security/patterns.py +724 -0
  55. attocode_intel/_internal/integrations/security/scanner.py +351 -0
  56. attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
  57. attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
  58. attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
  59. attocode_intel/_internal/tricks/__init__.py +1 -0
  60. attocode_intel/_internal/tricks/recursive_context.py +357 -0
  61. attocode_intel/_internal/types/__init__.py +1 -0
  62. attocode_intel/_internal/types/events.py +289 -0
  63. attocode_intel/_internal/types/severity.py +15 -0
  64. attocode_intel/_shared.py +341 -0
  65. attocode_intel/analysis_coverage.py +56 -0
  66. attocode_intel/api/__init__.py +1 -0
  67. attocode_intel/api/access.py +32 -0
  68. attocode_intel/api/app.py +247 -0
  69. attocode_intel/api/auth/__init__.py +128 -0
  70. attocode_intel/api/auth/api_keys.py +61 -0
  71. attocode_intel/api/auth/context.py +34 -0
  72. attocode_intel/api/auth/jwt.py +66 -0
  73. attocode_intel/api/auth/oauth.py +146 -0
  74. attocode_intel/api/auth/passwords.py +15 -0
  75. attocode_intel/api/deps.py +351 -0
  76. attocode_intel/api/knowledge_bridge.py +13 -0
  77. attocode_intel/api/mcp_transport.py +51 -0
  78. attocode_intel/api/middleware.py +411 -0
  79. attocode_intel/api/models.py +698 -0
  80. attocode_intel/api/providers/__init__.py +1 -0
  81. attocode_intel/api/providers/db_provider.py +1106 -0
  82. attocode_intel/api/providers/local_provider.py +476 -0
  83. attocode_intel/api/providers/remote_provider.py +740 -0
  84. attocode_intel/api/repository_source.py +21 -0
  85. attocode_intel/api/routes/__init__.py +1 -0
  86. attocode_intel/api/routes/_pin_helper.py +158 -0
  87. attocode_intel/api/routes/activity.py +148 -0
  88. attocode_intel/api/routes/adr.py +102 -0
  89. attocode_intel/api/routes/analysis.py +515 -0
  90. attocode_intel/api/routes/api_keys.py +170 -0
  91. attocode_intel/api/routes/auth.py +428 -0
  92. attocode_intel/api/routes/branches.py +262 -0
  93. attocode_intel/api/routes/cross_repo_search.py +198 -0
  94. attocode_intel/api/routes/embeddings.py +386 -0
  95. attocode_intel/api/routes/files.py +132 -0
  96. attocode_intel/api/routes/files_v2.py +388 -0
  97. attocode_intel/api/routes/gc.py +322 -0
  98. attocode_intel/api/routes/git_v2.py +433 -0
  99. attocode_intel/api/routes/graph.py +174 -0
  100. attocode_intel/api/routes/graph_viz.py +160 -0
  101. attocode_intel/api/routes/health.py +62 -0
  102. attocode_intel/api/routes/history.py +175 -0
  103. attocode_intel/api/routes/jobs.py +84 -0
  104. attocode_intel/api/routes/learning.py +205 -0
  105. attocode_intel/api/routes/lsp.py +272 -0
  106. attocode_intel/api/routes/notify.py +623 -0
  107. attocode_intel/api/routes/operations.py +27 -0
  108. attocode_intel/api/routes/orgs.py +924 -0
  109. attocode_intel/api/routes/preferences.py +100 -0
  110. attocode_intel/api/routes/presence.py +42 -0
  111. attocode_intel/api/routes/projects.py +116 -0
  112. attocode_intel/api/routes/repos.py +47 -0
  113. attocode_intel/api/routes/search.py +180 -0
  114. attocode_intel/api/routes/snapshots.py +614 -0
  115. attocode_intel/api/routes/webhooks.py +313 -0
  116. attocode_intel/api/routes/websocket.py +116 -0
  117. attocode_intel/api/static/graph.html +239 -0
  118. attocode_intel/api/utils.py +96 -0
  119. attocode_intel/artifacts/__init__.py +45 -0
  120. attocode_intel/artifacts/hashing.py +230 -0
  121. attocode_intel/artifacts/provenance.py +190 -0
  122. attocode_intel/artifacts/retrieval_pin.py +159 -0
  123. attocode_intel/audit.py +59 -0
  124. attocode_intel/bug_finder.py +206 -0
  125. attocode_intel/bundle.py +80 -0
  126. attocode_intel/catalog.py +242 -0
  127. attocode_intel/change_review.py +75 -0
  128. attocode_intel/cli.py +3062 -0
  129. attocode_intel/community.py +63 -0
  130. attocode_intel/confidence/__init__.py +144 -0
  131. attocode_intel/confidence/jev.py +234 -0
  132. attocode_intel/confidence/llm.py +330 -0
  133. attocode_intel/confidence/redact.py +44 -0
  134. attocode_intel/confidence/settings.py +74 -0
  135. attocode_intel/config.py +256 -0
  136. attocode_intel/crypto.py +31 -0
  137. attocode_intel/db/__init__.py +7 -0
  138. attocode_intel/db/base.py +33 -0
  139. attocode_intel/db/engine.py +61 -0
  140. attocode_intel/db/models.py +529 -0
  141. attocode_intel/entrypoint.py +185 -0
  142. attocode_intel/focused_evidence.py +130 -0
  143. attocode_intel/freshness.py +154 -0
  144. attocode_intel/gateway.py +734 -0
  145. attocode_intel/git/__init__.py +7 -0
  146. attocode_intel/git/credentials.py +75 -0
  147. attocode_intel/git/manager.py +493 -0
  148. attocode_intel/git/models.py +96 -0
  149. attocode_intel/git/storage.py +98 -0
  150. attocode_intel/graph_query_parser.py +761 -0
  151. attocode_intel/helpers.py +712 -0
  152. attocode_intel/indexing/__init__.py +29 -0
  153. attocode_intel/indexing/debouncer.py +136 -0
  154. attocode_intel/indexing/delta_indexer.py +151 -0
  155. attocode_intel/indexing/full_indexer.py +323 -0
  156. attocode_intel/indexing/incremental.py +240 -0
  157. attocode_intel/indexing/parser.py +403 -0
  158. attocode_intel/indexing/summarizer.py +264 -0
  159. attocode_intel/installer.py +1896 -0
  160. attocode_intel/knowledge.py +269 -0
  161. attocode_intel/local_knowledge.py +77 -0
  162. attocode_intel/migrations/alembic.ini +36 -0
  163. attocode_intel/migrations/env.py +71 -0
  164. attocode_intel/migrations/script.py.mako +27 -0
  165. attocode_intel/migrations/versions/001_initial_schema.py +219 -0
  166. attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
  167. attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
  168. attocode_intel/migrations/versions/004_audit_events.py +38 -0
  169. attocode_intel/migrations/versions/005_user_preferences.py +30 -0
  170. attocode_intel/migrations/versions/006_google_oauth.py +27 -0
  171. attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
  172. attocode_intel/migrations/versions/008_symbol_references.py +37 -0
  173. attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
  174. attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
  175. attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
  176. attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
  177. attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
  178. attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
  179. attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
  180. attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
  181. attocode_intel/migrations/versions/017_provenance_table.py +102 -0
  182. attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
  183. attocode_intel/migrations/versions/019_symref_caller.py +42 -0
  184. attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
  185. attocode_intel/onboarding.py +258 -0
  186. attocode_intel/output.py +178 -0
  187. attocode_intel/precision.py +133 -0
  188. attocode_intel/presence.py +160 -0
  189. attocode_intel/probe.py +110 -0
  190. attocode_intel/project_dir.py +97 -0
  191. attocode_intel/pubsub.py +113 -0
  192. attocode_intel/query_ranking.py +242 -0
  193. attocode_intel/readiness.py +972 -0
  194. attocode_intel/redis.py +51 -0
  195. attocode_intel/reference_pages.py +60 -0
  196. attocode_intel/remote.py +131 -0
  197. attocode_intel/repo_ranker.py +246 -0
  198. attocode_intel/request_context.py +82 -0
  199. attocode_intel/rules/__init__.py +28 -0
  200. attocode_intel/rules/ci.py +320 -0
  201. attocode_intel/rules/combinators.py +293 -0
  202. attocode_intel/rules/enricher.py +127 -0
  203. attocode_intel/rules/evolution.py +452 -0
  204. attocode_intel/rules/executor.py +531 -0
  205. attocode_intel/rules/filters/__init__.py +5 -0
  206. attocode_intel/rules/filters/pipeline.py +151 -0
  207. attocode_intel/rules/formatter.py +198 -0
  208. attocode_intel/rules/hygiene.py +242 -0
  209. attocode_intel/rules/importers/__init__.py +1 -0
  210. attocode_intel/rules/importers/semgrep.py +382 -0
  211. attocode_intel/rules/loader.py +292 -0
  212. attocode_intel/rules/marketplace.py +375 -0
  213. attocode_intel/rules/metavar.py +181 -0
  214. attocode_intel/rules/model.py +317 -0
  215. attocode_intel/rules/packs/__init__.py +1 -0
  216. attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
  217. attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
  218. attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
  219. attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
  220. attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
  221. attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
  222. attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
  223. attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
  224. attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
  225. attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
  226. attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
  227. attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
  228. attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
  229. attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
  230. attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
  231. attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
  232. attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
  233. attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
  234. attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
  235. attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
  236. attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
  237. attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
  238. attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
  239. attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
  240. attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
  241. attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
  242. attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
  243. attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
  244. attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
  245. attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
  246. attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
  247. attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
  248. attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
  249. attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
  250. attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
  251. attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
  252. attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
  253. attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
  254. attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
  255. attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
  256. attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
  257. attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
  258. attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
  259. attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
  260. attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
  261. attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
  262. attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
  263. attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
  264. attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
  265. attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
  266. attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
  267. attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
  268. attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
  269. attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
  270. attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
  271. attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
  272. attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
  273. attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
  274. attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
  275. attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
  276. attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
  277. attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
  278. attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
  279. attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
  280. attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
  281. attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
  282. attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
  283. attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
  284. attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
  285. attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
  286. attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
  287. attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
  288. attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
  289. attocode_intel/rules/packs/pack_loader.py +244 -0
  290. attocode_intel/rules/plugins/__init__.py +1 -0
  291. attocode_intel/rules/plugins/plugin_loader.py +132 -0
  292. attocode_intel/rules/profiling.py +332 -0
  293. attocode_intel/rules/registry.py +213 -0
  294. attocode_intel/rules/sarif.py +187 -0
  295. attocode_intel/rules/synthesis.py +420 -0
  296. attocode_intel/rules/testing.py +326 -0
  297. attocode_intel/server.py +734 -0
  298. attocode_intel/service.py +3416 -0
  299. attocode_intel/sharing.py +57 -0
  300. attocode_intel/storage/__init__.py +6 -0
  301. attocode_intel/storage/branch_overlay.py +352 -0
  302. attocode_intel/storage/content_store.py +220 -0
  303. attocode_intel/storage/dependency_store.py +143 -0
  304. attocode_intel/storage/diff_engine.py +187 -0
  305. attocode_intel/storage/embedding_store.py +594 -0
  306. attocode_intel/storage/provenance_store.py +137 -0
  307. attocode_intel/storage/security_scanner_db.py +164 -0
  308. attocode_intel/storage/symbol_store.py +160 -0
  309. attocode_intel/structural_evidence.py +144 -0
  310. attocode_intel/symbol_inspection.py +141 -0
  311. attocode_intel/symbol_links.py +167 -0
  312. attocode_intel/telemetry.py +42 -0
  313. attocode_intel/test_ranking.py +73 -0
  314. attocode_intel/testing/__init__.py +43 -0
  315. attocode_intel/testing/fixtures.py +122 -0
  316. attocode_intel/testing/helpers.py +75 -0
  317. attocode_intel/testing/mocks.py +99 -0
  318. attocode_intel/tools/__init__.py +2 -0
  319. attocode_intel/tools/adr_tools.py +628 -0
  320. attocode_intel/tools/analysis_tools.py +382 -0
  321. attocode_intel/tools/composite_tools.py +695 -0
  322. attocode_intel/tools/cross_mode_tools.py +173 -0
  323. attocode_intel/tools/dead_code_tools.py +582 -0
  324. attocode_intel/tools/distill_tools.py +353 -0
  325. attocode_intel/tools/frecency_tools.py +220 -0
  326. attocode_intel/tools/fuzzy_tools.py +255 -0
  327. attocode_intel/tools/history_tools.py +486 -0
  328. attocode_intel/tools/learning_tools.py +222 -0
  329. attocode_intel/tools/lsp_tools.py +218 -0
  330. attocode_intel/tools/maintenance_tools.py +1662 -0
  331. attocode_intel/tools/navigation_tools.py +220 -0
  332. attocode_intel/tools/overlay_tools.py +540 -0
  333. attocode_intel/tools/pin_store.py +432 -0
  334. attocode_intel/tools/pin_tools.py +266 -0
  335. attocode_intel/tools/query_constraints_tools.py +262 -0
  336. attocode_intel/tools/query_history_tools.py +158 -0
  337. attocode_intel/tools/readiness_tools.py +44 -0
  338. attocode_intel/tools/rule_tools.py +992 -0
  339. attocode_intel/tools/search_tools.py +770 -0
  340. attocode_intel/tools/snapshot_tools.py +624 -0
  341. attocode_intel/workers/__init__.py +1 -0
  342. attocode_intel/workers/job_utils.py +111 -0
  343. attocode_intel/workers/jobs.py +573 -0
  344. attocode_intel/workers/run.py +30 -0
  345. attocode_intel/workers/settings.py +61 -0
@@ -0,0 +1,734 @@
1
+ """Shared operation gateway for local and remote MCP/HTTP clients."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import contextlib
7
+ import inspect
8
+ import json
9
+ import time
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from contextvars import copy_context
12
+ from copy import deepcopy
13
+ from dataclasses import replace
14
+ from pathlib import Path
15
+
16
+ from filelock import FileLock
17
+ from mcp import types
18
+ from mcp.server.lowlevel import Server
19
+
20
+ from attocode_intel.catalog import (
21
+ INSTRUCTIONS,
22
+ INSTRUCTIONS_NO_WATCH,
23
+ compact_schema,
24
+ is_write,
25
+ registered_tools,
26
+ tool_catalog,
27
+ )
28
+ from attocode_intel.output import bounded_compact, bounded_text, response_tokens
29
+ from attocode_intel.project_dir import cache_folder
30
+ from attocode_intel.query_ranking import hit_evidence, next_search_top_k
31
+ from attocode_intel.request_context import RequestContext, bind_request, resolve_workspace
32
+
33
+
34
+ class OperationGateway:
35
+ def __init__(
36
+ self,
37
+ project: str = "",
38
+ profile: str = "full",
39
+ resolver=None,
40
+ *,
41
+ watch=True,
42
+ watch_debounce=500,
43
+ max_workspaces=8,
44
+ ):
45
+ self.project = project
46
+ self.profile = profile
47
+ self.resolver = resolver
48
+ self._workers = {}
49
+ self._stores = {}
50
+ self.watch, self.watch_debounce = watch, watch_debounce
51
+ self.instructions = INSTRUCTIONS if watch else INSTRUCTIONS_NO_WATCH
52
+ self._closed = False
53
+ self.max_workspaces = max(1, max_workspaces)
54
+ self._active = {}
55
+ self._last_used = {}
56
+ self._lifecycle = asyncio.Lock()
57
+ self._catalogs = {}
58
+
59
+ def effective_profile(self):
60
+ from attocode_intel.remote import remote_profile
61
+ return remote_profile.get() if self.resolver else self.profile
62
+
63
+ def _catalog(self):
64
+ key = (self.effective_profile(), bool(self.resolver))
65
+ if key not in self._catalogs:
66
+ from jsonschema.validators import validator_for
67
+ catalog = tool_catalog(key[0], remote=key[1])
68
+ self._catalogs[key] = (catalog, {tool.name: validator_for(tool.inputSchema)(tool.inputSchema)
69
+ for tool in catalog})
70
+ return self._catalogs[key]
71
+
72
+ def catalog(self, *, mcp=False):
73
+ catalog = deepcopy(self._catalog()[0])
74
+ if not mcp:
75
+ return catalog
76
+ if not self.resolver and self.effective_profile() == "daily":
77
+ # A local agent does not need these. They stay callable, for harnesses and diagnostics.
78
+ hidden = {"hydration_status", "cross_repo_search"} | ({"notify_file_changed"} if self.watch else set())
79
+ catalog = [tool for tool in catalog if tool.name not in hidden]
80
+ # A model reads this list in every request, so it gets no docstring indent and no pydantic noise.
81
+ for tool in catalog:
82
+ tool.description = inspect.cleandoc(tool.description or "")
83
+ tool.inputSchema = compact_schema(tool.inputSchema)
84
+ properties = tool.inputSchema["properties"]
85
+ if not self.resolver and "workspace" in properties:
86
+ properties["workspace"] = {"type": "string", "description": "Project path (default: this project)"}
87
+ if self.effective_profile() == "daily":
88
+ budget = {"type": "integer", "maximum": 32000, "default": 2000}
89
+ properties["max_tokens"] = {**budget, "minimum": 512} if is_write(tool.name) else budget
90
+ return catalog
91
+
92
+ async def close(self):
93
+ self._closed = True
94
+ async with self._lifecycle:
95
+ for key in list(self._workers):
96
+ await self._drop_workspace(key)
97
+
98
+ async def _drop_workspace(self, key):
99
+ stores, worker = self._stores[key], self._workers[key]
100
+ if watcher := stores.get("watcher"):
101
+ await asyncio.to_thread(watcher.close)
102
+ await asyncio.get_running_loop().run_in_executor(worker, self._close_stores, stores)
103
+ worker.shutdown(wait=True)
104
+ for mapping in (self._workers, self._stores, self._active, self._last_used):
105
+ mapping.pop(key, None)
106
+
107
+ async def _acquire_workspace(self, context):
108
+ key = (context.project_dir, context.source)
109
+ async with self._lifecycle:
110
+ if self._closed:
111
+ raise ValueError("Gateway is closed")
112
+ if key not in self._workers:
113
+ if len(self._workers) >= self.max_workspaces:
114
+ idle = [key for key in self._workers if not self._active[key]]
115
+ if not idle:
116
+ raise ValueError(
117
+ "All workspace slots are busy; retry after an operation completes"
118
+ )
119
+ await self._drop_workspace(min(idle, key=self._last_used.get))
120
+ self._workers[key] = ThreadPoolExecutor(
121
+ max_workers=1, thread_name_prefix="intel-workspace"
122
+ )
123
+ self._stores[key] = {}
124
+ self._active[key] = 0
125
+ self._active[key] += 1
126
+ self._last_used[key] = time.monotonic()
127
+ return replace(context, stores=self._stores[key])
128
+
129
+ @staticmethod
130
+ def _close_stores(stores):
131
+ service = stores.get("service")
132
+ if service:
133
+ if service._ast_service:
134
+ service._ast_service.stop_hydration()
135
+ if service._ast_service._store:
136
+ service._ast_service._store.close()
137
+ if service._lsp_manager:
138
+ runner = stores.get("async_runner") or stores.setdefault("async_runner", asyncio.Runner())
139
+ runner.run(service._lsp_manager.stop_all())
140
+ for resource in (service._memory_store, service._semantic_search):
141
+ if resource:
142
+ resource.close()
143
+ for name, resource in stores.items():
144
+ if name not in {"service", "watcher"} and hasattr(resource, "close"):
145
+ resource.close()
146
+
147
+ @staticmethod
148
+ def _warm_search(context):
149
+ with bind_request(context):
150
+ context.service._get_semantic_search().warm()
151
+
152
+ @staticmethod
153
+ def _refresh_sync(context):
154
+ from attocode_intel.freshness import FreshnessTracker
155
+
156
+ with bind_request(context):
157
+ directory = cache_folder(context.project_dir) / "cache"
158
+ directory.mkdir(parents=True, exist_ok=True)
159
+ with FileLock(str(directory / "operations.lock"), timeout=30):
160
+ tracker = context.stores.setdefault(
161
+ "freshness", FreshnessTracker(context.project_dir)
162
+ )
163
+ tracker.refresh(context.service)
164
+
165
+ async def execute_mcp(self, name: str, arguments: dict) -> types.CallToolResult:
166
+ return await self.execute(name, arguments, compact=self.effective_profile() == "daily")
167
+
168
+ async def execute(self, name: str, arguments: dict, *, compact=False) -> types.CallToolResult:
169
+ from attocode_intel.telemetry import emit, enabled
170
+ from attocode_intel.telemetry import timings as active_timings
171
+ start = time.monotonic()
172
+ timings = {}
173
+ token = active_timings.set(timings)
174
+ try:
175
+ result = await self._dispatch(name, arguments, compact, timings)
176
+ if enabled():
177
+ emit({"operation": name, "compact": compact, "timings_ms": timings,
178
+ "total_ms": round((time.monotonic() - start) * 1000, 2),
179
+ "response_bytes": len(result.model_dump_json(exclude_none=True).encode()),
180
+ "response_tokens": response_tokens(result), "error": bool(result.isError)})
181
+ return result
182
+ except Exception:
183
+ emit({"operation": name, "compact": compact, "timings_ms": timings,
184
+ "total_ms": round((time.monotonic() - start) * 1000, 2), "error": True})
185
+ raise
186
+ finally:
187
+ active_timings.reset(token)
188
+
189
+ async def _dispatch(self, name, arguments, compact, timings):
190
+ if self._closed:
191
+ raise ValueError("Gateway is closed")
192
+ args = dict(arguments)
193
+ if compact:
194
+ args.setdefault("max_tokens", 2000)
195
+ workspace = args.pop("workspace", "")
196
+ revision = args.pop("revision", "")
197
+ before = time.monotonic()
198
+ validators = self._catalog()[1]
199
+ if name not in validators:
200
+ raise ValueError(f"Tool {name!r} is not available in profile {self.profile!r}")
201
+ validators[name].validate(arguments)
202
+ if compact and is_write(name) and args["max_tokens"] < 512:
203
+ raise ValueError("Write operations require max_tokens >= 512 so their outcome can be acknowledged")
204
+ timings["validation"] = round((time.monotonic() - before) * 1000, 2)
205
+ if name == "cross_repo_search":
206
+ from attocode_intel.service import SEARCH_WARMUP_WAIT
207
+
208
+ results = []
209
+ warming_workspaces = []
210
+ # Each workspace gets the short wait of the loop below, not the full first-search wait.
211
+ wait = SEARCH_WARMUP_WAIT.set(0.75)
212
+ try:
213
+ for target in args["workspaces"]:
214
+ request = {
215
+ "workspace": target,
216
+ "query": args["query"],
217
+ "top_k": args.get("top_k", 10),
218
+ "mode": "keyword",
219
+ }
220
+ # Tiny/new workspaces commonly finish indexing within this bound.
221
+ # Keep the outer search responsive on large checkouts, where the
222
+ # explicit warming signal is more honest than an empty match set.
223
+ deadline = time.monotonic() + 0.75
224
+ while True:
225
+ response = await self.execute("semantic_search", request)
226
+ index = response.structuredContent["metadata"].get("ranking", {}).get("index", {})
227
+ if index.get("status") != "warming" or time.monotonic() >= deadline:
228
+ break
229
+ await asyncio.sleep(0.05)
230
+ structured = response.structuredContent
231
+ if index.get("status") == "warming":
232
+ warming_workspaces.append(target)
233
+ for rank, match in enumerate(structured["data"]["results"], 1):
234
+ results.append(
235
+ {
236
+ **match,
237
+ "workspace": structured["metadata"]["workspace"],
238
+ "revision": structured["metadata"]["revision"],
239
+ "source": structured["metadata"]["source"],
240
+ "freshness": structured["metadata"]["freshness"],
241
+ "index": structured["metadata"]["coverage"].get("phase", "unknown"),
242
+ "fusion_score": 1 / (60 + rank),
243
+ }
244
+ )
245
+ finally:
246
+ SEARCH_WARMUP_WAIT.reset(wait)
247
+ results.sort(key=lambda row: row["fusion_score"], reverse=True)
248
+ results = results[: args.get("top_k", 10)]
249
+ budget = args.get("max_tokens", 8000)
250
+ body = ("Search index warming in: " + ", ".join(warming_workspaces)
251
+ + ". Missing results do not prove absence."
252
+ if not results and warming_workspaces else json.dumps(results))
253
+ text, truncated = bounded_text(body, budget)
254
+ if compact:
255
+ return bounded_compact({"truncated": truncated, "ranking": "reciprocal_rank",
256
+ "workspaces": args["workspaces"],
257
+ "warming_workspaces": warming_workspaces,
258
+ "analysis": {"status": "partial", "absence_proven": False}}, results, budget)
259
+ return types.CallToolResult(
260
+ content=[types.TextContent(type="text", text=text)],
261
+ structuredContent={
262
+ "data": results,
263
+ "metadata": {
264
+ "truncated": truncated,
265
+ "ranking": "reciprocal_rank",
266
+ "workspaces": args["workspaces"],
267
+ "warming_workspaces": warming_workspaces,
268
+ },
269
+ },
270
+ )
271
+ if self.resolver:
272
+ context = await self.resolver(workspace, revision, name)
273
+ if compact and is_write(name):
274
+ self._check_write_budget(context, args["max_tokens"])
275
+ from attocode_intel.knowledge import KNOWLEDGE_TOOLS, execute_knowledge
276
+
277
+ if name in KNOWLEDGE_TOOLS:
278
+ budget = args.pop("max_tokens", 8000)
279
+ data = await execute_knowledge(context, name, args)
280
+ if compact:
281
+ return self._compact(name, {"workspace": context.workspace, "revision": context.revision,
282
+ "source": "remote", "freshness": "committed_snapshot",
283
+ "truncated": False}, data, budget)
284
+ header = f"Source: remote | Workspace: {context.workspace} | Revision: {context.revision}\n"
285
+ text, truncated = bounded_text(header + json.dumps(data), budget)
286
+ return types.CallToolResult(
287
+ content=[types.TextContent(type="text", text=text)],
288
+ structuredContent={
289
+ "result": text,
290
+ "data": data,
291
+ "metadata": {
292
+ "workspace": context.workspace,
293
+ "revision": context.revision,
294
+ "source": "remote",
295
+ "truncated": truncated,
296
+ },
297
+ },
298
+ )
299
+ else:
300
+ if revision:
301
+ raise ValueError(
302
+ "Local analysis uses the working tree; revision selection is remote-only."
303
+ )
304
+ path = resolve_workspace(workspace, self.project)
305
+ context = RequestContext(path, path)
306
+ key = (context.project_dir, context.source)
307
+ if self.resolver and name == "bootstrap" and args.get("task_hint"):
308
+ from attocode_intel.knowledge import execute_knowledge
309
+
310
+ args["_knowledge"] = await execute_knowledge(
311
+ context, "recall", {"query": args["task_hint"], "max_results": 5}
312
+ )
313
+ context = await self._acquire_workspace(context)
314
+ if self.watch and context.source == "local" and "watcher" not in context.stores:
315
+ from attocode_intel.freshness import WorkspaceWatcher
316
+
317
+ context.stores["watcher"] = WorkspaceWatcher(
318
+ context.project_dir,
319
+ lambda: (
320
+ None if self._closed else self._workers[key].submit(self._refresh_sync, context)
321
+ ),
322
+ self.watch_debounce,
323
+ )
324
+ try:
325
+ timings["enqueued_at"] = time.monotonic()
326
+ return await asyncio.get_running_loop().run_in_executor(
327
+ self._workers[key],
328
+ copy_context().run,
329
+ self._execute_sync,
330
+ context,
331
+ name,
332
+ args,
333
+ compact,
334
+ timings,
335
+ )
336
+ finally:
337
+ if key in self._active:
338
+ self._active[key] -= 1
339
+ if context.source == "local" and not self._closed and key in self._workers:
340
+ # Each call starts the search warm-up after its own work, so that this call
341
+ # is not slower and a later search finds ready indexes.
342
+ with contextlib.suppress(RuntimeError):
343
+ self._workers[key].submit(self._warm_search, context)
344
+
345
+ @staticmethod
346
+ def _check_write_budget(context, budget):
347
+ bounded_compact({"workspace": context.workspace, "source": context.source,
348
+ "revision": context.revision, "freshness": "committed_snapshot" if context.source == "remote" else "working_tree"},
349
+ {"status": "completed"}, budget - 128)
350
+
351
+ @staticmethod
352
+ def _compact(name, metadata, payload, budget):
353
+ try:
354
+ return bounded_compact(metadata, payload, budget)
355
+ except ValueError:
356
+ if not is_write(name):
357
+ raise
358
+ return bounded_compact({**metadata, "truncated": True},
359
+ {"status": "completed", "result_omitted": True}, budget)
360
+
361
+ def _execute_sync(self, context, name, args, compact=False, timings=None):
362
+ timings = timings if timings is not None else {}
363
+ timings["queue"] = round((time.monotonic() - timings.pop("enqueued_at", time.monotonic())) * 1000, 2)
364
+ lock_dir = cache_folder(context.project_dir) / "cache"
365
+ lock_dir.mkdir(parents=True, exist_ok=True)
366
+ start = time.monotonic()
367
+ budget = int(args.get("max_tokens", 8000))
368
+ knowledge = args.pop("_knowledge", None)
369
+ precision_result = None
370
+ ranking = None
371
+ operation_args = dict(args)
372
+ with FileLock(str(lock_dir / "operations.lock"), timeout=30), bind_request(context):
373
+ if compact and is_write(name):
374
+ self._check_write_budget(context, budget)
375
+ timings["lock"] = round((time.monotonic() - start) * 1000, 2)
376
+ if name == "capabilities":
377
+ from importlib.util import find_spec
378
+
379
+ from attocode_intel.analysis_coverage import language_capabilities
380
+ from attocode_intel.remote import remote_profile
381
+ payload = {
382
+ "workspace": context.workspace,
383
+ "source": context.source,
384
+ "revision": context.revision,
385
+ "profile": remote_profile.get() if self.resolver else self.profile,
386
+ "profiles": ["daily", "full"],
387
+ "tools": [t.name for t in self.catalog()],
388
+ "enhancements": {
389
+ "embeddings_package_installed": bool(find_spec("sentence_transformers")),
390
+ "watcher_active": "watcher" in context.stores,
391
+ },
392
+ "guidance": self.instructions,
393
+ "languages": language_capabilities(getattr(context.stores.get("precision"), "verified_languages", ())),
394
+ }
395
+ text = json.dumps(payload, indent=2)
396
+ else:
397
+ from attocode_intel.local_knowledge import LEARNING_TOOLS, execute_local_knowledge
398
+
399
+ tool = registered_tools().get(name)
400
+ # Validate even when called by a REST operation endpoint.
401
+ if tool:
402
+ args = tool.fn_metadata.arg_model.model_validate(args).model_dump()
403
+ else:
404
+ args.pop("max_tokens", None)
405
+ service = context.service
406
+ if context.source == "remote":
407
+ root = Path(context.project_dir).resolve()
408
+ for key, value in args.items():
409
+ if key in {
410
+ "path",
411
+ "file",
412
+ "file_path",
413
+ "files",
414
+ "changed_files",
415
+ "directory",
416
+ "scope",
417
+ }:
418
+ for candidate in value if isinstance(value, list) else [value]:
419
+ if candidate and not (root / candidate).resolve().is_relative_to(
420
+ root
421
+ ):
422
+ raise ValueError(
423
+ "Path must remain inside the selected repository"
424
+ )
425
+ if context.source == "local":
426
+ from attocode_intel.freshness import FreshnessTracker
427
+
428
+ tracker = context.stores.setdefault(
429
+ "freshness", FreshnessTracker(context.project_dir)
430
+ )
431
+ before = time.monotonic()
432
+ tracker.refresh(service)
433
+ timings["freshness"] = round((time.monotonic() - before) * 1000, 2)
434
+ if name == "notify_file_changed":
435
+ context.stores.pop("reference_pages", None)
436
+ if name in {"cross_references", "call_graph", "inspect_symbol"} and not args.get("cursor"):
437
+ from attocode_intel.precision import PrecisionSession
438
+ session = context.stores.setdefault("precision", PrecisionSession(service))
439
+ runner = context.stores.get("async_runner") or context.stores.setdefault("async_runner", asyncio.Runner())
440
+ before = time.monotonic()
441
+ precision_result = runner.run(session.enrich(args.get("symbol_name", args.get("symbol", "")),
442
+ args.get("file_path"), args.get("line")), context=copy_context())
443
+ timings["precision"] = round((time.monotonic() - before) * 1000, 2)
444
+ payload = None
445
+ before = time.monotonic()
446
+ if name in LEARNING_TOOLS:
447
+ payload = execute_local_knowledge(context, name, args)
448
+ text = json.dumps(payload)
449
+ elif name == "review_change":
450
+ from attocode_intel.change_review import review
451
+
452
+ payload = review(service, args.get("files"), args.get("mode", "full"))
453
+ text = json.dumps(payload, default=str, indent=2)
454
+ elif name == "semantic_search":
455
+ mgr = service._get_semantic_search()
456
+ candidates = service._search_after_warmup(mgr, lambda: (
457
+ mgr.search_candidates(args["query"], service._retrieval_depth(args["top_k"]), args["file_filter"])
458
+ if args.get("mode") == "keyword"
459
+ else mgr.search(
460
+ args["query"], top_k=service._retrieval_depth(args["top_k"]), file_filter=args["file_filter"]
461
+ )
462
+ ))
463
+ results, ranking = service._rank_search_results(
464
+ args["query"], candidates, args["top_k"], args["file_filter"],
465
+ )
466
+ ranking["candidate_pool_count"] = len(candidates)
467
+ if hasattr(mgr, "candidate_diagnostics"):
468
+ ranking["index"] = mgr.candidate_diagnostics()
469
+ result_cap = max(1, budget // 120)
470
+ omitted_results = max(0, len(results) - result_cap)
471
+ results = results[:result_cap]
472
+ ranking["delivered_count"] = len(results)
473
+ if omitted_results:
474
+ ranking["omitted_results_due_to_budget"] = omitted_results
475
+ snippet_cap = max(32, min(320, budget // max(1, len(results))))
476
+ hits = []
477
+ for result in results:
478
+ snippet = (result.text.split("\n", 1)[-1]
479
+ if result.start_line else result.text)
480
+ hits.append({
481
+ "file_path": result.file_path,
482
+ "score": result.score,
483
+ "snippet": snippet[:snippet_cap],
484
+ "snippet_truncated": len(snippet) > snippet_cap,
485
+ "line": result.start_line or None,
486
+ "end_line": result.end_line or None,
487
+ **hit_evidence(args["query"], result),
488
+ })
489
+ payload = {
490
+ "query": args["query"],
491
+ "results": hits,
492
+ "ranking": ranking,
493
+ }
494
+ if ranking.get("index", {}).get("status") == "ready":
495
+ next_k = next_search_top_k(
496
+ len(candidates), args["top_k"], len(results),
497
+ )
498
+ if next_k is not None:
499
+ request = {
500
+ "query": args["query"], "top_k": next_k,
501
+ "file_filter": args["file_filter"],
502
+ "mode": args.get("mode", "auto"),
503
+ "workspace": context.workspace,
504
+ "max_tokens": min(32000, max(budget * 2, next_k * 160 + 512)),
505
+ }
506
+ if args.get("branch"):
507
+ request["branch"] = args["branch"]
508
+ if args.get("revision"):
509
+ request["revision"] = args["revision"]
510
+ payload["follow_up"] = {
511
+ "tool": "semantic_search", "arguments": request,
512
+ "reruns_current_index": True,
513
+ }
514
+ note = (
515
+ "Broad query matches multiple components; narrow with file_filter or a component name.\n"
516
+ if ranking["query"]["ambiguous"] else ""
517
+ )
518
+ if payload.get("follow_up"):
519
+ note += (
520
+ "More retrieved candidates: rerun with "
521
+ f"top_k={payload['follow_up']['arguments']['top_k']} "
522
+ "(current index).\n"
523
+ )
524
+ text = note + mgr.format_results(results, query=args["query"])
525
+ if not results and ranking.get("index", {}).get("status") == "warming":
526
+ text = "Search index warming; retry shortly. Missing results do not prove absence."
527
+ elif name == "inspect_symbol":
528
+ from attocode_intel.symbol_inspection import inspect_symbol_data
529
+ payload = inspect_symbol_data(service, args["symbol_name"], args.get("file_path"), args.get("line"),
530
+ args.get("source_start_line"), args.get("task_hint"))
531
+ text = json.dumps(payload)
532
+ elif name == "suggest_tests":
533
+ from attocode_intel.tools.composite_tools import (
534
+ format_test_suggestions,
535
+ suggest_tests_data,
536
+ )
537
+ payload = suggest_tests_data(args["files"], symbol_name=args.get("symbol_name"), task_hint=args.get("task_hint"))
538
+ text = format_test_suggestions(payload)
539
+ elif name == "cross_references":
540
+ from attocode_intel.reference_pages import continuation
541
+ from attocode_intel.symbol_inspection import scoped_references
542
+ saved = continuation(context, args)
543
+ payload = saved["payload"] if saved else scoped_references(
544
+ service, args["symbol_name"], args.get("file_path"), args.get("line"))
545
+ text = json.dumps(payload)
546
+ elif name in {"symbols", "search_symbols", "dependencies", "call_graph", "file_analysis",
547
+ "hotspots", "dependency_graph", "impact_analysis"}:
548
+ method = getattr(service, name + "_data")
549
+ parameters = inspect.signature(method).parameters
550
+ payload = method(**{key: value for key, value in args.items() if key in parameters})
551
+ text = json.dumps(payload, default=str, indent=2)
552
+ else:
553
+ result = tool.fn(**args)
554
+ if inspect.isawaitable(result):
555
+ runner = context.stores.get("async_runner") or context.stores.setdefault("async_runner", asyncio.Runner())
556
+ result = runner.run(result, context=copy_context())
557
+ text = result if isinstance(result, str) else json.dumps(result, default=str)
558
+ if name in {
559
+ "install_pack",
560
+ "install_community_pack",
561
+ "import_rules",
562
+ "rule_hygiene",
563
+ "evolve_rules",
564
+ }:
565
+ context.stores.pop("rules", None)
566
+ timings["execution"] = round((time.monotonic() - before) * 1000, 2)
567
+ if name == "bootstrap" and args.get("task_hint"):
568
+ if context.source == "local":
569
+ from attocode_intel.local_knowledge import execute_local_knowledge
570
+
571
+ knowledge = execute_local_knowledge(
572
+ context, "recall", {"query": args["task_hint"], "max_results": 5}
573
+ )
574
+ knowledge = [entry for entry in knowledge or [] if not entry.get("stale")]
575
+ if knowledge:
576
+ knowledge_text = bounded_text(json.dumps(knowledge), max(1, budget // 4))[0]
577
+ section = ("## Relevant project knowledge (verify entries marked stale)\n"
578
+ + knowledge_text)
579
+ if text.startswith("## Relevant Code"):
580
+ boundary = text.find("\n\n## ")
581
+ text = (text[:boundary] + "\n\n" + section + text[boundary:]
582
+ if boundary >= 0 else text + "\n\n" + section)
583
+ else:
584
+ text = section + "\n\n" + text
585
+ service = context.service
586
+ ast_service = service._ast_service
587
+ coverage = ast_service.hydration_snapshot() if ast_service else {"phase": "not_started"}
588
+ phase = coverage.get("phase", "unknown")
589
+ header = f"Source: {context.source} | Workspace: {context.workspace} | Revision: {context.revision} | Index: {phase}\n"
590
+ if phase != "ready" or coverage.get("discovery_truncated"):
591
+ header += "Coverage is incomplete; missing results do not prove absence.\n"
592
+ from attocode_intel.analysis_coverage import analysis_report
593
+ analysis = (analysis_report(service, precision_result) if not compact or name == "capabilities" else
594
+ {"status": "partial" if ast_service else "not_started", "absence_proven": False,
595
+ "precision": precision_result or {"status": "not_requested"}})
596
+ if name == "capabilities":
597
+ payload["analysis"] = analysis
598
+ payload["coverage"] = coverage
599
+ text = json.dumps(payload)
600
+ if name in {"cross_references", "dependencies", "call_graph", "impact_analysis", "suggest_tests"}:
601
+ header += "Analysis: partial; relationships are candidates and missing results do not prove absence.\n"
602
+ body = text
603
+ text, truncated = bounded_text(header + text, budget) if not compact else (text, False)
604
+ truncated = truncated or "[Truncated;" in text
605
+ metadata = {
606
+ "workspace": context.workspace,
607
+ "source": context.source,
608
+ "revision": context.revision,
609
+ "coverage": coverage,
610
+ "analysis": analysis,
611
+ "freshness": "committed_snapshot" if context.source == "remote" else "working_tree",
612
+ "truncated": truncated or bool(ranking and ranking.get("omitted_results_due_to_budget")),
613
+ "duration_ms": round((time.monotonic() - start) * 1000, 2),
614
+ }
615
+ if ranking is not None:
616
+ metadata["ranking"] = ranking
617
+ if name == "inspect_symbol":
618
+ metadata["truncated"] |= bool(payload.get("source", {}).get("truncated")) or any(
619
+ payload.get("total_" + section, 0) > len(payload.get(section, []))
620
+ for section in ("references", "imports", "tests", "definitions"))
621
+ structured = {"result": text, "metadata": metadata}
622
+ before = time.monotonic()
623
+ if name == "cross_references" and (compact or operation_args.get("cursor") or operation_args.get("page_size") is not None):
624
+ from attocode_intel.reference_pages import paginate
625
+ page = paginate(context, operation_args, payload, metadata, budget, compact)
626
+ timings["serialization"] = round((time.monotonic() - before) * 1000, 2)
627
+ if compact:
628
+ return page
629
+ payload, metadata = page
630
+ text, _ = bounded_text(header + json.dumps(payload), budget)
631
+ structured = {"result": text, "metadata": metadata}
632
+ if compact:
633
+ if name == "bootstrap":
634
+ from attocode_intel.output import bounded_bootstrap
635
+ result = bounded_bootstrap(metadata, body, budget)
636
+ else:
637
+ result = self._compact(name, metadata, payload if payload is not None else {"result": body}, budget)
638
+ timings["serialization"] = round((time.monotonic() - before) * 1000, 2)
639
+ return result
640
+ if payload is not None:
641
+ structured["capabilities" if name == "capabilities" else "data"] = payload
642
+ return types.CallToolResult(
643
+ content=[types.TextContent(type="text", text=text)], structuredContent=structured
644
+ )
645
+
646
+
647
+ def create_mcp_server(gateway: OperationGateway) -> Server:
648
+ from attocode_intel import __version__
649
+
650
+ server = Server("attocode-code-intel", version=__version__, instructions=gateway.instructions)
651
+
652
+ @server.list_tools()
653
+ async def list_tools():
654
+ return gateway.catalog(mcp=True)
655
+
656
+ @server.call_tool()
657
+ async def call_tool(name: str, arguments: dict):
658
+ # Advertised roots are advisory, never a substitute for remote authorization.
659
+ if not gateway.resolver and not gateway.project and not arguments.get("workspace"):
660
+ session = server.request_context.session
661
+ try:
662
+ roots = await session.list_roots()
663
+ from urllib.parse import unquote, urlparse
664
+
665
+ paths = [
666
+ unquote(urlparse(str(root.uri)).path)
667
+ for root in roots.roots
668
+ if str(root.uri).startswith("file://")
669
+ ]
670
+ except Exception:
671
+ paths = []
672
+ # A client started in the home folder advertises home as its root.
673
+ # Indexing all of home is not a useful default; --project ~ still selects it.
674
+ home = Path.home().resolve()
675
+ paths = [path for path in paths if Path(path).resolve() != home]
676
+ if len(paths) == 1:
677
+ arguments = {**arguments, "workspace": paths[0]}
678
+ elif len(paths) > 1:
679
+ raise ValueError(f"Multiple workspace roots; supply workspace explicitly: {paths}")
680
+ return await gateway.execute_mcp(name, arguments)
681
+
682
+ @server.list_resources()
683
+ async def list_resources():
684
+ return [
685
+ types.Resource(
686
+ uri="attocode://guidelines", name="Agent guidelines", mimeType="text/plain"
687
+ )
688
+ ]
689
+
690
+ @server.list_resource_templates()
691
+ async def list_resource_templates():
692
+ return [
693
+ types.ResourceTemplate(uriTemplate=template, name=name, mimeType="text/plain")
694
+ for template, name in (
695
+ (
696
+ "attocode://project/{path}{?workspace,revision}",
697
+ "File analysis in a selected workspace",
698
+ ),
699
+ (
700
+ "attocode://symbols/{name}{?workspace,revision}",
701
+ "Symbol references in a selected workspace",
702
+ ),
703
+ ("attocode://learnings{?workspace,revision}", "Repository knowledge"),
704
+ )
705
+ ]
706
+
707
+ @server.read_resource()
708
+ async def read_resource(uri):
709
+ from urllib.parse import parse_qs, unquote, urlparse
710
+
711
+ from mcp.server.lowlevel.helper_types import ReadResourceContents
712
+
713
+ if str(uri) == "attocode://guidelines":
714
+ return [ReadResourceContents(gateway.instructions, mime_type="text/plain")]
715
+ parsed = urlparse(str(uri))
716
+ if parsed.scheme != "attocode":
717
+ raise ValueError("Unknown resource scheme")
718
+ args = {
719
+ key: value[-1]
720
+ for key, value in parse_qs(parsed.query).items()
721
+ if key in {"workspace", "revision"}
722
+ }
723
+ if parsed.netloc == "project":
724
+ name, args["path"] = "symbols", unquote(parsed.path.lstrip("/"))
725
+ elif parsed.netloc == "symbols":
726
+ name, args["symbol_name"] = "cross_references", unquote(parsed.path.lstrip("/"))
727
+ elif parsed.netloc == "learnings":
728
+ name = "list_learnings"
729
+ else:
730
+ raise ValueError("Unknown intelligence resource")
731
+ result = await call_tool(name, args)
732
+ return [ReadResourceContents(result.content[0].text, mime_type="text/plain")]
733
+
734
+ return server