@pugi/cli 0.1.0-beta.99 → 1.0.0-alpha.10

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 (448) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +11 -191
  3. package/bin/pugi +8 -0
  4. package/package.json +15 -71
  5. package/postinstall.mjs +31 -0
  6. package/CHANGELOG.md +0 -132
  7. package/THIRD_PARTY_NOTICES.md +0 -40
  8. package/assets/pugi-mascot.ansi +0 -16
  9. package/assets/pugi-prozr2-mascot.ansi +0 -9
  10. package/bin/run.js +0 -34
  11. package/dist/commands/deploy.js +0 -439
  12. package/dist/commands/flatten.js +0 -191
  13. package/dist/commands/jobs-watch.js +0 -201
  14. package/dist/commands/jobs.js +0 -260
  15. package/dist/commands/retro.js +0 -210
  16. package/dist/commands/smoke.js +0 -133
  17. package/dist/core/agent-progress/cleanup.js +0 -134
  18. package/dist/core/agent-progress/schema.js +0 -144
  19. package/dist/core/agent-progress/writer.js +0 -101
  20. package/dist/core/agents/adaptive-router.js +0 -330
  21. package/dist/core/agents/loader.js +0 -104
  22. package/dist/core/agents/query-decomposer.js +0 -297
  23. package/dist/core/agents/registry.js +0 -69
  24. package/dist/core/approvals/shortcut-resolver.js +0 -98
  25. package/dist/core/artifact-chain/dispatcher.js +0 -148
  26. package/dist/core/artifact-chain/exporter.js +0 -164
  27. package/dist/core/artifact-chain/state.js +0 -243
  28. package/dist/core/artifact-chain/steps.js +0 -169
  29. package/dist/core/ask-user/question.js +0 -92
  30. package/dist/core/audit/audit-trail.js +0 -275
  31. package/dist/core/auth/ensure-authenticated.js +0 -129
  32. package/dist/core/auth/env-provider.js +0 -238
  33. package/dist/core/auto-open-browser.js +0 -128
  34. package/dist/core/auto-update/channels.js +0 -122
  35. package/dist/core/auto-update/checker.js +0 -241
  36. package/dist/core/auto-update/state.js +0 -235
  37. package/dist/core/bare-mode/index.js +0 -107
  38. package/dist/core/bash/redirect.js +0 -281
  39. package/dist/core/bash-classifier.js +0 -1397
  40. package/dist/core/checkpoint/resumer.js +0 -149
  41. package/dist/core/checkpoint/rewinder.js +0 -291
  42. package/dist/core/checkpoints/shadow-git.js +0 -670
  43. package/dist/core/citations/parser.js +0 -109
  44. package/dist/core/classifier/yolo-classifier.js +0 -88
  45. package/dist/core/clipboard.js +0 -70
  46. package/dist/core/codegraph/decision-store.js +0 -248
  47. package/dist/core/codegraph/detect-repo.js +0 -459
  48. package/dist/core/codegraph/install.js +0 -134
  49. package/dist/core/codegraph/offer-hook.js +0 -220
  50. package/dist/core/compact/auto-trigger.js +0 -96
  51. package/dist/core/compact/buffer-rewriter.js +0 -115
  52. package/dist/core/compact/summarizer.js +0 -208
  53. package/dist/core/compact/token-counter.js +0 -108
  54. package/dist/core/consensus/anvil-fanout.js +0 -276
  55. package/dist/core/consensus/diff-capture.js +0 -491
  56. package/dist/core/consensus/rubric.js +0 -233
  57. package/dist/core/context/builder.js +0 -114
  58. package/dist/core/context/compaction-events.js +0 -99
  59. package/dist/core/context/compaction.js +0 -602
  60. package/dist/core/context/index.js +0 -28
  61. package/dist/core/context/invariants.js +0 -250
  62. package/dist/core/context/markdown-loader.js +0 -288
  63. package/dist/core/context/markdown-traverse.js +0 -255
  64. package/dist/core/context/pugiignore.js +0 -316
  65. package/dist/core/context/repo-skeleton.js +0 -533
  66. package/dist/core/context/tool-eviction.js +0 -55
  67. package/dist/core/context/watcher.js +0 -342
  68. package/dist/core/context/working-set.js +0 -165
  69. package/dist/core/coordinator/agent-tools.js +0 -77
  70. package/dist/core/coordinator/agent-toolset.js +0 -65
  71. package/dist/core/coordinator/fsm.js +0 -73
  72. package/dist/core/coordinator/mode-fsm.js +0 -70
  73. package/dist/core/cost/rate-card.js +0 -129
  74. package/dist/core/cost/tracker.js +0 -221
  75. package/dist/core/credentials.js +0 -355
  76. package/dist/core/cron/scheduler.js +0 -138
  77. package/dist/core/denial-tracking/index.js +0 -8
  78. package/dist/core/denial-tracking/state.js +0 -264
  79. package/dist/core/diagnostics/probe-runner.js +0 -93
  80. package/dist/core/diagnostics/probes/api.js +0 -46
  81. package/dist/core/diagnostics/probes/auth.js +0 -93
  82. package/dist/core/diagnostics/probes/bare-mode.js +0 -42
  83. package/dist/core/diagnostics/probes/cli-version.js +0 -127
  84. package/dist/core/diagnostics/probes/config.js +0 -72
  85. package/dist/core/diagnostics/probes/denial-tracking.js +0 -57
  86. package/dist/core/diagnostics/probes/disk.js +0 -81
  87. package/dist/core/diagnostics/probes/engine-live.js +0 -46
  88. package/dist/core/diagnostics/probes/git.js +0 -65
  89. package/dist/core/diagnostics/probes/hooks.js +0 -118
  90. package/dist/core/diagnostics/probes/mcp.js +0 -75
  91. package/dist/core/diagnostics/probes/node.js +0 -59
  92. package/dist/core/diagnostics/probes/pnpm.js +0 -36
  93. package/dist/core/diagnostics/probes/pugi-md.js +0 -89
  94. package/dist/core/diagnostics/probes/sandbox.js +0 -72
  95. package/dist/core/diagnostics/probes/session.js +0 -74
  96. package/dist/core/diagnostics/probes/status-snapshot.js +0 -488
  97. package/dist/core/diagnostics/probes/workspace.js +0 -63
  98. package/dist/core/diagnostics/types.js +0 -70
  99. package/dist/core/dispatch/cache-cleanup.js +0 -197
  100. package/dist/core/dispatch/cache-handoff.js +0 -295
  101. package/dist/core/edits/apply-patch-layer-e.js +0 -189
  102. package/dist/core/edits/dispatch.js +0 -511
  103. package/dist/core/edits/format-detector.js +0 -260
  104. package/dist/core/edits/format-matrix.js +0 -26
  105. package/dist/core/edits/fuzzy-ladder.js +0 -650
  106. package/dist/core/edits/index.js +0 -19
  107. package/dist/core/edits/journal.js +0 -199
  108. package/dist/core/edits/layer-a-apply.js +0 -217
  109. package/dist/core/edits/layer-a-fuzzy-apply.js +0 -198
  110. package/dist/core/edits/layer-b-apply.js +0 -211
  111. package/dist/core/edits/layer-c-apply.js +0 -160
  112. package/dist/core/edits/layer-d-ast.js +0 -572
  113. package/dist/core/edits/marker-parser.js +0 -401
  114. package/dist/core/edits/security-gate.js +0 -223
  115. package/dist/core/edits/verify-hook.js +0 -273
  116. package/dist/core/edits/worktree.js +0 -322
  117. package/dist/core/engine/adapter-runner.js +0 -8
  118. package/dist/core/engine/anvil-client.js +0 -344
  119. package/dist/core/engine/auto-compact.js +0 -179
  120. package/dist/core/engine/budgets.js +0 -195
  121. package/dist/core/engine/context-prefix.js +0 -155
  122. package/dist/core/engine/index.js +0 -12
  123. package/dist/core/engine/intensity.js +0 -163
  124. package/dist/core/engine/intent.js +0 -260
  125. package/dist/core/engine/native-pugi.js +0 -1616
  126. package/dist/core/engine/noop.js +0 -27
  127. package/dist/core/engine/prompts.js +0 -236
  128. package/dist/core/engine/strip-internal-fields.js +0 -124
  129. package/dist/core/engine/tool-bridge.js +0 -2173
  130. package/dist/core/engine/verification-patterns.js +0 -195
  131. package/dist/core/evaluation/golden-dataset.js +0 -293
  132. package/dist/core/feedback/queue.js +0 -177
  133. package/dist/core/feedback/submitter.js +0 -145
  134. package/dist/core/file-cache.js +0 -141
  135. package/dist/core/flatten/flatten-repo.js +0 -439
  136. package/dist/core/format/osc8-link.js +0 -28
  137. package/dist/core/hook-chains.js +0 -392
  138. package/dist/core/hooks/citation-verify-hook.js +0 -138
  139. package/dist/core/hooks/citation-verify.js +0 -112
  140. package/dist/core/hooks/events.js +0 -46
  141. package/dist/core/hooks/index.js +0 -15
  142. package/dist/core/hooks/registry.js +0 -216
  143. package/dist/core/hooks/runner.js +0 -236
  144. package/dist/core/hooks/v2/event-emitter.js +0 -115
  145. package/dist/core/hooks/v2/executor.js +0 -282
  146. package/dist/core/hooks/v2/index.js +0 -25
  147. package/dist/core/hooks/v2/lifecycle.js +0 -104
  148. package/dist/core/hooks/v2/loader.js +0 -216
  149. package/dist/core/hooks/v2/matcher.js +0 -125
  150. package/dist/core/hooks/v2/trust.js +0 -143
  151. package/dist/core/hooks/v2/types.js +0 -86
  152. package/dist/core/hooks/worktree-events.js +0 -158
  153. package/dist/core/hooks.js +0 -415
  154. package/dist/core/image/renderer.js +0 -71
  155. package/dist/core/index-store.js +0 -260
  156. package/dist/core/init/detector.js +0 -582
  157. package/dist/core/init/template-renderer.js +0 -242
  158. package/dist/core/jobs/registry.js +0 -462
  159. package/dist/core/ledger/results-tsv.js +0 -142
  160. package/dist/core/log-discipline/stdout-redirect.js +0 -51
  161. package/dist/core/lsp/cache.js +0 -105
  162. package/dist/core/lsp/client.js +0 -1229
  163. package/dist/core/lsp/language-detect.js +0 -66
  164. package/dist/core/lsp/post-edit-diagnostics.js +0 -171
  165. package/dist/core/lsp/server-detect.js +0 -173
  166. package/dist/core/lsp/symbol-cache.js +0 -162
  167. package/dist/core/lsp/symbol-tools.js +0 -664
  168. package/dist/core/mcp/client.js +0 -385
  169. package/dist/core/mcp/http-server.js +0 -553
  170. package/dist/core/mcp/orchestrator-config.js +0 -192
  171. package/dist/core/mcp/orchestrator-tools.js +0 -806
  172. package/dist/core/mcp/permission.js +0 -190
  173. package/dist/core/mcp/registry.js +0 -193
  174. package/dist/core/mcp/server-tools.js +0 -219
  175. package/dist/core/mcp/server.js +0 -397
  176. package/dist/core/mcp/trust.js +0 -91
  177. package/dist/core/memory/dual-write.js +0 -416
  178. package/dist/core/memory/passive-extract.js +0 -130
  179. package/dist/core/memory/phase1-kinds.js +0 -20
  180. package/dist/core/memory/secret-scanner.js +0 -304
  181. package/dist/core/memory-sync/queue.js +0 -170
  182. package/dist/core/metrics/extract.js +0 -113
  183. package/dist/core/modes/roo-modes.js +0 -68
  184. package/dist/core/onboarding/ensure-initialized.js +0 -133
  185. package/dist/core/onboarding/marker.js +0 -111
  186. package/dist/core/onboarding/telemetry-state.js +0 -108
  187. package/dist/core/output-style/presets.js +0 -176
  188. package/dist/core/output-style/state.js +0 -185
  189. package/dist/core/path-security.js +0 -345
  190. package/dist/core/permission.js +0 -369
  191. package/dist/core/permissions/auto-classifier.js +0 -124
  192. package/dist/core/permissions/bash-parser.js +0 -371
  193. package/dist/core/permissions/circuit-breaker.js +0 -83
  194. package/dist/core/permissions/constrained-edit.js +0 -91
  195. package/dist/core/permissions/gate.js +0 -278
  196. package/dist/core/permissions/index.js +0 -20
  197. package/dist/core/permissions/mode.js +0 -174
  198. package/dist/core/permissions/network-egress.js +0 -137
  199. package/dist/core/permissions/state.js +0 -241
  200. package/dist/core/permissions/tool-class.js +0 -107
  201. package/dist/core/plan-mode/ui-state.js +0 -51
  202. package/dist/core/plans/plan-artifact.js +0 -721
  203. package/dist/core/policy-limits/etag-store.js +0 -122
  204. package/dist/core/prd-check/parser.js +0 -215
  205. package/dist/core/prd-check/reporter.js +0 -127
  206. package/dist/core/prd-check/session-review.js +0 -557
  207. package/dist/core/prd-check/verifiers.js +0 -223
  208. package/dist/core/prompt-cache/client-cache.js +0 -99
  209. package/dist/core/prompts/assembly.js +0 -29
  210. package/dist/core/prompts/registry.js +0 -364
  211. package/dist/core/pugi-gitignore.js +0 -52
  212. package/dist/core/pugi-md/cc-compat-rules.js +0 -735
  213. package/dist/core/pugi-md/context-injector.js +0 -76
  214. package/dist/core/pugi-md/walk-up.js +0 -207
  215. package/dist/core/python/uv-installer.js +0 -270
  216. package/dist/core/python/uv-resolver.js +0 -83
  217. package/dist/core/rate-limit/narrator.js +0 -146
  218. package/dist/core/recipes/cli-types.js +0 -20
  219. package/dist/core/recipes/loader.js +0 -103
  220. package/dist/core/recipes/runner.js +0 -345
  221. package/dist/core/recipes/schema.js +0 -587
  222. package/dist/core/release-notes/parser.js +0 -241
  223. package/dist/core/release-notes/state.js +0 -116
  224. package/dist/core/repl/ask.js +0 -512
  225. package/dist/core/repl/cancellation.js +0 -98
  226. package/dist/core/repl/cap-warning.js +0 -91
  227. package/dist/core/repl/clipboard-read.js +0 -174
  228. package/dist/core/repl/dispatch-fsm.js +0 -220
  229. package/dist/core/repl/engine-bridge.js +0 -303
  230. package/dist/core/repl/history-search.js +0 -175
  231. package/dist/core/repl/history.js +0 -182
  232. package/dist/core/repl/kill-ring.js +0 -138
  233. package/dist/core/repl/model-pricing.js +0 -135
  234. package/dist/core/repl/privacy-banner.js +0 -71
  235. package/dist/core/repl/session.js +0 -4962
  236. package/dist/core/repl/slash-commands.js +0 -747
  237. package/dist/core/repl/store/index.js +0 -12
  238. package/dist/core/repl/store/jsonl-log.js +0 -321
  239. package/dist/core/repl/store/lockfile.js +0 -155
  240. package/dist/core/repl/store/session-store.js +0 -821
  241. package/dist/core/repl/store/types.js +0 -44
  242. package/dist/core/repl/store/uuid-v7.js +0 -68
  243. package/dist/core/repl/tool-route.js +0 -382
  244. package/dist/core/repl/workspace-context.js +0 -206
  245. package/dist/core/repo-map/build.js +0 -125
  246. package/dist/core/repo-map/cache.js +0 -185
  247. package/dist/core/repo-map/extractor.js +0 -254
  248. package/dist/core/repo-map/formatter.js +0 -145
  249. package/dist/core/repo-map/page-rank.js +0 -105
  250. package/dist/core/repo-map/scanner.js +0 -211
  251. package/dist/core/retro/git-collector.js +0 -251
  252. package/dist/core/retro/health-card.js +0 -25
  253. package/dist/core/retro/metrics.js +0 -342
  254. package/dist/core/retro/narrative.js +0 -249
  255. package/dist/core/retro/plane-collector.js +0 -274
  256. package/dist/core/retro/pr-issue-link.js +0 -65
  257. package/dist/core/retro/types.js +0 -16
  258. package/dist/core/retry-budget/budget.js +0 -284
  259. package/dist/core/retry-budget/index.js +0 -5
  260. package/dist/core/retry-budget/retry-cap.js +0 -74
  261. package/dist/core/routing/lead-worker.js +0 -43
  262. package/dist/core/routing/pre-flight-estimator.js +0 -108
  263. package/dist/core/runs/run-tree.js +0 -103
  264. package/dist/core/sandboxing/adapter.js +0 -29
  265. package/dist/core/sandboxing/index.js +0 -49
  266. package/dist/core/sandboxing/none.js +0 -19
  267. package/dist/core/sandboxing/seatbelt.js +0 -183
  268. package/dist/core/security/injection-scanner.js +0 -367
  269. package/dist/core/security/output-filter.js +0 -418
  270. package/dist/core/session/env-file.js +0 -105
  271. package/dist/core/session/section-budgets.js +0 -140
  272. package/dist/core/session.js +0 -377
  273. package/dist/core/settings.js +0 -400
  274. package/dist/core/share/formatter.js +0 -271
  275. package/dist/core/share/redactor.js +0 -221
  276. package/dist/core/share/uploader.js +0 -267
  277. package/dist/core/skills/defaults.js +0 -457
  278. package/dist/core/skills/loader.js +0 -454
  279. package/dist/core/skills/sources.js +0 -480
  280. package/dist/core/skills/trust.js +0 -172
  281. package/dist/core/smoke/headless-driver.js +0 -174
  282. package/dist/core/smoke/orchestrator.js +0 -194
  283. package/dist/core/smoke/runner.js +0 -238
  284. package/dist/core/smoke/scenario-parser.js +0 -316
  285. package/dist/core/statusline.js +0 -99
  286. package/dist/core/subagents/dispatcher-real.js +0 -600
  287. package/dist/core/subagents/dispatcher.js +0 -352
  288. package/dist/core/subagents/index.js +0 -39
  289. package/dist/core/subagents/isolation-matrix.js +0 -213
  290. package/dist/core/subagents/spawn.js +0 -101
  291. package/dist/core/telemetry/emitter.js +0 -229
  292. package/dist/core/telemetry/queue.js +0 -251
  293. package/dist/core/theme/context.js +0 -91
  294. package/dist/core/theme/presets.js +0 -228
  295. package/dist/core/theme/state.js +0 -181
  296. package/dist/core/todos/invariant.js +0 -10
  297. package/dist/core/todos/state.js +0 -177
  298. package/dist/core/tool-schema/compressor.js +0 -89
  299. package/dist/core/transport/version-interceptor.js +0 -166
  300. package/dist/core/trust.js +0 -109
  301. package/dist/core/tui/thinking-block.js +0 -64
  302. package/dist/core/vim/keymap.js +0 -288
  303. package/dist/core/vim/state.js +0 -92
  304. package/dist/core/watch-markers/marker-watcher.js +0 -133
  305. package/dist/core/worktree/include-parser.js +0 -249
  306. package/dist/core/worktree-manager/cleanup.js +0 -123
  307. package/dist/core/worktree-manager/manager.js +0 -303
  308. package/dist/index.js +0 -44
  309. package/dist/runtime/bootstrap.js +0 -190
  310. package/dist/runtime/cli.js +0 -8121
  311. package/dist/runtime/commands/agents.js +0 -385
  312. package/dist/runtime/commands/budget.js +0 -192
  313. package/dist/runtime/commands/cancel.js +0 -231
  314. package/dist/runtime/commands/chain.js +0 -489
  315. package/dist/runtime/commands/codegraph-status.js +0 -227
  316. package/dist/runtime/commands/compact.js +0 -297
  317. package/dist/runtime/commands/config.js +0 -595
  318. package/dist/runtime/commands/cost.js +0 -199
  319. package/dist/runtime/commands/delegate.js +0 -312
  320. package/dist/runtime/commands/dispatch.js +0 -126
  321. package/dist/runtime/commands/doctor.js +0 -579
  322. package/dist/runtime/commands/feedback.js +0 -184
  323. package/dist/runtime/commands/hooks.js +0 -187
  324. package/dist/runtime/commands/init.js +0 -254
  325. package/dist/runtime/commands/lsp.js +0 -368
  326. package/dist/runtime/commands/mcp.js +0 -935
  327. package/dist/runtime/commands/memory.js +0 -582
  328. package/dist/runtime/commands/model.js +0 -237
  329. package/dist/runtime/commands/onboarding.js +0 -275
  330. package/dist/runtime/commands/patch.js +0 -128
  331. package/dist/runtime/commands/permissions.js +0 -112
  332. package/dist/runtime/commands/plan.js +0 -143
  333. package/dist/runtime/commands/prd-check.js +0 -285
  334. package/dist/runtime/commands/privacy.js +0 -107
  335. package/dist/runtime/commands/recipe.js +0 -325
  336. package/dist/runtime/commands/redo-blob-store.js +0 -92
  337. package/dist/runtime/commands/redo.js +0 -361
  338. package/dist/runtime/commands/release-notes.js +0 -229
  339. package/dist/runtime/commands/repo-map.js +0 -95
  340. package/dist/runtime/commands/report.js +0 -299
  341. package/dist/runtime/commands/resume.js +0 -118
  342. package/dist/runtime/commands/review-consensus.js +0 -414
  343. package/dist/runtime/commands/rewind.js +0 -333
  344. package/dist/runtime/commands/roster.js +0 -117
  345. package/dist/runtime/commands/sessions.js +0 -163
  346. package/dist/runtime/commands/share.js +0 -316
  347. package/dist/runtime/commands/skills.js +0 -401
  348. package/dist/runtime/commands/status.js +0 -186
  349. package/dist/runtime/commands/stickers.js +0 -82
  350. package/dist/runtime/commands/style.js +0 -194
  351. package/dist/runtime/commands/theme.js +0 -196
  352. package/dist/runtime/commands/undo.js +0 -361
  353. package/dist/runtime/commands/update.js +0 -289
  354. package/dist/runtime/commands/vim.js +0 -140
  355. package/dist/runtime/commands/worktree.js +0 -177
  356. package/dist/runtime/commands/worktrees.js +0 -155
  357. package/dist/runtime/deprecation-warning.js +0 -69
  358. package/dist/runtime/engine-exit-code.js +0 -50
  359. package/dist/runtime/headless-repl.js +0 -195
  360. package/dist/runtime/headless.js +0 -548
  361. package/dist/runtime/load-hooks-or-exit.js +0 -71
  362. package/dist/runtime/plan-decompose.js +0 -531
  363. package/dist/runtime/sigint-guard.js +0 -272
  364. package/dist/runtime/stream-renderer.js +0 -195
  365. package/dist/runtime/update-check.js +0 -294
  366. package/dist/runtime/version.js +0 -65
  367. package/dist/runtime/worktree-bootstrap.js +0 -579
  368. package/dist/skills/bundled/batch.js +0 -617
  369. package/dist/skills/bundled/index.js +0 -45
  370. package/dist/skills/bundled/loop.js +0 -358
  371. package/dist/skills/bundled/remember.js +0 -383
  372. package/dist/skills/bundled/simplify.js +0 -289
  373. package/dist/skills/bundled/skillify.js +0 -373
  374. package/dist/skills/bundled/stuck.js +0 -558
  375. package/dist/skills/bundled/verify.js +0 -439
  376. package/dist/testing/vcr.js +0 -486
  377. package/dist/tools/agent-tool.js +0 -229
  378. package/dist/tools/apply-patch.js +0 -556
  379. package/dist/tools/ask-user-question.js +0 -337
  380. package/dist/tools/ask-user.js +0 -115
  381. package/dist/tools/bash.js +0 -1238
  382. package/dist/tools/brief.js +0 -224
  383. package/dist/tools/cron.js +0 -433
  384. package/dist/tools/enter-worktree.js +0 -250
  385. package/dist/tools/exit-worktree.js +0 -147
  386. package/dist/tools/file-tools.js +0 -553
  387. package/dist/tools/http-request.js +0 -336
  388. package/dist/tools/lsp-tools.js +0 -565
  389. package/dist/tools/mcp-tool.js +0 -260
  390. package/dist/tools/multi-edit.js +0 -361
  391. package/dist/tools/powershell.js +0 -268
  392. package/dist/tools/registry.js +0 -166
  393. package/dist/tools/server-tools.js +0 -892
  394. package/dist/tools/skill-tool.js +0 -96
  395. package/dist/tools/sleep.js +0 -99
  396. package/dist/tools/synthetic-output.js +0 -133
  397. package/dist/tools/tasks.js +0 -208
  398. package/dist/tools/todo-write.js +0 -184
  399. package/dist/tools/verify-plan-execution.js +0 -295
  400. package/dist/tools/web-fetch-injection-scanner.js +0 -207
  401. package/dist/tools/web-fetch.js +0 -720
  402. package/dist/tools/web-search.js +0 -458
  403. package/dist/tui/agent-progress-card.js +0 -111
  404. package/dist/tui/agent-tree-pane.js +0 -9
  405. package/dist/tui/agent-tree.js +0 -87
  406. package/dist/tui/ask-cli.js +0 -52
  407. package/dist/tui/ask-modal.js +0 -211
  408. package/dist/tui/ask-user-question-chips.js +0 -315
  409. package/dist/tui/ask-user-question-prompt.js +0 -203
  410. package/dist/tui/compact-banner.js +0 -81
  411. package/dist/tui/conversation-pane.js +0 -164
  412. package/dist/tui/cost-table.js +0 -111
  413. package/dist/tui/device-flow.js +0 -142
  414. package/dist/tui/doctor-table.js +0 -46
  415. package/dist/tui/feedback-prompt.js +0 -156
  416. package/dist/tui/input-box.js +0 -732
  417. package/dist/tui/login-picker.js +0 -69
  418. package/dist/tui/markdown-render.js +0 -266
  419. package/dist/tui/multi-file-diff-approval.js +0 -375
  420. package/dist/tui/onboarding-wizard.js +0 -240
  421. package/dist/tui/permissions-picker.js +0 -86
  422. package/dist/tui/render.js +0 -160
  423. package/dist/tui/repl-render.js +0 -770
  424. package/dist/tui/repl-splash-art.js +0 -64
  425. package/dist/tui/repl-splash-mascot.js +0 -154
  426. package/dist/tui/repl-splash.js +0 -117
  427. package/dist/tui/repl.js +0 -378
  428. package/dist/tui/slash-palette.js +0 -106
  429. package/dist/tui/splash-data.js +0 -61
  430. package/dist/tui/splash.js +0 -31
  431. package/dist/tui/status-bar.js +0 -209
  432. package/dist/tui/status-table.js +0 -7
  433. package/dist/tui/stickers-art.js +0 -136
  434. package/dist/tui/style-table.js +0 -28
  435. package/dist/tui/theme-table.js +0 -29
  436. package/dist/tui/thinking-spinner.js +0 -123
  437. package/dist/tui/tool-stream-pane.js +0 -140
  438. package/dist/tui/update-banner.js +0 -33
  439. package/dist/tui/vim-input.js +0 -267
  440. package/dist/tui/welcome-banner.js +0 -107
  441. package/dist/tui/welcome-data.js +0 -293
  442. package/dist/tui/workspace-context.js +0 -105
  443. package/docs/examples/codegraph.mcp.json +0 -10
  444. package/test/scenarios/codegen-create-file.scenario.txt +0 -13
  445. package/test/scenarios/compact-force.scenario.txt +0 -12
  446. package/test/scenarios/identity.scenario.txt +0 -11
  447. package/test/scenarios/persona-handoff.scenario.txt +0 -12
  448. package/test/scenarios/walkback.scenario.txt +0 -12
@@ -1,1238 +0,0 @@
1
- /**
2
- * Class-aware bash tool — Sprint .
3
- *
4
- * The agent loop invokes this tool through the registry name `bash`.
5
- * It supersedes `file-tools.ts::bashTool`, which used the legacy
6
- * blocklist gate. The tool-bridge wires this new entry point so the
7
- * registry entry (`registry.ts` `bash`) is not duplicated.
8
- *
9
- * Behavioural changes vs the legacy tool:
10
- * 1. Permission decision routes through `evaluateBashPermission`
11
- * (7-class taxonomy, mode-aware, destructive override gate).
12
- * 2. Output cap is 32 KB combined stdout+stderr per call (down
13
- * from 64 KB). Overflow is persisted to
14
- * `.pugi/artifacts/<sessionId>/bash-<callId>.out` with the path
15
- * returned as `artifactRef`.
16
- * 3. Cwd carry-over: the tool receives `cwd` from the previous
17
- * turn's session state and writes the new cwd back when the
18
- * command was a `cd <path>` that landed inside
19
- * `workspaceRoot ∪ additionalDirectories`. Escapes reset the
20
- * cwd to workspaceRoot and emit `bash.cwd_escape`.
21
- * 4. Background jobs: when `background: true`, spawn detached,
22
- * track in `~/.pugi/jobs.json`, return immediately with
23
- * `jobId`. `listJobs()` and `killJob(jobId)` are exported.
24
- * 5. 60s default timeout. SIGTERM at deadline, SIGKILL 5s later.
25
- * Emit `bash.timeout`.
26
- * 6. POSIX-only (`/bin/sh`). The non-goal in explicitly
27
- * drops Windows shell support for M1.
28
- */
29
- import { randomUUID } from 'node:crypto';
30
- import { appendFileSync, closeSync, existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from 'node:fs';
31
- import { homedir } from 'node:os';
32
- import { isAbsolute, join, resolve } from 'node:path';
33
- import { spawn, spawnSync } from 'node:child_process';
34
- import { classifyBash } from '../core/bash-classifier.js';
35
- import { applyRedirect, finaliseRedirectFile, normalizeTailLines, openRedirectFile, resolveRedirectTarget, } from '../core/bash/redirect.js';
36
- import { evaluateBashPermission } from '../core/permission.js';
37
- import { writeAuditEvent } from '../core/audit/audit-trail.js';
38
- import { getJobRegistry, } from '../core/jobs/registry.js';
39
- import { recordToolCall, recordToolResult } from '../core/session.js';
40
- export const BASH_OUTPUT_CAP_BYTES = 32 * 1024;
41
- export const BASH_DEFAULT_TIMEOUT_MS = 60_000;
42
- export const BASH_SIGKILL_GRACE_MS = 5_000;
43
- /**
44
- * Mid-stream cap. The 32 KB BASH_OUTPUT_CAP_BYTES is the report cap;
45
- * this is the in-memory ceiling beyond which we stop buffering and
46
- * SIGTERM the child to prevent a `yes`-style stream from pinning
47
- * 60+ MB before the timeout watchdog fires.
48
- *
49
- * Code Reviewer P1 retro: the async path previously
50
- * accumulated stdout chunks without bound; only spawnSync had a
51
- * 10 MB maxBuffer ceiling. Aligning the async path closes the gap.
52
- */
53
- export const BASH_LIVE_OUTPUT_CAP_BYTES = 1024 * 1024;
54
- /**
55
- * Bash tool entry point. Returns the standard shape the engine loop
56
- * consumes; throws only on argument-shape errors (e.g. negative
57
- * timeouts) and otherwise surfaces the failure through
58
- * `{ exitCode: 126, stderr }`.
59
- */
60
- export async function bashTool(input, ctx) {
61
- const cmd = input.cmd ?? '';
62
- const additionalDirectories = ctx.additionalDirectories ?? [];
63
- const source = ctx.source ?? 'agent';
64
- const toolCallId = recordToolCall(ctx.session, 'bash', cmd);
65
- // Cwd carry-over decision (also re-checked post-run).
66
- const startCwd = resolveStartCwd(input.cwd ?? ctx.lastBashCwd, ctx.root, additionalDirectories);
67
- // Workspace-git-boundary guard (CEO P0 #51).
68
- // Runs BEFORE the permission gate so the boundary escape message is
69
- // the one the operator/engine sees, regardless of permission policy.
70
- // The leak is structural (git silently writes to an ancestor .git
71
- // when the workspace lacks one), not a policy violation, so the
72
- // diagnostic must surface even when the permission gate would
73
- // otherwise have asked or auto-allowed.
74
- const boundaryBlock = enforceGitBoundary(cmd, startCwd, ctx.root);
75
- if (boundaryBlock !== null) {
76
- emitEvent(ctx.session, 'bash.git_boundary_escape', {
77
- cmd,
78
- workspaceRoot: ctx.root,
79
- resolvedToplevel: boundaryBlock.resolvedToplevel ?? null,
80
- });
81
- recordToolResult(ctx.session, toolCallId, 'error', boundaryBlock.reason);
82
- return {
83
- stdout: '',
84
- stderr: boundaryBlock.reason,
85
- exitCode: 126,
86
- nextCwd: ctx.lastBashCwd ?? ctx.root,
87
- truncated: false,
88
- timedOut: false,
89
- cancelled: false,
90
- };
91
- }
92
- // Permission gate via the new class-aware engine.
93
- const decision = evaluateBashPermission(cmd, ctx.settings.permissions.mode, {
94
- workspaceRoot: ctx.root,
95
- additionalDirectories,
96
- source,
97
- });
98
- if (decision.decision !== 'allow') {
99
- const reason = `Permission ${decision.decision}: ${decision.reason}`;
100
- recordToolResult(ctx.session, toolCallId, 'error', reason);
101
- // #21 : emit `permission_denied` to
102
- // the tenant-wide audit trail. Truncate the cmd preview to 200
103
- // chars so a long here-doc does not bloat the JSONL row; the
104
- // session log keeps the full text for forensic replay.
105
- writeAuditEvent({
106
- event: 'permission_denied',
107
- sessionId: ctx.session.id,
108
- workspaceRoot: ctx.root,
109
- data: {
110
- tool: 'bash',
111
- source,
112
- decision: decision.decision,
113
- reason: decision.reason,
114
- cmdPreview: cmd.slice(0, 200),
115
- },
116
- });
117
- return {
118
- stdout: '',
119
- stderr: `Permission denied: ${decision.reason}`,
120
- exitCode: 126,
121
- nextCwd: ctx.lastBashCwd ?? ctx.root,
122
- truncated: false,
123
- timedOut: false,
124
- cancelled: false,
125
- };
126
- }
127
- // CEO P1 #25 — pre-spawn cancellation check. Fires
128
- // AFTER the permission gate so a cancelled brief never reaches
129
- // /bin/sh even when the command would have been allowed. Mirrors
130
- // the `gateOnCancellation` pattern from file-tools.ts.
131
- if (ctx.cancellation?.isAborted === true) {
132
- const reason = 'operator_aborted: bash refused before spawn';
133
- emitEvent(ctx.session, 'bash.cancelled', { cmd, phase: 'pre_spawn' });
134
- recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
135
- return {
136
- stdout: '',
137
- stderr: reason,
138
- exitCode: 130,
139
- nextCwd: ctx.lastBashCwd ?? ctx.root,
140
- truncated: false,
141
- timedOut: false,
142
- cancelled: true,
143
- };
144
- }
145
- // Background job branch.
146
- if (input.background === true) {
147
- return runBackground({ cmd, ctx, toolCallId, startCwd, additionalDirectories });
148
- }
149
- // Foreground branch with timeout watchdog.
150
- const timeoutMs = sanitizeTimeout(input.timeoutMs);
151
- const childEnv = buildChildEnv();
152
- // Pugi backlog P2 — redirect path. When the caller opted into
153
- // stdout redirect, we open a write-only fd at the resolved log
154
- // path and hand it directly to the child's stdio array so the
155
- // child writes through the kernel pipe → file fd without buffering
156
- // hundreds of MB in the Node process. The buffered code path below
157
- // is the fallback for callers that did not opt in.
158
- let redirectState = null;
159
- if (input.redirect !== undefined) {
160
- try {
161
- const target = resolveRedirectTarget({
162
- workspaceRoot: ctx.root,
163
- sessionId: ctx.session.id,
164
- toolCallId,
165
- command: cmd,
166
- override: input.redirect.path,
167
- });
168
- const { fd, tempPath } = openRedirectFile(target);
169
- redirectState = {
170
- target,
171
- fd,
172
- tempPath,
173
- tailLines: normalizeTailLines(input.redirect.tailLines),
174
- };
175
- }
176
- catch (error) {
177
- // Bad caller-supplied path (absolute, traversal escape). Fall
178
- // back to a structured error rather than crashing the engine
179
- // loop. Mirrors how the permission gate surfaces a refusal —
180
- // the model can adjust the redirect spec and retry.
181
- const reason = `redirect refused: ${error.message}`;
182
- recordToolResult(ctx.session, toolCallId, 'error', reason);
183
- return {
184
- stdout: '',
185
- stderr: reason,
186
- exitCode: 126,
187
- nextCwd: ctx.lastBashCwd ?? ctx.root,
188
- truncated: false,
189
- timedOut: false,
190
- cancelled: false,
191
- };
192
- }
193
- }
194
- // POSIX-only `/bin/sh -c <cmd>`. The non-goals explicitly
195
- // exclude Windows for M1.
196
- //
197
- // stdio layout:
198
- // - default: ['ignore', 'pipe', 'pipe'] — buffer chunks in
199
- // Node so the post-run capToCombined can size them
200
- // to the report cap.
201
- // - redirect: ['ignore', fd, fd] — kernel pipes stdout+stderr
202
- // straight into the log file fd. No Node-side
203
- // buffering, no truncation marker, no in-memory
204
- // ceiling. The tail-reader fishes the trailing
205
- // lines out of the file after the child exits.
206
- const stdioLayout = redirectState !== null
207
- ? ['ignore', redirectState.fd, redirectState.fd]
208
- : ['ignore', 'pipe', 'pipe'];
209
- const child = spawn('/bin/sh', ['-c', cmd], {
210
- cwd: startCwd,
211
- env: childEnv,
212
- stdio: stdioLayout,
213
- detached: false,
214
- });
215
- const stdoutChunks = [];
216
- const stderrChunks = [];
217
- let stdoutBytes = 0;
218
- let stderrBytes = 0;
219
- // We keep collecting beyond the report cap (BASH_OUTPUT_CAP_BYTES)
220
- // for the artifact-overflow file but flag `truncated` so the
221
- // agent-facing payload is the head. To prevent a runaway producer
222
- // (`yes`, `cat /dev/urandom`) from pinning hundreds of megabytes
223
- // before the timeout watchdog fires, we enforce a live ceiling
224
- // (BASH_LIVE_OUTPUT_CAP_BYTES) and SIGTERM the child when crossed.
225
- let truncatedMidStream = false;
226
- // CEO P1 #25 — mid-stream operator cancellation. The
227
- // listener registered against the CancellationToken below flips
228
- // this flag and SIGTERMs the child. The close handler reads it to
229
- // decide between `cancelled` (operator abort) and `timedOut`
230
- // (watchdog).
231
- let cancelledMidStream = false;
232
- const enforceLiveCap = () => {
233
- if (truncatedMidStream)
234
- return;
235
- if (stdoutBytes + stderrBytes <= BASH_LIVE_OUTPUT_CAP_BYTES)
236
- return;
237
- truncatedMidStream = true;
238
- try {
239
- child.kill('SIGTERM');
240
- }
241
- catch {
242
- // child already exited; the close handler will run
243
- }
244
- };
245
- // CEO P1 #25 — live stream callback. When the REPL
246
- // host wires `onStreamChunk`, we forward each stdout/stderr chunk
247
- // in real time so the conversation pane / tool-stream pane paint
248
- // bytes as they arrive instead of waiting for the child to exit.
249
- // We invoke the callback inside a try/catch so a buggy sink
250
- // (renderer crash, assertion error) never escalates to killing
251
- // the bash dispatch. The buffered path below still captures the
252
- // chunk so the model + audit trail stay consistent regardless of
253
- // renderer health.
254
- const onStreamChunk = ctx.onStreamChunk;
255
- const emitStreamChunk = onStreamChunk
256
- ? (stream, chunk) => {
257
- try {
258
- onStreamChunk({ stream, data: chunk.toString('utf8') });
259
- }
260
- catch {
261
- // Sink crash — swallow.
262
- }
263
- }
264
- : null;
265
- // When redirect is on, child.stdout / child.stderr are null
266
- // because the spawn handed the log-file fd in directly. The data
267
- // listeners only fire on the buffered path, which is exactly what
268
- // we want — the redirect contract is "no in-memory buffer, full
269
- // output goes to disk".
270
- if (redirectState === null) {
271
- child.stdout?.on('data', (chunk) => {
272
- if (truncatedMidStream || cancelledMidStream)
273
- return;
274
- stdoutChunks.push(chunk);
275
- stdoutBytes += chunk.length;
276
- if (emitStreamChunk)
277
- emitStreamChunk('stdout', chunk);
278
- enforceLiveCap();
279
- });
280
- child.stderr?.on('data', (chunk) => {
281
- if (truncatedMidStream || cancelledMidStream)
282
- return;
283
- stderrChunks.push(chunk);
284
- stderrBytes += chunk.length;
285
- if (emitStreamChunk)
286
- emitStreamChunk('stderr', chunk);
287
- enforceLiveCap();
288
- });
289
- }
290
- // CEO P1 #25 — wire the cancellation token to SIGTERM. We track
291
- // the detach handle so a successful run releases the listener
292
- // instead of leaving it pinned to a long-lived REPL
293
- // CancellationToken (same anti-leak pattern as
294
- // native-pugi.ts:262).
295
- let detachCancelListener;
296
- if (ctx.cancellation && !ctx.cancellation.isAborted) {
297
- const onAbort = () => {
298
- if (cancelledMidStream)
299
- return;
300
- cancelledMidStream = true;
301
- emitEvent(ctx.session, 'bash.cancelled', { cmd, phase: 'mid_stream' });
302
- try {
303
- child.kill('SIGTERM');
304
- }
305
- catch {
306
- // child already exited; close handler will run
307
- }
308
- // SIGKILL escalation if the child does not honour SIGTERM
309
- // within the grace window. Mirrors the timeout watchdog's
310
- // two-phase shutdown.
311
- setTimeout(() => {
312
- if (child.exitCode !== null || child.signalCode !== null)
313
- return;
314
- try {
315
- child.kill('SIGKILL');
316
- }
317
- catch {
318
- // gone between the check and the signal
319
- }
320
- }, BASH_SIGKILL_GRACE_MS).unref();
321
- };
322
- detachCancelListener = ctx.cancellation.onAbort(onAbort);
323
- }
324
- const timeoutOutcome = await waitWithTimeout(child, timeoutMs);
325
- // Detach the cancellation listener on completion so a long-lived
326
- // REPL token does not retain a reference to the dead child + this
327
- // closure.
328
- if (detachCancelListener) {
329
- try {
330
- detachCancelListener();
331
- }
332
- catch { /* listener already drained */ }
333
- }
334
- // Pugi backlog P2 — redirect path. Close the log fd, rename
335
- // the temp file into place, and return the envelope before the
336
- // buffered-path code paths run. We do this for every exit shape
337
- // (success, non-zero, timeout, cancel) so the log file always
338
- // lands on disk and the tail reflects whatever the child produced
339
- // before termination. The cancel/timeout branches still surface
340
- // the appropriate exitCode through the envelope; the operator
341
- // discovers the failure via `tail` + `exitCode`, not via the
342
- // legacy stdout/stderr strings.
343
- if (redirectState !== null) {
344
- // Close our copy of the fd before rename so the inode is no
345
- // longer held open by the parent process. The child's stdio
346
- // already inherited a separate fd; closing ours does not affect
347
- // the child's writes that already happened.
348
- try {
349
- closeSync(redirectState.fd);
350
- }
351
- catch {
352
- // already closed (shouldn't happen on the happy path)
353
- }
354
- try {
355
- finaliseRedirectFile(redirectState.target, redirectState.tempPath);
356
- }
357
- catch {
358
- // best-effort — the temp file still exists on disk for the
359
- // operator to inspect even if the rename failed.
360
- }
361
- const redirectExitCode = cancelledMidStream
362
- ? 130
363
- : timeoutOutcome.timedOut
364
- ? 124
365
- : timeoutOutcome.exitCode;
366
- const envelope = applyRedirect({
367
- target: redirectState.target,
368
- exitCode: redirectExitCode,
369
- tailLines: redirectState.tailLines,
370
- });
371
- const nextCwdRedirect = computeNextCwd(cmd, startCwd, ctx.root, additionalDirectories, ctx.session);
372
- // Emit the same lifecycle events the buffered path emits so the
373
- // session audit trail is symmetric across redirect vs non-redirect
374
- // dispatches.
375
- if (cancelledMidStream) {
376
- recordToolResult(ctx.session, toolCallId, 'cancelled', `operator_aborted: bash killed mid-stream (redirect=${envelope.logPath})`);
377
- }
378
- else if (timeoutOutcome.timedOut) {
379
- emitEvent(ctx.session, 'bash.timeout', { cmd, timeoutMs });
380
- recordToolResult(ctx.session, toolCallId, 'error', `bash timed out after ${timeoutMs}ms (redirect=${envelope.logPath})`);
381
- }
382
- else {
383
- recordToolResult(ctx.session, toolCallId, 'success', `bash exit=${redirectExitCode} redirect=${envelope.logPath}`);
384
- }
385
- return {
386
- stdout: envelope.stdout,
387
- stderr: envelope.stderr,
388
- exitCode: redirectExitCode,
389
- nextCwd: nextCwdRedirect,
390
- truncated: envelope.truncated,
391
- timedOut: timeoutOutcome.timedOut,
392
- cancelled: cancelledMidStream,
393
- logPath: envelope.logPath,
394
- tail: envelope.tail,
395
- };
396
- }
397
- const stdoutFull = Buffer.concat(stdoutChunks).toString('utf8');
398
- const stderrFull = Buffer.concat(stderrChunks).toString('utf8');
399
- const combinedBytes = stdoutBytes + stderrBytes;
400
- const truncated = combinedBytes > BASH_OUTPUT_CAP_BYTES || truncatedMidStream;
401
- // Cwd carry-over: detect `cd <path> && <rest>` shapes from the
402
- // command itself (we cannot observe the child's final cwd without
403
- // a wrapper script). The classifier already flagged escapes; we
404
- // re-validate here against allowed roots.
405
- const nextCwd = computeNextCwd(cmd, startCwd, ctx.root, additionalDirectories, ctx.session);
406
- // Overflow artifact when needed.
407
- let artifactRef;
408
- let stdoutOut = stdoutFull;
409
- let stderrOut = stderrFull;
410
- if (truncated) {
411
- artifactRef = persistOverflow({
412
- root: ctx.root,
413
- sessionId: ctx.session.id,
414
- toolCallId,
415
- stdout: stdoutFull,
416
- stderr: stderrFull,
417
- });
418
- stdoutOut = capToCombined(stdoutFull, stderrFull).stdout;
419
- stderrOut = capToCombined(stdoutFull, stderrFull).stderr;
420
- }
421
- // CEO P1 #25 — cancellation wins races against timeout / cap
422
- // overflow. The token already aborted by the time the close
423
- // handler fires; we distinguish operator-driven termination from
424
- // the watchdog so the REPL transcript reads "Aborted." rather
425
- // than "Timed out."
426
- if (cancelledMidStream) {
427
- const reason = 'operator_aborted: bash killed mid-stream';
428
- recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
429
- return {
430
- stdout: stdoutOut,
431
- stderr: stderrOut === '' ? reason : `${stderrOut}\n${reason}`,
432
- exitCode: 130,
433
- artifactRef,
434
- nextCwd,
435
- truncated,
436
- timedOut: false,
437
- cancelled: true,
438
- };
439
- }
440
- if (truncatedMidStream) {
441
- // We killed the child because output cap exceeded mid-stream.
442
- // Report that as the failure cause rather than as a timeout —
443
- // the watchdog never fired, only our cap enforcer did.
444
- const reason = `bash output cap exceeded mid-stream (cap=${BASH_LIVE_OUTPUT_CAP_BYTES} bytes)`;
445
- emitEvent(ctx.session, 'bash.output_cap_exceeded', {
446
- cmd,
447
- capBytes: BASH_LIVE_OUTPUT_CAP_BYTES,
448
- });
449
- recordToolResult(ctx.session, toolCallId, 'error', reason);
450
- return {
451
- stdout: stdoutOut,
452
- stderr: stderrOut === '' ? reason : `${stderrOut}\n${reason}`,
453
- exitCode: 137,
454
- artifactRef,
455
- nextCwd,
456
- truncated: true,
457
- timedOut: false,
458
- cancelled: false,
459
- };
460
- }
461
- if (timeoutOutcome.timedOut) {
462
- emitEvent(ctx.session, 'bash.timeout', { cmd, timeoutMs });
463
- recordToolResult(ctx.session, toolCallId, 'error', `bash timed out after ${timeoutMs}ms`);
464
- return {
465
- stdout: stdoutOut,
466
- stderr: stderrOut === '' ? `bash timed out after ${timeoutMs}ms` : `${stderrOut}\nbash timed out after ${timeoutMs}ms`,
467
- exitCode: 124,
468
- artifactRef,
469
- nextCwd,
470
- truncated,
471
- timedOut: true,
472
- cancelled: false,
473
- };
474
- }
475
- const exitCode = timeoutOutcome.exitCode;
476
- recordToolResult(ctx.session, toolCallId, 'success', `bash exit=${exitCode} bytes=${combinedBytes}${artifactRef ? ` overflow=${artifactRef}` : ''}`);
477
- return {
478
- stdout: stdoutOut,
479
- stderr: stderrOut,
480
- exitCode,
481
- artifactRef,
482
- nextCwd,
483
- truncated,
484
- timedOut: false,
485
- cancelled: false,
486
- };
487
- }
488
- function sanitizeTimeout(value) {
489
- if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0) {
490
- return BASH_DEFAULT_TIMEOUT_MS;
491
- }
492
- // Cap user-supplied timeouts at 15 minutes so a runaway tool call
493
- // cannot wedge the engine loop.
494
- return Math.min(value, 15 * 60 * 1000);
495
- }
496
- function buildChildEnv() {
497
- const childEnv = {};
498
- const SAFE_ENV_ALLOW = new Set([
499
- 'PATH',
500
- 'HOME',
501
- 'USER',
502
- 'LOGNAME',
503
- 'SHELL',
504
- 'LANG',
505
- 'TZ',
506
- 'TERM',
507
- 'PWD',
508
- ]);
509
- for (const [key, value] of Object.entries(process.env)) {
510
- if (value === undefined)
511
- continue;
512
- if (SAFE_ENV_ALLOW.has(key) || key.startsWith('LC_')) {
513
- childEnv[key] = value;
514
- }
515
- }
516
- return childEnv;
517
- }
518
- function resolveStartCwd(requested, root, additionalDirectories) {
519
- if (!requested)
520
- return root;
521
- const absolute = isAbsolute(requested) ? requested : resolve(root, requested);
522
- const allowedRoots = [root, ...additionalDirectories];
523
- for (const allowedRaw of allowedRoots) {
524
- const allowed = allowedRaw.endsWith('/') ? allowedRaw.slice(0, -1) : allowedRaw;
525
- if (absolute === allowed || absolute.startsWith(`${allowed}/`)) {
526
- return absolute;
527
- }
528
- }
529
- return root;
530
- }
531
- function computeNextCwd(cmd, startCwd, root, additionalDirectories, session) {
532
- // We mirror the classifier's view: only `cd <path>` at the head of
533
- // the command updates cwd for the next turn. We do not attempt to
534
- // chase `cd` calls that fired inside subshells or compound chains
535
- // (`(cd foo && ls)` leaves the parent cwd untouched).
536
- const firstComponent = cmd.trim().split(/\s*(?:&&|\|\||;|\|)\s*/)[0]?.trim() ?? '';
537
- const match = firstComponent.match(/^cd(?:\s+(\S+))?\s*$/);
538
- if (!match)
539
- return startCwd;
540
- const target = match[1];
541
- if (target === undefined || target === '-' || target === '~') {
542
- emitEvent(session, 'bash.cwd_escape', { cmd, reason: 'cd to HOME or last dir' });
543
- return root;
544
- }
545
- const resolved = isAbsolute(target) || target.startsWith('~')
546
- ? resolve(target.startsWith('~') ? target.replace(/^~/, homedir()) : target)
547
- : resolve(startCwd, target);
548
- const allowedRoots = [root, ...additionalDirectories];
549
- for (const allowedRaw of allowedRoots) {
550
- const allowed = allowedRaw.endsWith('/') ? allowedRaw.slice(0, -1) : allowedRaw;
551
- if (resolved === allowed || resolved.startsWith(`${allowed}/`)) {
552
- return resolved;
553
- }
554
- }
555
- emitEvent(session, 'bash.cwd_escape', { cmd, reason: 'cd target outside workspace' });
556
- return root;
557
- }
558
- function emitEvent(session, name, body) {
559
- if (!session.enabled)
560
- return;
561
- const line = JSON.stringify({
562
- id: randomUUID(),
563
- sessionId: session.id,
564
- timestamp: new Date().toISOString(),
565
- type: 'bash',
566
- name,
567
- ...body,
568
- });
569
- try {
570
- appendFileSync(session.eventsPath, `${line}\n`, { encoding: 'utf8', mode: 0o600 });
571
- }
572
- catch {
573
- // Event log is best-effort; never crash the tool because of it.
574
- }
575
- }
576
- function persistOverflow(input) {
577
- const dir = join(input.root, '.pugi', 'artifacts', input.sessionId);
578
- try {
579
- mkdirSync(dir, { recursive: true });
580
- const path = join(dir, `bash-${input.toolCallId}.out`);
581
- const body = `--- stdout ---\n${input.stdout}\n--- stderr ---\n${input.stderr}\n`;
582
- writeFileSync(path, body, { encoding: 'utf8', mode: 0o600 });
583
- return path;
584
- }
585
- catch {
586
- return '';
587
- }
588
- }
589
- function capToCombined(stdout, stderr) {
590
- // Split the budget proportionally so the head of each stream is
591
- // preserved. When one stream is empty the other gets the full
592
- // budget.
593
- if (stdout.length + stderr.length <= BASH_OUTPUT_CAP_BYTES) {
594
- return { stdout, stderr };
595
- }
596
- if (stdout.length === 0) {
597
- return { stdout: '', stderr: trimWithMarker(stderr, BASH_OUTPUT_CAP_BYTES) };
598
- }
599
- if (stderr.length === 0) {
600
- return { stdout: trimWithMarker(stdout, BASH_OUTPUT_CAP_BYTES), stderr: '' };
601
- }
602
- const total = stdout.length + stderr.length;
603
- const stdoutBudget = Math.max(1024, Math.floor((stdout.length / total) * BASH_OUTPUT_CAP_BYTES));
604
- const stderrBudget = BASH_OUTPUT_CAP_BYTES - stdoutBudget;
605
- return {
606
- stdout: trimWithMarker(stdout, stdoutBudget),
607
- stderr: trimWithMarker(stderr, stderrBudget),
608
- };
609
- }
610
- function trimWithMarker(text, budget) {
611
- if (text.length <= budget)
612
- return text;
613
- return `${text.slice(0, budget)}\n(...truncated at ${budget} bytes; full output in artifactRef)`;
614
- }
615
- async function waitWithTimeout(child, timeoutMs) {
616
- return await new Promise((resolvePromise) => {
617
- let settled = false;
618
- const sigtermTimer = setTimeout(() => {
619
- if (settled)
620
- return;
621
- try {
622
- child.kill('SIGTERM');
623
- }
624
- catch {
625
- // child already exited
626
- }
627
- const sigkillTimer = setTimeout(() => {
628
- if (settled)
629
- return;
630
- try {
631
- child.kill('SIGKILL');
632
- }
633
- catch {
634
- // already gone
635
- }
636
- }, BASH_SIGKILL_GRACE_MS);
637
- sigkillTimer.unref();
638
- }, timeoutMs);
639
- sigtermTimer.unref();
640
- const onClose = (code, signal) => {
641
- if (settled)
642
- return;
643
- settled = true;
644
- clearTimeout(sigtermTimer);
645
- if (signal === 'SIGTERM' || signal === 'SIGKILL') {
646
- // Heuristic: if we sent the signal because of the timer,
647
- // report timeout. If the child raced and exited with the
648
- // signal anyway (e.g. user pressed ^C through SIGINT trap)
649
- // we still report timeout when the wall-clock crossed.
650
- resolvePromise({ timedOut: true, exitCode: 124 });
651
- return;
652
- }
653
- resolvePromise({ timedOut: false, exitCode: code ?? 1 });
654
- };
655
- child.on('close', onClose);
656
- child.on('error', () => {
657
- if (settled)
658
- return;
659
- settled = true;
660
- clearTimeout(sigtermTimer);
661
- resolvePromise({ timedOut: false, exitCode: 1 });
662
- });
663
- });
664
- }
665
- function runBackground(input) {
666
- const { cmd, ctx, toolCallId, startCwd } = input;
667
- const childEnv = buildChildEnv();
668
- const child = spawn('/bin/sh', ['-c', cmd], {
669
- cwd: startCwd,
670
- env: childEnv,
671
- stdio: 'ignore',
672
- detached: true,
673
- });
674
- child.unref();
675
- const jobId = `pj-${randomUUID()}`;
676
- const classification = classifyBash(cmd, {
677
- workspaceRoot: ctx.root,
678
- additionalDirectories: input.additionalDirectories,
679
- });
680
- const registry = getJobRegistry();
681
- // Persist into the new registry. The promise is intentionally not
682
- // awaited — `runBackground` is a synchronous control path inside the
683
- // bash tool's async wrapper. The registry's atomic write is
684
- // synchronous under the hood so the ledger is consistent before the
685
- // caller observes the returned jobId, even when we drop the
686
- // promise. Wire as a `.catch` so an unhandled-rejection never
687
- // crashes the engine loop.
688
- void registry
689
- .add({
690
- id: jobId,
691
- pid: child.pid ?? -1,
692
- command: cmd,
693
- bashClass: classification.class,
694
- cwd: startCwd,
695
- sessionId: ctx.session.id,
696
- })
697
- .catch(() => {
698
- // Best-effort persistence; the in-process tool still returned
699
- // the jobId so the engine loop knows the spawn succeeded.
700
- });
701
- emitEvent(ctx.session, 'bash.background_started', {
702
- jobId,
703
- pid: child.pid ?? -1,
704
- cmd,
705
- });
706
- recordToolResult(ctx.session, toolCallId, 'success', `bash background jobId=${jobId} pid=${child.pid ?? -1}`);
707
- return {
708
- stdout: `bash started in background as ${jobId}`,
709
- stderr: '',
710
- exitCode: 0,
711
- jobId,
712
- nextCwd: ctx.lastBashCwd ?? ctx.root,
713
- truncated: false,
714
- timedOut: false,
715
- cancelled: false,
716
- };
717
- }
718
- /**
719
- * Legacy export preserved for callers / tests. Delegates to the
720
- * new JobRegistry and projects entries back into the historical
721
- * `PugiJob` shape.
722
- */
723
- export function listJobs() {
724
- const entries = readRegistryEntriesSync();
725
- return entries.map(entryToLegacyJob);
726
- }
727
- /**
728
- * Legacy export preserved for callers / tests. Delegates to the
729
- * new JobRegistry. Returns the same `{ killed, reason? }` shape so the
730
- * existing bash-tool test suite continues to pass without an
731
- * end-to-end rewrite.
732
- */
733
- export function killJob(jobId) {
734
- const entries = readRegistryEntriesSync();
735
- const target = entries.find((entry) => entry.id === jobId);
736
- if (!target)
737
- return { killed: false, reason: `unknown jobId: ${jobId}` };
738
- // Mirror the legacy semantics: synchronous SIGTERM + best-effort
739
- // SIGKILL escalation + remove the entry from the ledger so the
740
- // bash-tool test suite's `listJobs().find(...) === undefined`
741
- // assertion keeps holding. The richer `JobRegistry.kill` (status
742
- // transitions, async exit polling) is what the new `pugi jobs kill`
743
- // CLI command uses.
744
- try {
745
- process.kill(target.pid, 'SIGTERM');
746
- }
747
- catch (error) {
748
- const code = error.code;
749
- if (code === 'ESRCH') {
750
- removeRegistryEntrySync(jobId);
751
- return { killed: false, reason: 'job already exited' };
752
- }
753
- return { killed: false, reason: `kill failed: ${error.message}` };
754
- }
755
- setTimeout(() => {
756
- try {
757
- process.kill(target.pid, 0);
758
- try {
759
- process.kill(target.pid, 'SIGKILL');
760
- }
761
- catch {
762
- // gone between the check and the signal
763
- }
764
- }
765
- catch {
766
- // already dead
767
- }
768
- }, BASH_SIGKILL_GRACE_MS).unref();
769
- removeRegistryEntrySync(jobId);
770
- return { killed: true };
771
- }
772
- function entryToLegacyJob(entry) {
773
- return {
774
- jobId: entry.id,
775
- pid: entry.pid,
776
- cwd: entry.cwd,
777
- cmd: entry.command,
778
- class: entry.bashClass,
779
- startedAt: entry.startedAt,
780
- sessionId: entry.sessionId,
781
- };
782
- }
783
- /**
784
- * Synchronous read of the registry file. Used by the legacy
785
- * `listJobs()` / `killJob()` exports because they cannot return a
786
- * promise without a breaking signature change. The new `JobRegistry`
787
- * interface is the async path.
788
- */
789
- function readRegistryEntriesSync() {
790
- const path = join(homedir(), '.pugi', 'jobs.json');
791
- // Inline read so the legacy listJobs/killJob entry points do not
792
- // require an async hop into JobRegistry. The shape parsing mirrors
793
- // `normalizeEntry` inside `core/jobs/registry.ts`.
794
- if (!existsSync(path))
795
- return [];
796
- try {
797
- const raw = readFileSync(path, 'utf8');
798
- if (raw.trim() === '')
799
- return [];
800
- const parsed = JSON.parse(raw);
801
- if (!Array.isArray(parsed))
802
- return [];
803
- const out = [];
804
- for (const candidate of parsed) {
805
- if (typeof candidate !== 'object' || candidate === null)
806
- continue;
807
- const c = candidate;
808
- const id = typeof c['id'] === 'string'
809
- ? c['id']
810
- : typeof c['jobId'] === 'string'
811
- ? c['jobId']
812
- : undefined;
813
- const pid = typeof c['pid'] === 'number' ? c['pid'] : undefined;
814
- const command = typeof c['command'] === 'string'
815
- ? c['command']
816
- : typeof c['cmd'] === 'string'
817
- ? c['cmd']
818
- : undefined;
819
- if (!id || pid === undefined || command === undefined)
820
- continue;
821
- const bashClassRaw = typeof c['bashClass'] === 'string'
822
- ? c['bashClass']
823
- : typeof c['class'] === 'string'
824
- ? c['class']
825
- : 'unknown';
826
- const status = c['status'] === 'finished' ||
827
- c['status'] === 'killed' ||
828
- c['status'] === 'failed' ||
829
- c['status'] === 'abandoned'
830
- ? c['status']
831
- : 'running';
832
- out.push({
833
- id,
834
- pid,
835
- command,
836
- bashClass: bashClassRaw,
837
- cwd: typeof c['cwd'] === 'string' ? c['cwd'] : '',
838
- startedAt: typeof c['startedAt'] === 'string'
839
- ? c['startedAt']
840
- : new Date().toISOString(),
841
- status,
842
- sessionId: typeof c['sessionId'] === 'string' ? c['sessionId'] : 'unknown',
843
- });
844
- }
845
- return out;
846
- }
847
- catch {
848
- return [];
849
- }
850
- }
851
- /**
852
- * Workspace-git-boundary guard (CEO P0 #51).
853
- *
854
- * Background: CEO live REPL surfaced a scenario where the customer
855
- * workspace dir was created INSIDE another git repository (the Pugi
856
- * monorepo itself). The model emitted `git init && git add . && git
857
- * commit -m ...` against that workspace. The workspace had no `.git`
858
- * of its own so git silently walked up to the outer repo's `.git` and
859
- * committed the customer's files directly to the monorepo's main
860
- * branch. Had the outer remote been FF-permissive, those files would
861
- * have pushed to production. This is a customer-of-customer leak.
862
- *
863
- * The guard: when the agent emits a mutating git op (add / commit /
864
- * push / rebase / reset / checkout) and the effective git toplevel
865
- * (`git -C $cwd rev-parse --show-toplevel`) sits OUTSIDE the workspace
866
- * root, block the command. The model is steered (via the persona
867
- * prompt) to run `git init` first; the guard is the defensive net so
868
- * a careless model emission cannot cross the boundary.
869
- *
870
- * Exported so the spec can exercise the predicate in isolation without
871
- * having to drive the whole bash tool.
872
- */
873
- export const GIT_BOUNDARY_BLOCK_PREFIX = 'git boundary escape:';
874
- /**
875
- * Subcommands we treat as definitely mutating for the boundary check.
876
- * We intentionally OMIT subcommands that have common read-only modes
877
- * (`branch --list`, `tag --list`, `stash list`, `remote -v`) to keep
878
- * the guard precise. The CEO P0 #51 leak vector is files written to
879
- * an ancestor repo's working tree / refs, which the included set
880
- * fully covers. The omitted subcommands can still create refs in the
881
- * outer .git, but they do not move customer files into the outer
882
- * repo's commit graph, so the leak severity is lower and the
883
- * ergonomic cost of false positives on `--list` flags is higher.
884
- */
885
- const MUTATING_GIT_SUBCOMMANDS = new Set([
886
- 'add',
887
- 'commit',
888
- 'push',
889
- 'rebase',
890
- 'reset',
891
- 'checkout',
892
- 'merge',
893
- 'restore',
894
- 'switch',
895
- 'cherry-pick',
896
- 'am',
897
- 'apply',
898
- 'clean',
899
- 'rm',
900
- 'mv',
901
- ]);
902
- /**
903
- * Inspect a shell command for mutating git operations. Returns the
904
- * first matching subcommand (e.g. 'commit') or null when none of the
905
- * components are mutating git ops.
906
- *
907
- * We split on `&&`, `||`, `;`, `|` so a compound like
908
- * `mkdir foo && cd foo && git add .` is correctly flagged on the
909
- * trailing git component.
910
- */
911
- export function detectMutatingGitOp(cmd) {
912
- const components = cmd.split(/\s*(?:&&|\|\||;|\|)\s*/);
913
- for (const raw of components) {
914
- const component = raw.trim();
915
- if (component === '')
916
- continue;
917
- // Strip leading `sudo` wrapper which would otherwise hide the verb.
918
- const stripped = component.replace(/^sudo\s+/, '');
919
- // Match `git [<global-flags>] <subcommand> ...`. Global flags we
920
- // tolerate:
921
- // - long flag: `--no-pager`, `--git-dir=.git`
922
- // - short flag with attached value: `-C <path>`, `-c <k=v>`
923
- // - bare short flag: `-P`
924
- // Anything weirder falls through and the predicate returns null,
925
- // which means the guard does not fire on that component — safer
926
- // to err open here because the destructive classifier and the
927
- // outer permission gate are independent defences.
928
- const match = stripped.match(/^git(?:\s+(?:--[A-Za-z][A-Za-z0-9-]*(?:=\S+)?|-[CcP](?:\s+\S+)?|-[A-Za-z]+))*\s+([a-z][a-z0-9-]*)\b/);
929
- if (!match)
930
- continue;
931
- const subcommand = match[1];
932
- if (subcommand && MUTATING_GIT_SUBCOMMANDS.has(subcommand)) {
933
- return subcommand;
934
- }
935
- }
936
- return null;
937
- }
938
- /**
939
- * Resolve the workspace's effective git boundary. Returns:
940
- * - the absolute path of the .git toplevel that owns `cwd`
941
- * - null when no .git ancestor exists at all (standalone, no repo)
942
- *
943
- * Pure filesystem walk so the guard does not depend on git being on
944
- * PATH. We look for either a `.git` directory or a `.git` file (the
945
- * worktree case where `.git` is a pointer file).
946
- */
947
- export function resolveGitToplevel(cwd) {
948
- let dir = cwd;
949
- while (true) {
950
- const dotGit = join(dir, '.git');
951
- if (existsSync(dotGit))
952
- return dir;
953
- const parent = resolve(dir, '..');
954
- if (parent === dir)
955
- return null;
956
- dir = parent;
957
- }
958
- }
959
- /**
960
- * The actual guard. Returns null when the command is allowed; returns
961
- * a block descriptor when it should be denied. The block message uses
962
- * the literal prefix `git boundary escape:` so callers (and the spec)
963
- * can pattern-match.
964
- */
965
- export function enforceGitBoundary(cmd, startCwd, workspaceRoot) {
966
- const subcommand = detectMutatingGitOp(cmd);
967
- if (subcommand === null)
968
- return null;
969
- // Resolve symlinks on both sides so a /var → /private/var macOS
970
- // realpath divergence does not produce a false escape.
971
- const root = safeRealpath(workspaceRoot);
972
- const toplevel = resolveGitToplevel(safeRealpath(startCwd));
973
- const resolvedToplevel = toplevel === null ? null : safeRealpath(toplevel);
974
- if (resolvedToplevel === root)
975
- return null;
976
- // Either no .git anywhere (standalone) OR the .git that wins is an
977
- // ancestor — both are escape scenarios. Operator must run `git init`
978
- // explicitly inside the workspace.
979
- if (resolvedToplevel === null) {
980
- return {
981
- subcommand,
982
- resolvedToplevel: null,
983
- reason: `${GIT_BOUNDARY_BLOCK_PREFIX} workspace root '${workspaceRoot}' has no .git ` +
984
- `and no ancestor repository exists. Run \`git init\` in the workspace first ` +
985
- `before \`git ${subcommand}\`.`,
986
- };
987
- }
988
- return {
989
- subcommand,
990
- resolvedToplevel,
991
- reason: `${GIT_BOUNDARY_BLOCK_PREFIX} workspace root '${workspaceRoot}' has no .git; ` +
992
- `outer toplevel is '${resolvedToplevel}'. Run \`git init\` in the workspace ` +
993
- `first before \`git ${subcommand}\` (otherwise the operation would write to ` +
994
- `the ancestor repository, not the workspace).`,
995
- };
996
- }
997
- function safeRealpath(path) {
998
- try {
999
- return realpathSync(path);
1000
- }
1001
- catch {
1002
- return path;
1003
- }
1004
- }
1005
- function removeRegistryEntrySync(jobId) {
1006
- const path = join(homedir(), '.pugi', 'jobs.json');
1007
- const entries = readRegistryEntriesSync().filter((entry) => entry.id !== jobId);
1008
- try {
1009
- mkdirSync(join(homedir(), '.pugi'), { recursive: true });
1010
- writeFileSync(path, `${JSON.stringify(entries, null, 2)}\n`, {
1011
- encoding: 'utf8',
1012
- mode: 0o600,
1013
- });
1014
- }
1015
- catch {
1016
- // best-effort
1017
- }
1018
- }
1019
- /**
1020
- * Synchronous helper used by the legacy tool-bridge path. It wraps
1021
- * `spawnSync` for the simplest case (no background, no overflow
1022
- * artifact, default timeout) so callers that cannot await a promise
1023
- * still get the class-aware permission gate. Returns the same shape
1024
- * as the async tool minus the cwd carry-over (since spawnSync
1025
- * cannot stream we approximate the cap by post-truncation).
1026
- */
1027
- export function bashToolSync(input, ctx) {
1028
- const cmd = input.cmd ?? '';
1029
- const additionalDirectories = ctx.additionalDirectories ?? [];
1030
- const source = ctx.source ?? 'agent';
1031
- const toolCallId = recordToolCall(ctx.session, 'bash', cmd);
1032
- const startCwd = resolveStartCwd(input.cwd ?? ctx.lastBashCwd, ctx.root, additionalDirectories);
1033
- // Workspace-git-boundary guard (CEO P0 #51). Fires
1034
- // BEFORE the permission gate so the structural boundary diagnostic
1035
- // is the one the operator sees. See the async path for the full
1036
- // rationale.
1037
- const boundaryBlock = enforceGitBoundary(cmd, startCwd, ctx.root);
1038
- if (boundaryBlock !== null) {
1039
- emitEvent(ctx.session, 'bash.git_boundary_escape', {
1040
- cmd,
1041
- workspaceRoot: ctx.root,
1042
- resolvedToplevel: boundaryBlock.resolvedToplevel ?? null,
1043
- });
1044
- recordToolResult(ctx.session, toolCallId, 'error', boundaryBlock.reason);
1045
- return {
1046
- stdout: '',
1047
- stderr: boundaryBlock.reason,
1048
- exitCode: 126,
1049
- nextCwd: ctx.lastBashCwd ?? ctx.root,
1050
- truncated: false,
1051
- timedOut: false,
1052
- cancelled: false,
1053
- };
1054
- }
1055
- const decision = evaluateBashPermission(cmd, ctx.settings.permissions.mode, {
1056
- workspaceRoot: ctx.root,
1057
- additionalDirectories,
1058
- source,
1059
- });
1060
- if (decision.decision !== 'allow') {
1061
- const reason = `Permission ${decision.decision}: ${decision.reason}`;
1062
- recordToolResult(ctx.session, toolCallId, 'error', reason);
1063
- // #21: mirror the async-path emission so sync callers
1064
- // (spawnSync fallback) produce the same tenant-wide audit trail.
1065
- writeAuditEvent({
1066
- event: 'permission_denied',
1067
- sessionId: ctx.session.id,
1068
- workspaceRoot: ctx.root,
1069
- data: {
1070
- tool: 'bash',
1071
- source,
1072
- decision: decision.decision,
1073
- reason: decision.reason,
1074
- cmdPreview: cmd.slice(0, 200),
1075
- },
1076
- });
1077
- return {
1078
- stdout: '',
1079
- stderr: `Permission denied: ${decision.reason}`,
1080
- exitCode: 126,
1081
- nextCwd: ctx.lastBashCwd ?? ctx.root,
1082
- truncated: false,
1083
- timedOut: false,
1084
- cancelled: false,
1085
- };
1086
- }
1087
- // CEO P1 #25 — sync path observes pre-spawn cancellation too. The
1088
- // sync path is used by the engine-loop tool-bridge (`bashToolSync`
1089
- // from tool-bridge.ts:1385); we cannot mid-stream cancel that path
1090
- // without rewriting spawnSync, but the pre-spawn gate still gives
1091
- // the operator a quick-exit window between permission and shell
1092
- // launch.
1093
- if (ctx.cancellation?.isAborted === true) {
1094
- const reason = 'operator_aborted: bash refused before spawn';
1095
- emitEvent(ctx.session, 'bash.cancelled', { cmd, phase: 'pre_spawn_sync' });
1096
- recordToolResult(ctx.session, toolCallId, 'cancelled', reason);
1097
- return {
1098
- stdout: '',
1099
- stderr: reason,
1100
- exitCode: 130,
1101
- nextCwd: ctx.lastBashCwd ?? ctx.root,
1102
- truncated: false,
1103
- timedOut: false,
1104
- cancelled: true,
1105
- };
1106
- }
1107
- const timeoutMs = sanitizeTimeout(input.timeoutMs);
1108
- const childEnv = buildChildEnv();
1109
- // Pugi backlog P2 — redirect path for the sync entry. The
1110
- // engine loop's tool-bridge dispatches through `bashToolSync`, so
1111
- // the redirect contract has to be honoured here too — otherwise a
1112
- // model that asks for log discipline through the bash tool surface
1113
- // would get its stdout buffered + truncated through the legacy
1114
- // pipeline. `spawnSync` accepts file descriptors in `stdio` so we
1115
- // can hand the log fd in directly, same as the async path.
1116
- let redirectState = null;
1117
- if (input.redirect !== undefined) {
1118
- try {
1119
- const target = resolveRedirectTarget({
1120
- workspaceRoot: ctx.root,
1121
- sessionId: ctx.session.id,
1122
- toolCallId,
1123
- command: cmd,
1124
- override: input.redirect.path,
1125
- });
1126
- const { fd, tempPath } = openRedirectFile(target);
1127
- redirectState = {
1128
- target,
1129
- fd,
1130
- tempPath,
1131
- tailLines: normalizeTailLines(input.redirect.tailLines),
1132
- };
1133
- }
1134
- catch (error) {
1135
- const reason = `redirect refused: ${error.message}`;
1136
- recordToolResult(ctx.session, toolCallId, 'error', reason);
1137
- return {
1138
- stdout: '',
1139
- stderr: reason,
1140
- exitCode: 126,
1141
- nextCwd: ctx.lastBashCwd ?? ctx.root,
1142
- truncated: false,
1143
- timedOut: false,
1144
- cancelled: false,
1145
- };
1146
- }
1147
- }
1148
- const stdioLayout = redirectState !== null
1149
- ? ['ignore', redirectState.fd, redirectState.fd]
1150
- : ['ignore', 'pipe', 'pipe'];
1151
- const result = spawnSync('/bin/sh', ['-c', cmd], {
1152
- cwd: startCwd,
1153
- env: childEnv,
1154
- encoding: 'utf8',
1155
- stdio: stdioLayout,
1156
- timeout: timeoutMs,
1157
- maxBuffer: 10 * 1024 * 1024,
1158
- });
1159
- const timedOut = result.error?.code === 'ETIMEDOUT' ||
1160
- result.signal === 'SIGTERM';
1161
- const nextCwd = computeNextCwd(cmd, startCwd, ctx.root, additionalDirectories, ctx.session);
1162
- // Redirect short-circuit before the buffered-path artifact logic.
1163
- // We close our copy of the fd before rename so the inode is no
1164
- // longer held open by the parent process.
1165
- if (redirectState !== null) {
1166
- try {
1167
- closeSync(redirectState.fd);
1168
- }
1169
- catch {
1170
- // already closed
1171
- }
1172
- try {
1173
- finaliseRedirectFile(redirectState.target, redirectState.tempPath);
1174
- }
1175
- catch {
1176
- // best-effort
1177
- }
1178
- const redirectExitCode = timedOut ? 124 : result.status ?? 1;
1179
- const envelope = applyRedirect({
1180
- target: redirectState.target,
1181
- exitCode: redirectExitCode,
1182
- tailLines: redirectState.tailLines,
1183
- });
1184
- if (timedOut) {
1185
- emitEvent(ctx.session, 'bash.timeout', { cmd, timeoutMs });
1186
- recordToolResult(ctx.session, toolCallId, 'error', `bash timed out after ${timeoutMs}ms (redirect=${envelope.logPath})`);
1187
- }
1188
- else {
1189
- recordToolResult(ctx.session, toolCallId, 'success', `bash exit=${redirectExitCode} redirect=${envelope.logPath}`);
1190
- }
1191
- return {
1192
- stdout: envelope.stdout,
1193
- stderr: envelope.stderr,
1194
- exitCode: redirectExitCode,
1195
- nextCwd,
1196
- truncated: envelope.truncated,
1197
- timedOut,
1198
- cancelled: false,
1199
- logPath: envelope.logPath,
1200
- tail: envelope.tail,
1201
- };
1202
- }
1203
- const stdoutFull = (result.stdout ?? '').toString();
1204
- const stderrFull = (result.stderr ?? '').toString();
1205
- const truncated = stdoutFull.length + stderrFull.length > BASH_OUTPUT_CAP_BYTES;
1206
- let artifactRef;
1207
- let stdoutOut = stdoutFull;
1208
- let stderrOut = stderrFull;
1209
- if (truncated) {
1210
- artifactRef = persistOverflow({
1211
- root: ctx.root,
1212
- sessionId: ctx.session.id,
1213
- toolCallId,
1214
- stdout: stdoutFull,
1215
- stderr: stderrFull,
1216
- });
1217
- ({ stdout: stdoutOut, stderr: stderrOut } = capToCombined(stdoutFull, stderrFull));
1218
- }
1219
- const exitCode = timedOut ? 124 : result.status ?? 1;
1220
- if (timedOut) {
1221
- emitEvent(ctx.session, 'bash.timeout', { cmd, timeoutMs });
1222
- recordToolResult(ctx.session, toolCallId, 'error', `bash timed out after ${timeoutMs}ms`);
1223
- }
1224
- else {
1225
- recordToolResult(ctx.session, toolCallId, 'success', `bash exit=${exitCode} bytes=${stdoutFull.length + stderrFull.length}${artifactRef ? ` overflow=${artifactRef}` : ''}`);
1226
- }
1227
- return {
1228
- stdout: stdoutOut,
1229
- stderr: stderrOut,
1230
- exitCode,
1231
- artifactRef,
1232
- nextCwd,
1233
- truncated,
1234
- timedOut,
1235
- cancelled: false,
1236
- };
1237
- }
1238
- //# sourceMappingURL=bash.js.map