@pugi/cli 0.1.0-beta.7 → 0.1.0-beta.87

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 (402) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/deploy.js +40 -40
  6. package/dist/commands/flatten.js +191 -0
  7. package/dist/commands/jobs-watch.js +201 -0
  8. package/dist/commands/jobs.js +42 -27
  9. package/dist/commands/smoke.js +133 -0
  10. package/dist/core/agent-progress/cleanup.js +134 -0
  11. package/dist/core/agent-progress/schema.js +144 -0
  12. package/dist/core/agent-progress/writer.js +101 -0
  13. package/dist/core/agents/adaptive-router.js +330 -0
  14. package/dist/core/agents/query-decomposer.js +297 -0
  15. package/dist/core/agents/registry.js +2 -2
  16. package/dist/core/approvals/shortcut-resolver.js +98 -0
  17. package/dist/core/artifact-chain/dispatcher.js +148 -0
  18. package/dist/core/artifact-chain/exporter.js +164 -0
  19. package/dist/core/artifact-chain/state.js +243 -0
  20. package/dist/core/artifact-chain/steps.js +169 -0
  21. package/dist/core/ask-user/question.js +92 -0
  22. package/dist/core/audit/audit-trail.js +275 -0
  23. package/dist/core/auth/ensure-authenticated.js +129 -0
  24. package/dist/core/auth/env-provider.js +238 -0
  25. package/dist/core/auto-open-browser.js +4 -4
  26. package/dist/core/auto-update/channels.js +122 -0
  27. package/dist/core/auto-update/checker.js +241 -0
  28. package/dist/core/auto-update/state.js +235 -0
  29. package/dist/core/bare-mode/index.js +107 -0
  30. package/dist/core/bash/redirect.js +281 -0
  31. package/dist/core/bash-classifier.js +436 -40
  32. package/dist/core/checkpoint/resumer.js +149 -0
  33. package/dist/core/checkpoint/rewinder.js +291 -0
  34. package/dist/core/checkpoints/shadow-git.js +670 -0
  35. package/dist/core/citations/parser.js +109 -0
  36. package/dist/core/classifier/yolo-classifier.js +88 -0
  37. package/dist/core/codegraph/decision-store.js +248 -0
  38. package/dist/core/codegraph/detect-repo.js +459 -0
  39. package/dist/core/codegraph/install.js +134 -0
  40. package/dist/core/codegraph/offer-hook.js +220 -0
  41. package/dist/core/compact/auto-trigger.js +96 -0
  42. package/dist/core/compact/buffer-rewriter.js +115 -0
  43. package/dist/core/compact/summarizer.js +208 -0
  44. package/dist/core/compact/token-counter.js +108 -0
  45. package/dist/core/consensus/anvil-fanout.js +25 -25
  46. package/dist/core/consensus/diff-capture.js +121 -12
  47. package/dist/core/consensus/rubric.js +21 -21
  48. package/dist/core/context/builder.js +6 -6
  49. package/dist/core/context/compaction-events.js +8 -8
  50. package/dist/core/context/compaction.js +31 -31
  51. package/dist/core/context/index.js +15 -8
  52. package/dist/core/context/invariants.js +51 -51
  53. package/dist/core/context/markdown-loader.js +28 -10
  54. package/dist/core/context/markdown-traverse.js +255 -0
  55. package/dist/core/context/pugiignore.js +41 -41
  56. package/dist/core/context/repo-skeleton.js +37 -37
  57. package/dist/core/context/tool-eviction.js +55 -0
  58. package/dist/core/context/watcher.js +32 -32
  59. package/dist/core/context/working-set.js +23 -23
  60. package/dist/core/coordinator/agent-tools.js +77 -0
  61. package/dist/core/coordinator/agent-toolset.js +65 -0
  62. package/dist/core/coordinator/fsm.js +73 -0
  63. package/dist/core/coordinator/mode-fsm.js +70 -0
  64. package/dist/core/cost/rate-card.js +129 -0
  65. package/dist/core/cost/tracker.js +221 -0
  66. package/dist/core/credentials.js +12 -12
  67. package/dist/core/cron/scheduler.js +138 -0
  68. package/dist/core/denial-tracking/index.js +8 -0
  69. package/dist/core/denial-tracking/state.js +264 -0
  70. package/dist/core/diagnostics/probe-runner.js +93 -0
  71. package/dist/core/diagnostics/probes/api.js +46 -0
  72. package/dist/core/diagnostics/probes/auth.js +93 -0
  73. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  74. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  75. package/dist/core/diagnostics/probes/config.js +72 -0
  76. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  77. package/dist/core/diagnostics/probes/disk.js +81 -0
  78. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  79. package/dist/core/diagnostics/probes/git.js +65 -0
  80. package/dist/core/diagnostics/probes/hooks.js +118 -0
  81. package/dist/core/diagnostics/probes/mcp.js +75 -0
  82. package/dist/core/diagnostics/probes/node.js +59 -0
  83. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  84. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  85. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  86. package/dist/core/diagnostics/probes/session.js +74 -0
  87. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  88. package/dist/core/diagnostics/probes/workspace.js +63 -0
  89. package/dist/core/diagnostics/types.js +70 -0
  90. package/dist/core/dispatch/cache-cleanup.js +197 -0
  91. package/dist/core/dispatch/cache-handoff.js +295 -0
  92. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  93. package/dist/core/edits/dispatch.js +293 -7
  94. package/dist/core/edits/format-matrix.js +26 -0
  95. package/dist/core/edits/fuzzy-ladder.js +650 -0
  96. package/dist/core/edits/index.js +3 -1
  97. package/dist/core/edits/journal.js +199 -0
  98. package/dist/core/edits/layer-a-apply.js +15 -15
  99. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  100. package/dist/core/edits/layer-b-apply.js +9 -9
  101. package/dist/core/edits/layer-c-apply.js +6 -6
  102. package/dist/core/edits/layer-d-ast.js +557 -14
  103. package/dist/core/edits/marker-parser.js +12 -12
  104. package/dist/core/edits/security-gate.js +27 -27
  105. package/dist/core/edits/verify-hook.js +273 -0
  106. package/dist/core/edits/worktree.js +322 -0
  107. package/dist/core/engine/anvil-client.js +140 -26
  108. package/dist/core/engine/auto-compact.js +179 -0
  109. package/dist/core/engine/budgets.js +186 -0
  110. package/dist/core/engine/context-prefix.js +155 -0
  111. package/dist/core/engine/index.js +1 -1
  112. package/dist/core/engine/intensity.js +158 -0
  113. package/dist/core/engine/intent.js +260 -0
  114. package/dist/core/engine/native-pugi.js +1295 -227
  115. package/dist/core/engine/prompts.js +134 -16
  116. package/dist/core/engine/strip-internal-fields.js +124 -0
  117. package/dist/core/engine/tool-bridge.js +1295 -59
  118. package/dist/core/evaluation/golden-dataset.js +293 -0
  119. package/dist/core/feedback/queue.js +177 -0
  120. package/dist/core/feedback/submitter.js +145 -0
  121. package/dist/core/file-cache.js +113 -1
  122. package/dist/core/flatten/flatten-repo.js +439 -0
  123. package/dist/core/format/osc8-link.js +28 -0
  124. package/dist/core/hook-chains.js +392 -0
  125. package/dist/core/hooks/citation-verify-hook.js +138 -0
  126. package/dist/core/hooks/citation-verify.js +112 -0
  127. package/dist/core/hooks/events.js +44 -0
  128. package/dist/core/hooks/index.js +15 -0
  129. package/dist/core/hooks/registry.js +213 -0
  130. package/dist/core/hooks/runner.js +236 -0
  131. package/dist/core/hooks/v2/event-emitter.js +115 -0
  132. package/dist/core/hooks/v2/executor.js +282 -0
  133. package/dist/core/hooks/v2/index.js +25 -0
  134. package/dist/core/hooks/v2/lifecycle.js +104 -0
  135. package/dist/core/hooks/v2/loader.js +216 -0
  136. package/dist/core/hooks/v2/matcher.js +125 -0
  137. package/dist/core/hooks/v2/trust.js +143 -0
  138. package/dist/core/hooks/v2/types.js +86 -0
  139. package/dist/core/image/renderer.js +71 -0
  140. package/dist/core/init/detector.js +582 -0
  141. package/dist/core/init/template-renderer.js +242 -0
  142. package/dist/core/jobs/registry.js +18 -18
  143. package/dist/core/ledger/results-tsv.js +142 -0
  144. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  145. package/dist/core/lsp/cache.js +105 -0
  146. package/dist/core/lsp/client.js +776 -0
  147. package/dist/core/lsp/language-detect.js +66 -0
  148. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  149. package/dist/core/lsp/symbol-tools.js +372 -0
  150. package/dist/core/mcp/client.js +97 -28
  151. package/dist/core/mcp/http-server.js +553 -0
  152. package/dist/core/mcp/orchestrator-tools.js +662 -0
  153. package/dist/core/mcp/permission.js +190 -0
  154. package/dist/core/mcp/registry.js +39 -17
  155. package/dist/core/mcp/server-tools.js +219 -0
  156. package/dist/core/mcp/server.js +397 -0
  157. package/dist/core/mcp/trust.js +10 -10
  158. package/dist/core/memory/dual-write.js +416 -0
  159. package/dist/core/memory/passive-extract.js +130 -0
  160. package/dist/core/memory/phase1-kinds.js +20 -0
  161. package/dist/core/memory/secret-scanner.js +304 -0
  162. package/dist/core/memory-sync/queue.js +170 -0
  163. package/dist/core/metrics/extract.js +113 -0
  164. package/dist/core/modes/roo-modes.js +68 -0
  165. package/dist/core/onboarding/ensure-initialized.js +133 -0
  166. package/dist/core/onboarding/marker.js +111 -0
  167. package/dist/core/onboarding/telemetry-state.js +108 -0
  168. package/dist/core/output-style/presets.js +176 -0
  169. package/dist/core/output-style/state.js +185 -0
  170. package/dist/core/path-security.js +287 -5
  171. package/dist/core/permission.js +82 -22
  172. package/dist/core/permissions/auto-classifier.js +124 -0
  173. package/dist/core/permissions/bash-parser.js +371 -0
  174. package/dist/core/permissions/circuit-breaker.js +83 -0
  175. package/dist/core/permissions/constrained-edit.js +91 -0
  176. package/dist/core/permissions/gate.js +278 -0
  177. package/dist/core/permissions/index.js +20 -0
  178. package/dist/core/permissions/mode.js +174 -0
  179. package/dist/core/permissions/network-egress.js +137 -0
  180. package/dist/core/permissions/state.js +241 -0
  181. package/dist/core/permissions/tool-class.js +93 -0
  182. package/dist/core/plan-mode/ui-state.js +51 -0
  183. package/dist/core/plans/plan-artifact.js +721 -0
  184. package/dist/core/policy-limits/etag-store.js +122 -0
  185. package/dist/core/prd-check/parser.js +215 -0
  186. package/dist/core/prd-check/reporter.js +127 -0
  187. package/dist/core/prd-check/session-review.js +557 -0
  188. package/dist/core/prd-check/verifiers.js +223 -0
  189. package/dist/core/prompt-cache/client-cache.js +99 -0
  190. package/dist/core/prompts/assembly.js +29 -0
  191. package/dist/core/prompts/registry.js +364 -0
  192. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  193. package/dist/core/pugi-md/context-injector.js +76 -0
  194. package/dist/core/pugi-md/walk-up.js +207 -0
  195. package/dist/core/python/uv-installer.js +270 -0
  196. package/dist/core/python/uv-resolver.js +83 -0
  197. package/dist/core/rate-limit/narrator.js +146 -0
  198. package/dist/core/recipes/cli-types.js +20 -0
  199. package/dist/core/recipes/loader.js +103 -0
  200. package/dist/core/recipes/runner.js +345 -0
  201. package/dist/core/recipes/schema.js +587 -0
  202. package/dist/core/release-notes/parser.js +241 -0
  203. package/dist/core/release-notes/state.js +116 -0
  204. package/dist/core/repl/ask.js +37 -37
  205. package/dist/core/repl/cancellation.js +26 -26
  206. package/dist/core/repl/cap-warning.js +4 -4
  207. package/dist/core/repl/clipboard-read.js +11 -11
  208. package/dist/core/repl/dispatch-fsm.js +12 -12
  209. package/dist/core/repl/history-search.js +15 -15
  210. package/dist/core/repl/history.js +28 -18
  211. package/dist/core/repl/kill-ring.js +5 -5
  212. package/dist/core/repl/model-pricing.js +135 -0
  213. package/dist/core/repl/privacy-banner.js +22 -22
  214. package/dist/core/repl/session.js +2157 -214
  215. package/dist/core/repl/slash-commands.js +533 -40
  216. package/dist/core/repl/store/index.js +1 -1
  217. package/dist/core/repl/store/jsonl-log.js +22 -22
  218. package/dist/core/repl/store/lockfile.js +10 -10
  219. package/dist/core/repl/store/session-store.js +136 -107
  220. package/dist/core/repl/store/types.js +15 -15
  221. package/dist/core/repl/store/uuid-v7.js +12 -12
  222. package/dist/core/repl/workspace-context.js +43 -21
  223. package/dist/core/repo-map/build.js +125 -0
  224. package/dist/core/repo-map/cache.js +185 -0
  225. package/dist/core/repo-map/extractor.js +254 -0
  226. package/dist/core/repo-map/formatter.js +145 -0
  227. package/dist/core/repo-map/page-rank.js +105 -0
  228. package/dist/core/repo-map/scanner.js +211 -0
  229. package/dist/core/retry-budget/budget.js +284 -0
  230. package/dist/core/retry-budget/index.js +5 -0
  231. package/dist/core/retry-budget/retry-cap.js +74 -0
  232. package/dist/core/routing/lead-worker.js +43 -0
  233. package/dist/core/routing/pre-flight-estimator.js +108 -0
  234. package/dist/core/runs/run-tree.js +103 -0
  235. package/dist/core/security/injection-scanner.js +367 -0
  236. package/dist/core/security/output-filter.js +418 -0
  237. package/dist/core/session/env-file.js +105 -0
  238. package/dist/core/session/section-budgets.js +140 -0
  239. package/dist/core/session.js +92 -0
  240. package/dist/core/settings.js +286 -5
  241. package/dist/core/share/formatter.js +271 -0
  242. package/dist/core/share/redactor.js +221 -0
  243. package/dist/core/share/uploader.js +267 -0
  244. package/dist/core/skills/defaults.js +457 -0
  245. package/dist/core/skills/loader.js +22 -22
  246. package/dist/core/skills/sources.js +27 -27
  247. package/dist/core/smoke/headless-driver.js +174 -0
  248. package/dist/core/smoke/orchestrator.js +194 -0
  249. package/dist/core/smoke/runner.js +238 -0
  250. package/dist/core/smoke/scenario-parser.js +316 -0
  251. package/dist/core/statusline.js +99 -0
  252. package/dist/core/subagents/dispatcher-real.js +600 -0
  253. package/dist/core/subagents/dispatcher.js +132 -43
  254. package/dist/core/subagents/index.js +19 -6
  255. package/dist/core/subagents/isolation-matrix.js +213 -0
  256. package/dist/core/subagents/spawn.js +19 -4
  257. package/dist/core/telemetry/emitter.js +229 -0
  258. package/dist/core/telemetry/queue.js +251 -0
  259. package/dist/core/theme/context.js +91 -0
  260. package/dist/core/theme/presets.js +228 -0
  261. package/dist/core/theme/state.js +181 -0
  262. package/dist/core/todos/invariant.js +10 -0
  263. package/dist/core/todos/state.js +177 -0
  264. package/dist/core/tool-schema/compressor.js +89 -0
  265. package/dist/core/transport/version-interceptor.js +166 -0
  266. package/dist/core/trust.js +2 -2
  267. package/dist/core/tui/thinking-block.js +64 -0
  268. package/dist/core/vim/keymap.js +288 -0
  269. package/dist/core/vim/state.js +92 -0
  270. package/dist/core/watch-markers/marker-watcher.js +133 -0
  271. package/dist/core/worktree-manager/cleanup.js +123 -0
  272. package/dist/core/worktree-manager/manager.js +303 -0
  273. package/dist/index.js +28 -0
  274. package/dist/runtime/bootstrap.js +190 -0
  275. package/dist/runtime/cli.js +4162 -488
  276. package/dist/runtime/commands/agents.js +30 -30
  277. package/dist/runtime/commands/budget.js +5 -5
  278. package/dist/runtime/commands/cancel.js +231 -0
  279. package/dist/runtime/commands/chain.js +489 -0
  280. package/dist/runtime/commands/codegraph-status.js +227 -0
  281. package/dist/runtime/commands/compact.js +297 -0
  282. package/dist/runtime/commands/config.js +32 -32
  283. package/dist/runtime/commands/cost.js +199 -0
  284. package/dist/runtime/commands/delegate.js +244 -13
  285. package/dist/runtime/commands/dispatch.js +126 -0
  286. package/dist/runtime/commands/doctor.js +579 -0
  287. package/dist/runtime/commands/feedback.js +184 -0
  288. package/dist/runtime/commands/hooks.js +184 -0
  289. package/dist/runtime/commands/init.js +254 -0
  290. package/dist/runtime/commands/lsp.js +368 -0
  291. package/dist/runtime/commands/mcp.js +879 -0
  292. package/dist/runtime/commands/memory.js +582 -0
  293. package/dist/runtime/commands/model.js +237 -0
  294. package/dist/runtime/commands/onboarding.js +275 -0
  295. package/dist/runtime/commands/patch.js +128 -0
  296. package/dist/runtime/commands/permissions.js +112 -0
  297. package/dist/runtime/commands/plan.js +143 -0
  298. package/dist/runtime/commands/prd-check.js +285 -0
  299. package/dist/runtime/commands/privacy.js +17 -17
  300. package/dist/runtime/commands/recipe.js +325 -0
  301. package/dist/runtime/commands/redo-blob-store.js +92 -0
  302. package/dist/runtime/commands/redo.js +361 -0
  303. package/dist/runtime/commands/release-notes.js +229 -0
  304. package/dist/runtime/commands/repo-map.js +95 -0
  305. package/dist/runtime/commands/report.js +299 -0
  306. package/dist/runtime/commands/resume.js +118 -0
  307. package/dist/runtime/commands/review-consensus.js +68 -53
  308. package/dist/runtime/commands/rewind.js +333 -0
  309. package/dist/runtime/commands/roster.js +14 -14
  310. package/dist/runtime/commands/sessions.js +163 -0
  311. package/dist/runtime/commands/share.js +316 -0
  312. package/dist/runtime/commands/skills.js +31 -31
  313. package/dist/runtime/commands/status.js +186 -0
  314. package/dist/runtime/commands/stickers.js +82 -0
  315. package/dist/runtime/commands/style.js +194 -0
  316. package/dist/runtime/commands/theme.js +196 -0
  317. package/dist/runtime/commands/undo.js +54 -22
  318. package/dist/runtime/commands/update.js +289 -0
  319. package/dist/runtime/commands/vim.js +140 -0
  320. package/dist/runtime/commands/worktree.js +177 -0
  321. package/dist/runtime/commands/worktrees.js +155 -0
  322. package/dist/runtime/headless-repl.js +195 -0
  323. package/dist/runtime/headless.js +543 -0
  324. package/dist/runtime/load-hooks-or-exit.js +71 -0
  325. package/dist/runtime/plan-decompose.js +531 -0
  326. package/dist/runtime/update-check.js +28 -28
  327. package/dist/runtime/version.js +65 -0
  328. package/dist/skills/bundled/batch.js +617 -0
  329. package/dist/skills/bundled/index.js +45 -0
  330. package/dist/skills/bundled/loop.js +358 -0
  331. package/dist/skills/bundled/remember.js +383 -0
  332. package/dist/skills/bundled/simplify.js +289 -0
  333. package/dist/skills/bundled/skillify.js +373 -0
  334. package/dist/skills/bundled/stuck.js +558 -0
  335. package/dist/skills/bundled/verify.js +439 -0
  336. package/dist/testing/vcr.js +486 -0
  337. package/dist/tools/agent-tool.js +229 -0
  338. package/dist/tools/apply-patch.js +556 -0
  339. package/dist/tools/ask-user-question.js +222 -0
  340. package/dist/tools/ask-user.js +115 -0
  341. package/dist/tools/bash.js +623 -45
  342. package/dist/tools/brief.js +224 -0
  343. package/dist/tools/enter-worktree.js +250 -0
  344. package/dist/tools/exit-worktree.js +147 -0
  345. package/dist/tools/file-tools.js +161 -44
  346. package/dist/tools/lsp-tools.js +189 -0
  347. package/dist/tools/mcp-tool.js +260 -0
  348. package/dist/tools/multi-edit.js +361 -0
  349. package/dist/tools/powershell.js +268 -0
  350. package/dist/tools/registry.js +85 -0
  351. package/dist/tools/skill-tool.js +96 -0
  352. package/dist/tools/sleep.js +99 -0
  353. package/dist/tools/synthetic-output.js +133 -0
  354. package/dist/tools/tasks.js +208 -0
  355. package/dist/tools/todo-write.js +184 -0
  356. package/dist/tools/verify-plan-execution.js +295 -0
  357. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  358. package/dist/tools/web-fetch.js +195 -10
  359. package/dist/tools/web-search.js +458 -0
  360. package/dist/tui/agent-progress-card.js +111 -0
  361. package/dist/tui/agent-tree.js +11 -1
  362. package/dist/tui/ask-modal.js +14 -14
  363. package/dist/tui/ask-user-question-prompt.js +203 -0
  364. package/dist/tui/compact-banner.js +81 -0
  365. package/dist/tui/conversation-pane.js +85 -11
  366. package/dist/tui/cost-table.js +111 -0
  367. package/dist/tui/device-flow.js +2 -2
  368. package/dist/tui/doctor-table.js +46 -0
  369. package/dist/tui/feedback-prompt.js +156 -0
  370. package/dist/tui/input-box.js +247 -32
  371. package/dist/tui/login-picker.js +3 -3
  372. package/dist/tui/markdown-render.js +6 -6
  373. package/dist/tui/onboarding-wizard.js +240 -0
  374. package/dist/tui/permissions-picker.js +86 -0
  375. package/dist/tui/render.js +35 -0
  376. package/dist/tui/repl-render.js +332 -54
  377. package/dist/tui/repl-splash-art.js +16 -16
  378. package/dist/tui/repl-splash-mascot.js +48 -24
  379. package/dist/tui/repl-splash.js +22 -22
  380. package/dist/tui/repl.js +124 -44
  381. package/dist/tui/slash-palette.js +6 -6
  382. package/dist/tui/splash.js +2 -2
  383. package/dist/tui/status-bar.js +109 -31
  384. package/dist/tui/status-table.js +7 -0
  385. package/dist/tui/stickers-art.js +136 -0
  386. package/dist/tui/style-table.js +28 -0
  387. package/dist/tui/theme-table.js +29 -0
  388. package/dist/tui/thinking-spinner.js +123 -0
  389. package/dist/tui/tool-stream-pane.js +53 -4
  390. package/dist/tui/update-banner.js +27 -2
  391. package/dist/tui/vim-input.js +267 -0
  392. package/dist/tui/welcome-banner.js +107 -0
  393. package/dist/tui/welcome-data.js +293 -0
  394. package/dist/tui/workspace-context.js +2 -2
  395. package/docs/examples/codegraph.mcp.json +10 -0
  396. package/package.json +23 -6
  397. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  398. package/test/scenarios/compact-force.scenario.txt +11 -0
  399. package/test/scenarios/identity.scenario.txt +11 -0
  400. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  401. package/test/scenarios/walkback.scenario.txt +12 -0
  402. package/dist/core/engine/compaction-hook.js +0 -154
@@ -1,13 +1,42 @@
1
+ /**
2
+ * file-tools - Pugi CLI file/bash/glob/grep tool surface.
3
+ *
4
+ * Workspace-binding contract (CEO red-alert follow-up):
5
+ *
6
+ * Every tool dispatch path threads `ctx.root` from the operator's
7
+ * `process.cwd()` through `EngineTask.workspaceRoot` ->
8
+ * `native-pugi.run()` -> `toolCtx.root` -> here. Tools call
9
+ * `resolveWorkspacePath(ctx.root, path)` for every on-disk operation
10
+ * so a dispatched specialist (e.g. Hiroshi writing tic-tac-toe HTML)
11
+ * produces files in the OPERATOR'S cwd, never in a server-side temp
12
+ * space. The path-security gate refuses traversal (`../etc/passwd`,
13
+ * URL-encoded variants, symlink escapes at the target).
14
+ *
15
+ * Wiring chain:
16
+ * 1. runtime/cli.ts: workspaceRoot = process.cwd()
17
+ * 2. EngineTask.workspaceRoot threads through to native-pugi.run().
18
+ * 3. native-pugi: const root = task.workspaceRoot
19
+ * 4. tool-bridge: passes ctx.root to file-tools / bash.
20
+ * 5. file-tools: resolveWorkspacePath(ctx.root, path).
21
+ *
22
+ * The contract is locked by `test/tools-write-to-workspace.spec.ts`
23
+ * (6 cases covering relative + nested + absolute paths + traversal
24
+ * refusal). If any layer of the chain regressed silently, dispatched
25
+ * files would land in `/tmp` instead of the operator's repo, which
26
+ * is the same failure surface as the menu-mode anti-pattern the
27
+ * sibling commits close.
28
+ */
1
29
  import { spawnSync } from 'node:child_process';
2
- import { existsSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
30
+ import { existsSync, readFileSync, realpathSync, renameSync, statSync, writeFileSync } from 'node:fs';
3
31
  import { dirname, isAbsolute, relative } from 'node:path';
4
32
  import { globSync } from 'node:fs';
5
33
  import { decidePermission } from '../core/permission.js';
6
- import { createReadRecord, hashContent } from '../core/file-cache.js';
34
+ import { StaleReadError, createReadRecord, hashContent, } from '../core/file-cache.js';
7
35
  import { resolveWorkspacePath } from '../core/path-security.js';
36
+ import { scanForInjection, summarizeFindings } from '../core/security/injection-scanner.js';
8
37
  import { recordFileMutation, recordToolCall, recordToolResult } from '../core/session.js';
9
38
  /**
10
- * α6.9 WriteGate marker — thrown by `gateOnCancellation` when the
39
+ * WriteGate marker — thrown by `gateOnCancellation` when the
11
40
  * caller supplied a cancellation token that has already aborted. The
12
41
  * tool dispatch loop in `tool-bridge.ts` recognises the name and folds
13
42
  * the throw into a `status: 'aborted'` tool result rather than a hard
@@ -19,8 +48,13 @@ export class OperatorAbortedError extends Error {
19
48
  this.name = 'OperatorAbortedError';
20
49
  }
21
50
  }
51
+ // Re-export StaleReadError so tool-bridge / test consumers can import
52
+ // the typed error from a single file-tools surface alongside
53
+ // OperatorAbortedError. Same shape as the existing OperatorAbortedError
54
+ // re-surface pattern.
55
+ export { StaleReadError } from '../core/file-cache.js';
22
56
  /**
23
- * α6.9 WriteGate: refuse the tool dispatch when the active
57
+ * WriteGate: refuse the tool dispatch when the active
24
58
  * cancellation token has aborted. Idempotent (the token's `isAborted`
25
59
  * is a getter, no side effects). Returns void on the happy path so the
26
60
  * tool can proceed; throws `OperatorAbortedError` when cancelled.
@@ -71,7 +105,7 @@ function permissionGatedResolve(ctx, inputPath, action, toolName) {
71
105
  }
72
106
  export function readTool(ctx, path) {
73
107
  const toolCallId = recordToolCall(ctx.session, 'read', path);
74
- // α6.9 WriteGate: fail fast on operator cancel BEFORE permission
108
+ // WriteGate: fail fast on operator cancel BEFORE permission
75
109
  // decision so a half-second post-cancel race never lands the read.
76
110
  if (ctx.cancellation && ctx.cancellation.isAborted) {
77
111
  const reason = 'operator_aborted: read refused';
@@ -100,7 +134,7 @@ export function readTool(ctx, path) {
100
134
  }
101
135
  export function writeTool(ctx, path, content) {
102
136
  const toolCallId = recordToolCall(ctx.session, 'write', path);
103
- // α6.9 WriteGate: refuse the write when the operator has cancelled
137
+ // WriteGate: refuse the write when the operator has cancelled
104
138
  // the dispatch. The audit log captures the cancellation reason so a
105
139
  // post-mortem can distinguish operator_aborted from settings-deny.
106
140
  if (ctx.cancellation && ctx.cancellation.isAborted) {
@@ -124,10 +158,45 @@ export function writeTool(ctx, path, content) {
124
158
  throw error;
125
159
  }
126
160
  const existed = existsSync(resolved);
127
- const before = existed ? readFileSync(resolved, 'utf8') : undefined;
161
+ // stale-read gate for writeTool's update-existing path. The
162
+ // model uses writeTool for two distinct intents:
163
+ //
164
+ // - create-new: path does not exist on disk. There is no prior
165
+ // read to validate against; skip the gate. This is the
166
+ // intentional escape hatch the leak spec also calls out.
167
+ // - overwrite-existing: path exists. Without the gate the model
168
+ // could blind-clobber an externally-modified file, losing the
169
+ // concurrent change silently. Force the model to re-read first.
170
+ //
171
+ // We deliberately apply the SAME stale-validation primitive editTool
172
+ // uses so the two write surfaces stay symmetric and a future fix to
173
+ // either one cannot accidentally weaken the other.
174
+ let before;
175
+ if (existed) {
176
+ before = readFileSync(resolved, 'utf8');
177
+ const currentStat = statSync(resolved);
178
+ const validation = ctx.readCache.validate(ctx.root, path, currentStat.mtimeMs, before);
179
+ if (validation.stale) {
180
+ const reason = `stale_read: write ${path} refused — ${validation.detail}`;
181
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
182
+ throw new StaleReadError(path, validation.reason, validation.detail);
183
+ }
184
+ }
128
185
  const tmp = `${resolved}.pugi-tmp-${Date.now()}`;
129
186
  writeFileSync(tmp, content, { encoding: 'utf8', mode: 0o600 });
130
187
  renameSync(tmp, resolved);
188
+ // Injection scan (ported an external utility,
189
+ // Apache-2.0). Scan the BODY (never the path — path security is
190
+ // owned by `path-security.ts`). Findings are SURFACED as an extra
191
+ // line on the session tool-result, never block the write. Hard-
192
+ // block requires a separate CEO-signed PR. Failure here must NOT
193
+ // throw: a buggy scanner cannot rugpull the write that already
194
+ // landed on disk above.
195
+ surfaceInjectionWarning(ctx, toolCallId, 'write', path, content);
196
+ // Refresh the cache with the post-write content so the model can
197
+ // chain a follow-up read+edit on the same file without an extra
198
+ // round-trip. Same pattern editTool uses below.
199
+ ctx.readCache.set(createReadRecord(ctx.root, path, content, 'read_tool'));
131
200
  recordFileMutation(ctx.session, {
132
201
  toolCallId,
133
202
  path,
@@ -137,9 +206,39 @@ export function writeTool(ctx, path, content) {
137
206
  });
138
207
  recordToolResult(ctx.session, toolCallId, 'success', `${existed ? 'Updated' : 'Created'} ${path}`);
139
208
  }
209
+ /**
210
+ * Surface an injection-scan warning on a file write/edit BODY. The
211
+ * scan never blocks — it folds findings into the session as a
212
+ * `tool_result` with status `warn` so an operator (or SOC pipeline
213
+ * tailing `<workspace>/.pugi/events.jsonl`) sees the signal without a
214
+ * mid-dispatch rollback.
215
+ *
216
+ * Wrapped in try/catch so a malformed scanner never crashes the tool
217
+ * loop — the write itself has already landed on disk by the time we
218
+ * call this.
219
+ */
220
+ function surfaceInjectionWarning(ctx, triggeringToolCallId, tool, path, body) {
221
+ try {
222
+ const findings = scanForInjection(body);
223
+ if (findings.length === 0)
224
+ return;
225
+ const summary = summarizeFindings(findings);
226
+ const warnCallId = recordToolCall(ctx.session, 'injection_warning', path);
227
+ const message = `injection_warning: ${tool} ${path} — ${summary.total} pattern(s) ` +
228
+ `(score=${summary.score}, kinds=${summary.kinds.join('|')}). ` +
229
+ `Triggering call: ${triggeringToolCallId}. ` +
230
+ `Detector: external-injection-patterns. Write was NOT blocked.`;
231
+ recordToolResult(ctx.session, warnCallId, 'success', message);
232
+ }
233
+ catch {
234
+ // Scanner failure must NEVER throw — the write has already
235
+ // landed and the tool loop has to continue. Silent no-op
236
+ // mirrors the audit-trail contract.
237
+ }
238
+ }
140
239
  export function editTool(ctx, path, oldString, newString) {
141
240
  const toolCallId = recordToolCall(ctx.session, 'edit', path);
142
- // α6.9 WriteGate: refuse the edit when the operator has cancelled
241
+ // WriteGate: refuse the edit when the operator has cancelled
143
242
  // the dispatch. Edits are higher-risk than reads — surface the abort
144
243
  // BEFORE we even consult permissions so a cancel-during-tool-loop
145
244
  // never partially mutates the workspace.
@@ -154,10 +253,6 @@ export function editTool(ctx, path, oldString, newString) {
154
253
  recordToolResult(ctx.session, toolCallId, 'error', reason);
155
254
  throw new Error(reason);
156
255
  }
157
- const readRecord = ctx.readCache.get(ctx.root, path);
158
- if (!readRecord) {
159
- throw new Error(`Cannot edit ${path}: file must be read first`);
160
- }
161
256
  let resolved;
162
257
  try {
163
258
  resolved = permissionGatedResolve(ctx, path, 'edit', 'edit');
@@ -167,20 +262,42 @@ export function editTool(ctx, path, oldString, newString) {
167
262
  recordToolResult(ctx.session, toolCallId, 'error', reason);
168
263
  throw error;
169
264
  }
265
+ // stale-read gate. Validate the model's read-time view of
266
+ // the file against the on-disk state BEFORE applying the mutation.
267
+ // We read disk content once and feed it to the validator so a single
268
+ // syscall covers both the gate decision AND the oldString/newString
269
+ // replacement below.
170
270
  const before = readFileSync(resolved, 'utf8');
171
- const currentHash = hashContent(before);
172
- if (currentHash !== readRecord.sha256) {
173
- throw new Error(`Cannot edit ${path}: file changed since last read`);
271
+ const currentStat = statSync(resolved);
272
+ const validation = ctx.readCache.validate(ctx.root, path, currentStat.mtimeMs, before);
273
+ if (validation.stale) {
274
+ const reason = `stale_read: edit ${path} refused — ${validation.detail}`;
275
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
276
+ throw new StaleReadError(path, validation.reason, validation.detail);
174
277
  }
278
+ const currentHash = hashContent(before);
175
279
  const matches = before.split(oldString).length - 1;
176
- if (matches === 0)
177
- throw new Error(`Cannot edit ${path}: oldString not found`);
178
- if (matches > 1)
179
- throw new Error(`Cannot edit ${path}: oldString is not unique`);
280
+ if (matches === 0) {
281
+ const reason = `Cannot edit ${path}: oldString not found`;
282
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
283
+ throw new Error(reason);
284
+ }
285
+ if (matches > 1) {
286
+ const reason = `Cannot edit ${path}: oldString is not unique`;
287
+ recordToolResult(ctx.session, toolCallId, 'error', reason);
288
+ throw new Error(reason);
289
+ }
180
290
  const after = before.replace(oldString, newString);
181
291
  const tmp = `${resolved}.pugi-tmp-${Date.now()}`;
182
292
  writeFileSync(tmp, after, { encoding: 'utf8', mode: 0o600 });
183
293
  renameSync(tmp, resolved);
294
+ // Injection scan (ported an external utility,
295
+ // Apache-2.0). We scan the NEW SUBSTRING the model is inserting,
296
+ // not the full post-edit file — the rest of the file is operator-
297
+ // owned content that pre-dates this dispatch. False-positive on
298
+ // legitimate prose that mentions banned phrases is the worst
299
+ // outcome and the warn-only contract bounds the cost.
300
+ surfaceInjectionWarning(ctx, toolCallId, 'edit', path, newString);
184
301
  ctx.readCache.set(createReadRecord(ctx.root, path, after, 'read_tool'));
185
302
  recordFileMutation(ctx.session, {
186
303
  toolCallId,
@@ -193,7 +310,7 @@ export function editTool(ctx, path, oldString, newString) {
193
310
  }
194
311
  export function globTool(ctx, pattern) {
195
312
  const toolCallId = recordToolCall(ctx.session, 'glob', pattern);
196
- // α6.9 WriteGate: cancel-aware short-circuit. Glob is read-only but
313
+ // WriteGate: cancel-aware short-circuit. Glob is read-only but
197
314
  // can be expensive on large trees; respecting the abort here keeps
198
315
  // the tool loop responsive when the operator hits Ctrl+C mid-scan.
199
316
  if (ctx.cancellation && ctx.cancellation.isAborted) {
@@ -203,11 +320,11 @@ export function globTool(ctx, pattern) {
203
320
  }
204
321
  // Pugi globs are workspace-scoped. Reject any pattern that could enumerate
205
322
  // outside the workspace:
206
- // 1. absolute paths (`/etc/**/*`) — globSync resolves these against `/`
207
- // regardless of `cwd`, so they fan out outside the repo.
208
- // 2. `..` as a path SEGMENT (`../*`, `src/../etc`) — parent traversal.
209
- // A substring check would over-reject legitimate names like
210
- // `src/v1..v2/*` so we split on `/` instead.
323
+ // 1. absolute paths (`/etc/**/*`) — globSync resolves these against `/`
324
+ // regardless of `cwd`, so they fan out outside the repo.
325
+ // 2. `..` as a path SEGMENT (`../*`, `src/../etc`) — parent traversal.
326
+ // A substring check would over-reject legitimate names like
327
+ // `src/v1..v2/*` so we split on `/` instead.
211
328
  if (isAbsolute(pattern)) {
212
329
  const reason = `Absolute glob patterns are not allowed: ${pattern}`;
213
330
  recordToolResult(ctx.session, toolCallId, 'error', reason);
@@ -230,7 +347,7 @@ export function globTool(ctx, pattern) {
230
347
  }
231
348
  export function grepTool(ctx, query) {
232
349
  const toolCallId = recordToolCall(ctx.session, 'grep', query);
233
- // α6.9 WriteGate: refuse before scanning. Grep walks the whole
350
+ // WriteGate: refuse before scanning. Grep walks the whole
234
351
  // workspace and can take seconds on a large repo; check abort first
235
352
  // so a cancel mid-scan returns immediately rather than after the
236
353
  // full walk completes.
@@ -244,7 +361,7 @@ export function grepTool(ctx, query) {
244
361
  for (const path of files) {
245
362
  if (matches.length >= 200)
246
363
  break;
247
- // α6.9 WriteGate: poll abort inside the file loop so a cancel
364
+ // WriteGate: poll abort inside the file loop so a cancel
248
365
  // arriving mid-scan terminates early. The per-file branch keeps
249
366
  // the responsiveness bounded by the slowest single-file read.
250
367
  if (ctx.cancellation && ctx.cancellation.isAborted) {
@@ -289,18 +406,18 @@ export function grepTool(ctx, query) {
289
406
  }
290
407
  /**
291
408
  * Workspace-scoped bash tool. Sized for the M1 engine adapter:
292
- * - Runs through `/bin/sh -c <command>` so the model can use pipes,
293
- * redirection, and shell builtins (`ls | wc -l`, `git status`).
294
- * - `cwd` is pinned to the workspace root so a stray `cd /` cannot
295
- * leak commands outside the repo (the child process inherits root
296
- * filesystem visibility — destructive patterns are blocked by
297
- * `decidePermission`, not by chroot).
298
- * - Output capped at 64KB combined stdout/stderr to keep the
299
- * transcript bounded; the model gets the head + a `(...truncated)`
300
- * marker if the cap fires.
301
- * - 30s wall-clock timeout. The engine loop's per-tool error path
302
- * surfaces the timeout to the model so it can retry with a narrower
303
- * command or give up.
409
+ * - Runs through `/bin/sh -c <command>` so the model can use pipes,
410
+ * redirection, and shell builtins (`ls | wc -l`, `git status`).
411
+ * - `cwd` is pinned to the workspace root so a stray `cd /` cannot
412
+ * leak commands outside the repo (the child process inherits root
413
+ * filesystem visibility — destructive patterns are blocked by
414
+ * `decidePermission`, not by chroot).
415
+ * - Output capped at 64KB combined stdout/stderr to keep the
416
+ * transcript bounded; the model gets the head + a `(...truncated)`
417
+ * marker if the cap fires.
418
+ * - 30s wall-clock timeout. The engine loop's per-tool error path
419
+ * surfaces the timeout to the model so it can retry with a narrower
420
+ * command or give up.
304
421
  *
305
422
  * Permission gating: `kind: 'bash'`. The CLI's permission module already
306
423
  * hard-denies the destructive-patterns list (rm -rf /, DROP DATABASE,
@@ -311,7 +428,7 @@ export const BASH_OUTPUT_CAP = 64 * 1024;
311
428
  export const BASH_DEFAULT_TIMEOUT_MS = 30_000;
312
429
  // Child-process stdio buffer — large enough that the model-facing
313
430
  // truncation cap (`BASH_OUTPUT_CAP`) is always the gate, never the
314
- // child's internal buffer. Code Reviewer P2 retro 2026-05-23 flagged
431
+ // child's internal buffer. Code Reviewer P2 retro flagged
315
432
  // `BASH_OUTPUT_CAP * 2` as too tight: real builds (`pnpm build`,
316
433
  // `tsc --noEmit`) routinely exceed 128 KB combined and the model
317
434
  // then saw a fatal `ERR_CHILD_PROCESS_STDIO_MAXBUFFER` instead of a
@@ -319,7 +436,7 @@ export const BASH_DEFAULT_TIMEOUT_MS = 30_000;
319
436
  export const BASH_CHILD_MAXBUFFER = 10 * 1024 * 1024;
320
437
  export function bashTool(ctx, command, options = {}) {
321
438
  const toolCallId = recordToolCall(ctx.session, 'bash', command);
322
- // α6.9 WriteGate: bash is the highest-risk tool surface. Refuse
439
+ // WriteGate: bash is the highest-risk tool surface. Refuse
323
440
  // before the destructive-pattern classifier even runs so a
324
441
  // cancelled dispatch never spawns a child process. Note: this is
325
442
  // pre-spawn cancellation only; once the /bin/sh -c process is
@@ -343,7 +460,7 @@ export function bashTool(ctx, command, options = {}) {
343
460
  //
344
461
  // Env sanitisation strategy: build the child env from an explicit
345
462
  // allow-list rather than inheriting `process.env` and trying to
346
- // strip secrets after the fact. Code Reviewer P1 2026-05-23 flagged
463
+ // strip secrets after the fact. Code Reviewer P1 flagged
347
464
  // that the deny-list approach missed ANTHROPIC_API_KEY / GH_TOKEN
348
465
  // / AWS_SECRET_ACCESS_KEY / DATABASE_URL / arbitrary *_TOKEN /
349
466
  // *_SECRET / *_KEY variables — every CI agent rotation would risk
@@ -372,7 +489,7 @@ export function bashTool(ctx, command, options = {}) {
372
489
  }
373
490
  const timeoutMs = options.timeoutMs ?? BASH_DEFAULT_TIMEOUT_MS;
374
491
  // `spawnSync` (vs `execFileSync`) captures stdout AND stderr on
375
- // BOTH success and failure paths. Code Reviewer P1 2026-05-23:
492
+ // BOTH success and failure paths. Code Reviewer P1:
376
493
  // `execFileSync` returns only stdout on exit 0, silently dropping
377
494
  // stderr output from `tsc`, `eslint`, `pytest`, etc. — the model
378
495
  // would see `(no output)` for successful runs that emitted real
@@ -381,7 +498,7 @@ export function bashTool(ctx, command, options = {}) {
381
498
  // maxBuffer is generous (10 MB) so the child process is never the
382
499
  // truncation gate — the post-hoc `.slice(0, BASH_OUTPUT_CAP)` below
383
500
  // is the single source of truth for what the model sees. Code
384
- // Reviewer P2 retro 2026-05-23: the previous `BASH_OUTPUT_CAP * 2`
501
+ // Reviewer P2 retro: the previous `BASH_OUTPUT_CAP * 2`
385
502
  // (128 KB) would hard-throw `ERR_CHILD_PROCESS_STDIO_MAXBUFFER`
386
503
  // on noisy commands (`pnpm build`, `tsc --noEmit` on the whole
387
504
  // monorepo) instead of returning the truncated head.
@@ -0,0 +1,189 @@
1
+ import { gateOnCancellation, OperatorAbortedError } from './file-tools.js';
2
+ import { recordToolCall, recordToolResult } from '../core/session.js';
3
+ /** Cap for any single LSP tool's payload size. Keeps model context lean. */
4
+ const LSP_PAYLOAD_CAP_BYTES = 8 * 1024;
5
+ export async function lspHover(ctx, lang, file, line, col) {
6
+ const toolCallId = recordToolCall(ctx.session, 'lsp_hover', `${lang}:${file}:${line}:${col}`);
7
+ return guard(ctx, 'lsp_hover', toolCallId, async () => {
8
+ const client = ctx.lspClients?.get(lang);
9
+ if (!client)
10
+ return unavailable(lang);
11
+ const result = await client.hover(file, { line, character: col }, ctx.cancellation);
12
+ if (!result.ok)
13
+ return failure(result);
14
+ if (!result.value) {
15
+ return { ok: true, value: { content: '' } };
16
+ }
17
+ const content = truncate(result.value.content);
18
+ return {
19
+ ok: true,
20
+ value: {
21
+ content: content.text,
22
+ ...(result.value.range ? { range: result.value.range } : {}),
23
+ },
24
+ ...(content.truncated ? { truncated: true } : {}),
25
+ };
26
+ });
27
+ }
28
+ export async function lspDefinition(ctx, lang, file, line, col) {
29
+ const toolCallId = recordToolCall(ctx.session, 'lsp_definition', `${lang}:${file}:${line}:${col}`);
30
+ return guard(ctx, 'lsp_definition', toolCallId, async () => {
31
+ const client = ctx.lspClients?.get(lang);
32
+ if (!client)
33
+ return unavailable(lang);
34
+ const result = await client.definition(file, { line, character: col }, ctx.cancellation);
35
+ if (!result.ok)
36
+ return failure(result);
37
+ const capped = capLocations(result.value);
38
+ return {
39
+ ok: true,
40
+ value: capped.value,
41
+ ...(capped.truncated ? { truncated: true } : {}),
42
+ };
43
+ });
44
+ }
45
+ export async function lspReferences(ctx, lang, file, line, col) {
46
+ const toolCallId = recordToolCall(ctx.session, 'lsp_references', `${lang}:${file}:${line}:${col}`);
47
+ return guard(ctx, 'lsp_references', toolCallId, async () => {
48
+ const client = ctx.lspClients?.get(lang);
49
+ if (!client)
50
+ return unavailable(lang);
51
+ const result = await client.references(file, { line, character: col }, ctx.cancellation);
52
+ if (!result.ok)
53
+ return failure(result);
54
+ const capped = capLocations(result.value);
55
+ return {
56
+ ok: true,
57
+ value: capped.value,
58
+ ...(capped.truncated ? { truncated: true } : {}),
59
+ };
60
+ });
61
+ }
62
+ export async function lspDiagnostics(ctx, lang, file) {
63
+ const toolCallId = recordToolCall(ctx.session, 'lsp_diagnostics', `${lang}:${file}`);
64
+ return guard(ctx, 'lsp_diagnostics', toolCallId, async () => {
65
+ const client = ctx.lspClients?.get(lang);
66
+ if (!client)
67
+ return unavailable(lang);
68
+ const result = await client.diagnostics(file, ctx.cancellation);
69
+ if (!result.ok)
70
+ return failure(result);
71
+ const capped = capDiagnostics(result.value);
72
+ return {
73
+ ok: true,
74
+ value: capped.value,
75
+ ...(capped.truncated ? { truncated: true } : {}),
76
+ };
77
+ });
78
+ }
79
+ async function guard(ctx, toolName, toolCallId, op) {
80
+ try {
81
+ gateOnCancellation(ctx, toolName);
82
+ }
83
+ catch (error) {
84
+ if (error instanceof OperatorAbortedError) {
85
+ recordToolResult(ctx.session, toolCallId, 'cancelled', error.message);
86
+ return { ok: false, reason: 'operator_aborted', detail: error.message };
87
+ }
88
+ throw error;
89
+ }
90
+ try {
91
+ const result = await op();
92
+ if (result.ok) {
93
+ recordToolResult(ctx.session, toolCallId, 'success', summarize(result.value));
94
+ }
95
+ else {
96
+ recordToolResult(ctx.session, toolCallId, 'error', `${result.reason ?? 'error'}: ${result.detail ?? ''}`);
97
+ }
98
+ return result;
99
+ }
100
+ catch (error) {
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ recordToolResult(ctx.session, toolCallId, 'error', message);
103
+ return { ok: false, reason: 'lsp_error', detail: message };
104
+ }
105
+ }
106
+ function unavailable(lang) {
107
+ return {
108
+ ok: false,
109
+ reason: 'lsp_unavailable',
110
+ detail: `no LSP server started for ${lang}. Install the server and re-run ` +
111
+ `with --lsp ${lang}, or fall back to grep.`,
112
+ };
113
+ }
114
+ function failure(result) {
115
+ if (result.ok) {
116
+ // Shouldn't be hit — caller checks first.
117
+ return { ok: true, value: result.value };
118
+ }
119
+ return { ok: false, reason: result.reason, detail: result.detail };
120
+ }
121
+ function summarize(value) {
122
+ if (value === null || value === undefined)
123
+ return 'no result';
124
+ if (Array.isArray(value))
125
+ return `${value.length} items`;
126
+ if (typeof value === 'object')
127
+ return Object.keys(value).join(',');
128
+ return String(value);
129
+ }
130
+ function truncate(text) {
131
+ const bytes = Buffer.byteLength(text, 'utf8');
132
+ if (bytes <= LSP_PAYLOAD_CAP_BYTES)
133
+ return { text, truncated: false };
134
+ // Truncate to the cap byte boundary. We don't try to honor codepoint
135
+ // alignment — UTF-8 surrogate splits show up as a single ? at the
136
+ // boundary, which is acceptable for a debug surface; the dispatcher
137
+ // is the trust boundary for "this is what the model will see".
138
+ const buf = Buffer.from(text, 'utf8').subarray(0, LSP_PAYLOAD_CAP_BYTES);
139
+ return { text: `${buf.toString('utf8')}\n... [truncated]`, truncated: true };
140
+ }
141
+ function capLocations(locations) {
142
+ // Cap at 200 locations OR the byte cap, whichever hits first. The
143
+ // 200 number is the operator-facing "this is a hot symbol" threshold —
144
+ // a richer surface (paginated `pugi lsp references --offset N`) is
145
+ // open backlog.
146
+ const COUNT_CAP = 200;
147
+ if (locations.length === 0)
148
+ return { value: locations, truncated: false };
149
+ const trimmed = locations.slice(0, COUNT_CAP);
150
+ const serialized = JSON.stringify(trimmed);
151
+ if (Buffer.byteLength(serialized, 'utf8') <= LSP_PAYLOAD_CAP_BYTES && trimmed.length === locations.length) {
152
+ return { value: trimmed, truncated: false };
153
+ }
154
+ // Trim by halves until we fit the byte cap. Worst case ~10 iterations
155
+ // for the 200 max, fine for an interactive tool.
156
+ let upper = trimmed.length;
157
+ while (upper > 1) {
158
+ const half = Math.floor(upper / 2);
159
+ const sub = trimmed.slice(0, half);
160
+ if (Buffer.byteLength(JSON.stringify(sub), 'utf8') <= LSP_PAYLOAD_CAP_BYTES) {
161
+ return { value: sub, truncated: true };
162
+ }
163
+ upper = half;
164
+ }
165
+ return { value: trimmed.slice(0, 1), truncated: true };
166
+ }
167
+ function capDiagnostics(items) {
168
+ if (items.length === 0)
169
+ return { value: items, truncated: false };
170
+ const serialized = JSON.stringify(items);
171
+ if (Buffer.byteLength(serialized, 'utf8') <= LSP_PAYLOAD_CAP_BYTES) {
172
+ return { value: items, truncated: false };
173
+ }
174
+ // Diagnostics are sorted error-first in LSP convention; trim from the
175
+ // tail so we keep the highest-severity items.
176
+ let upper = items.length;
177
+ while (upper > 1) {
178
+ const half = Math.floor(upper / 2);
179
+ const sub = items.slice(0, half);
180
+ if (Buffer.byteLength(JSON.stringify(sub), 'utf8') <= LSP_PAYLOAD_CAP_BYTES) {
181
+ return { value: sub, truncated: true };
182
+ }
183
+ upper = half;
184
+ }
185
+ return { value: items.slice(0, 1), truncated: true };
186
+ }
187
+ /** Test-only surface so specs can poke truncation directly. */
188
+ export const __test__ = { truncate, capLocations, capDiagnostics, LSP_PAYLOAD_CAP_BYTES };
189
+ //# sourceMappingURL=lsp-tools.js.map