gitnexus 1.6.8-rc.9 → 1.6.8

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 (430) hide show
  1. package/README.md +3 -3
  2. package/dist/_shared/graph/types.d.ts +31 -1
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/_shared/language-detection.d.ts.map +1 -1
  5. package/dist/_shared/language-detection.js +11 -1
  6. package/dist/_shared/language-detection.js.map +1 -1
  7. package/dist/_shared/lbug/schema-constants.d.ts +1 -1
  8. package/dist/_shared/lbug/schema-constants.d.ts.map +1 -1
  9. package/dist/_shared/lbug/schema-constants.js +6 -0
  10. package/dist/_shared/lbug/schema-constants.js.map +1 -1
  11. package/dist/_shared/scope-resolution/parsed-file.d.ts +21 -0
  12. package/dist/_shared/scope-resolution/parsed-file.d.ts.map +1 -1
  13. package/dist/_shared/scope-resolution/symbol-definition.d.ts +6 -0
  14. package/dist/_shared/scope-resolution/symbol-definition.d.ts.map +1 -1
  15. package/dist/cli/ai-context.d.ts +31 -1
  16. package/dist/cli/ai-context.js +18 -16
  17. package/dist/cli/analyze-config.js +11 -0
  18. package/dist/cli/analyze.d.ts +14 -0
  19. package/dist/cli/analyze.js +98 -0
  20. package/dist/cli/embedding-dims.d.ts +33 -0
  21. package/dist/cli/embedding-dims.js +39 -0
  22. package/dist/cli/eval-server.d.ts +8 -0
  23. package/dist/cli/eval-server.js +242 -2
  24. package/dist/cli/help-i18n.js +19 -0
  25. package/dist/cli/i18n/en.d.ts +15 -1
  26. package/dist/cli/i18n/en.js +15 -1
  27. package/dist/cli/i18n/resources.d.ts +29 -1
  28. package/dist/cli/i18n/zh-CN.d.ts +14 -0
  29. package/dist/cli/i18n/zh-CN.js +15 -1
  30. package/dist/cli/index.js +89 -1
  31. package/dist/cli/mcp.d.ts +6 -1
  32. package/dist/cli/mcp.js +26 -1
  33. package/dist/cli/optional-grammars.d.ts +6 -8
  34. package/dist/cli/optional-grammars.js +8 -11
  35. package/dist/cli/setup.d.ts +3 -1
  36. package/dist/cli/setup.js +67 -17
  37. package/dist/cli/skill-gen.js +2 -1
  38. package/dist/cli/tool.d.ts +19 -1
  39. package/dist/cli/tool.js +96 -3
  40. package/dist/core/embeddings/hf-env.d.ts +0 -66
  41. package/dist/core/embeddings/http-client.d.ts +7 -0
  42. package/dist/core/embeddings/http-client.js +5 -3
  43. package/dist/core/embeddings/server-mapping.d.ts +1 -1
  44. package/dist/core/embeddings/server-mapping.js +7 -3
  45. package/dist/core/graph/import-cycles.d.ts +10 -0
  46. package/dist/core/graph/import-cycles.js +103 -0
  47. package/dist/core/group/cross-impact.d.ts +2 -1
  48. package/dist/core/group/cross-impact.js +5 -1
  49. package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
  50. package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
  51. package/dist/core/group/extractors/http-route-extractor.d.ts +24 -0
  52. package/dist/core/group/extractors/http-route-extractor.js +22 -4
  53. package/dist/core/group/extractors/include-extractor.js +22 -13
  54. package/dist/core/group/sync.d.ts +27 -1
  55. package/dist/core/group/sync.js +0 -0
  56. package/dist/core/incremental/subgraph-extract.js +18 -1
  57. package/dist/core/ingestion/cfg/cfg-builder.d.ts +114 -0
  58. package/dist/core/ingestion/cfg/cfg-builder.js +196 -0
  59. package/dist/core/ingestion/cfg/collect.d.ts +51 -0
  60. package/dist/core/ingestion/cfg/collect.js +77 -0
  61. package/dist/core/ingestion/cfg/control-dependence.d.ts +58 -0
  62. package/dist/core/ingestion/cfg/control-dependence.js +182 -0
  63. package/dist/core/ingestion/cfg/control-flow-context.d.ts +106 -0
  64. package/dist/core/ingestion/cfg/control-flow-context.js +124 -0
  65. package/dist/core/ingestion/cfg/emit.d.ts +293 -0
  66. package/dist/core/ingestion/cfg/emit.js +646 -0
  67. package/dist/core/ingestion/cfg/post-dominators.d.ts +79 -0
  68. package/dist/core/ingestion/cfg/post-dominators.js +176 -0
  69. package/dist/core/ingestion/cfg/reaching-def-reason-codec.d.ts +106 -0
  70. package/dist/core/ingestion/cfg/reaching-def-reason-codec.js +133 -0
  71. package/dist/core/ingestion/cfg/reaching-defs-graph.d.ts +18 -0
  72. package/dist/core/ingestion/cfg/reaching-defs-graph.js +312 -0
  73. package/dist/core/ingestion/cfg/reaching-defs.d.ts +162 -0
  74. package/dist/core/ingestion/cfg/reaching-defs.js +785 -0
  75. package/dist/core/ingestion/cfg/synthetic-escape.d.ts +124 -0
  76. package/dist/core/ingestion/cfg/synthetic-escape.js +300 -0
  77. package/dist/core/ingestion/cfg/traversal-result.d.ts +20 -0
  78. package/dist/core/ingestion/cfg/traversal-result.js +2 -0
  79. package/dist/core/ingestion/cfg/types.d.ts +274 -0
  80. package/dist/core/ingestion/cfg/types.js +13 -0
  81. package/dist/core/ingestion/cfg/visitors/c-cpp-harvest.d.ts +111 -0
  82. package/dist/core/ingestion/cfg/visitors/c-cpp-harvest.js +541 -0
  83. package/dist/core/ingestion/cfg/visitors/c-cpp.d.ts +74 -0
  84. package/dist/core/ingestion/cfg/visitors/c-cpp.js +584 -0
  85. package/dist/core/ingestion/cfg/visitors/call-site-harvest.d.ts +195 -0
  86. package/dist/core/ingestion/cfg/visitors/call-site-harvest.js +350 -0
  87. package/dist/core/ingestion/cfg/visitors/csharp-harvest.d.ts +124 -0
  88. package/dist/core/ingestion/cfg/visitors/csharp-harvest.js +589 -0
  89. package/dist/core/ingestion/cfg/visitors/csharp.d.ts +87 -0
  90. package/dist/core/ingestion/cfg/visitors/csharp.js +867 -0
  91. package/dist/core/ingestion/cfg/visitors/dart-harvest.d.ts +278 -0
  92. package/dist/core/ingestion/cfg/visitors/dart-harvest.js +870 -0
  93. package/dist/core/ingestion/cfg/visitors/dart.d.ts +119 -0
  94. package/dist/core/ingestion/cfg/visitors/dart.js +836 -0
  95. package/dist/core/ingestion/cfg/visitors/go-harvest.d.ts +160 -0
  96. package/dist/core/ingestion/cfg/visitors/go-harvest.js +621 -0
  97. package/dist/core/ingestion/cfg/visitors/go.d.ts +108 -0
  98. package/dist/core/ingestion/cfg/visitors/go.js +638 -0
  99. package/dist/core/ingestion/cfg/visitors/java-harvest.d.ts +108 -0
  100. package/dist/core/ingestion/cfg/visitors/java-harvest.js +513 -0
  101. package/dist/core/ingestion/cfg/visitors/java.d.ts +101 -0
  102. package/dist/core/ingestion/cfg/visitors/java.js +812 -0
  103. package/dist/core/ingestion/cfg/visitors/kotlin-harvest.d.ts +253 -0
  104. package/dist/core/ingestion/cfg/visitors/kotlin-harvest.js +719 -0
  105. package/dist/core/ingestion/cfg/visitors/kotlin.d.ts +121 -0
  106. package/dist/core/ingestion/cfg/visitors/kotlin.js +809 -0
  107. package/dist/core/ingestion/cfg/visitors/php-harvest.d.ts +172 -0
  108. package/dist/core/ingestion/cfg/visitors/php-harvest.js +626 -0
  109. package/dist/core/ingestion/cfg/visitors/php.d.ts +96 -0
  110. package/dist/core/ingestion/cfg/visitors/php.js +721 -0
  111. package/dist/core/ingestion/cfg/visitors/python-harvest.d.ts +212 -0
  112. package/dist/core/ingestion/cfg/visitors/python-harvest.js +739 -0
  113. package/dist/core/ingestion/cfg/visitors/python.d.ts +103 -0
  114. package/dist/core/ingestion/cfg/visitors/python.js +558 -0
  115. package/dist/core/ingestion/cfg/visitors/ruby-harvest.d.ts +223 -0
  116. package/dist/core/ingestion/cfg/visitors/ruby-harvest.js +587 -0
  117. package/dist/core/ingestion/cfg/visitors/ruby.d.ts +112 -0
  118. package/dist/core/ingestion/cfg/visitors/ruby.js +756 -0
  119. package/dist/core/ingestion/cfg/visitors/rust-harvest.d.ts +324 -0
  120. package/dist/core/ingestion/cfg/visitors/rust-harvest.js +873 -0
  121. package/dist/core/ingestion/cfg/visitors/rust.d.ts +95 -0
  122. package/dist/core/ingestion/cfg/visitors/rust.js +558 -0
  123. package/dist/core/ingestion/cfg/visitors/scope-tree-harvest.d.ts +87 -0
  124. package/dist/core/ingestion/cfg/visitors/scope-tree-harvest.js +116 -0
  125. package/dist/core/ingestion/cfg/visitors/swift-harvest.d.ts +255 -0
  126. package/dist/core/ingestion/cfg/visitors/swift-harvest.js +679 -0
  127. package/dist/core/ingestion/cfg/visitors/swift.d.ts +116 -0
  128. package/dist/core/ingestion/cfg/visitors/swift.js +787 -0
  129. package/dist/core/ingestion/cfg/visitors/typescript-harvest.d.ts +149 -0
  130. package/dist/core/ingestion/cfg/visitors/typescript-harvest.js +1056 -0
  131. package/dist/core/ingestion/cfg/visitors/typescript.d.ts +66 -0
  132. package/dist/core/ingestion/cfg/visitors/typescript.js +583 -0
  133. package/dist/core/ingestion/import-resolvers/utils.js +2 -0
  134. package/dist/core/ingestion/language-provider.d.ts +11 -0
  135. package/dist/core/ingestion/languages/c-cpp.js +6 -1
  136. package/dist/core/ingestion/languages/cobol.js +4 -0
  137. package/dist/core/ingestion/languages/cpp/arity-metadata.js +41 -2
  138. package/dist/core/ingestion/languages/cpp/captures.js +50 -1
  139. package/dist/core/ingestion/languages/cpp/conversion-rank.d.ts +2 -0
  140. package/dist/core/ingestion/languages/cpp/conversion-rank.js +89 -0
  141. package/dist/core/ingestion/languages/cpp/header-scan.d.ts +1 -1
  142. package/dist/core/ingestion/languages/cpp/header-scan.js +2 -2
  143. package/dist/core/ingestion/languages/cpp/inline-namespaces.js +7 -2
  144. package/dist/core/ingestion/languages/cpp/member-lookup.js +2 -1
  145. package/dist/core/ingestion/languages/cpp/query.js +23 -0
  146. package/dist/core/ingestion/languages/cpp/scope-resolver.js +2 -1
  147. package/dist/core/ingestion/languages/csharp.js +2 -0
  148. package/dist/core/ingestion/languages/dart.js +2 -0
  149. package/dist/core/ingestion/languages/go.js +2 -0
  150. package/dist/core/ingestion/languages/java.js +3 -0
  151. package/dist/core/ingestion/languages/kotlin/query.js +3 -2
  152. package/dist/core/ingestion/languages/kotlin.js +3 -0
  153. package/dist/core/ingestion/languages/php.js +2 -0
  154. package/dist/core/ingestion/languages/python.js +2 -0
  155. package/dist/core/ingestion/languages/ruby.js +2 -0
  156. package/dist/core/ingestion/languages/rust.js +2 -0
  157. package/dist/core/ingestion/languages/swift.js +2 -0
  158. package/dist/core/ingestion/languages/typescript.js +5 -0
  159. package/dist/core/ingestion/languages/vue.js +6 -0
  160. package/dist/core/ingestion/method-extractors/configs/c-cpp.js +6 -13
  161. package/dist/core/ingestion/method-extractors/generic.js +1 -0
  162. package/dist/core/ingestion/method-types.d.ts +2 -0
  163. package/dist/core/ingestion/model/symbol-table.d.ts +1 -0
  164. package/dist/core/ingestion/model/symbol-table.js +1 -0
  165. package/dist/core/ingestion/mro-processor.js +109 -0
  166. package/dist/core/ingestion/parsing-processor.js +23 -0
  167. package/dist/core/ingestion/pipeline-phases/call-summaries.d.ts +26 -0
  168. package/dist/core/ingestion/pipeline-phases/call-summaries.js +46 -0
  169. package/dist/core/ingestion/pipeline-phases/index.d.ts +2 -0
  170. package/dist/core/ingestion/pipeline-phases/index.js +2 -0
  171. package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +0 -21
  172. package/dist/core/ingestion/pipeline-phases/parse-impl.js +18 -1
  173. package/dist/core/ingestion/pipeline-phases/routes.d.ts +10 -0
  174. package/dist/core/ingestion/pipeline-phases/routes.js +30 -1
  175. package/dist/core/ingestion/pipeline-phases/taint-summaries.d.ts +30 -0
  176. package/dist/core/ingestion/pipeline-phases/taint-summaries.js +81 -0
  177. package/dist/core/ingestion/pipeline.d.ts +92 -10
  178. package/dist/core/ingestion/pipeline.js +11 -2
  179. package/dist/core/ingestion/scope-extractor.js +12 -2
  180. package/dist/core/ingestion/scope-resolution/contract/scope-resolver.d.ts +9 -0
  181. package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.d.ts +75 -0
  182. package/dist/core/ingestion/scope-resolution/graph-bridge/callee-id-sink.js +67 -0
  183. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.d.ts +12 -2
  184. package/dist/core/ingestion/scope-resolution/graph-bridge/edges.js +14 -2
  185. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.d.ts +6 -1
  186. package/dist/core/ingestion/scope-resolution/graph-bridge/references-to-edges.js +13 -1
  187. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.d.ts +9 -1
  188. package/dist/core/ingestion/scope-resolution/passes/free-call-fallback.js +28 -2
  189. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.d.ts +4 -0
  190. package/dist/core/ingestion/scope-resolution/passes/overload-narrowing.js +9 -0
  191. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.d.ts +6 -1
  192. package/dist/core/ingestion/scope-resolution/passes/receiver-bound-calls.js +174 -40
  193. package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +24 -0
  194. package/dist/core/ingestion/scope-resolution/pipeline/phase.js +276 -190
  195. package/dist/core/ingestion/scope-resolution/pipeline/reconcile-ownership.js +43 -3
  196. package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +75 -0
  197. package/dist/core/ingestion/scope-resolution/pipeline/run.js +349 -1
  198. package/dist/core/ingestion/scope-resolution/resolution-outcome.d.ts +1 -1
  199. package/dist/core/ingestion/taint/call-summary-codec.d.ts +75 -0
  200. package/dist/core/ingestion/taint/call-summary-codec.js +128 -0
  201. package/dist/core/ingestion/taint/call-summary-emit.d.ts +43 -0
  202. package/dist/core/ingestion/taint/call-summary-emit.js +70 -0
  203. package/dist/core/ingestion/taint/call-summary-harvest.d.ts +77 -0
  204. package/dist/core/ingestion/taint/call-summary-harvest.js +179 -0
  205. package/dist/core/ingestion/taint/call-summary-model.d.ts +47 -0
  206. package/dist/core/ingestion/taint/call-summary-model.js +25 -0
  207. package/dist/core/ingestion/taint/emit.d.ts +125 -0
  208. package/dist/core/ingestion/taint/emit.js +208 -0
  209. package/dist/core/ingestion/taint/interproc-emit.d.ts +50 -0
  210. package/dist/core/ingestion/taint/interproc-emit.js +88 -0
  211. package/dist/core/ingestion/taint/interproc-solver.d.ts +110 -0
  212. package/dist/core/ingestion/taint/interproc-solver.js +312 -0
  213. package/dist/core/ingestion/taint/match.d.ts +153 -0
  214. package/dist/core/ingestion/taint/match.js +278 -0
  215. package/dist/core/ingestion/taint/path-codec.d.ts +134 -0
  216. package/dist/core/ingestion/taint/path-codec.js +190 -0
  217. package/dist/core/ingestion/taint/propagate.d.ts +216 -0
  218. package/dist/core/ingestion/taint/propagate.js +659 -0
  219. package/dist/core/ingestion/taint/site-safety.d.ts +29 -0
  220. package/dist/core/ingestion/taint/site-safety.js +98 -0
  221. package/dist/core/ingestion/taint/source-sink-config.d.ts +107 -23
  222. package/dist/core/ingestion/taint/source-sink-config.js +35 -12
  223. package/dist/core/ingestion/taint/source-sink-registry.d.ts +6 -4
  224. package/dist/core/ingestion/taint/source-sink-registry.js +6 -4
  225. package/dist/core/ingestion/taint/summary-harvest-driver.d.ts +80 -0
  226. package/dist/core/ingestion/taint/summary-harvest-driver.js +179 -0
  227. package/dist/core/ingestion/taint/summary-harvest.d.ts +89 -0
  228. package/dist/core/ingestion/taint/summary-harvest.js +537 -0
  229. package/dist/core/ingestion/taint/summary-model.d.ts +200 -0
  230. package/dist/core/ingestion/taint/summary-model.js +86 -0
  231. package/dist/core/ingestion/taint/typescript-model.d.ts +38 -0
  232. package/dist/core/ingestion/taint/typescript-model.js +102 -0
  233. package/dist/core/ingestion/type-extractors/c-cpp.js +21 -0
  234. package/dist/core/ingestion/utils/env.d.ts +7 -0
  235. package/dist/core/ingestion/utils/env.js +12 -0
  236. package/dist/core/ingestion/utils/method-props.js +1 -0
  237. package/dist/core/ingestion/workers/parse-worker.d.ts +13 -0
  238. package/dist/core/ingestion/workers/parse-worker.js +76 -17
  239. package/dist/core/ingestion/workers/worker-pool.d.ts +9 -40
  240. package/dist/core/ingestion/workers/worker-pool.js +15 -2
  241. package/dist/core/lbug/csv-generator.d.ts +51 -3
  242. package/dist/core/lbug/csv-generator.js +369 -255
  243. package/dist/core/lbug/lbug-adapter.d.ts +58 -18
  244. package/dist/core/lbug/lbug-adapter.js +346 -84
  245. package/dist/core/lbug/lbug-config.d.ts +19 -0
  246. package/dist/core/lbug/lbug-config.js +29 -0
  247. package/dist/core/lbug/pdg-emit-sink.d.ts +127 -0
  248. package/dist/core/lbug/pdg-emit-sink.js +358 -0
  249. package/dist/core/lbug/pool-adapter.d.ts +35 -0
  250. package/dist/core/lbug/pool-adapter.js +90 -1
  251. package/dist/core/lbug/rel-pair-routing.d.ts +54 -0
  252. package/dist/core/lbug/rel-pair-routing.js +140 -0
  253. package/dist/core/lbug/schema.d.ts +2 -2
  254. package/dist/core/lbug/schema.js +3 -0
  255. package/dist/core/logger.d.ts +0 -5
  256. package/dist/core/run-analyze.d.ts +96 -0
  257. package/dist/core/run-analyze.js +207 -3
  258. package/dist/core/tree-sitter/parser-loader.js +5 -4
  259. package/dist/core/tree-sitter/safe-parse.d.ts +0 -5
  260. package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
  261. package/dist/core/tree-sitter/vendored-grammars.js +57 -0
  262. package/dist/core/wiki/generator.js +3 -1
  263. package/dist/core/wiki/graph-queries.d.ts +6 -0
  264. package/dist/core/wiki/graph-queries.js +9 -1
  265. package/dist/mcp/http-transport.d.ts +134 -0
  266. package/dist/mcp/http-transport.js +503 -0
  267. package/dist/mcp/local/local-backend.d.ts +110 -24
  268. package/dist/mcp/local/local-backend.js +1225 -19
  269. package/dist/mcp/local/pdg-impact.d.ts +384 -0
  270. package/dist/mcp/local/pdg-impact.js +1972 -0
  271. package/dist/mcp/resources.js +13 -1
  272. package/dist/mcp/tools.d.ts +12 -0
  273. package/dist/mcp/tools.js +214 -7
  274. package/dist/server/analyze-launch.d.ts +29 -0
  275. package/dist/server/analyze-launch.js +137 -0
  276. package/dist/server/analyze-upload.d.ts +33 -0
  277. package/dist/server/analyze-upload.js +123 -0
  278. package/dist/server/analyze-worker.d.ts +17 -1
  279. package/dist/server/api.d.ts +17 -0
  280. package/dist/server/api.js +132 -168
  281. package/dist/server/git-clone.d.ts +43 -2
  282. package/dist/server/git-clone.js +211 -16
  283. package/dist/server/mcp-http.d.ts +5 -6
  284. package/dist/server/mcp-http.js +9 -82
  285. package/dist/server/middleware.d.ts +40 -0
  286. package/dist/server/middleware.js +92 -0
  287. package/dist/server/private-ip.d.ts +1 -0
  288. package/dist/server/private-ip.js +15 -0
  289. package/dist/server/upload-ingest.d.ts +56 -0
  290. package/dist/server/upload-ingest.js +276 -0
  291. package/dist/server/upload-paths.d.ts +37 -0
  292. package/dist/server/upload-paths.js +57 -0
  293. package/dist/server/upload-sweep.d.ts +20 -0
  294. package/dist/server/upload-sweep.js +57 -0
  295. package/dist/storage/git.js +5 -8
  296. package/dist/storage/parse-cache.d.ts +22 -1
  297. package/dist/storage/parse-cache.js +41 -10
  298. package/dist/storage/repo-manager.d.ts +121 -10
  299. package/dist/storage/repo-manager.js +43 -5
  300. package/dist/types/pipeline.d.ts +9 -0
  301. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +110 -10
  302. package/hooks/claude/gitnexus-hook.cjs +110 -13
  303. package/hooks/claude/hook-db-lock-probe.cjs +513 -26
  304. package/package.json +8 -2
  305. package/scripts/assert-publish-grammar-coverage.cjs +31 -0
  306. package/scripts/build-tree-sitter-grammars.cjs +16 -10
  307. package/scripts/cross-platform-tests.ts +1 -0
  308. package/skills/gitnexus-debugging.md +16 -4
  309. package/skills/gitnexus-exploring.md +3 -3
  310. package/skills/gitnexus-guide.md +33 -0
  311. package/skills/gitnexus-pdg-query.md +89 -0
  312. package/skills/gitnexus-refactoring.md +1 -1
  313. package/skills/gitnexus-taint-analysis.md +178 -0
  314. package/vendor/tree-sitter-c/package.json +1 -1
  315. package/vendor/tree-sitter-dart/package.json +1 -1
  316. package/vendor/tree-sitter-kotlin/package.json +1 -1
  317. package/vendor/tree-sitter-proto/package.json +1 -1
  318. package/vendor/tree-sitter-swift/package.json +1 -1
  319. package/web/assets/{agent-CKPMqImC.js → agent-D3Xd7zZE.js} +123 -123
  320. package/web/assets/architecture-7EHR7CIX-6QZW5X65-DxJw65fT.js +1 -0
  321. package/web/assets/architectureDiagram-UL44E2DR-ChH9xW-N.js +36 -0
  322. package/web/assets/{blockDiagram-7IZFK4PR-B4niSLvR.js → blockDiagram-7IZFK4PR-BkWhpzWB.js} +1 -1
  323. package/web/assets/{c4Diagram-Y2BXMSZH-DIzEXalR.js → c4Diagram-Y2BXMSZH-DtBvpzWD.js} +1 -1
  324. package/web/assets/{chunk-3SSMPTDK-BzGeeheI.js → chunk-3SSMPTDK-B4_etUhr.js} +2 -2
  325. package/web/assets/{chunk-5IMINLNL-CnTLsXIV.js → chunk-5IMINLNL-DOdTeQri.js} +1 -1
  326. package/web/assets/{chunk-5VCL7Z4A-w1BCx98Z.js → chunk-5VCL7Z4A-CQ-2dYWj.js} +1 -1
  327. package/web/assets/chunk-6764PJDD-DpIS7hxD.js +1 -0
  328. package/web/assets/{chunk-67TQ5CYL-CChC2cAv.js → chunk-67TQ5CYL-4sNPJU1q.js} +3 -3
  329. package/web/assets/{chunk-7W6UQGC5-CgFyfZ6k.js → chunk-7W6UQGC5-KyEG0HQg.js} +1 -1
  330. package/web/assets/{chunk-AZZRMDJM-5E9ARdDQ.js → chunk-AZZRMDJM-y-xJBuMz.js} +1 -1
  331. package/web/assets/{chunk-INKRHTLW-ELpfuk9h.js → chunk-INKRHTLW-Nu4ri9P-.js} +1 -1
  332. package/web/assets/{chunk-JQRUD6KW-Dcq_Qnyy.js → chunk-JQRUD6KW-Dbva2Z17.js} +1 -1
  333. package/web/assets/{chunk-KGFNY3KK-W-VR57xb.js → chunk-KGFNY3KK-JGMJvKCK.js} +1 -1
  334. package/web/assets/chunk-KGYTTC2M-CWC_c3H9.js +161 -0
  335. package/web/assets/{chunk-KNLZD3CH-BuZBOcso.js → chunk-KNLZD3CH-BGlP6X9R.js} +1 -1
  336. package/web/assets/{chunk-KRXBNO2N-DH2NPM7I.js → chunk-KRXBNO2N-BW5o0KUN.js} +1 -1
  337. package/web/assets/{chunk-LCXTWHL2-Dfmux4m1.js → chunk-LCXTWHL2-zoomRtWX.js} +1 -1
  338. package/web/assets/{chunk-LII3EMHJ-3X33tCpU.js → chunk-LII3EMHJ-D-jm-dLa.js} +1 -1
  339. package/web/assets/{chunk-QA3QBVWF-D5Ha3GgB.js → chunk-QA3QBVWF-BgRW3SXd.js} +1 -1
  340. package/web/assets/{chunk-RG4AUYOV-CAkzcoRj.js → chunk-RG4AUYOV-DD0B2N2B.js} +1 -1
  341. package/web/assets/{chunk-T2UQINTJ-kGYG7ppP.js → chunk-T2UQINTJ-DSBqkzH7.js} +1 -1
  342. package/web/assets/chunk-T5OCTHI4-DdZvN-9i.js +1 -0
  343. package/web/assets/{chunk-UY5QBCOK-Du-c56gs.js → chunk-UY5QBCOK-DyF0vyGd.js} +1 -1
  344. package/web/assets/{chunk-W44A43WB-lrCpuupA.js → chunk-W44A43WB-JqMLIpR0.js} +1 -1
  345. package/web/assets/{chunk-ZXARS5L4-CSZRiyOf.js → chunk-ZXARS5L4-CxN8oiwI.js} +1 -1
  346. package/web/assets/classDiagram-KGZ6W3CR-DJm9BgF-.js +1 -0
  347. package/web/assets/classDiagram-v2-72OJOZXJ-DJm9BgF-.js +1 -0
  348. package/web/assets/{context-builder-_HS0v2ma.js → context-builder-BHiFUA8O.js} +10 -2
  349. package/web/assets/{cose-bilkent-UX7MHV2Q-BHv2bbtl.js → cose-bilkent-UX7MHV2Q-D71wNYRJ.js} +1 -1
  350. package/web/assets/dagre-ND4H6XIP-BIkxmhcu.js +4 -0
  351. package/web/assets/diagram-3NCE3AQN-BTa6iH0T.js +43 -0
  352. package/web/assets/diagram-GF46GFSD-DZDV0vBj.js +24 -0
  353. package/web/assets/{diagram-HNR7UZ2L-fUWreZzx.js → diagram-HNR7UZ2L-DFCfd5LI.js} +2 -2
  354. package/web/assets/diagram-QXG6HAR7-BzM--Jj0.js +24 -0
  355. package/web/assets/diagram-WEQXMOUZ-DFFwz0k4.js +10 -0
  356. package/web/assets/{erDiagram-L5TCEMPS-QN2eEP1e.js → erDiagram-L5TCEMPS-CfHMJJqo.js} +1 -1
  357. package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BQm9QzEa.js +1 -0
  358. package/web/assets/{flowDiagram-H6V6AXG4-CXbXImlN.js → flowDiagram-H6V6AXG4-Be805QiJ.js} +3 -3
  359. package/web/assets/{ganttDiagram-JCBTUEKG-BHQssTz6.js → ganttDiagram-JCBTUEKG-D3iL2Aet.js} +1 -1
  360. package/web/assets/gitGraph-WXDBUCRP-R675I2BI-Cr_Bm2Nb.js +1 -0
  361. package/web/assets/gitGraphDiagram-S2ZK5IYY-CPIDgxGm.js +106 -0
  362. package/web/assets/index-BvYev1UJ.js +635 -0
  363. package/web/assets/index-_lgn7hs5.css +2 -0
  364. package/web/assets/info-J43DQDTF-KCYPFFUO-Cs4p2oyk.js +1 -0
  365. package/web/assets/infoDiagram-3YFTVSEB-Y7BKSoke.js +2 -0
  366. package/web/assets/{ishikawaDiagram-BNXS4ZKH-6LKOvBfp.js → ishikawaDiagram-BNXS4ZKH-CT4SHFuI.js} +1 -1
  367. package/web/assets/{journeyDiagram-M6C3CM3L-DNPYCpvq.js → journeyDiagram-M6C3CM3L-CvArszLo.js} +1 -1
  368. package/web/assets/{kanban-definition-75IXJCU3-DgDi9oJT.js → kanban-definition-75IXJCU3-C6yDxr7N.js} +3 -3
  369. package/web/assets/{katex-K3KEBU37-CrVLz8l7.js → katex-K3KEBU37-CTc5BslQ.js} +1 -1
  370. package/web/assets/{mindmap-definition-2TDM6QVE-CwR5sBB-.js → mindmap-definition-2TDM6QVE-BOc2WEne.js} +1 -1
  371. package/web/assets/packet-YPE3B663-LP52Z2RK-CKCrztD2.js +1 -0
  372. package/web/assets/pie-LRSECV5Y-TCRJHUBD-BB7pHqoD.js +1 -0
  373. package/web/assets/{pieDiagram-CU6KROY3-By8g6f6B.js → pieDiagram-CU6KROY3-Cvc0f_Pd.js} +1 -1
  374. package/web/assets/{quadrantDiagram-VICAPDV7-bAm6Hi_5.js → quadrantDiagram-VICAPDV7-yXSZ2lLz.js} +1 -1
  375. package/web/assets/radar-GUYGQ44K-RDLRG3WG-DhoTOcuK.js +1 -0
  376. package/web/assets/{requirementDiagram-JXO7QTGE-7oDcJ1_J.js → requirementDiagram-JXO7QTGE-BRcZvHs4.js} +1 -1
  377. package/web/assets/{sankeyDiagram-URQDO5SZ-LFJz53u5.js → sankeyDiagram-URQDO5SZ-1qEjlFIB.js} +1 -1
  378. package/web/assets/{sequenceDiagram-VS2MUI6T-FySeKCUy.js → sequenceDiagram-VS2MUI6T-PcSCuvrB.js} +3 -3
  379. package/web/assets/stateDiagram-7D4R322I-D6qYSV1E.js +1 -0
  380. package/web/assets/stateDiagram-v2-36443NZ5-Bg-ImOSX.js +1 -0
  381. package/web/assets/{timeline-definition-O6YCAMPW-Ds2CnVZK.js → timeline-definition-O6YCAMPW-QWkh01xw.js} +1 -1
  382. package/web/assets/treeView-BLDUP644-QA4HXRO3-ODehyGKL.js +1 -0
  383. package/web/assets/treemap-LRROVOQU-LLAWBHMP-BfpgC-7o.js +1 -0
  384. package/web/assets/{vennDiagram-MWXL3ELB-DUIEwXWp.js → vennDiagram-MWXL3ELB-ak4DfwuM.js} +3 -3
  385. package/web/assets/wardley-L42UT6IY-5TKZOOLJ-CNeL6VHE.js +1 -0
  386. package/web/assets/{wardleyDiagram-CUQ6CDDI-DEiFPQih.js → wardleyDiagram-CUQ6CDDI-CTzR4Loq.js} +1 -1
  387. package/web/assets/{xychartDiagram-N2JHSOCM-BUbayhST.js → xychartDiagram-N2JHSOCM-DA1BEaQz.js} +1 -1
  388. package/web/index.html +13 -15
  389. package/scripts/materialize-vendor-grammars.cjs +0 -97
  390. package/web/assets/architecture-7EHR7CIX-6QZW5X65-BETj2x4g.js +0 -1
  391. package/web/assets/architectureDiagram-UL44E2DR-D1EXI0zA.js +0 -36
  392. package/web/assets/chunk-2T2R6R2M-n6s9JZqv.js +0 -4
  393. package/web/assets/chunk-2UTLFMKG-CMBB1TMN.js +0 -1
  394. package/web/assets/chunk-4R4BOZG6-mKhL59ul.js +0 -159
  395. package/web/assets/chunk-6764PJDD-ChwMM2z4.js +0 -1
  396. package/web/assets/chunk-7J6CGLKN-DT-b53FT.js +0 -10
  397. package/web/assets/chunk-C62D2QBJ-CDAWj26E.js +0 -1
  398. package/web/assets/chunk-CEXFNPSA-D68Tk6ls.js +0 -1
  399. package/web/assets/chunk-CilyBKbf.js +0 -1
  400. package/web/assets/chunk-J5EP6P6S-DxWW0yvu.js +0 -1
  401. package/web/assets/chunk-KGYTTC2M-C6PHeuay.js +0 -1
  402. package/web/assets/chunk-RERM46MO-DQNbXtfw.js +0 -1
  403. package/web/assets/chunk-RKZBBQEN-BmTPLSyv.js +0 -1
  404. package/web/assets/chunk-RLI5ZMPA-DExu2DOK.js +0 -1
  405. package/web/assets/chunk-T5OCTHI4--9wWpVws.js +0 -1
  406. package/web/assets/chunk-UP6H54XL-DsKdC6jC.js +0 -1
  407. package/web/assets/chunk-UXSXWOXI-DR81EqLr.js +0 -1
  408. package/web/assets/classDiagram-KGZ6W3CR-Bv93af_b.js +0 -1
  409. package/web/assets/classDiagram-v2-72OJOZXJ-CLfEqHUa.js +0 -1
  410. package/web/assets/dagre-ND4H6XIP-DsIr9MOv.js +0 -4
  411. package/web/assets/diagram-3NCE3AQN-BqAtKUpW.js +0 -43
  412. package/web/assets/diagram-GF46GFSD-CaBG6n6o.js +0 -24
  413. package/web/assets/diagram-QXG6HAR7-CZ-O3rcV.js +0 -24
  414. package/web/assets/diagram-WEQXMOUZ-P4lSL4GH.js +0 -10
  415. package/web/assets/eventmodeling-FCH6USID-MREXMVOE-B3myHUst.js +0 -1
  416. package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DF3rtf4A.js +0 -1
  417. package/web/assets/gitGraphDiagram-S2ZK5IYY-CCwVLT__.js +0 -106
  418. package/web/assets/index-BKWA-m7o.css +0 -2
  419. package/web/assets/index-CG6q8eTs.js +0 -626
  420. package/web/assets/info-J43DQDTF-KCYPFFUO-R4XV_akJ.js +0 -1
  421. package/web/assets/infoDiagram-3YFTVSEB-DmLICZx1.js +0 -2
  422. package/web/assets/packet-YPE3B663-LP52Z2RK-CgQPdNZa.js +0 -1
  423. package/web/assets/pie-LRSECV5Y-TCRJHUBD-BzWOwc_c.js +0 -1
  424. package/web/assets/radar-GUYGQ44K-RDLRG3WG-B7KG0FN7.js +0 -1
  425. package/web/assets/stateDiagram-7D4R322I-CB2nABwH.js +0 -1
  426. package/web/assets/stateDiagram-v2-36443NZ5-COBGd2RL.js +0 -1
  427. package/web/assets/treeView-BLDUP644-QA4HXRO3-CRSxEwLO.js +0 -1
  428. package/web/assets/treemap-LRROVOQU-LLAWBHMP-a0PfKoVn.js +0 -1
  429. package/web/assets/wardley-L42UT6IY-5TKZOOLJ-BKQ6e5mC.js +0 -1
  430. /package/web/assets/{chunk-AQ6EADP3-CCFje6lL.js → chunk-AQ6EADP3-CZhslHi-.js} +0 -0
@@ -3,14 +3,57 @@
3
3
  * with a command line that looks like a GitNexus MCP/serve server?
4
4
  *
5
5
  * Backends (no user-installed Sysinternals):
6
- * - Linux: scan procfs under /proc (per-PID fd entries) via stat(2) (dev+inode); works without lsof;
7
- * optional lsof fallback when proc scan finds nothing.
6
+ * - Linux: cmdline-first procfs scan under /proc, no lsof at all (#2180). Three
7
+ * phases, cheapest first: (0) read /proc/<pid>/comm a tiny task->comm read
8
+ * that never touches the target's mm — and keep only PIDs whose comm is a
9
+ * plausible node/gitnexus server; (1) read up to GITNEXUS_HOOK_PROC_CMDLINE_MAX
10
+ * bytes of /proc/<pid>/cmdline via openSync+readSync (bounded, so a D-state
11
+ * holder stuck on mmap_lock or a giant argv can't wedge the hook) and prefilter
12
+ * with isGitNexusServerCommand; (2) only for the 0..N survivors, stat their
13
+ * /proc/<pid>/fd/* and compare dev+inode against the target lbug. The lbug
14
+ * handle is fd-visible (a @ladybugdb/core property), so this finds every real
15
+ * owner without scanning every fd of every process.
8
16
  * - macOS / *BSD / etc.: trusted lsof + ps (absolute paths first).
9
17
  * - Windows: Restart Manager (rstrtmgr) via bundled PowerShell script +
10
18
  * Win32_Process for command lines; trusted powershell.exe under %SystemRoot%.
11
19
  *
12
- * Fail-open on most errors; fail-closed only on lsof ETIMEDOUT (Unix) or
13
- * PowerShell ETIMEDOUT (Windows), matching the hook contract.
20
+ * Fail matrix:
21
+ * - Linux proc scan: owner found -> fail-closed (skip augment); budget exhausted
22
+ * (GITNEXUS_HOOK_LINUX_PROC_BUDGET_MS) -> fail-CLOSED (#2180). This is a
23
+ * deliberate change from the old "timeout -> fail-open then try lsof" path.
24
+ * End-to-end the busy-host outcome is unchanged: the old code's lsof fallback
25
+ * ETIMEDOUT'd on the very hosts where the scan ran out of budget and ALSO
26
+ * failed closed there — the lsof leg only ever added 1-2s of dead work plus
27
+ * the orphan-storm risk it caused (#2163). What changes is that an overloaded
28
+ * host now self-throttles immediately (the throttle the incident needed)
29
+ * instead of paying for a doomed lsof. Mid-load hosts that used to fall
30
+ * through to a successful lsof now answer from the scan directly (faster) or,
31
+ * if even the scan can't finish in budget, fail closed (self-throttle) — a
32
+ * bounded, documented tradeoff, never an orphan.
33
+ * - macOS / other Unix: fail-open on most errors; fail-closed only on lsof
34
+ * ETIMEDOUT, matching the hook contract.
35
+ * - Windows: fail-closed only on PowerShell ETIMEDOUT.
36
+ *
37
+ * Unix subprocess containment contract (#2163):
38
+ * - lsof/ps are wrapped in coreutils `timeout`/`gtimeout` when a working
39
+ * wrapper is found (`timeout -k 1 <budget> lsof ...`). If this hook process
40
+ * is itself SIGKILLed (e.g. by the runner's 10s hook timeout) the wrapper
41
+ * survives, SIGTERMs its child at the budget (2s lsof / 1s ps) and SIGKILLs
42
+ * it 1s later — orphan lifetime is bounded at ~3s instead of unbounded.
43
+ * - GITNEXUS_HOOK_TIMEOUT_PATH: the sentinel value `disabled` switches the
44
+ * wrapper off deterministically; any other value is adopted only when it
45
+ * exists AND passes a one-shot `-k` exit-propagation self-test — otherwise
46
+ * resolution FALLS THROUGH to the built-in candidate list (first self-test
47
+ * pass wins), so no malformed value of any shape can silently disable
48
+ * orphan containment.
49
+ * - The gitnexus server is lazy-open + sticky-hold: an idle MCP server holds
50
+ * ZERO lbug fds until the repo's first MCP query, then keeps the fd open.
51
+ * A probe before that first query is therefore always false — a known,
52
+ * pre-existing race, not a bug in this probe.
53
+ * - resolveUnixGuardTimeout is exported so the hook adapters can wrap the
54
+ * `gitnexus augment` CLI child — the longest-lived hook subprocess (7s
55
+ * local / 12s npx inner budgets) — in the same guard; see runGitNexusCli
56
+ * in the adapters (#2163 follow-up).
14
57
  */
15
58
 
16
59
  const fs = require('fs');
@@ -25,6 +68,16 @@ function isGitNexusServerCommand(command) {
25
68
  return hasServerMode && hasGitNexus;
26
69
  }
27
70
 
71
+ // GITNEXUS_DEBUG-gated stderr diagnostics. Reuses the exact gating predicate the
72
+ // Windows ps1-load warning already uses (===' 1' / ==='true') so there is one
73
+ // debug convention in this file, and writes via process.stderr.write (NOT a
74
+ // spawn) so it never perturbs the windowsHide spawn-count invariant.
75
+ function debugLog(msg) {
76
+ if (process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true') {
77
+ process.stderr.write(`[GitNexus hook] ${msg}\n`);
78
+ }
79
+ }
80
+
28
81
  function resolveHookBinary(tool) {
29
82
  const envKey = tool === 'lsof' ? 'GITNEXUS_HOOK_LSOF_PATH' : 'GITNEXUS_HOOK_PS_PATH';
30
83
  const fromEnv = process.env[envKey];
@@ -46,6 +99,106 @@ function resolveHookBinary(tool) {
46
99
  return tool;
47
100
  }
48
101
 
102
+ function hasMissingHookBinaryOverride(tool) {
103
+ const envKey = tool === 'lsof' ? 'GITNEXUS_HOOK_LSOF_PATH' : 'GITNEXUS_HOOK_PS_PATH';
104
+ const fromEnv = process.env[envKey];
105
+ if (!fromEnv || !String(fromEnv).trim()) return false;
106
+ try {
107
+ return !fs.existsSync(String(fromEnv).trim());
108
+ } catch {
109
+ return true;
110
+ }
111
+ }
112
+
113
+ // Sentinel:
114
+ // undefined = not resolved yet (resolve lazily, on first lsof/ps fallback)
115
+ // string = self-tested coreutils timeout/gtimeout path (use as wrapper)
116
+ // null = no usable wrapper (disabled, none found, or self-test failed)
117
+ let unixGuardTimeoutCache;
118
+
119
+ /**
120
+ * Resolve a coreutils `timeout`/`gtimeout` binary to wrap lsof/ps with
121
+ * (#2163). Unix-only by contract: the probe's win32 dispatch returns before
122
+ * reaching it, and the exported callers (the adapters' runGitNexusCli,
123
+ * #2163 follow-up) must check the platform first — the self-test below
124
+ * spawns /bin/sh. The memoized result is module-wide, so probe and adapter
125
+ * share one lazy self-test per hook process.
126
+ *
127
+ * GITNEXUS_HOOK_TIMEOUT_PATH semantics: the sentinel `disabled` turns the
128
+ * wrapper off; any other value is only a CANDIDATE — an existing file path
129
+ * is tried first, but it must pass the `-k` exit-propagation self-test to
130
+ * be adopted. On any failure (non-existent path, directory, non-executable
131
+ * file, wrapper without `-k` support, always-exit-0 stub, …) resolution
132
+ * falls through to the built-in candidates below, tried in order, first
133
+ * self-test pass wins. This is strictly stronger than the sibling
134
+ * GITNEXUS_HOOK_LSOF_PATH / GITNEXUS_HOOK_PS_PATH overrides (which only
135
+ * check existence): no bad env value of ANY shape can silently disable
136
+ * orphan containment.
137
+ *
138
+ * Lazy self-test: candidates are probed only when the lsof/ps fallback is
139
+ * first reached, and the result is memoized. A candidate is adopted only
140
+ * when `timeout -k 1 1 /bin/sh -c 'exit 42'` exits 42 — i.e. it must RUN
141
+ * the wrapped command AND PROPAGATE its exit status. This rejects two
142
+ * failure shapes: wrappers without the coreutils `-k` flag — busybox <1.34,
143
+ * toybox, broken symlinks — which would exit with a usage error without
144
+ * ever running lsof, silently converting the lsof-ETIMEDOUT fail-closed
145
+ * contract into fail-open (#1492 regression); and always-exit-0 stubs
146
+ * (/bin/true shapes), which would otherwise be adopted and "succeed" every
147
+ * wrapped spawn instantly without running it — a constant no-owner probe
148
+ * answer and, worse, a silently dead augment (status 0, empty stderr passes
149
+ * the adapters' success check with no context; #2163 follow-up review).
150
+ * Only when EVERY candidate fails does the probe fall back to the unwrapped
151
+ * status quo (memoized null). busybox ≥1.34 passes the test and is fully
152
+ * usable for everything THIS file spawns (lsof/ps are the guard's direct
153
+ * children) and for the adapters' direct-exec arm. The adapters' npx arm
154
+ * additionally relies on coreutils' process-GROUP signalling for its
155
+ * `-s KILL` grandchild reaping; busybox signals only its direct child, and
156
+ * this self-test deliberately does not probe that capability — see the
157
+ * adapter docblocks for the residual-gap statement.
158
+ */
159
+ function passesGuardSelfTest(guard) {
160
+ try {
161
+ const selfTest = spawnSync(guard, ['-k', '1', '1', '/bin/sh', '-c', 'exit 42'], {
162
+ encoding: 'utf-8',
163
+ timeout: 3000,
164
+ stdio: ['ignore', 'ignore', 'ignore'],
165
+ windowsHide: true,
166
+ });
167
+ return !selfTest.error && selfTest.status === 42;
168
+ } catch {
169
+ return false;
170
+ }
171
+ }
172
+
173
+ function resolveUnixGuardTimeout() {
174
+ if (unixGuardTimeoutCache !== undefined) return unixGuardTimeoutCache;
175
+ unixGuardTimeoutCache = null;
176
+ const fromEnv = process.env.GITNEXUS_HOOK_TIMEOUT_PATH;
177
+ const trimmed = fromEnv ? String(fromEnv).trim() : '';
178
+ if (trimmed === 'disabled') return unixGuardTimeoutCache;
179
+ const candidates = [];
180
+ if (trimmed && fs.existsSync(trimmed)) candidates.push(trimmed);
181
+ for (const builtin of [
182
+ '/usr/bin/timeout',
183
+ '/bin/timeout',
184
+ '/opt/homebrew/bin/gtimeout',
185
+ '/usr/local/bin/gtimeout',
186
+ ]) {
187
+ try {
188
+ if (fs.existsSync(builtin)) candidates.push(builtin);
189
+ } catch {
190
+ /* ignore */
191
+ }
192
+ }
193
+ for (const candidate of candidates) {
194
+ if (passesGuardSelfTest(candidate)) {
195
+ unixGuardTimeoutCache = candidate;
196
+ break;
197
+ }
198
+ }
199
+ return unixGuardTimeoutCache;
200
+ }
201
+
49
202
  function resolveWindowsPowerShellPath() {
50
203
  const fromEnv = process.env.GITNEXUS_HOOK_POWERSHELL_PATH;
51
204
  if (fromEnv && String(fromEnv).trim() && fs.existsSync(String(fromEnv).trim())) {
@@ -132,76 +285,374 @@ function hasGitNexusServerOwnerWindows(dbPathAbs, myPid) {
132
285
  return false;
133
286
  }
134
287
 
135
- function readLinuxCmdline(pidStr) {
288
+ // The procfs root every Linux scan path reads from. Production is always /proc;
289
+ // GITNEXUS_HOOK_PROC_ROOT only exists so unit tests can inject a fixture tree
290
+ // (comm + cmdline + fd symlinks) and assert the three-phase logic without
291
+ // scanning the real, ~hundreds-of-process /proc of the test host.
292
+ //
293
+ // Test-only gate (F4): the override is honored ONLY under a test runner —
294
+ // vitest injects VITEST="true" and NODE_ENV="test" into every worker (verified;
295
+ // a production hook is `node <file>.cjs` with neither set). Without the gate, a
296
+ // production env that accidentally leaked GITNEXUS_HOOK_PROC_ROOT (pointing at an
297
+ // empty/bad tree) would make readdirSync find no pids -> 'not-owned' -> Linux
298
+ // owner detection silently OFF (fail-OPEN: augment races the real server for the
299
+ // lbug, the #1492 class). Gating to the test signal makes that leak inert in
300
+ // production (always /proc) while the fake-procfs unit tests, which run under
301
+ // vitest, still inject freely. Unset env (or non-test context) => /proc, so the
302
+ // production path is byte-for-byte the historical behavior.
303
+ function isTestContext() {
304
+ return (
305
+ process.env.VITEST === 'true' || process.env.VITEST === '1' || process.env.NODE_ENV === 'test'
306
+ );
307
+ }
308
+ function getProcRoot() {
309
+ if (!isTestContext()) return '/proc';
310
+ const raw = process.env.GITNEXUS_HOOK_PROC_ROOT;
311
+ return raw && String(raw).trim() ? String(raw) : '/proc';
312
+ }
313
+
314
+ // Max bytes read from /proc/<pid>/cmdline in Phase 1. Bounded by default so a
315
+ // D-state holder wedged on mmap_lock, or a process with a pathological multi-MB
316
+ // argv, can't stall the hook. 16 KiB comfortably clears a realistic
317
+ // `node <abs path to .../node_modules/gitnexus/dist/cli/index.js> mcp` line
318
+ // (the `mcp`/`serve` mode token lives at the very tail, so the cap must be large
319
+ // enough to reach it — see PROC_CMDLINE_FLOOR escalation below). Overridable for
320
+ // tests; never goes below PROC_CMDLINE_FLOOR.
321
+ const PROC_CMDLINE_FLOOR = 4096;
322
+ function getCmdlineMaxBytes() {
323
+ const raw = process.env.GITNEXUS_HOOK_PROC_CMDLINE_MAX;
324
+ // Number() (not parseInt) so "8e3" reads as 8000, not 8 (parseInt stops at
325
+ // 'e'). The `raw && String(raw).trim()` guard keeps empty/whitespace on the
326
+ // default; trailing garbage ("8abc") now -> NaN -> default (stricter).
327
+ const n = raw && String(raw).trim() ? Number(String(raw).trim()) : NaN;
328
+ if (Number.isFinite(n) && n >= PROC_CMDLINE_FLOOR) return n;
329
+ return 16384;
330
+ }
331
+
332
+ // Phase 0 comm prefilter. /proc/<pid>/comm is the kernel task->comm string,
333
+ // capped at 16 bytes INCLUDING the trailing NUL — i.e. at most 15 visible
334
+ // chars, truncated by the kernel with no marker. So a process whose real name
335
+ // is longer than 15 chars shows a 15-char prefix here. The match below is
336
+ // therefore truncation-safe in BOTH directions (a whitelist name that is a
337
+ // prefix of comm, or comm that is a prefix of a whitelist name, both count) to
338
+ // guarantee we never drop a real owner at this cheap stage — Phase 2's dev+ino
339
+ // fd check is the real authority; Phase 0/1 only exist to skip the overwhelming
340
+ // majority (kernel threads, shells, editors) cheaply.
341
+ //
342
+ // The whitelist is calibrated against what a real `gitnexus mcp`/`serve` server
343
+ // actually reports for comm. Observed on production hosts: the server renames
344
+ // its main thread, so comm reads `MainThread` (via @ladybugdb/core's
345
+ // worker_threads setup), NOT `node` — omitting it would blind the probe to
346
+ // every real server (#1492-class owner miss). We also keep the plausible
347
+ // launcher/runtime basenames in case a future build does not rename the thread.
348
+ // Conservative by design: over-collecting a few extra candidates only costs a
349
+ // bounded number of Phase 1 cmdline reads.
350
+ const COMM_CANDIDATES = ['node', 'gitnexus', 'bun', 'deno', 'npm', 'npx', 'MainThread'];
351
+ function commLooksLikeServer(comm) {
352
+ const c = comm.trim();
353
+ if (!c) return false;
354
+ for (const name of COMM_CANDIDATES) {
355
+ if (name === c || name.startsWith(c) || c.startsWith(name)) return true;
356
+ }
357
+ return false;
358
+ }
359
+
360
+ function readProcComm(procRoot, pidStr) {
136
361
  try {
137
- return fs.readFileSync(`/proc/${pidStr}/cmdline`, 'utf8').replace(/\0+/g, ' ').trim();
362
+ return fs
363
+ .readFileSync(path.join(procRoot, pidStr, 'comm'), 'utf8')
364
+ .replace(/\0+/g, '')
365
+ .trim();
138
366
  } catch {
139
367
  return '';
140
368
  }
141
369
  }
142
370
 
143
- function linuxProcScanFindGitNexusServer(dbPathAbs, myPid) {
371
+ // Timeout sentinel for readLinuxCmdline (F3). MUST be distinct from the
372
+ // "unreadable/empty" return value (''): '' flows through isGitNexusServerCommand
373
+ // as a NON-candidate (both regexes are false on ''), so the Phase 1 caller
374
+ // `continue`s past it — correct for a raced/openSync-failed pid, but a FAIL-OPEN
375
+ // bug if it ever meant "I ran out of budget mid-read" (a real owner whose
376
+ // escalation timed out would be silently dropped, racing the lbug -> #1492). A
377
+ // unique Symbol can never collide with any cmdline string, so the caller can
378
+ // branch on it explicitly and map a mid-read timeout to the tri-state 'timeout'
379
+ // (fail-CLOSED) instead of swallowing it as a non-candidate.
380
+ const CMDLINE_TIMEOUT = Symbol('gitnexus.cmdline.timeout');
381
+
382
+ // Bounded /proc/<pid>/cmdline read for Phase 1. openSync+readSync (not
383
+ // readFileSync) so a D-state holder cannot stall the hook on a huge or
384
+ // never-EOF argv: we read at most `cap` bytes and stop. cmdline separates argv
385
+ // with NULs; convert to spaces for isGitNexusServerCommand.
386
+ //
387
+ // Owner-miss guard for the 4 KB cap: the `gitnexus` token usually sits in the
388
+ // first path component while the `mcp`/`serve` mode token is the LAST argv, so
389
+ // a naive 4 KB read could clip the mode token off a server launched with a very
390
+ // long interpreter path and silently miss a real owner. We mitigate two ways:
391
+ // (a) the default cap (16 KiB) already clears realistic lines; (b) if the first
392
+ // read fills the cap AND already contains the `gitnexus` token but no mode
393
+ // token yet, we keep reading in bounded chunks (up to a hard ceiling) until the
394
+ // mode token appears or the file ends — so a genuine server is never missed for
395
+ // want of a few more bytes, while non-candidates still pay only the initial
396
+ // bounded read.
397
+ //
398
+ // Budget (F3): the escalation loop above is the one place a SINGLE pathological
399
+ // candidate could read up to HARD_CEIL (256 KiB) before the next scan-level
400
+ // budget check, weakening the timeout contract. `outOfBudget` (the scan's shared
401
+ // deadline callback) is checked once per escalation iteration; on expiry we
402
+ // return CMDLINE_TIMEOUT (NOT '') so the caller can fail-closed honestly rather
403
+ // than mistake the partial read for a non-candidate. Reads that simply can't
404
+ // open / error out still return '' (genuinely "not a readable candidate").
405
+ function readLinuxCmdline(procRoot, pidStr, cap, outOfBudget) {
406
+ const file = path.join(procRoot, pidStr, 'cmdline');
407
+ let fd;
408
+ try {
409
+ fd = fs.openSync(file, 'r');
410
+ } catch {
411
+ return '';
412
+ }
413
+ try {
414
+ const HARD_CEIL = 262144; // 256 KiB absolute ceiling for the escalation path
415
+ let collected = Buffer.alloc(0);
416
+ let offset = 0;
417
+ let chunkCap = cap;
418
+ for (;;) {
419
+ // allocUnsafe is safe here: readSync fills exactly [0, bytes), only
420
+ // buf.subarray(0, bytes) is consumed, and Buffer.concat deep-copies that
421
+ // slice into `collected`, so the uninitialized tail never reaches decode.
422
+ const buf = Buffer.allocUnsafe(chunkCap);
423
+ const bytes = fs.readSync(fd, buf, 0, chunkCap, offset);
424
+ if (bytes <= 0) break;
425
+ collected = Buffer.concat([collected, buf.subarray(0, bytes)]);
426
+ offset += bytes;
427
+ const text = collected.toString('utf8').replace(/\0+/g, ' ');
428
+ // Stop early when we can already decide "owner": has both the gitnexus
429
+ // token and a mode token. Keep going only when gitnexus is present but
430
+ // the mode token might be just past the boundary.
431
+ const hasGitNexus =
432
+ /(?:^|[/\\\s])gitnexus(?:\.cmd)?(?:\s|$)/.test(text) ||
433
+ /node_modules[/\\]gitnexus[/\\]/.test(text);
434
+ const hasMode = /(?:^|\s)(mcp|serve)(?:\s|$)/.test(text);
435
+ if (hasMode) break; // decided (positive); isGitNexusServerCommand re-checks below
436
+ if (bytes < chunkCap) break; // EOF: full cmdline read, definitive
437
+ if (!hasGitNexus) break; // not a candidate; do not escalate the read
438
+ if (offset >= HARD_CEIL) break; // bounded escalation only
439
+ // Budget gate the escalation: a single huge-argv candidate must not burn
440
+ // the whole scan deadline before we re-check. Return the timeout sentinel
441
+ // (never '') so the caller fails closed instead of treating us as a
442
+ // non-candidate. The sole caller (linuxProcScanFindGitNexusServer) always
443
+ // passes outOfBudget, so no presence guard is needed.
444
+ if (outOfBudget()) return CMDLINE_TIMEOUT;
445
+ chunkCap = cap; // keep reading more in cap-sized chunks
446
+ }
447
+ return collected.toString('utf8').replace(/\0+/g, ' ').trim();
448
+ } catch {
449
+ return '';
450
+ } finally {
451
+ try {
452
+ fs.closeSync(fd);
453
+ } catch {
454
+ /* ignore */
455
+ }
456
+ }
457
+ }
458
+
459
+ function resolveLinuxProcBudgetMs() {
144
460
  const raw = process.env.GITNEXUS_HOOK_LINUX_PROC_BUDGET_MS;
145
- const budget = Number(raw && String(raw).trim()) ? Number.parseInt(String(raw), 10) : 1200;
461
+ // Gate on the STRING's emptiness, NOT the parsed number's truthiness — the
462
+ // old `Number(raw && trim()) ? ... : 1200` form treated "0" as falsy and
463
+ // silently fell back to 1200 (#2180). Use Number() (not parseInt) so "16e3"
464
+ // reads as 16000, not 16 (parseInt stops at 'e'). The `&& String(raw).trim()`
465
+ // guard is load-bearing: without it a set-but-empty/whitespace value would be
466
+ // `Number("")===0` => budget 0 => immediate fail-CLOSED timeout (augment
467
+ // permanently skipped). With it, ''/whitespace => NaN => 1200 default, while a
468
+ // finite "0" still parses to an explicit, deterministic "no budget" =>
469
+ // immediate timeout. Non-numeric / unset => default 1200.
470
+ const n = raw != null && String(raw).trim() ? Number(String(raw).trim()) : NaN;
471
+ if (!Number.isFinite(n)) return 1200;
472
+ return n; // may be <= 0, meaning "out of budget on the first check"
473
+ }
474
+
475
+ // Returns one of: 'owned' (a non-self process with a GitNexus-server cmdline
476
+ // holds the target lbug fd), 'not-owned' (scan completed, no such owner), or
477
+ // 'timeout' (the per-scan budget was exhausted before a verdict). The name is
478
+ // pinned by a source-contract test; only the return TYPE changed (#2180:
479
+ // boolean -> tri-state, so the dispatcher can fail-closed on 'timeout').
480
+ function linuxProcScanFindGitNexusServer(dbPathAbs, myPid) {
481
+ const budget = resolveLinuxProcBudgetMs();
482
+ // A non-positive budget is an explicit, deterministic "no time to scan" =>
483
+ // immediate timeout (the #2180 test vector, and the only correct reading of
484
+ // the fixed parse: "0" must NOT mean 1200). Returning before any procfs read
485
+ // keeps it instantaneous regardless of host load.
486
+ if (budget <= 0) return 'timeout';
487
+ const procRoot = getProcRoot();
488
+ const cmdlineCap = getCmdlineMaxBytes();
146
489
  const start = Date.now();
490
+ const outOfBudget = () => Date.now() - start > budget;
491
+
147
492
  let targetStat;
148
493
  try {
149
494
  targetStat = fs.statSync(dbPathAbs);
150
495
  } catch {
151
- return false;
496
+ // Caller already existsSync'd the path; a stat failure here is a transient
497
+ // race, treat as no owner (historical semantics).
498
+ return 'not-owned';
152
499
  }
500
+
153
501
  let procEntries;
154
502
  try {
155
- procEntries = fs.readdirSync('/proc', { withFileTypes: true });
503
+ procEntries = fs.readdirSync(procRoot, { withFileTypes: true });
156
504
  } catch {
157
- return false;
505
+ return 'not-owned';
158
506
  }
507
+
508
+ // Phase 0 + Phase 1: collect the few PIDs whose comm AND cmdline look like a
509
+ // GitNexus server, without touching any fd yet.
510
+ const candidates = [];
159
511
  for (const ent of procEntries) {
160
- if (Date.now() - start > budget) return false;
512
+ if (outOfBudget()) return 'timeout';
161
513
  if (!ent.isDirectory() || !/^\d+$/.test(ent.name)) continue;
162
514
  const pid = Number.parseInt(ent.name, 10);
163
515
  if (!Number.isFinite(pid) || pid === myPid) continue;
164
- const fdDir = path.join('/proc', ent.name, 'fd');
516
+
517
+ // Phase 0: cheap comm prefilter.
518
+ const comm = readProcComm(procRoot, ent.name);
519
+ if (!comm) continue; // unreadable comm (kernel thread, raced exit) -> skip
520
+ if (!commLooksLikeServer(comm)) continue;
521
+
522
+ // Phase 1: bounded cmdline read + isGitNexusServerCommand prefilter.
523
+ if (outOfBudget()) return 'timeout';
524
+ const cmdline = readLinuxCmdline(procRoot, ent.name, cmdlineCap, outOfBudget);
525
+ // F3: a mid-read budget timeout returns the CMDLINE_TIMEOUT sentinel (a
526
+ // Symbol, never a string). Fail CLOSED on it rather than letting it fall
527
+ // through isGitNexusServerCommand as a non-candidate — a real owner whose
528
+ // escalation timed out must not be silently dropped (would fail-OPEN).
529
+ if (cmdline === CMDLINE_TIMEOUT) return 'timeout';
530
+ if (!isGitNexusServerCommand(cmdline)) continue;
531
+ candidates.push(ent.name);
532
+ }
533
+
534
+ // Phase 2: only now stat the fds of the (typically 0-2) survivors.
535
+ for (const pidStr of candidates) {
536
+ if (outOfBudget()) return 'timeout';
537
+ const fdDir = path.join(procRoot, pidStr, 'fd');
165
538
  let fds;
166
539
  try {
167
540
  fds = fs.readdirSync(fdDir);
168
- } catch {
541
+ } catch (err) {
542
+ // F1: the old code returned 'owned' for EVERY non-ENOENT error. That was
543
+ // a correctness bug: /proc/<pid>/fd is owner-only (mode 0500), so a
544
+ // cross-user/root `gitnexus mcp` serving a DIFFERENT repo passes Phase 0+1
545
+ // (its cmdline matches) and then EACCES'es here — yet its dev+ino was
546
+ // NEVER compared against THIS lbug. Claiming 'owned' lets it permanently,
547
+ // silently suppress augment for a repo it does not actually lock. We now
548
+ // distinguish the failure shapes (all still fail-closed where we can't
549
+ // prove non-ownership, but 'timeout' is the HONEST verdict for
550
+ // "inconclusive", not the false-positive 'owned'):
551
+ const code = err && err.code;
552
+ if (code === 'ENOENT') {
553
+ // Process raced away between the candidate scan and now -> genuinely no
554
+ // longer an owner. Move on.
555
+ continue;
556
+ }
557
+ if (code === 'EACCES' || code === 'EPERM') {
558
+ // Permission-denied fd dir: cannot read fds, so ownership is
559
+ // UNVERIFIABLE. Fail closed honestly via 'timeout' (the dispatcher maps
560
+ // timeout -> true, same protective skip as before) WITHOUT lying that we
561
+ // confirmed ownership. Do NOT degrade to not-owned/fail-open: if this
562
+ // really is the owner, fail-open re-opens the #1492 lbug race; augment
563
+ // is optional context, so a conservative skip costs little.
564
+ debugLog(
565
+ `fd dir unreadable for candidate pid ${pidStr} (${code}); ownership ` +
566
+ `unverifiable, probe inconclusive -> fail-closed (timeout)`,
567
+ );
568
+ return 'timeout';
569
+ }
570
+ if (code === 'EIO' || code === 'ESTALE') {
571
+ // Genuine transient I/O against this candidate's fd dir — not evidence
572
+ // it does NOT hold the lbug. Treat as inconclusive and fail closed
573
+ // (timeout) rather than continue, so a real owner mid-I/O-blip is not
574
+ // dropped (would fail-open).
575
+ debugLog(
576
+ `fd dir transient I/O error for candidate pid ${pidStr} (${code}); ` +
577
+ `probe inconclusive -> fail-closed (timeout)`,
578
+ );
579
+ return 'timeout';
580
+ }
581
+ // Any other shape (ENOTDIR — fd path is not a directory at all, so this
582
+ // is not a plausible live-procfs owner — and the long tail) is treated as
583
+ // "this candidate is not an owner": move to the next candidate instead of
584
+ // the old blanket 'owned'. If no other candidate owns the lbug the scan
585
+ // ends not-owned (dispatcher fail-open) — acceptable because ENOTDIR means
586
+ // the fd entry is structurally not a real /proc/<pid>/fd.
587
+ debugLog(
588
+ `fd dir not a readable directory for candidate pid ${pidStr} ` +
589
+ `(${code || 'unknown'}); treating candidate as non-owner -> continue`,
590
+ );
169
591
  continue;
170
592
  }
171
- let holds = false;
172
593
  for (const fd of fds) {
173
- if (Date.now() - start > budget) return false;
594
+ if (outOfBudget()) return 'timeout';
174
595
  try {
175
596
  const st = fs.statSync(path.join(fdDir, fd));
176
597
  if (st.dev === targetStat.dev && st.ino === targetStat.ino) {
177
- holds = true;
178
- break;
598
+ return 'owned';
179
599
  }
180
600
  } catch {
181
- /* ignore */
601
+ /* fd raced closed; ignore */
182
602
  }
183
603
  }
184
- if (!holds) continue;
185
- if (isGitNexusServerCommand(readLinuxCmdline(ent.name))) return true;
186
604
  }
187
- return false;
605
+
606
+ return 'not-owned';
188
607
  }
189
608
 
190
609
  function unixLsofPsFindGitNexusServer(dbPathAbs, myPid) {
610
+ const guard = resolveUnixGuardTimeout();
611
+ // An explicit missing override models ENOENT and must fail open instead of
612
+ // falling through to a host binary with different process-table visibility.
613
+ if (hasMissingHookBinaryOverride('lsof')) return false;
191
614
  const lsofPath = resolveHookBinary('lsof');
192
- const lsof = spawnSync(lsofPath, ['-nP', '-t', '--', dbPathAbs], {
615
+ // The spawnSync timeouts below (lsof 1000ms / ps 500ms) are deliberately
616
+ // SHORTER than the wrapper budgets (2s / 1s): on the supervised path Node's
617
+ // SIGTERM always fires first, so `error.code === 'ETIMEDOUT'` and the
618
+ // fail-closed contract are untouched. The wrapper only matters once this
619
+ // hook process has been SIGKILLed and can no longer deliver that SIGTERM.
620
+ const [lsofCmd, lsofArgs] = guard
621
+ ? [guard, ['-k', '1', '2', lsofPath, '-nP', '-t', '--', dbPathAbs]]
622
+ : [lsofPath, ['-nP', '-t', '--', dbPathAbs]];
623
+ const lsof = spawnSync(lsofCmd, lsofArgs, {
193
624
  encoding: 'utf-8',
194
625
  timeout: 1000,
195
626
  stdio: ['ignore', 'pipe', 'ignore'],
196
627
  windowsHide: true,
197
628
  });
198
629
  if (lsof.error) return lsof.error.code === 'ETIMEDOUT';
630
+ // Guard-mediated deaths map to "unresponsive holder" (fail-closed). Three
631
+ // result shapes, verified against coreutils 9.1:
632
+ // - signal-death: when `-k` escalates to SIGKILL, coreutils timeout
633
+ // SELF-RAISES the signal, so spawnSync reports {status: null, signal}
634
+ // with no .error (spawnSync's own ETIMEDOUT was handled above). The
635
+ // same shape appears when this hook is frozen >2s (SIGSTOP, laptop
636
+ // suspend) and the guard expires while it sleeps. By construction, a
637
+ // guard-wrapped probe that died by signal without spawnSync ETIMEDOUT
638
+ // is a budget/kill outcome.
639
+ // - 124: budget expired and the child exited after the plain SIGTERM.
640
+ // - 137: NOT the coreutils -k path — only exit-code-propagating wrappers,
641
+ // or a child SIGKILLed externally (e.g. the OOM killer).
642
+ if (guard && lsof.status === null && lsof.signal) return true;
643
+ if (guard && (lsof.status === 124 || lsof.status === 137)) return true;
199
644
 
200
645
  const pids = (lsof.stdout || '').split(/\s+/).filter(Boolean);
646
+ const psMissing = hasMissingHookBinaryOverride('ps');
201
647
  const psPath = resolveHookBinary('ps');
202
648
  for (const pid of pids) {
203
649
  if (Number(pid) === myPid) continue;
204
- const ps = spawnSync(psPath, ['-p', pid, '-o', 'command='], {
650
+ // Missing ps means we cannot verify that this pid is a GitNexus server.
651
+ if (psMissing) continue;
652
+ const [psCmd, psArgs] = guard
653
+ ? [guard, ['-k', '1', '1', psPath, '-p', pid, '-o', 'command=']]
654
+ : [psPath, ['-p', pid, '-o', 'command=']];
655
+ const ps = spawnSync(psCmd, psArgs, {
205
656
  encoding: 'utf-8',
206
657
  timeout: 500,
207
658
  stdio: ['ignore', 'pipe', 'ignore'],
@@ -211,6 +662,11 @@ function unixLsofPsFindGitNexusServer(dbPathAbs, myPid) {
211
662
  if (ps.error.code === 'ETIMEDOUT') return true;
212
663
  continue;
213
664
  }
665
+ // Same guard-mediated-death mapping as the lsof call above (signal-death
666
+ // from the -k escalation or a frozen hook; 124 budget expiry; 137 only
667
+ // for exit-code-propagating wrappers / external SIGKILL).
668
+ if (guard && ps.status === null && ps.signal) return true;
669
+ if (guard && (ps.status === 124 || ps.status === 137)) return true;
214
670
  if (isGitNexusServerCommand(ps.stdout || '')) return true;
215
671
  }
216
672
  return false;
@@ -229,8 +685,13 @@ function hasGitNexusDbLockedByGitNexusServer(dbPath, myPid) {
229
685
  }
230
686
 
231
687
  if (process.platform === 'linux') {
232
- if (linuxProcScanFindGitNexusServer(dbPathAbs, myPid)) return true;
233
- return unixLsofPsFindGitNexusServer(dbPathAbs, myPid);
688
+ // #2180: cmdline-first procfs scan, no lsof. 'timeout' fails CLOSED
689
+ // (overloaded host self-throttles — the throttle the orphan-storm incident
690
+ // needed; the old lsof fallback ETIMEDOUT'd and failed closed on these same
691
+ // hosts anyway, only slower and with the orphan risk). 'not-owned' is the
692
+ // only false. See the fail matrix in the file header.
693
+ const verdict = linuxProcScanFindGitNexusServer(dbPathAbs, myPid);
694
+ return verdict !== 'not-owned';
234
695
  }
235
696
 
236
697
  return unixLsofPsFindGitNexusServer(dbPathAbs, myPid);
@@ -238,4 +699,30 @@ function hasGitNexusDbLockedByGitNexusServer(dbPath, myPid) {
238
699
 
239
700
  module.exports = {
240
701
  hasGitNexusDbLockedByGitNexusServer,
702
+ // Exported for white-box unit tests that must assert the tri-state verdict
703
+ // ('owned' | 'not-owned' | 'timeout') directly — the dispatcher collapses
704
+ // timeout and owned to the same boolean true, so the boolean API alone cannot
705
+ // distinguish the F1 EACCES->timeout fix from the old EACCES->owned bug. The
706
+ // Probe interface already declares this optional. Linux-only by contract; the
707
+ // name is pinned by a source-contract test.
708
+ linuxProcScanFindGitNexusServer,
709
+ // #2163 follow-up: the hook adapters wrap the augment CLI in the same
710
+ // guard. Returns a self-tested wrapper path — the built-in candidates are
711
+ // always absolute; a GITNEXUS_HOOK_TIMEOUT_PATH override is adopted as the
712
+ // exact string that passed the self-test. Same string is also the same
713
+ // RESOLUTION for absolute paths and for slashless names (PATH lookup is
714
+ // cwd-independent); a slash-containing RELATIVE override, however, is
715
+ // existsSync-checked and self-tested against this process's cwd while the
716
+ // adapters spawn the CLI with a `cwd` option (chdir-before-exec), so such
717
+ // a value can pass here yet ENOENT at the augment call site — set the
718
+ // override to an absolute path. Returns null when the wrapper is
719
+ // disabled/unavailable. Never call on win32 (see its JSDoc).
720
+ resolveUnixGuardTimeout,
721
+ // Exported for white-box unit tests of the numeric-env parsing (#2183 review):
722
+ // Number()-not-parseInt so "16e3" reads as 16000, plus the empty/whitespace
723
+ // guard that keeps a set-but-empty budget on the 1200 default instead of an
724
+ // immediate fail-closed timeout. Tested directly because the values are
725
+ // otherwise only observable indirectly through scan timing/escalation.
726
+ getCmdlineMaxBytes,
727
+ resolveLinuxProcBudgetMs,
241
728
  };