@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.88

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 (405) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +1 -1
  3. package/THIRD_PARTY_NOTICES.md +40 -0
  4. package/assets/pugi-prozr2-mascot.ansi +9 -0
  5. package/bin/run.js +33 -1
  6. package/dist/commands/deploy.js +40 -40
  7. package/dist/commands/flatten.js +191 -0
  8. package/dist/commands/jobs-watch.js +201 -0
  9. package/dist/commands/jobs.js +42 -27
  10. package/dist/commands/smoke.js +133 -0
  11. package/dist/core/agent-progress/cleanup.js +134 -0
  12. package/dist/core/agent-progress/schema.js +144 -0
  13. package/dist/core/agent-progress/writer.js +101 -0
  14. package/dist/core/agents/adaptive-router.js +330 -0
  15. package/dist/core/agents/query-decomposer.js +297 -0
  16. package/dist/core/agents/registry.js +3 -3
  17. package/dist/core/approvals/shortcut-resolver.js +98 -0
  18. package/dist/core/artifact-chain/dispatcher.js +148 -0
  19. package/dist/core/artifact-chain/exporter.js +164 -0
  20. package/dist/core/artifact-chain/state.js +243 -0
  21. package/dist/core/artifact-chain/steps.js +169 -0
  22. package/dist/core/ask-user/question.js +92 -0
  23. package/dist/core/audit/audit-trail.js +275 -0
  24. package/dist/core/auth/ensure-authenticated.js +129 -0
  25. package/dist/core/auth/env-provider.js +238 -0
  26. package/dist/core/auto-open-browser.js +4 -4
  27. package/dist/core/auto-update/channels.js +122 -0
  28. package/dist/core/auto-update/checker.js +241 -0
  29. package/dist/core/auto-update/state.js +235 -0
  30. package/dist/core/bare-mode/index.js +107 -0
  31. package/dist/core/bash/redirect.js +281 -0
  32. package/dist/core/bash-classifier.js +436 -40
  33. package/dist/core/checkpoint/resumer.js +149 -0
  34. package/dist/core/checkpoint/rewinder.js +291 -0
  35. package/dist/core/checkpoints/shadow-git.js +670 -0
  36. package/dist/core/citations/parser.js +109 -0
  37. package/dist/core/classifier/yolo-classifier.js +88 -0
  38. package/dist/core/codegraph/decision-store.js +248 -0
  39. package/dist/core/codegraph/detect-repo.js +459 -0
  40. package/dist/core/codegraph/install.js +134 -0
  41. package/dist/core/codegraph/offer-hook.js +220 -0
  42. package/dist/core/compact/auto-trigger.js +96 -0
  43. package/dist/core/compact/buffer-rewriter.js +115 -0
  44. package/dist/core/compact/summarizer.js +208 -0
  45. package/dist/core/compact/token-counter.js +108 -0
  46. package/dist/core/consensus/anvil-fanout.js +25 -25
  47. package/dist/core/consensus/diff-capture.js +121 -12
  48. package/dist/core/consensus/rubric.js +21 -21
  49. package/dist/core/context/builder.js +6 -6
  50. package/dist/core/context/compaction-events.js +8 -8
  51. package/dist/core/context/compaction.js +31 -31
  52. package/dist/core/context/index.js +15 -8
  53. package/dist/core/context/invariants.js +51 -51
  54. package/dist/core/context/markdown-loader.js +28 -10
  55. package/dist/core/context/markdown-traverse.js +255 -0
  56. package/dist/core/context/pugiignore.js +41 -41
  57. package/dist/core/context/repo-skeleton.js +37 -37
  58. package/dist/core/context/tool-eviction.js +55 -0
  59. package/dist/core/context/watcher.js +32 -32
  60. package/dist/core/context/working-set.js +23 -23
  61. package/dist/core/coordinator/agent-tools.js +77 -0
  62. package/dist/core/coordinator/agent-toolset.js +65 -0
  63. package/dist/core/coordinator/fsm.js +73 -0
  64. package/dist/core/coordinator/mode-fsm.js +70 -0
  65. package/dist/core/cost/rate-card.js +129 -0
  66. package/dist/core/cost/tracker.js +221 -0
  67. package/dist/core/credentials.js +12 -12
  68. package/dist/core/cron/scheduler.js +138 -0
  69. package/dist/core/denial-tracking/index.js +8 -0
  70. package/dist/core/denial-tracking/state.js +264 -0
  71. package/dist/core/diagnostics/probe-runner.js +93 -0
  72. package/dist/core/diagnostics/probes/api.js +46 -0
  73. package/dist/core/diagnostics/probes/auth.js +93 -0
  74. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  75. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  76. package/dist/core/diagnostics/probes/config.js +72 -0
  77. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  78. package/dist/core/diagnostics/probes/disk.js +81 -0
  79. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  80. package/dist/core/diagnostics/probes/git.js +65 -0
  81. package/dist/core/diagnostics/probes/hooks.js +118 -0
  82. package/dist/core/diagnostics/probes/mcp.js +75 -0
  83. package/dist/core/diagnostics/probes/node.js +59 -0
  84. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  85. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  86. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  87. package/dist/core/diagnostics/probes/session.js +74 -0
  88. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  89. package/dist/core/diagnostics/probes/workspace.js +63 -0
  90. package/dist/core/diagnostics/types.js +70 -0
  91. package/dist/core/dispatch/cache-cleanup.js +197 -0
  92. package/dist/core/dispatch/cache-handoff.js +295 -0
  93. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  94. package/dist/core/edits/dispatch.js +293 -7
  95. package/dist/core/edits/format-matrix.js +26 -0
  96. package/dist/core/edits/fuzzy-ladder.js +650 -0
  97. package/dist/core/edits/index.js +3 -1
  98. package/dist/core/edits/journal.js +199 -0
  99. package/dist/core/edits/layer-a-apply.js +15 -15
  100. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  101. package/dist/core/edits/layer-b-apply.js +9 -9
  102. package/dist/core/edits/layer-c-apply.js +6 -6
  103. package/dist/core/edits/layer-d-ast.js +557 -14
  104. package/dist/core/edits/marker-parser.js +12 -12
  105. package/dist/core/edits/security-gate.js +27 -27
  106. package/dist/core/edits/verify-hook.js +273 -0
  107. package/dist/core/edits/worktree.js +322 -0
  108. package/dist/core/engine/anvil-client.js +151 -26
  109. package/dist/core/engine/auto-compact.js +179 -0
  110. package/dist/core/engine/budgets.js +186 -0
  111. package/dist/core/engine/context-prefix.js +155 -0
  112. package/dist/core/engine/index.js +1 -1
  113. package/dist/core/engine/intensity.js +158 -0
  114. package/dist/core/engine/intent.js +260 -0
  115. package/dist/core/engine/native-pugi.js +1295 -227
  116. package/dist/core/engine/prompts.js +134 -16
  117. package/dist/core/engine/strip-internal-fields.js +124 -0
  118. package/dist/core/engine/tool-bridge.js +1295 -59
  119. package/dist/core/evaluation/golden-dataset.js +293 -0
  120. package/dist/core/feedback/queue.js +177 -0
  121. package/dist/core/feedback/submitter.js +145 -0
  122. package/dist/core/file-cache.js +113 -1
  123. package/dist/core/flatten/flatten-repo.js +439 -0
  124. package/dist/core/format/osc8-link.js +28 -0
  125. package/dist/core/hook-chains.js +392 -0
  126. package/dist/core/hooks/citation-verify-hook.js +138 -0
  127. package/dist/core/hooks/citation-verify.js +112 -0
  128. package/dist/core/hooks/events.js +44 -0
  129. package/dist/core/hooks/index.js +15 -0
  130. package/dist/core/hooks/registry.js +213 -0
  131. package/dist/core/hooks/runner.js +236 -0
  132. package/dist/core/hooks/v2/event-emitter.js +115 -0
  133. package/dist/core/hooks/v2/executor.js +282 -0
  134. package/dist/core/hooks/v2/index.js +25 -0
  135. package/dist/core/hooks/v2/lifecycle.js +104 -0
  136. package/dist/core/hooks/v2/loader.js +216 -0
  137. package/dist/core/hooks/v2/matcher.js +125 -0
  138. package/dist/core/hooks/v2/trust.js +143 -0
  139. package/dist/core/hooks/v2/types.js +86 -0
  140. package/dist/core/image/renderer.js +71 -0
  141. package/dist/core/init/detector.js +582 -0
  142. package/dist/core/init/template-renderer.js +242 -0
  143. package/dist/core/jobs/registry.js +18 -18
  144. package/dist/core/ledger/results-tsv.js +142 -0
  145. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  146. package/dist/core/lsp/cache.js +105 -0
  147. package/dist/core/lsp/client.js +776 -0
  148. package/dist/core/lsp/language-detect.js +66 -0
  149. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  150. package/dist/core/lsp/symbol-tools.js +372 -0
  151. package/dist/core/mcp/client.js +97 -28
  152. package/dist/core/mcp/http-server.js +553 -0
  153. package/dist/core/mcp/orchestrator-tools.js +662 -0
  154. package/dist/core/mcp/permission.js +190 -0
  155. package/dist/core/mcp/registry.js +39 -17
  156. package/dist/core/mcp/server-tools.js +219 -0
  157. package/dist/core/mcp/server.js +397 -0
  158. package/dist/core/mcp/trust.js +10 -10
  159. package/dist/core/memory/dual-write.js +416 -0
  160. package/dist/core/memory/passive-extract.js +130 -0
  161. package/dist/core/memory/phase1-kinds.js +20 -0
  162. package/dist/core/memory/secret-scanner.js +304 -0
  163. package/dist/core/memory-sync/queue.js +170 -0
  164. package/dist/core/metrics/extract.js +113 -0
  165. package/dist/core/modes/roo-modes.js +68 -0
  166. package/dist/core/onboarding/ensure-initialized.js +133 -0
  167. package/dist/core/onboarding/marker.js +111 -0
  168. package/dist/core/onboarding/telemetry-state.js +108 -0
  169. package/dist/core/output-style/presets.js +176 -0
  170. package/dist/core/output-style/state.js +185 -0
  171. package/dist/core/path-security.js +287 -5
  172. package/dist/core/permission.js +82 -22
  173. package/dist/core/permissions/auto-classifier.js +124 -0
  174. package/dist/core/permissions/bash-parser.js +371 -0
  175. package/dist/core/permissions/circuit-breaker.js +83 -0
  176. package/dist/core/permissions/constrained-edit.js +91 -0
  177. package/dist/core/permissions/gate.js +278 -0
  178. package/dist/core/permissions/index.js +20 -0
  179. package/dist/core/permissions/mode.js +174 -0
  180. package/dist/core/permissions/network-egress.js +137 -0
  181. package/dist/core/permissions/state.js +241 -0
  182. package/dist/core/permissions/tool-class.js +93 -0
  183. package/dist/core/plan-mode/ui-state.js +51 -0
  184. package/dist/core/plans/plan-artifact.js +721 -0
  185. package/dist/core/policy-limits/etag-store.js +122 -0
  186. package/dist/core/prd-check/parser.js +215 -0
  187. package/dist/core/prd-check/reporter.js +127 -0
  188. package/dist/core/prd-check/session-review.js +557 -0
  189. package/dist/core/prd-check/verifiers.js +223 -0
  190. package/dist/core/prompt-cache/client-cache.js +99 -0
  191. package/dist/core/prompts/assembly.js +29 -0
  192. package/dist/core/prompts/registry.js +364 -0
  193. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  194. package/dist/core/pugi-md/context-injector.js +76 -0
  195. package/dist/core/pugi-md/walk-up.js +207 -0
  196. package/dist/core/python/uv-installer.js +270 -0
  197. package/dist/core/python/uv-resolver.js +83 -0
  198. package/dist/core/rate-limit/narrator.js +146 -0
  199. package/dist/core/recipes/cli-types.js +20 -0
  200. package/dist/core/recipes/loader.js +103 -0
  201. package/dist/core/recipes/runner.js +345 -0
  202. package/dist/core/recipes/schema.js +587 -0
  203. package/dist/core/release-notes/parser.js +241 -0
  204. package/dist/core/release-notes/state.js +116 -0
  205. package/dist/core/repl/ask.js +37 -37
  206. package/dist/core/repl/cancellation.js +26 -26
  207. package/dist/core/repl/cap-warning.js +4 -4
  208. package/dist/core/repl/clipboard-read.js +11 -11
  209. package/dist/core/repl/dispatch-fsm.js +12 -12
  210. package/dist/core/repl/history-search.js +15 -15
  211. package/dist/core/repl/history.js +28 -18
  212. package/dist/core/repl/kill-ring.js +5 -5
  213. package/dist/core/repl/model-pricing.js +135 -0
  214. package/dist/core/repl/privacy-banner.js +22 -22
  215. package/dist/core/repl/session.js +2157 -214
  216. package/dist/core/repl/slash-commands.js +533 -40
  217. package/dist/core/repl/store/index.js +1 -1
  218. package/dist/core/repl/store/jsonl-log.js +22 -22
  219. package/dist/core/repl/store/lockfile.js +10 -10
  220. package/dist/core/repl/store/session-store.js +136 -107
  221. package/dist/core/repl/store/types.js +15 -15
  222. package/dist/core/repl/store/uuid-v7.js +12 -12
  223. package/dist/core/repl/workspace-context.js +43 -21
  224. package/dist/core/repo-map/build.js +125 -0
  225. package/dist/core/repo-map/cache.js +185 -0
  226. package/dist/core/repo-map/extractor.js +254 -0
  227. package/dist/core/repo-map/formatter.js +145 -0
  228. package/dist/core/repo-map/page-rank.js +105 -0
  229. package/dist/core/repo-map/scanner.js +211 -0
  230. package/dist/core/retry-budget/budget.js +284 -0
  231. package/dist/core/retry-budget/index.js +5 -0
  232. package/dist/core/retry-budget/retry-cap.js +74 -0
  233. package/dist/core/routing/lead-worker.js +43 -0
  234. package/dist/core/routing/pre-flight-estimator.js +108 -0
  235. package/dist/core/runs/run-tree.js +103 -0
  236. package/dist/core/security/injection-scanner.js +367 -0
  237. package/dist/core/security/output-filter.js +418 -0
  238. package/dist/core/session/env-file.js +105 -0
  239. package/dist/core/session/section-budgets.js +140 -0
  240. package/dist/core/session.js +92 -0
  241. package/dist/core/settings.js +298 -5
  242. package/dist/core/share/formatter.js +271 -0
  243. package/dist/core/share/redactor.js +221 -0
  244. package/dist/core/share/uploader.js +267 -0
  245. package/dist/core/skills/defaults.js +457 -0
  246. package/dist/core/skills/loader.js +22 -22
  247. package/dist/core/skills/sources.js +27 -27
  248. package/dist/core/smoke/headless-driver.js +174 -0
  249. package/dist/core/smoke/orchestrator.js +194 -0
  250. package/dist/core/smoke/runner.js +238 -0
  251. package/dist/core/smoke/scenario-parser.js +316 -0
  252. package/dist/core/statusline.js +99 -0
  253. package/dist/core/subagents/dispatcher-real.js +600 -0
  254. package/dist/core/subagents/dispatcher.js +132 -43
  255. package/dist/core/subagents/index.js +19 -6
  256. package/dist/core/subagents/isolation-matrix.js +213 -0
  257. package/dist/core/subagents/spawn.js +19 -4
  258. package/dist/core/telemetry/emitter.js +229 -0
  259. package/dist/core/telemetry/queue.js +251 -0
  260. package/dist/core/theme/context.js +91 -0
  261. package/dist/core/theme/presets.js +228 -0
  262. package/dist/core/theme/state.js +181 -0
  263. package/dist/core/todos/invariant.js +10 -0
  264. package/dist/core/todos/state.js +177 -0
  265. package/dist/core/tool-schema/compressor.js +89 -0
  266. package/dist/core/transport/version-interceptor.js +166 -0
  267. package/dist/core/trust.js +2 -2
  268. package/dist/core/tui/thinking-block.js +64 -0
  269. package/dist/core/vim/keymap.js +288 -0
  270. package/dist/core/vim/state.js +92 -0
  271. package/dist/core/watch-markers/marker-watcher.js +133 -0
  272. package/dist/core/worktree-manager/cleanup.js +123 -0
  273. package/dist/core/worktree-manager/manager.js +303 -0
  274. package/dist/index.js +36 -0
  275. package/dist/runtime/bootstrap.js +190 -0
  276. package/dist/runtime/cli.js +4203 -493
  277. package/dist/runtime/commands/agents.js +30 -30
  278. package/dist/runtime/commands/budget.js +5 -5
  279. package/dist/runtime/commands/cancel.js +231 -0
  280. package/dist/runtime/commands/chain.js +489 -0
  281. package/dist/runtime/commands/codegraph-status.js +227 -0
  282. package/dist/runtime/commands/compact.js +297 -0
  283. package/dist/runtime/commands/config.js +73 -39
  284. package/dist/runtime/commands/cost.js +199 -0
  285. package/dist/runtime/commands/delegate.js +244 -13
  286. package/dist/runtime/commands/dispatch.js +126 -0
  287. package/dist/runtime/commands/doctor.js +579 -0
  288. package/dist/runtime/commands/feedback.js +184 -0
  289. package/dist/runtime/commands/hooks.js +184 -0
  290. package/dist/runtime/commands/init.js +254 -0
  291. package/dist/runtime/commands/lsp.js +368 -0
  292. package/dist/runtime/commands/mcp.js +879 -0
  293. package/dist/runtime/commands/memory.js +582 -0
  294. package/dist/runtime/commands/model.js +237 -0
  295. package/dist/runtime/commands/onboarding.js +275 -0
  296. package/dist/runtime/commands/patch.js +128 -0
  297. package/dist/runtime/commands/permissions.js +112 -0
  298. package/dist/runtime/commands/plan.js +143 -0
  299. package/dist/runtime/commands/prd-check.js +285 -0
  300. package/dist/runtime/commands/privacy.js +17 -17
  301. package/dist/runtime/commands/recipe.js +325 -0
  302. package/dist/runtime/commands/redo-blob-store.js +92 -0
  303. package/dist/runtime/commands/redo.js +361 -0
  304. package/dist/runtime/commands/release-notes.js +229 -0
  305. package/dist/runtime/commands/repo-map.js +95 -0
  306. package/dist/runtime/commands/report.js +299 -0
  307. package/dist/runtime/commands/resume.js +118 -0
  308. package/dist/runtime/commands/review-consensus.js +68 -53
  309. package/dist/runtime/commands/rewind.js +333 -0
  310. package/dist/runtime/commands/roster.js +14 -14
  311. package/dist/runtime/commands/sessions.js +163 -0
  312. package/dist/runtime/commands/share.js +316 -0
  313. package/dist/runtime/commands/skills.js +31 -31
  314. package/dist/runtime/commands/status.js +186 -0
  315. package/dist/runtime/commands/stickers.js +82 -0
  316. package/dist/runtime/commands/style.js +194 -0
  317. package/dist/runtime/commands/theme.js +196 -0
  318. package/dist/runtime/commands/undo.js +54 -22
  319. package/dist/runtime/commands/update.js +289 -0
  320. package/dist/runtime/commands/vim.js +140 -0
  321. package/dist/runtime/commands/worktree.js +177 -0
  322. package/dist/runtime/commands/worktrees.js +155 -0
  323. package/dist/runtime/headless-repl.js +195 -0
  324. package/dist/runtime/headless.js +543 -0
  325. package/dist/runtime/load-hooks-or-exit.js +71 -0
  326. package/dist/runtime/plan-decompose.js +531 -0
  327. package/dist/runtime/sigint-guard.js +272 -0
  328. package/dist/runtime/update-check.js +28 -28
  329. package/dist/runtime/version.js +65 -0
  330. package/dist/skills/bundled/batch.js +617 -0
  331. package/dist/skills/bundled/index.js +45 -0
  332. package/dist/skills/bundled/loop.js +358 -0
  333. package/dist/skills/bundled/remember.js +383 -0
  334. package/dist/skills/bundled/simplify.js +289 -0
  335. package/dist/skills/bundled/skillify.js +373 -0
  336. package/dist/skills/bundled/stuck.js +558 -0
  337. package/dist/skills/bundled/verify.js +439 -0
  338. package/dist/testing/vcr.js +486 -0
  339. package/dist/tools/agent-tool.js +229 -0
  340. package/dist/tools/apply-patch.js +556 -0
  341. package/dist/tools/ask-user-question.js +288 -0
  342. package/dist/tools/ask-user.js +115 -0
  343. package/dist/tools/bash.js +624 -46
  344. package/dist/tools/brief.js +224 -0
  345. package/dist/tools/enter-worktree.js +250 -0
  346. package/dist/tools/exit-worktree.js +147 -0
  347. package/dist/tools/file-tools.js +161 -44
  348. package/dist/tools/lsp-tools.js +189 -0
  349. package/dist/tools/mcp-tool.js +260 -0
  350. package/dist/tools/multi-edit.js +361 -0
  351. package/dist/tools/powershell.js +268 -0
  352. package/dist/tools/registry.js +85 -0
  353. package/dist/tools/skill-tool.js +96 -0
  354. package/dist/tools/sleep.js +99 -0
  355. package/dist/tools/synthetic-output.js +133 -0
  356. package/dist/tools/tasks.js +208 -0
  357. package/dist/tools/todo-write.js +184 -0
  358. package/dist/tools/verify-plan-execution.js +295 -0
  359. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  360. package/dist/tools/web-fetch.js +195 -10
  361. package/dist/tools/web-search.js +458 -0
  362. package/dist/tui/agent-progress-card.js +111 -0
  363. package/dist/tui/agent-tree.js +11 -1
  364. package/dist/tui/ask-modal.js +14 -14
  365. package/dist/tui/ask-user-question-chips.js +257 -0
  366. package/dist/tui/ask-user-question-prompt.js +203 -0
  367. package/dist/tui/compact-banner.js +81 -0
  368. package/dist/tui/conversation-pane.js +85 -11
  369. package/dist/tui/cost-table.js +111 -0
  370. package/dist/tui/device-flow.js +2 -2
  371. package/dist/tui/doctor-table.js +46 -0
  372. package/dist/tui/feedback-prompt.js +156 -0
  373. package/dist/tui/input-box.js +247 -32
  374. package/dist/tui/login-picker.js +3 -3
  375. package/dist/tui/markdown-render.js +6 -6
  376. package/dist/tui/onboarding-wizard.js +240 -0
  377. package/dist/tui/permissions-picker.js +86 -0
  378. package/dist/tui/render.js +35 -0
  379. package/dist/tui/repl-render.js +332 -54
  380. package/dist/tui/repl-splash-art.js +16 -16
  381. package/dist/tui/repl-splash-mascot.js +48 -24
  382. package/dist/tui/repl-splash.js +22 -22
  383. package/dist/tui/repl.js +124 -44
  384. package/dist/tui/slash-palette.js +6 -6
  385. package/dist/tui/splash.js +2 -2
  386. package/dist/tui/status-bar.js +109 -31
  387. package/dist/tui/status-table.js +7 -0
  388. package/dist/tui/stickers-art.js +136 -0
  389. package/dist/tui/style-table.js +28 -0
  390. package/dist/tui/theme-table.js +29 -0
  391. package/dist/tui/thinking-spinner.js +123 -0
  392. package/dist/tui/tool-stream-pane.js +53 -4
  393. package/dist/tui/update-banner.js +27 -2
  394. package/dist/tui/vim-input.js +267 -0
  395. package/dist/tui/welcome-banner.js +107 -0
  396. package/dist/tui/welcome-data.js +293 -0
  397. package/dist/tui/workspace-context.js +2 -2
  398. package/docs/examples/codegraph.mcp.json +10 -0
  399. package/package.json +25 -7
  400. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  401. package/test/scenarios/compact-force.scenario.txt +11 -0
  402. package/test/scenarios/identity.scenario.txt +11 -0
  403. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  404. package/test/scenarios/walkback.scenario.txt +12 -0
  405. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,662 @@
1
+ /**
2
+ * Pugi MCP server — orchestrator-tools surface .
3
+ *
4
+ * SCOPE — this module is intentionally orthogonal to `server-tools.ts`.
5
+ *
6
+ * - `server-tools.ts` exposes the *engine* surface (read / grep / glob /
7
+ * edit / write / bash) — workspace-scoped, file-tools-backed, used by
8
+ * "external agent borrows Pugi's in-process executor".
9
+ *
10
+ * - `orchestrator-tools.ts` (THIS FILE) exposes the *orchestrator*
11
+ * surface — `pugi.run` / `pugi.read` / `pugi.write` / `pugi.dispatch`
12
+ * / `pugi.publish` / `pugi.deploy`. These are CLI-level operations
13
+ * used by an EXTERNAL the upstream tool (or Cursor) session that wants to
14
+ * loop fix-publish-test against the LIVE Pugi runtime. The motivating
15
+ * use case is the CEO dogfood blocker: Pugi REPL emits
16
+ * pseudo-tool-tags inline (no real file writes / no real shell exec);
17
+ * the operator wants to drive a remote the upstream tool session that
18
+ * programmatically invokes Pugi against the engine VM, captures
19
+ * output, edits source, republishes the CLI, and re-tests — all
20
+ * without an interactive human at every step.
21
+ *
22
+ * SECURITY POSTURE — every orchestrator tool is gated by an env-var
23
+ * permission switch that defaults to OFF. The MCP server's
24
+ * `permissionGate` still applies on top (deny-by-default), but env
25
+ * gates are a coarser kill-switch the operator can flip per-machine
26
+ * without rebuilding the CLI.
27
+ *
28
+ * - PUGI_MCP_EXEC_ENABLED=1 — enables `pugi.run`
29
+ * - PUGI_MCP_PUBLISH_ENABLED=1 — enables `pugi.publish`
30
+ * - PUGI_MCP_DEPLOY_ENABLED=1 — enables `pugi.deploy`
31
+ *
32
+ * `pugi.read` / `pugi.write` do not require an env gate (read+write
33
+ * enforce workspace + protected-path containment). `pugi.dispatch`
34
+ * uses PUGI_MCP_EXEC_ENABLED (shared with `pugi.run`) because it
35
+ * shells the local `pugi` binary to drive the full engine loop
36
+ * client-side. All three still pass through the MCP-server
37
+ * permissionGate, so an operator running `pugi mcp serve` without
38
+ * `--allow-write` still sees `pugi.write` refused at dispatch.
39
+ */
40
+ import { execFile } from 'node:child_process';
41
+ import { promisify } from 'node:util';
42
+ import { closeSync, fstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, writeFileSync, } from 'node:fs';
43
+ import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
44
+ import { fileURLToPath } from 'node:url';
45
+ const execFileAsync = promisify(execFile);
46
+ /**
47
+ * Protected basename patterns — mirror of
48
+ * `core/bash-classifier.ts::PROTECTED_BASENAME_PATTERNS`. We DO NOT
49
+ * import from there because that module is bash-classifier specific
50
+ * (the regex shapes there carry shell-quote boundaries). For path-only
51
+ * matching we use simpler RegExps anchored on the basename. Keeps the
52
+ * two modules independently auditable.
53
+ */
54
+ const PROTECTED_BASENAMES = [
55
+ /^\.env$/,
56
+ /^\.env\.[A-Za-z0-9_-]+$/,
57
+ /^id_(rsa|ed25519|ecdsa|dsa)(\.pub)?$/,
58
+ /^\.npmrc$/,
59
+ /^\.pypirc$/,
60
+ /^\.gitconfig$/,
61
+ /^credentials(\.json)?$/,
62
+ ];
63
+ const PROTECTED_DIR_SEGMENTS = new Set([
64
+ '.git',
65
+ '.ssh',
66
+ '.gnupg',
67
+ 'node_modules',
68
+ ]);
69
+ /**
70
+ * Resolve + validate a caller-supplied path against the workspace
71
+ * root. Refuses absolute paths outside the root, parent-traversal
72
+ * escapes, and protected basenames / dir segments.
73
+ *
74
+ * Exported so the spec can drive it directly — pinning the security
75
+ * boundary at a single audited entry point.
76
+ */
77
+ export function resolveWorkspacePathOrThrow(ctx, requested) {
78
+ if (typeof requested !== 'string' || requested.length === 0) {
79
+ throw new Error('path must be a non-empty string');
80
+ }
81
+ if (requested.includes('\0')) {
82
+ throw new Error('path contains a null byte');
83
+ }
84
+ const root = resolve(ctx.workspaceRoot);
85
+ const candidate = isAbsolute(requested) ? requested : resolve(root, requested);
86
+ const absolute = resolve(candidate);
87
+ // Containment check — absolute must live under root. We use
88
+ // `relative` + `..` detection rather than `startsWith(root)` so a
89
+ // sibling dir whose name happens to share a prefix (e.g. /tmp/wsX
90
+ // vs /tmp/ws) does not accidentally pass.
91
+ const rel = relative(root, absolute);
92
+ if (rel === '' || rel === '.') {
93
+ throw new Error(`path "${requested}" resolves to the workspace root itself`);
94
+ }
95
+ if (rel.startsWith('..') || isAbsolute(rel)) {
96
+ throw new Error(`path "${requested}" escapes the workspace root`);
97
+ }
98
+ // Protected segment / basename check — applied to EVERY component of
99
+ // the resolved path under the root. We split on the OS separator so
100
+ // Windows + POSIX share the same gate.
101
+ const segments = rel.split(sep);
102
+ for (const segment of segments) {
103
+ if (PROTECTED_DIR_SEGMENTS.has(segment)) {
104
+ throw new Error(`path "${requested}" touches protected segment "${segment}"`);
105
+ }
106
+ for (const pattern of PROTECTED_BASENAMES) {
107
+ if (pattern.test(segment)) {
108
+ throw new Error(`path "${requested}" touches protected basename "${segment}"`);
109
+ }
110
+ }
111
+ }
112
+ return { absolute, relativeToRoot: rel };
113
+ }
114
+ /**
115
+ * Build the orchestrator tool surface. The MCP server consumes the
116
+ * returned array via `createPugiMcpServer({ tools })`. Permission
117
+ * gating happens at TWO layers:
118
+ *
119
+ * 1. `capabilities.{exec,publish,deploy}` — env-var kill-switch
120
+ * checked at tool-execute time. A tool whose capability is OFF
121
+ * throws a deterministic refusal message; the MCP wire surfaces
122
+ * it as `isError: true` content.
123
+ *
124
+ * 2. The MCP server's `permissionGate` — checked BEFORE execute
125
+ * runs. The `pugi mcp serve` wiring in `runtime/commands/mcp.ts`
126
+ * synthesises a default gate; callers (tests) can pass
127
+ * `() => true` to bypass.
128
+ *
129
+ * The double-layer design is intentional — it lets an operator
130
+ * configure `PUGI_MCP_EXEC_ENABLED=1` system-wide AND still refuse a
131
+ * specific `pugi.run` call via the per-tool prompt without restarting
132
+ * the server.
133
+ */
134
+ /**
135
+ * Allowed dispatch subcommands. Mirror of the `command` enum in the
136
+ * admin-api `EngineRequestDto` (apps/admin-api/src/pugi-engine/
137
+ * pugi-engine.controller.ts). Kept as a local literal so this surface
138
+ * stays decoupled from the admin-api package — the CLI must work
139
+ * standalone after `npm i -g @pugi/cli`.
140
+ */
141
+ const ALLOWED_DISPATCH_COMMANDS = ['code', 'explain', 'fix', 'plan', 'build'];
142
+ export function buildOrchestratorTools(ctx) {
143
+ const execImpl = ctx.execFileImpl ?? execFileAsync;
144
+ const tools = [
145
+ {
146
+ name: 'pugi.run',
147
+ description: 'Execute a pugi CLI subcommand and capture stdout/stderr/exitCode. ' +
148
+ 'Use for `--version`, `explain`, `smoke`, etc. ' +
149
+ 'Requires PUGI_MCP_EXEC_ENABLED=1 at server boot.',
150
+ permission: 'bash',
151
+ inputSchema: {
152
+ type: 'object',
153
+ additionalProperties: false,
154
+ required: ['command'],
155
+ properties: {
156
+ command: {
157
+ type: 'string',
158
+ description: 'Whitespace-tokenised argv tail (e.g. "explain README.md").',
159
+ },
160
+ cwd: {
161
+ type: 'string',
162
+ description: 'Optional workspace-relative cwd; defaults to workspace root.',
163
+ },
164
+ timeoutMs: {
165
+ type: 'number',
166
+ minimum: 100,
167
+ maximum: 300000,
168
+ description: 'Hard timeout in ms (default 30000).',
169
+ },
170
+ },
171
+ },
172
+ async execute(args) {
173
+ if (!ctx.capabilities.exec) {
174
+ throw new Error('pugi.run: PUGI_MCP_EXEC_ENABLED is not set. ' +
175
+ 'Restart `pugi mcp serve` with PUGI_MCP_EXEC_ENABLED=1 to enable shell execution.');
176
+ }
177
+ const command = requireString(args, 'command');
178
+ const tokens = tokeniseArgv(command);
179
+ if (tokens.length === 0) {
180
+ throw new Error('pugi.run: command tokenises to zero args');
181
+ }
182
+ const timeoutMs = optionalNumber(args, 'timeoutMs', 30000);
183
+ const cwdInput = optionalString(args, 'cwd');
184
+ const cwd = cwdInput
185
+ ? resolveWorkspacePathOrThrow(ctx, cwdInput).absolute
186
+ : ctx.workspaceRoot;
187
+ const started = Date.now();
188
+ try {
189
+ const { stdout, stderr } = await execImpl(ctx.pugiBin, tokens, {
190
+ cwd,
191
+ timeout: timeoutMs,
192
+ maxBuffer: 4 * 1024 * 1024,
193
+ // Strip secret envs — orchestrator-driven CLI runs do NOT
194
+ // need the operator's NPM_TOKEN / GH_TOKEN / OPENAI_API_KEY
195
+ // visible. We pass through only PATH + HOME + a minimal
196
+ // shell. Same posture as bashToolSync(source='mcp').
197
+ env: sanitisedEnv(),
198
+ });
199
+ const durationMs = Date.now() - started;
200
+ return JSON.stringify({
201
+ stdout: clamp(stdout, 32 * 1024),
202
+ stderr: clamp(stderr, 32 * 1024),
203
+ exitCode: 0,
204
+ durationMs,
205
+ });
206
+ }
207
+ catch (err) {
208
+ const e = err;
209
+ const durationMs = Date.now() - started;
210
+ return JSON.stringify({
211
+ stdout: clamp(e.stdout ?? '', 32 * 1024),
212
+ stderr: clamp(e.stderr ?? (e.message ?? ''), 32 * 1024),
213
+ exitCode: typeof e.code === 'number' ? e.code : 1,
214
+ durationMs,
215
+ ...(e.signal ? { signal: e.signal } : {}),
216
+ ...(e.killed ? { killed: true } : {}),
217
+ });
218
+ }
219
+ },
220
+ },
221
+ {
222
+ name: 'pugi.read',
223
+ description: 'Read a file inside the configured workspace root. Refuses paths outside ' +
224
+ 'the root, parent-traversal escapes, and protected basenames (.env / .git / ' +
225
+ '.ssh / id_rsa / .npmrc / credentials.json). Default cap 256KB.',
226
+ permission: 'read',
227
+ inputSchema: {
228
+ type: 'object',
229
+ additionalProperties: false,
230
+ required: ['path'],
231
+ properties: {
232
+ path: { type: 'string' },
233
+ },
234
+ },
235
+ async execute(args) {
236
+ const path = requireString(args, 'path');
237
+ const { absolute, relativeToRoot } = resolveWorkspacePathOrThrow(ctx, path);
238
+ const stat = statSync(absolute);
239
+ if (!stat.isFile()) {
240
+ throw new Error(`pugi.read: "${relativeToRoot}" is not a regular file`);
241
+ }
242
+ const CAP = 256 * 1024;
243
+ const content = readFileSync(absolute, 'utf8');
244
+ const sizeBytes = Buffer.byteLength(content, 'utf8');
245
+ const truncated = sizeBytes > CAP;
246
+ return JSON.stringify({
247
+ path: relativeToRoot,
248
+ content: truncated ? content.slice(0, CAP) : content,
249
+ sizeBytes,
250
+ mtime: stat.mtime.toISOString(),
251
+ ...(truncated ? { truncated: true, capBytes: CAP } : {}),
252
+ });
253
+ },
254
+ },
255
+ {
256
+ name: 'pugi.write',
257
+ description: 'Create or overwrite a workspace file using atomic tmp+rename. Refuses paths ' +
258
+ 'outside the workspace root and protected basenames.',
259
+ permission: 'edit',
260
+ inputSchema: {
261
+ type: 'object',
262
+ additionalProperties: false,
263
+ required: ['path', 'content'],
264
+ properties: {
265
+ path: { type: 'string' },
266
+ content: { type: 'string' },
267
+ },
268
+ },
269
+ async execute(args) {
270
+ const path = requireString(args, 'path');
271
+ const content = requireString(args, 'content');
272
+ const { absolute, relativeToRoot } = resolveWorkspacePathOrThrow(ctx, path);
273
+ mkdirSync(dirname(absolute), { recursive: true });
274
+ const tmpPath = `${absolute}.pugi-mcp-tmp-${process.pid}-${Date.now()}`;
275
+ // Open with O_CREAT|O_EXCL so a concurrent writer cannot race
276
+ // a same-named tmp file out from under us. Mode 0o600 (operator
277
+ // only) — orchestrator writes are NOT shared artefacts.
278
+ const fd = openSync(tmpPath, 'wx', 0o600);
279
+ try {
280
+ writeFileSync(fd, content, 'utf8');
281
+ // fsync via fstatSync is a no-op on most kernels — the real
282
+ // durability win comes from rename being atomic at the inode
283
+ // layer. We still touch the fd to surface any late-EIO before
284
+ // rename commits.
285
+ fstatSync(fd);
286
+ }
287
+ finally {
288
+ closeSync(fd);
289
+ }
290
+ renameSync(tmpPath, absolute);
291
+ const bytesWritten = Buffer.byteLength(content, 'utf8');
292
+ return JSON.stringify({
293
+ path: relativeToRoot,
294
+ bytesWritten,
295
+ });
296
+ },
297
+ },
298
+ {
299
+ name: 'pugi.dispatch',
300
+ description: 'Run the Pugi engine loop end-to-end by shelling to `pugi <command> <prompt>` ' +
301
+ '(default command "code"). Drives the full client-side tool-use loop, so the ' +
302
+ 'caller sees real file writes, real shell exec, real cost — not just one Anvil ' +
303
+ 'turn. Workspace cwd must be `pugi init`-ed already; auth resolves through the ' +
304
+ 'CLI (PUGI_API_KEY env or on-disk `pugi login` state). ' +
305
+ 'Requires PUGI_MCP_EXEC_ENABLED=1 at server boot.',
306
+ permission: 'bash',
307
+ inputSchema: {
308
+ type: 'object',
309
+ additionalProperties: false,
310
+ required: ['prompt'],
311
+ properties: {
312
+ prompt: { type: 'string' },
313
+ command: {
314
+ type: 'string',
315
+ enum: ['code', 'explain', 'fix', 'plan', 'build'],
316
+ description: 'Pugi CLI subcommand. Default "code".',
317
+ },
318
+ cwd: {
319
+ type: 'string',
320
+ description: 'Optional workspace-relative cwd; defaults to the MCP workspace root. ' +
321
+ 'Must already be `pugi init`-ed.',
322
+ },
323
+ timeoutMs: {
324
+ type: 'number',
325
+ minimum: 100,
326
+ maximum: 600000,
327
+ description: 'Hard timeout in ms (default 180000).',
328
+ },
329
+ },
330
+ },
331
+ async execute(args) {
332
+ if (!ctx.capabilities.exec) {
333
+ throw new Error('pugi.dispatch: PUGI_MCP_EXEC_ENABLED is not set. ' +
334
+ 'Restart `pugi mcp serve` with PUGI_MCP_EXEC_ENABLED=1 to enable shell-driven dispatch.');
335
+ }
336
+ const prompt = requireString(args, 'prompt');
337
+ // Argv-injection guard. The `pugi` CLI parser (runtime/cli.ts) uses
338
+ // an allowlist of known global flags (`--remote`, `--allow-fetch`,
339
+ // `--allow-search`, `--triple`, etc.) and does not honour a `--`
340
+ // end-of-options sentinel. Passing a prompt that begins with `--`
341
+ // risks the parser swallowing it as a CLI flag (e.g. an attacker-
342
+ // controlled MCP client sending `prompt: "--allow-fetch"` to
343
+ // silently unlock a capability the operator did not intend to
344
+ // grant for this turn). Reject at the MCP boundary so we fail
345
+ // loud rather than silently shift CLI behaviour. Operators with
346
+ // legitimate prompts starting with `--` can prepend a space.
347
+ if (prompt.startsWith('--')) {
348
+ throw new Error('pugi.dispatch: prompt cannot start with "--" — the child CLI parser would ' +
349
+ 'interpret it as a flag. Prepend a space (" --foo") or rephrase.');
350
+ }
351
+ const command = optionalString(args, 'command') ?? 'code';
352
+ if (!ALLOWED_DISPATCH_COMMANDS.includes(command)) {
353
+ throw new Error(`pugi.dispatch: invalid command "${command}" (allowed: ${ALLOWED_DISPATCH_COMMANDS.join(', ')})`);
354
+ }
355
+ const cwdInput = optionalString(args, 'cwd');
356
+ const cwd = cwdInput
357
+ ? resolveWorkspacePathOrThrow(ctx, cwdInput).absolute
358
+ : ctx.workspaceRoot;
359
+ const timeoutMs = optionalNumber(args, 'timeoutMs', 180000);
360
+ const started = Date.now();
361
+ try {
362
+ const { stdout, stderr } = await execImpl(ctx.pugiBin, [command, prompt, '--no-tty'], {
363
+ cwd,
364
+ timeout: timeoutMs,
365
+ maxBuffer: 8 * 1024 * 1024,
366
+ // Auth-bearing envs are passed through here even though
367
+ // `sanitisedEnv()` strips them for `pugi.run`. Rationale:
368
+ // dispatch is explicitly an authenticated engine call, so
369
+ // the child must reach Anvil. The CLI prefers on-disk
370
+ // `pugi login` state when both are present.
371
+ env: dispatchEnv(),
372
+ });
373
+ return JSON.stringify({
374
+ command,
375
+ cwd,
376
+ exitCode: 0,
377
+ durationMs: Date.now() - started,
378
+ stdout: clamp(stdout, 16 * 1024),
379
+ stderr: clamp(stderr, 4 * 1024),
380
+ });
381
+ }
382
+ catch (err) {
383
+ const e = err;
384
+ // `||` chain (not `??`) so an empty / whitespace-only `e.stderr`
385
+ // does not swallow a spawn-side `e.message` like `"spawn pugi
386
+ // ENOENT"`. Operators need to distinguish "pugi binary missing"
387
+ // from "pugi ran and exited 1 silently."
388
+ const stderrText = e.stderr || e.message || '';
389
+ return JSON.stringify({
390
+ command,
391
+ cwd,
392
+ exitCode: typeof e.code === 'number' ? e.code : 1,
393
+ durationMs: Date.now() - started,
394
+ stdout: clamp(e.stdout ?? '', 16 * 1024),
395
+ stderr: clamp(stderrText, 4 * 1024),
396
+ ...(e.signal ? { signal: e.signal } : {}),
397
+ ...(e.killed ? { killed: true } : {}),
398
+ });
399
+ }
400
+ },
401
+ },
402
+ {
403
+ name: 'pugi.publish',
404
+ description: 'Bump @pugi/cli version + build + publish to npm. Use bumpType "beta" for ' +
405
+ 'prerelease bumps (default) or "patch" for stable. Requires ' +
406
+ 'PUGI_MCP_PUBLISH_ENABLED=1 AND a configured ~/.npmrc auth token.',
407
+ permission: 'network',
408
+ inputSchema: {
409
+ type: 'object',
410
+ additionalProperties: false,
411
+ properties: {
412
+ bumpType: {
413
+ type: 'string',
414
+ enum: ['patch', 'beta'],
415
+ description: 'Default "beta" — pre-release bump.',
416
+ },
417
+ },
418
+ },
419
+ async execute(args) {
420
+ if (!ctx.capabilities.publish) {
421
+ throw new Error('pugi.publish: PUGI_MCP_PUBLISH_ENABLED is not set. ' +
422
+ 'Restart `pugi mcp serve` with PUGI_MCP_PUBLISH_ENABLED=1 to enable.');
423
+ }
424
+ const bumpType = optionalString(args, 'bumpType') ?? 'beta';
425
+ if (bumpType !== 'patch' && bumpType !== 'beta') {
426
+ throw new Error(`pugi.publish: invalid bumpType "${bumpType}"`);
427
+ }
428
+ // npm version semantics: "patch" bumps z; "prerelease --preid beta"
429
+ // bumps the beta tag. We thread through `pnpm` because the
430
+ // monorepo build expects the workspace-aware variant.
431
+ const versionArgs = bumpType === 'beta'
432
+ ? ['version', 'prerelease', '--preid', 'beta', '--no-git-tag-version']
433
+ : ['version', 'patch', '--no-git-tag-version'];
434
+ const versionOut = await execImpl('npm', versionArgs, {
435
+ cwd: ctx.workspaceRoot,
436
+ timeout: 60000,
437
+ env: sanitisedEnv(),
438
+ });
439
+ const newVersion = (versionOut.stdout || '').trim().replace(/^v/, '');
440
+ const buildOut = await execImpl('pnpm', ['build'], {
441
+ cwd: ctx.workspaceRoot,
442
+ timeout: 180000,
443
+ env: sanitisedEnv(),
444
+ });
445
+ const publishOut = await execImpl('pnpm', ['publish', '--no-git-checks', '--access', 'public'], {
446
+ cwd: ctx.workspaceRoot,
447
+ timeout: 180000,
448
+ env: sanitisedEnv(),
449
+ });
450
+ return JSON.stringify({
451
+ newVersion,
452
+ registry: 'https://registry.npmjs.org',
453
+ npmExitCode: 0,
454
+ buildStdoutTail: clamp(buildOut.stdout, 2000),
455
+ publishStdoutTail: clamp(publishOut.stdout, 2000),
456
+ });
457
+ },
458
+ },
459
+ {
460
+ name: 'pugi.deploy',
461
+ description: 'SSH-redeploy a Pugi service on the engine VM (admin-api / admin-web / ' +
462
+ 'pugi-web / all). Runs git pull + pnpm install + build + pm2 restart. ' +
463
+ 'Requires PUGI_MCP_DEPLOY_ENABLED=1.',
464
+ permission: 'network',
465
+ inputSchema: {
466
+ type: 'object',
467
+ additionalProperties: false,
468
+ required: ['target'],
469
+ properties: {
470
+ target: {
471
+ type: 'string',
472
+ enum: ['admin-api', 'admin-web', 'pugi-web', 'all'],
473
+ },
474
+ },
475
+ },
476
+ async execute(args) {
477
+ if (!ctx.capabilities.deploy) {
478
+ throw new Error('pugi.deploy: PUGI_MCP_DEPLOY_ENABLED is not set. ' +
479
+ 'Restart `pugi mcp serve` with PUGI_MCP_DEPLOY_ENABLED=1 to enable.');
480
+ }
481
+ const target = requireString(args, 'target');
482
+ const allowed = ['admin-api', 'admin-web', 'pugi-web', 'all'];
483
+ if (!allowed.includes(target)) {
484
+ throw new Error(`pugi.deploy: invalid target "${target}" (allowed: ${allowed.join(', ')})`);
485
+ }
486
+ // The redeploy script lives on the engine VM at ~/deploy/<target>.sh.
487
+ // We do NOT inline the shell — the operator owns the remote
488
+ // script and can tune it without rebuilding the CLI.
489
+ const remoteCmd = `set -euo pipefail; ~/deploy/${target}.sh`;
490
+ const started = Date.now();
491
+ const { stdout, stderr } = await execImpl('ssh', [
492
+ // BatchMode rejects password prompts so a misconfigured
493
+ // ssh-agent fails fast instead of blocking the dispatch.
494
+ '-o',
495
+ 'BatchMode=yes',
496
+ '-o',
497
+ 'StrictHostKeyChecking=accept-new',
498
+ ctx.sshAlias,
499
+ remoteCmd,
500
+ ], {
501
+ cwd: ctx.workspaceRoot,
502
+ timeout: 300000,
503
+ maxBuffer: 4 * 1024 * 1024,
504
+ env: sanitisedEnv(),
505
+ });
506
+ const durationMs = Date.now() - started;
507
+ return JSON.stringify({
508
+ host: ctx.sshAlias,
509
+ target,
510
+ gitPullHead: extractGitHead(stdout) ?? null,
511
+ pm2Status: extractPm2Status(stdout, stderr) ?? null,
512
+ durationMs,
513
+ stdoutTail: clamp(stdout, 4000),
514
+ stderrTail: clamp(stderr, 2000),
515
+ });
516
+ },
517
+ },
518
+ ];
519
+ return tools.sort((a, b) => a.name.localeCompare(b.name));
520
+ }
521
+ /* ---------- helpers ---------------------------------------------------- */
522
+ function requireString(args, key) {
523
+ const v = args[key];
524
+ if (typeof v !== 'string' || v.length === 0) {
525
+ throw new Error(`argument "${key}" must be a non-empty string`);
526
+ }
527
+ return v;
528
+ }
529
+ function optionalString(args, key) {
530
+ const v = args[key];
531
+ if (v === undefined || v === null)
532
+ return undefined;
533
+ if (typeof v !== 'string') {
534
+ throw new Error(`argument "${key}" must be a string when set`);
535
+ }
536
+ return v;
537
+ }
538
+ function optionalNumber(args, key, fallback) {
539
+ const v = args[key];
540
+ if (v === undefined || v === null)
541
+ return fallback;
542
+ if (typeof v !== 'number' || !Number.isFinite(v)) {
543
+ throw new Error(`argument "${key}" must be a finite number when set`);
544
+ }
545
+ return v;
546
+ }
547
+ function clamp(s, max) {
548
+ if (typeof s !== 'string')
549
+ return '';
550
+ if (s.length <= max)
551
+ return s;
552
+ return `${s.slice(0, max)}\n…(truncated at ${max} bytes)`;
553
+ }
554
+ /**
555
+ * Tokenise an argv tail the same way the upstream tool's `pugi run` quoting
556
+ * convention does — whitespace-split with double-quote groups
557
+ * preserved. We do NOT eval a shell because that would let the model
558
+ * inject arbitrary commands (e.g. `; rm -rf ~`) into the orchestrator
559
+ * surface. Anything fancier (env-var expansion, globbing) must be
560
+ * delegated to the model via a `bash` capability flag — which is
561
+ * intentionally not part of this surface.
562
+ *
563
+ * Exported for the spec.
564
+ */
565
+ export function tokeniseArgv(command) {
566
+ const out = [];
567
+ let buf = '';
568
+ let inQuotes = false;
569
+ for (let i = 0; i < command.length; i += 1) {
570
+ const ch = command[i];
571
+ if (ch === '"') {
572
+ inQuotes = !inQuotes;
573
+ continue;
574
+ }
575
+ if (ch === '\\' && command[i + 1] === '"') {
576
+ buf += '"';
577
+ i += 1;
578
+ continue;
579
+ }
580
+ if (!inQuotes && (ch === ' ' || ch === '\t')) {
581
+ if (buf.length > 0) {
582
+ out.push(buf);
583
+ buf = '';
584
+ }
585
+ continue;
586
+ }
587
+ buf += ch;
588
+ }
589
+ if (inQuotes) {
590
+ throw new Error('pugi.run: unterminated double-quote in command');
591
+ }
592
+ if (buf.length > 0)
593
+ out.push(buf);
594
+ return out;
595
+ }
596
+ function sanitisedEnv() {
597
+ // Allowlist — pass through only what `pugi` needs to find itself
598
+ // and the local toolchain. NPM_TOKEN is added back for
599
+ // `pugi.publish` via the npm CLI's own ~/.npmrc lookup — we do not
600
+ // pass it via env because that surface ends up in `ps` output on
601
+ // some kernels.
602
+ const allow = ['PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'LC_ALL', 'TERM', 'NODE_OPTIONS'];
603
+ const out = {};
604
+ for (const key of allow) {
605
+ const value = process.env[key];
606
+ if (value !== undefined)
607
+ out[key] = value;
608
+ }
609
+ return out;
610
+ }
611
+ function dispatchEnv() {
612
+ // Like sanitisedEnv() but threads PUGI_API_KEY / PUGI_API_URL through
613
+ // so the child `pugi <command>` invocation can resolve auth from env
614
+ // when on-disk `pugi login` state is unavailable (CI, fresh container).
615
+ const allow = [
616
+ 'PATH',
617
+ 'HOME',
618
+ 'USER',
619
+ 'SHELL',
620
+ 'LANG',
621
+ 'LC_ALL',
622
+ 'TERM',
623
+ 'NODE_OPTIONS',
624
+ 'PUGI_API_KEY',
625
+ 'PUGI_API_URL',
626
+ ];
627
+ const out = {};
628
+ for (const key of allow) {
629
+ const value = process.env[key];
630
+ if (value !== undefined)
631
+ out[key] = value;
632
+ }
633
+ return out;
634
+ }
635
+ function extractGitHead(stdout) {
636
+ // Match "HEAD is now at <sha> …" or "<sha> commit message" — the
637
+ // remote redeploy script logs `git rev-parse HEAD` after pull.
638
+ const m = stdout.match(/(?:HEAD is now at|^|\n)([0-9a-f]{7,40})\b/);
639
+ return m ? m[1] : null;
640
+ }
641
+ function extractPm2Status(stdout, stderr) {
642
+ const haystack = `${stdout}\n${stderr}`;
643
+ // Match "[PM2] Process pugi-admin-api restarted" or "online" / "stopped"
644
+ const restart = haystack.match(/\[PM2\][^\n]+(restarted|online|stopped|errored)/i);
645
+ if (restart)
646
+ return restart[0].trim();
647
+ return null;
648
+ }
649
+ /* ---------- helper: load this module from compiled JS at runtime ------- */
650
+ // `fileURLToPath(import.meta.url)` is used by sibling modules to find
651
+ // fixtures at runtime; we re-export it here so the spec can build an
652
+ // isolated workspace next to the compiled module without hard-coding
653
+ // paths. Defensive — not currently used by the production wiring.
654
+ export const ORCHESTRATOR_TOOLS_MODULE_FILE = (() => {
655
+ try {
656
+ return fileURLToPath(import.meta.url);
657
+ }
658
+ catch {
659
+ return '';
660
+ }
661
+ })();
662
+ //# sourceMappingURL=orchestrator-tools.js.map