@pugi/cli 0.1.0-beta.10 → 0.1.0-beta.101

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 (464) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +1 -1
  3. package/README.md +55 -11
  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/retro.js +210 -0
  11. package/dist/commands/smoke.js +133 -0
  12. package/dist/core/agent-progress/cleanup.js +134 -0
  13. package/dist/core/agent-progress/schema.js +144 -0
  14. package/dist/core/agent-progress/writer.js +101 -0
  15. package/dist/core/agents/adaptive-router.js +330 -0
  16. package/dist/core/agents/query-decomposer.js +297 -0
  17. package/dist/core/agents/registry.js +3 -3
  18. package/dist/core/approvals/shortcut-resolver.js +98 -0
  19. package/dist/core/artifact-chain/dispatcher.js +148 -0
  20. package/dist/core/artifact-chain/exporter.js +164 -0
  21. package/dist/core/artifact-chain/state.js +243 -0
  22. package/dist/core/artifact-chain/steps.js +169 -0
  23. package/dist/core/ask-user/question.js +92 -0
  24. package/dist/core/audit/audit-trail.js +275 -0
  25. package/dist/core/auth/ensure-authenticated.js +129 -0
  26. package/dist/core/auth/env-provider.js +238 -0
  27. package/dist/core/auto-open-browser.js +4 -4
  28. package/dist/core/auto-update/channels.js +122 -0
  29. package/dist/core/auto-update/checker.js +241 -0
  30. package/dist/core/auto-update/state.js +235 -0
  31. package/dist/core/bare-mode/index.js +107 -0
  32. package/dist/core/bash/redirect.js +281 -0
  33. package/dist/core/bash-classifier.js +436 -40
  34. package/dist/core/checkpoint/resumer.js +149 -0
  35. package/dist/core/checkpoint/rewinder.js +291 -0
  36. package/dist/core/checkpoints/shadow-git.js +670 -0
  37. package/dist/core/citations/parser.js +109 -0
  38. package/dist/core/classifier/yolo-classifier.js +88 -0
  39. package/dist/core/codegraph/db.js +506 -0
  40. package/dist/core/codegraph/decision-store.js +248 -0
  41. package/dist/core/codegraph/detect-repo.js +459 -0
  42. package/dist/core/codegraph/install.js +134 -0
  43. package/dist/core/codegraph/offer-hook.js +220 -0
  44. package/dist/core/codegraph/parser.js +598 -0
  45. package/dist/core/codegraph/queries/go.scm +57 -0
  46. package/dist/core/codegraph/queries/javascript.scm +56 -0
  47. package/dist/core/codegraph/queries/python.scm +55 -0
  48. package/dist/core/codegraph/queries/rust.scm +63 -0
  49. package/dist/core/codegraph/queries/typescript.scm +91 -0
  50. package/dist/core/codegraph/reindex.js +218 -0
  51. package/dist/core/codegraph/resolve-edges.js +107 -0
  52. package/dist/core/codegraph/types.js +34 -0
  53. package/dist/core/codegraph/watcher.js +440 -0
  54. package/dist/core/compact/auto-trigger.js +96 -0
  55. package/dist/core/compact/buffer-rewriter.js +115 -0
  56. package/dist/core/compact/summarizer.js +208 -0
  57. package/dist/core/compact/token-counter.js +108 -0
  58. package/dist/core/consensus/anvil-fanout.js +25 -25
  59. package/dist/core/consensus/diff-capture.js +121 -12
  60. package/dist/core/consensus/rubric.js +21 -21
  61. package/dist/core/context/builder.js +6 -6
  62. package/dist/core/context/compaction-events.js +8 -8
  63. package/dist/core/context/compaction.js +31 -31
  64. package/dist/core/context/index.js +15 -8
  65. package/dist/core/context/invariants.js +51 -51
  66. package/dist/core/context/markdown-loader.js +28 -10
  67. package/dist/core/context/markdown-traverse.js +255 -0
  68. package/dist/core/context/pugiignore.js +41 -41
  69. package/dist/core/context/repo-skeleton.js +37 -37
  70. package/dist/core/context/tool-eviction.js +55 -0
  71. package/dist/core/context/watcher.js +32 -32
  72. package/dist/core/context/working-set.js +23 -23
  73. package/dist/core/coordinator/agent-tools.js +77 -0
  74. package/dist/core/coordinator/agent-toolset.js +65 -0
  75. package/dist/core/coordinator/fsm.js +73 -0
  76. package/dist/core/coordinator/mode-fsm.js +70 -0
  77. package/dist/core/cost/rate-card.js +129 -0
  78. package/dist/core/cost/tracker.js +221 -0
  79. package/dist/core/credentials.js +13 -13
  80. package/dist/core/cron/scheduler.js +138 -0
  81. package/dist/core/denial-tracking/index.js +8 -0
  82. package/dist/core/denial-tracking/state.js +264 -0
  83. package/dist/core/diagnostics/probe-runner.js +93 -0
  84. package/dist/core/diagnostics/probes/api.js +46 -0
  85. package/dist/core/diagnostics/probes/auth.js +93 -0
  86. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  87. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  88. package/dist/core/diagnostics/probes/config.js +72 -0
  89. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  90. package/dist/core/diagnostics/probes/disk.js +81 -0
  91. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  92. package/dist/core/diagnostics/probes/git.js +65 -0
  93. package/dist/core/diagnostics/probes/hooks.js +118 -0
  94. package/dist/core/diagnostics/probes/mcp.js +75 -0
  95. package/dist/core/diagnostics/probes/node.js +59 -0
  96. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  97. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  98. package/dist/core/diagnostics/probes/sandbox.js +67 -0
  99. package/dist/core/diagnostics/probes/session.js +74 -0
  100. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  101. package/dist/core/diagnostics/probes/workspace.js +63 -0
  102. package/dist/core/diagnostics/types.js +70 -0
  103. package/dist/core/dispatch/cache-cleanup.js +197 -0
  104. package/dist/core/dispatch/cache-handoff.js +295 -0
  105. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  106. package/dist/core/edits/dispatch.js +333 -7
  107. package/dist/core/edits/format-detector.js +260 -0
  108. package/dist/core/edits/format-matrix.js +26 -0
  109. package/dist/core/edits/fuzzy-ladder.js +650 -0
  110. package/dist/core/edits/index.js +5 -1
  111. package/dist/core/edits/journal.js +199 -0
  112. package/dist/core/edits/layer-a-apply.js +15 -15
  113. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  114. package/dist/core/edits/layer-b-apply.js +9 -9
  115. package/dist/core/edits/layer-c-apply.js +6 -6
  116. package/dist/core/edits/layer-d-ast.js +557 -14
  117. package/dist/core/edits/marker-parser.js +12 -12
  118. package/dist/core/edits/security-gate.js +27 -27
  119. package/dist/core/edits/verify-hook.js +273 -0
  120. package/dist/core/edits/worktree.js +29 -29
  121. package/dist/core/engine/anvil-client.js +214 -26
  122. package/dist/core/engine/auto-compact.js +247 -0
  123. package/dist/core/engine/budgets.js +220 -0
  124. package/dist/core/engine/compact-llm-summarizer.js +124 -0
  125. package/dist/core/engine/context-prefix.js +155 -0
  126. package/dist/core/engine/index.js +1 -1
  127. package/dist/core/engine/intensity.js +163 -0
  128. package/dist/core/engine/intent.js +260 -0
  129. package/dist/core/engine/native-pugi.js +1559 -227
  130. package/dist/core/engine/prompts.js +219 -19
  131. package/dist/core/engine/strip-internal-fields.js +124 -0
  132. package/dist/core/engine/tool-bridge.js +1887 -59
  133. package/dist/core/engine/verification-patterns.js +195 -0
  134. package/dist/core/eval/v1/ledger.js +83 -0
  135. package/dist/core/eval/v1/runner.js +280 -0
  136. package/dist/core/eval/v1/scoring.js +68 -0
  137. package/dist/core/eval/v1/task-loader.js +191 -0
  138. package/dist/core/eval/v1/types.js +14 -0
  139. package/dist/core/eval/v1/verifier.js +176 -0
  140. package/dist/core/eval/v1/yaml-parser.js +250 -0
  141. package/dist/core/evaluation/golden-dataset.js +293 -0
  142. package/dist/core/feedback/queue.js +177 -0
  143. package/dist/core/feedback/submitter.js +145 -0
  144. package/dist/core/file-cache.js +113 -1
  145. package/dist/core/flatten/flatten-repo.js +439 -0
  146. package/dist/core/format/osc8-link.js +28 -0
  147. package/dist/core/hook-chains.js +392 -0
  148. package/dist/core/hooks/citation-verify-hook.js +138 -0
  149. package/dist/core/hooks/citation-verify.js +112 -0
  150. package/dist/core/hooks/events.js +46 -0
  151. package/dist/core/hooks/index.js +15 -0
  152. package/dist/core/hooks/registry.js +216 -0
  153. package/dist/core/hooks/runner.js +236 -0
  154. package/dist/core/hooks/v2/event-emitter.js +115 -0
  155. package/dist/core/hooks/v2/executor.js +282 -0
  156. package/dist/core/hooks/v2/index.js +25 -0
  157. package/dist/core/hooks/v2/lifecycle.js +104 -0
  158. package/dist/core/hooks/v2/loader.js +216 -0
  159. package/dist/core/hooks/v2/matcher.js +125 -0
  160. package/dist/core/hooks/v2/trust.js +143 -0
  161. package/dist/core/hooks/v2/types.js +86 -0
  162. package/dist/core/hooks/worktree-events.js +158 -0
  163. package/dist/core/image/renderer.js +71 -0
  164. package/dist/core/init/detector.js +582 -0
  165. package/dist/core/init/template-renderer.js +242 -0
  166. package/dist/core/jobs/registry.js +18 -18
  167. package/dist/core/ledger/results-tsv.js +142 -0
  168. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  169. package/dist/core/lsp/cache.js +105 -0
  170. package/dist/core/lsp/client.js +551 -41
  171. package/dist/core/lsp/language-detect.js +66 -0
  172. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  173. package/dist/core/lsp/server-detect.js +173 -0
  174. package/dist/core/lsp/symbol-cache.js +162 -0
  175. package/dist/core/lsp/symbol-tools.js +664 -0
  176. package/dist/core/mcp/client.js +97 -28
  177. package/dist/core/mcp/http-server.js +553 -0
  178. package/dist/core/mcp/orchestrator-config.js +192 -0
  179. package/dist/core/mcp/orchestrator-tools.js +806 -0
  180. package/dist/core/mcp/permission.js +190 -0
  181. package/dist/core/mcp/registry.js +39 -17
  182. package/dist/core/mcp/server-tools.js +219 -0
  183. package/dist/core/mcp/server.js +397 -0
  184. package/dist/core/mcp/trust.js +10 -10
  185. package/dist/core/memory/dual-write.js +416 -0
  186. package/dist/core/memory/passive-extract.js +130 -0
  187. package/dist/core/memory/phase1-kinds.js +20 -0
  188. package/dist/core/memory/secret-scanner.js +304 -0
  189. package/dist/core/memory-sync/queue.js +170 -0
  190. package/dist/core/metrics/extract.js +113 -0
  191. package/dist/core/modes/roo-modes.js +68 -0
  192. package/dist/core/notes/notes-paths.js +113 -0
  193. package/dist/core/notes/notes-recorder.js +140 -0
  194. package/dist/core/notes/notes-writer.js +53 -0
  195. package/dist/core/notes/renderers.js +0 -0
  196. package/dist/core/notes/slug.js +105 -0
  197. package/dist/core/onboarding/ensure-initialized.js +133 -0
  198. package/dist/core/onboarding/marker.js +111 -0
  199. package/dist/core/onboarding/telemetry-state.js +108 -0
  200. package/dist/core/output-style/presets.js +176 -0
  201. package/dist/core/output-style/state.js +185 -0
  202. package/dist/core/path-security.js +287 -5
  203. package/dist/core/permission.js +82 -22
  204. package/dist/core/permissions/auto-classifier.js +124 -0
  205. package/dist/core/permissions/bash-parser.js +371 -0
  206. package/dist/core/permissions/circuit-breaker.js +83 -0
  207. package/dist/core/permissions/constrained-edit.js +91 -0
  208. package/dist/core/permissions/gate.js +278 -0
  209. package/dist/core/permissions/index.js +20 -0
  210. package/dist/core/permissions/mode.js +174 -0
  211. package/dist/core/permissions/network-egress.js +137 -0
  212. package/dist/core/permissions/state.js +241 -0
  213. package/dist/core/permissions/tool-class.js +107 -0
  214. package/dist/core/plan-mode/ui-state.js +51 -0
  215. package/dist/core/plans/plan-artifact.js +721 -0
  216. package/dist/core/policy-limits/etag-store.js +122 -0
  217. package/dist/core/prd-check/parser.js +215 -0
  218. package/dist/core/prd-check/reporter.js +127 -0
  219. package/dist/core/prd-check/session-review.js +557 -0
  220. package/dist/core/prd-check/verifiers.js +223 -0
  221. package/dist/core/prompt-cache/client-cache.js +99 -0
  222. package/dist/core/prompts/assembly.js +29 -0
  223. package/dist/core/prompts/registry.js +364 -0
  224. package/dist/core/pugi-gitignore.js +52 -0
  225. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  226. package/dist/core/pugi-md/context-injector.js +76 -0
  227. package/dist/core/pugi-md/walk-up.js +207 -0
  228. package/dist/core/python/uv-installer.js +270 -0
  229. package/dist/core/python/uv-resolver.js +83 -0
  230. package/dist/core/rate-limit/narrator.js +146 -0
  231. package/dist/core/recipes/cli-types.js +20 -0
  232. package/dist/core/recipes/loader.js +103 -0
  233. package/dist/core/recipes/runner.js +345 -0
  234. package/dist/core/recipes/schema.js +587 -0
  235. package/dist/core/release-notes/parser.js +241 -0
  236. package/dist/core/release-notes/state.js +116 -0
  237. package/dist/core/repl/ask.js +37 -37
  238. package/dist/core/repl/cancellation.js +26 -26
  239. package/dist/core/repl/cap-warning.js +4 -4
  240. package/dist/core/repl/clipboard-read.js +11 -11
  241. package/dist/core/repl/dispatch-fsm.js +12 -12
  242. package/dist/core/repl/engine-bridge.js +303 -0
  243. package/dist/core/repl/history-search.js +15 -15
  244. package/dist/core/repl/history.js +28 -18
  245. package/dist/core/repl/kill-ring.js +5 -5
  246. package/dist/core/repl/model-pricing.js +135 -0
  247. package/dist/core/repl/privacy-banner.js +22 -22
  248. package/dist/core/repl/session.js +2690 -229
  249. package/dist/core/repl/slash-commands.js +540 -41
  250. package/dist/core/repl/store/index.js +1 -1
  251. package/dist/core/repl/store/jsonl-log.js +22 -22
  252. package/dist/core/repl/store/lockfile.js +10 -10
  253. package/dist/core/repl/store/session-store.js +136 -107
  254. package/dist/core/repl/store/types.js +15 -15
  255. package/dist/core/repl/store/uuid-v7.js +12 -12
  256. package/dist/core/repl/tool-route.js +382 -0
  257. package/dist/core/repl/workspace-context.js +43 -21
  258. package/dist/core/repo-map/build.js +125 -0
  259. package/dist/core/repo-map/cache.js +185 -0
  260. package/dist/core/repo-map/extractor.js +254 -0
  261. package/dist/core/repo-map/formatter.js +145 -0
  262. package/dist/core/repo-map/page-rank.js +105 -0
  263. package/dist/core/repo-map/scanner.js +211 -0
  264. package/dist/core/retro/git-collector.js +251 -0
  265. package/dist/core/retro/health-card.js +25 -0
  266. package/dist/core/retro/metrics.js +342 -0
  267. package/dist/core/retro/narrative.js +249 -0
  268. package/dist/core/retro/plane-collector.js +274 -0
  269. package/dist/core/retro/pr-issue-link.js +65 -0
  270. package/dist/core/retro/types.js +16 -0
  271. package/dist/core/retry-budget/budget.js +284 -0
  272. package/dist/core/retry-budget/index.js +5 -0
  273. package/dist/core/retry-budget/retry-cap.js +74 -0
  274. package/dist/core/routing/lead-worker.js +43 -0
  275. package/dist/core/routing/pre-flight-estimator.js +108 -0
  276. package/dist/core/runs/run-tree.js +103 -0
  277. package/dist/core/sandboxing/adapter.js +43 -0
  278. package/dist/core/sandboxing/bubblewrap.js +209 -0
  279. package/dist/core/sandboxing/index.js +78 -0
  280. package/dist/core/sandboxing/none.js +19 -0
  281. package/dist/core/sandboxing/policy.js +97 -0
  282. package/dist/core/sandboxing/seatbelt.js +231 -0
  283. package/dist/core/security/injection-scanner.js +367 -0
  284. package/dist/core/security/output-filter.js +418 -0
  285. package/dist/core/session/env-file.js +105 -0
  286. package/dist/core/session/section-budgets.js +140 -0
  287. package/dist/core/session.js +119 -0
  288. package/dist/core/settings.js +402 -5
  289. package/dist/core/share/formatter.js +271 -0
  290. package/dist/core/share/redactor.js +221 -0
  291. package/dist/core/share/uploader.js +267 -0
  292. package/dist/core/skills/defaults.js +30 -30
  293. package/dist/core/skills/loader.js +22 -22
  294. package/dist/core/skills/sources.js +27 -27
  295. package/dist/core/smoke/headless-driver.js +174 -0
  296. package/dist/core/smoke/orchestrator.js +194 -0
  297. package/dist/core/smoke/runner.js +238 -0
  298. package/dist/core/smoke/scenario-parser.js +316 -0
  299. package/dist/core/statusline.js +99 -0
  300. package/dist/core/subagents/dispatcher-real.js +600 -0
  301. package/dist/core/subagents/dispatcher.js +146 -52
  302. package/dist/core/subagents/index.js +19 -6
  303. package/dist/core/subagents/isolation-matrix.js +213 -0
  304. package/dist/core/subagents/spawn.js +19 -4
  305. package/dist/core/telemetry/emitter.js +229 -0
  306. package/dist/core/telemetry/queue.js +251 -0
  307. package/dist/core/theme/context.js +91 -0
  308. package/dist/core/theme/presets.js +228 -0
  309. package/dist/core/theme/state.js +181 -0
  310. package/dist/core/todos/invariant.js +10 -0
  311. package/dist/core/todos/state.js +177 -0
  312. package/dist/core/tool-schema/compressor.js +89 -0
  313. package/dist/core/transport/version-interceptor.js +166 -0
  314. package/dist/core/trust.js +2 -2
  315. package/dist/core/tui/thinking-block.js +64 -0
  316. package/dist/core/vim/keymap.js +288 -0
  317. package/dist/core/vim/state.js +92 -0
  318. package/dist/core/watch-markers/marker-watcher.js +133 -0
  319. package/dist/core/worktree/include-parser.js +249 -0
  320. package/dist/core/worktree-manager/cleanup.js +123 -0
  321. package/dist/core/worktree-manager/manager.js +303 -0
  322. package/dist/index.js +36 -0
  323. package/dist/runtime/bootstrap.js +190 -0
  324. package/dist/runtime/cli.js +4403 -561
  325. package/dist/runtime/commands/agents.js +31 -31
  326. package/dist/runtime/commands/budget.js +5 -5
  327. package/dist/runtime/commands/cancel.js +231 -0
  328. package/dist/runtime/commands/chain.js +489 -0
  329. package/dist/runtime/commands/codegraph-status.js +227 -0
  330. package/dist/runtime/commands/compact.js +297 -0
  331. package/dist/runtime/commands/config.js +74 -40
  332. package/dist/runtime/commands/cost.js +199 -0
  333. package/dist/runtime/commands/delegate.js +27 -4
  334. package/dist/runtime/commands/dispatch.js +126 -0
  335. package/dist/runtime/commands/doctor.js +579 -0
  336. package/dist/runtime/commands/eval-v1.js +266 -0
  337. package/dist/runtime/commands/feedback.js +184 -0
  338. package/dist/runtime/commands/hooks.js +187 -0
  339. package/dist/runtime/commands/index-cmd.js +459 -0
  340. package/dist/runtime/commands/init.js +254 -0
  341. package/dist/runtime/commands/lsp.js +200 -38
  342. package/dist/runtime/commands/mcp.js +935 -0
  343. package/dist/runtime/commands/memory.js +582 -0
  344. package/dist/runtime/commands/model.js +237 -0
  345. package/dist/runtime/commands/onboarding.js +275 -0
  346. package/dist/runtime/commands/patch.js +12 -12
  347. package/dist/runtime/commands/permissions.js +112 -0
  348. package/dist/runtime/commands/plan.js +143 -0
  349. package/dist/runtime/commands/prd-check.js +285 -0
  350. package/dist/runtime/commands/privacy.js +17 -17
  351. package/dist/runtime/commands/recipe.js +325 -0
  352. package/dist/runtime/commands/redo-blob-store.js +92 -0
  353. package/dist/runtime/commands/redo.js +361 -0
  354. package/dist/runtime/commands/release-notes.js +229 -0
  355. package/dist/runtime/commands/repo-map.js +95 -0
  356. package/dist/runtime/commands/report.js +299 -0
  357. package/dist/runtime/commands/resume.js +118 -0
  358. package/dist/runtime/commands/review-consensus.js +68 -53
  359. package/dist/runtime/commands/rewind.js +333 -0
  360. package/dist/runtime/commands/roster.js +14 -14
  361. package/dist/runtime/commands/servers-cli.js +182 -0
  362. package/dist/runtime/commands/servers.js +236 -0
  363. package/dist/runtime/commands/sessions.js +163 -0
  364. package/dist/runtime/commands/share.js +316 -0
  365. package/dist/runtime/commands/skills.js +31 -31
  366. package/dist/runtime/commands/status.js +186 -0
  367. package/dist/runtime/commands/stickers.js +82 -0
  368. package/dist/runtime/commands/style.js +194 -0
  369. package/dist/runtime/commands/theme.js +196 -0
  370. package/dist/runtime/commands/undo.js +54 -22
  371. package/dist/runtime/commands/update.js +289 -0
  372. package/dist/runtime/commands/vim.js +140 -0
  373. package/dist/runtime/commands/worktree.js +8 -8
  374. package/dist/runtime/commands/worktrees.js +155 -0
  375. package/dist/runtime/deprecation-warning.js +69 -0
  376. package/dist/runtime/engine-exit-code.js +50 -0
  377. package/dist/runtime/headless-repl.js +195 -0
  378. package/dist/runtime/headless.js +548 -0
  379. package/dist/runtime/load-hooks-or-exit.js +71 -0
  380. package/dist/runtime/plan-decompose.js +22 -22
  381. package/dist/runtime/sigint-guard.js +272 -0
  382. package/dist/runtime/stream-renderer.js +195 -0
  383. package/dist/runtime/update-check.js +28 -28
  384. package/dist/runtime/version.js +65 -0
  385. package/dist/runtime/worktree-bootstrap.js +579 -0
  386. package/dist/skills/bundled/batch.js +617 -0
  387. package/dist/skills/bundled/index.js +45 -0
  388. package/dist/skills/bundled/loop.js +358 -0
  389. package/dist/skills/bundled/remember.js +383 -0
  390. package/dist/skills/bundled/simplify.js +289 -0
  391. package/dist/skills/bundled/skillify.js +373 -0
  392. package/dist/skills/bundled/stuck.js +558 -0
  393. package/dist/skills/bundled/verify.js +439 -0
  394. package/dist/testing/vcr.js +486 -0
  395. package/dist/tools/agent-tool.js +229 -0
  396. package/dist/tools/apply-patch.js +89 -28
  397. package/dist/tools/ask-user-question.js +337 -0
  398. package/dist/tools/ask-user.js +115 -0
  399. package/dist/tools/bash.js +811 -49
  400. package/dist/tools/brief.js +224 -0
  401. package/dist/tools/cron.js +433 -0
  402. package/dist/tools/enter-worktree.js +250 -0
  403. package/dist/tools/exit-worktree.js +147 -0
  404. package/dist/tools/file-tools.js +161 -44
  405. package/dist/tools/http-request.js +336 -0
  406. package/dist/tools/lsp-tools.js +377 -1
  407. package/dist/tools/mcp-tool.js +260 -0
  408. package/dist/tools/multi-edit.js +361 -0
  409. package/dist/tools/powershell.js +268 -0
  410. package/dist/tools/registry.js +120 -5
  411. package/dist/tools/server-tools.js +892 -0
  412. package/dist/tools/skill-tool.js +96 -0
  413. package/dist/tools/sleep.js +99 -0
  414. package/dist/tools/synthetic-output.js +133 -0
  415. package/dist/tools/tasks.js +208 -0
  416. package/dist/tools/todo-write.js +184 -0
  417. package/dist/tools/verify-plan-execution.js +295 -0
  418. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  419. package/dist/tools/web-fetch.js +195 -10
  420. package/dist/tools/web-search.js +458 -0
  421. package/dist/tui/agent-progress-card.js +111 -0
  422. package/dist/tui/agent-tree.js +22 -1
  423. package/dist/tui/ask-modal.js +14 -14
  424. package/dist/tui/ask-user-question-chips.js +315 -0
  425. package/dist/tui/ask-user-question-prompt.js +203 -0
  426. package/dist/tui/compact-banner.js +81 -0
  427. package/dist/tui/conversation-pane.js +85 -11
  428. package/dist/tui/cost-table.js +111 -0
  429. package/dist/tui/device-flow.js +2 -2
  430. package/dist/tui/doctor-table.js +46 -0
  431. package/dist/tui/feedback-prompt.js +156 -0
  432. package/dist/tui/input-box.js +247 -32
  433. package/dist/tui/login-picker.js +3 -3
  434. package/dist/tui/markdown-render.js +6 -6
  435. package/dist/tui/multi-file-diff-approval.js +375 -0
  436. package/dist/tui/onboarding-wizard.js +240 -0
  437. package/dist/tui/permissions-picker.js +86 -0
  438. package/dist/tui/render.js +36 -1
  439. package/dist/tui/repl-render.js +239 -25
  440. package/dist/tui/repl-splash-art.js +16 -16
  441. package/dist/tui/repl-splash-mascot.js +48 -24
  442. package/dist/tui/repl-splash.js +22 -22
  443. package/dist/tui/repl.js +125 -45
  444. package/dist/tui/slash-palette.js +6 -6
  445. package/dist/tui/splash.js +2 -2
  446. package/dist/tui/status-bar.js +109 -31
  447. package/dist/tui/status-table.js +7 -0
  448. package/dist/tui/stickers-art.js +136 -0
  449. package/dist/tui/style-table.js +28 -0
  450. package/dist/tui/theme-table.js +29 -0
  451. package/dist/tui/thinking-spinner.js +123 -0
  452. package/dist/tui/tool-stream-pane.js +53 -4
  453. package/dist/tui/update-banner.js +27 -2
  454. package/dist/tui/vim-input.js +267 -0
  455. package/dist/tui/welcome-banner.js +107 -0
  456. package/dist/tui/welcome-data.js +293 -0
  457. package/dist/tui/workspace-context.js +2 -2
  458. package/package.json +29 -6
  459. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  460. package/test/scenarios/compact-force.scenario.txt +12 -0
  461. package/test/scenarios/identity.scenario.txt +11 -0
  462. package/test/scenarios/persona-handoff.scenario.txt +12 -0
  463. package/test/scenarios/walkback.scenario.txt +12 -0
  464. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,806 @@
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
+ // PUGI-VERIFY-GATE: dispatch the child with `--json` so we
362
+ // can parse its structured outcome envelope. The CLI's JSON
363
+ // mode includes verified / verificationCommands /
364
+ // verificationFailures / unverifiedReason /
365
+ // regressionOwnershipDispute. The MCP response now carries
366
+ // those fields through alongside the honest exit code so
367
+ // callers see the gate state, not just a "ran" boolean.
368
+ try {
369
+ const { stdout, stderr } = await execImpl(ctx.pugiBin, [command, prompt, '--no-tty', '--json'], {
370
+ cwd,
371
+ timeout: timeoutMs,
372
+ maxBuffer: 8 * 1024 * 1024,
373
+ // Auth-bearing envs are passed through here even though
374
+ // `sanitisedEnv()` strips them for `pugi.run`. Rationale:
375
+ // dispatch is explicitly an authenticated engine call, so
376
+ // the child must reach Anvil. The CLI prefers on-disk
377
+ // `pugi login` state when both are present.
378
+ env: dispatchEnv(),
379
+ });
380
+ // Codex dogfood 2026-06-04: the prior implementation
381
+ // hardcoded `exitCode: 0` on the happy execImpl path even
382
+ // when the child surfaced a verification failure through
383
+ // `--json`. The child's `--json` envelope is the source of
384
+ // truth — parse it and mirror `verified` / `status` back
385
+ // to the MCP caller. The execImpl-level "no throw" signal
386
+ // is no longer trusted as "exit 0".
387
+ const parsed = parseDispatchEnvelope(stdout);
388
+ const dispatchExitCode = resolveDispatchExitCode(parsed);
389
+ return JSON.stringify({
390
+ command,
391
+ cwd,
392
+ // CRITICAL: derived from parsed envelope, not constant 0.
393
+ exitCode: dispatchExitCode,
394
+ durationMs: Date.now() - started,
395
+ stdout: clamp(stdout, 16 * 1024),
396
+ stderr: clamp(stderr, 4 * 1024),
397
+ ...(parsed
398
+ ? {
399
+ status: parsed.status,
400
+ verified: parsed.verified,
401
+ verificationCommands: parsed.verificationCommands,
402
+ verificationFailures: parsed.verificationFailures,
403
+ unverifiedReason: parsed.unverifiedReason,
404
+ regressionOwnershipDispute: parsed.regressionOwnershipDispute,
405
+ }
406
+ : {}),
407
+ });
408
+ }
409
+ catch (err) {
410
+ const e = err;
411
+ // `||` chain (not `??`) so an empty / whitespace-only `e.stderr`
412
+ // does not swallow a spawn-side `e.message` like `"spawn pugi
413
+ // ENOENT"`. Operators need to distinguish "pugi binary missing"
414
+ // from "pugi ran and exited 1 silently."
415
+ const stderrText = e.stderr || e.message || '';
416
+ // PUGI-VERIFY-GATE: even on the throw path, parse stdout
417
+ // when present so the verification gate state surfaces.
418
+ // The child's CLI exits non-zero for failed / blocked /
419
+ // needs_verification, which puts execImpl on this path.
420
+ const parsed = parseDispatchEnvelope(e.stdout ?? '');
421
+ const dispatchExitCode = typeof e.code === 'number'
422
+ ? e.code
423
+ : parsed
424
+ ? resolveDispatchExitCode(parsed)
425
+ : 1;
426
+ return JSON.stringify({
427
+ command,
428
+ cwd,
429
+ exitCode: dispatchExitCode,
430
+ durationMs: Date.now() - started,
431
+ stdout: clamp(e.stdout ?? '', 16 * 1024),
432
+ stderr: clamp(stderrText, 4 * 1024),
433
+ ...(e.signal ? { signal: e.signal } : {}),
434
+ ...(e.killed ? { killed: true } : {}),
435
+ ...(parsed
436
+ ? {
437
+ status: parsed.status,
438
+ verified: parsed.verified,
439
+ verificationCommands: parsed.verificationCommands,
440
+ verificationFailures: parsed.verificationFailures,
441
+ unverifiedReason: parsed.unverifiedReason,
442
+ regressionOwnershipDispute: parsed.regressionOwnershipDispute,
443
+ }
444
+ : {}),
445
+ });
446
+ }
447
+ },
448
+ },
449
+ {
450
+ name: 'pugi.publish',
451
+ description: 'Bump @pugi/cli version + build + publish to npm. Use bumpType "beta" for ' +
452
+ 'prerelease bumps (default) or "patch" for stable. Requires ' +
453
+ 'PUGI_MCP_PUBLISH_ENABLED=1 AND a configured ~/.npmrc auth token.',
454
+ permission: 'network',
455
+ inputSchema: {
456
+ type: 'object',
457
+ additionalProperties: false,
458
+ properties: {
459
+ bumpType: {
460
+ type: 'string',
461
+ enum: ['patch', 'beta'],
462
+ description: 'Default "beta" — pre-release bump.',
463
+ },
464
+ },
465
+ },
466
+ async execute(args) {
467
+ if (!ctx.capabilities.publish) {
468
+ throw new Error('pugi.publish: PUGI_MCP_PUBLISH_ENABLED is not set. ' +
469
+ 'Restart `pugi mcp serve` with PUGI_MCP_PUBLISH_ENABLED=1 to enable.');
470
+ }
471
+ const bumpType = optionalString(args, 'bumpType') ?? 'beta';
472
+ if (bumpType !== 'patch' && bumpType !== 'beta') {
473
+ throw new Error(`pugi.publish: invalid bumpType "${bumpType}"`);
474
+ }
475
+ // npm version semantics: "patch" bumps z; "prerelease --preid beta"
476
+ // bumps the beta tag. We thread through `pnpm` because the
477
+ // monorepo build expects the workspace-aware variant.
478
+ const versionArgs = bumpType === 'beta'
479
+ ? ['version', 'prerelease', '--preid', 'beta', '--no-git-tag-version']
480
+ : ['version', 'patch', '--no-git-tag-version'];
481
+ const versionOut = await execImpl('npm', versionArgs, {
482
+ cwd: ctx.workspaceRoot,
483
+ timeout: 60000,
484
+ env: sanitisedEnv(),
485
+ });
486
+ const newVersion = (versionOut.stdout || '').trim().replace(/^v/, '');
487
+ const buildOut = await execImpl('pnpm', ['build'], {
488
+ cwd: ctx.workspaceRoot,
489
+ timeout: 180000,
490
+ env: sanitisedEnv(),
491
+ });
492
+ const publishOut = await execImpl('pnpm', ['publish', '--no-git-checks', '--access', 'public'], {
493
+ cwd: ctx.workspaceRoot,
494
+ timeout: 180000,
495
+ env: sanitisedEnv(),
496
+ });
497
+ return JSON.stringify({
498
+ newVersion,
499
+ registry: 'https://registry.npmjs.org',
500
+ npmExitCode: 0,
501
+ buildStdoutTail: clamp(buildOut.stdout, 2000),
502
+ publishStdoutTail: clamp(publishOut.stdout, 2000),
503
+ });
504
+ },
505
+ },
506
+ {
507
+ name: 'pugi.deploy',
508
+ description: 'SSH-redeploy a Pugi service on the engine VM (admin-api / admin-web / ' +
509
+ 'pugi-web / all). Runs git pull + pnpm install + build + pm2 restart. ' +
510
+ 'Requires PUGI_MCP_DEPLOY_ENABLED=1.',
511
+ permission: 'network',
512
+ inputSchema: {
513
+ type: 'object',
514
+ additionalProperties: false,
515
+ required: ['target'],
516
+ properties: {
517
+ target: {
518
+ type: 'string',
519
+ enum: ['admin-api', 'admin-web', 'pugi-web', 'all'],
520
+ },
521
+ },
522
+ },
523
+ async execute(args) {
524
+ if (!ctx.capabilities.deploy) {
525
+ throw new Error('pugi.deploy: PUGI_MCP_DEPLOY_ENABLED is not set. ' +
526
+ 'Restart `pugi mcp serve` with PUGI_MCP_DEPLOY_ENABLED=1 to enable.');
527
+ }
528
+ const target = requireString(args, 'target');
529
+ const allowed = ['admin-api', 'admin-web', 'pugi-web', 'all'];
530
+ if (!allowed.includes(target)) {
531
+ throw new Error(`pugi.deploy: invalid target "${target}" (allowed: ${allowed.join(', ')})`);
532
+ }
533
+ // The redeploy script lives on the engine VM at ~/deploy/<target>.sh.
534
+ // We do NOT inline the shell — the operator owns the remote
535
+ // script and can tune it without rebuilding the CLI.
536
+ const remoteCmd = `set -euo pipefail; ~/deploy/${target}.sh`;
537
+ const started = Date.now();
538
+ const { stdout, stderr } = await execImpl('ssh', [
539
+ // BatchMode rejects password prompts so a misconfigured
540
+ // ssh-agent fails fast instead of blocking the dispatch.
541
+ '-o',
542
+ 'BatchMode=yes',
543
+ '-o',
544
+ 'StrictHostKeyChecking=accept-new',
545
+ ctx.sshAlias,
546
+ remoteCmd,
547
+ ], {
548
+ cwd: ctx.workspaceRoot,
549
+ timeout: 300000,
550
+ maxBuffer: 4 * 1024 * 1024,
551
+ env: sanitisedEnv(),
552
+ });
553
+ const durationMs = Date.now() - started;
554
+ return JSON.stringify({
555
+ host: ctx.sshAlias,
556
+ target,
557
+ gitPullHead: extractGitHead(stdout) ?? null,
558
+ pm2Status: extractPm2Status(stdout, stderr) ?? null,
559
+ durationMs,
560
+ stdoutTail: clamp(stdout, 4000),
561
+ stderrTail: clamp(stderr, 2000),
562
+ });
563
+ },
564
+ },
565
+ ];
566
+ return tools.sort((a, b) => a.name.localeCompare(b.name));
567
+ }
568
+ /* ---------- helpers ---------------------------------------------------- */
569
+ function requireString(args, key) {
570
+ const v = args[key];
571
+ if (typeof v !== 'string' || v.length === 0) {
572
+ throw new Error(`argument "${key}" must be a non-empty string`);
573
+ }
574
+ return v;
575
+ }
576
+ function optionalString(args, key) {
577
+ const v = args[key];
578
+ if (v === undefined || v === null)
579
+ return undefined;
580
+ if (typeof v !== 'string') {
581
+ throw new Error(`argument "${key}" must be a string when set`);
582
+ }
583
+ return v;
584
+ }
585
+ function optionalNumber(args, key, fallback) {
586
+ const v = args[key];
587
+ if (v === undefined || v === null)
588
+ return fallback;
589
+ if (typeof v !== 'number' || !Number.isFinite(v)) {
590
+ throw new Error(`argument "${key}" must be a finite number when set`);
591
+ }
592
+ return v;
593
+ }
594
+ function clamp(s, max) {
595
+ if (typeof s !== 'string')
596
+ return '';
597
+ if (s.length <= max)
598
+ return s;
599
+ return `${s.slice(0, max)}\n…(truncated at ${max} bytes)`;
600
+ }
601
+ /**
602
+ * Tokenise an argv tail the same way the upstream tool's `pugi run` quoting
603
+ * convention does — whitespace-split with double-quote groups
604
+ * preserved. We do NOT eval a shell because that would let the model
605
+ * inject arbitrary commands (e.g. `; rm -rf ~`) into the orchestrator
606
+ * surface. Anything fancier (env-var expansion, globbing) must be
607
+ * delegated to the model via a `bash` capability flag — which is
608
+ * intentionally not part of this surface.
609
+ *
610
+ * Exported for the spec.
611
+ */
612
+ export function tokeniseArgv(command) {
613
+ const out = [];
614
+ let buf = '';
615
+ let inQuotes = false;
616
+ for (let i = 0; i < command.length; i += 1) {
617
+ const ch = command[i];
618
+ if (ch === '"') {
619
+ inQuotes = !inQuotes;
620
+ continue;
621
+ }
622
+ if (ch === '\\' && command[i + 1] === '"') {
623
+ buf += '"';
624
+ i += 1;
625
+ continue;
626
+ }
627
+ if (!inQuotes && (ch === ' ' || ch === '\t')) {
628
+ if (buf.length > 0) {
629
+ out.push(buf);
630
+ buf = '';
631
+ }
632
+ continue;
633
+ }
634
+ buf += ch;
635
+ }
636
+ if (inQuotes) {
637
+ throw new Error('pugi.run: unterminated double-quote in command');
638
+ }
639
+ if (buf.length > 0)
640
+ out.push(buf);
641
+ return out;
642
+ }
643
+ function sanitisedEnv() {
644
+ // Allowlist — pass through only what `pugi` needs to find itself
645
+ // and the local toolchain. NPM_TOKEN is added back for
646
+ // `pugi.publish` via the npm CLI's own ~/.npmrc lookup — we do not
647
+ // pass it via env because that surface ends up in `ps` output on
648
+ // some kernels.
649
+ const allow = ['PATH', 'HOME', 'USER', 'SHELL', 'LANG', 'LC_ALL', 'TERM', 'NODE_OPTIONS'];
650
+ const out = {};
651
+ for (const key of allow) {
652
+ const value = process.env[key];
653
+ if (value !== undefined)
654
+ out[key] = value;
655
+ }
656
+ return out;
657
+ }
658
+ function dispatchEnv() {
659
+ // Like sanitisedEnv() but threads PUGI_API_KEY / PUGI_API_URL through
660
+ // so the child `pugi <command>` invocation can resolve auth from env
661
+ // when on-disk `pugi login` state is unavailable (CI, fresh container).
662
+ const allow = [
663
+ 'PATH',
664
+ 'HOME',
665
+ 'USER',
666
+ 'SHELL',
667
+ 'LANG',
668
+ 'LC_ALL',
669
+ 'TERM',
670
+ 'NODE_OPTIONS',
671
+ 'PUGI_API_KEY',
672
+ 'PUGI_API_URL',
673
+ ];
674
+ const out = {};
675
+ for (const key of allow) {
676
+ const value = process.env[key];
677
+ if (value !== undefined)
678
+ out[key] = value;
679
+ }
680
+ return out;
681
+ }
682
+ function extractGitHead(stdout) {
683
+ // Match "HEAD is now at <sha> …" or "<sha> commit message" — the
684
+ // remote redeploy script logs `git rev-parse HEAD` after pull.
685
+ const m = stdout.match(/(?:HEAD is now at|^|\n)([0-9a-f]{7,40})\b/);
686
+ return m ? m[1] : null;
687
+ }
688
+ function extractPm2Status(stdout, stderr) {
689
+ const haystack = `${stdout}\n${stderr}`;
690
+ // Match "[PM2] Process pugi-admin-api restarted" or "online" / "stopped"
691
+ const restart = haystack.match(/\[PM2\][^\n]+(restarted|online|stopped|errored)/i);
692
+ if (restart)
693
+ return restart[0].trim();
694
+ return null;
695
+ }
696
+ /* ---------- helper: load this module from compiled JS at runtime ------- */
697
+ // `fileURLToPath(import.meta.url)` is used by sibling modules to find
698
+ // fixtures at runtime; we re-export it here so the spec can build an
699
+ // isolated workspace next to the compiled module without hard-coding
700
+ // paths. Defensive — not currently used by the production wiring.
701
+ export const ORCHESTRATOR_TOOLS_MODULE_FILE = (() => {
702
+ try {
703
+ return fileURLToPath(import.meta.url);
704
+ }
705
+ catch {
706
+ return '';
707
+ }
708
+ })();
709
+ /**
710
+ * Try to extract the JSON envelope from the child CLI's stdout.
711
+ * The CLI prints a single JSON object on the trailing line when
712
+ * `--json` is passed; older builds may interleave status events on
713
+ * stderr but always emit the final JSON on stdout. Scan from the
714
+ * end of stdout backwards looking for the first balanced JSON
715
+ * object so a mixed stdout (e.g. with leading banner) still
716
+ * parses.
717
+ *
718
+ * Returns null on any parse failure; the caller falls back to
719
+ * legacy behaviour (no verification fields surfaced).
720
+ */
721
+ export function parseDispatchEnvelope(stdout) {
722
+ if (typeof stdout !== 'string' || stdout.trim() === '')
723
+ return null;
724
+ const trimmed = stdout.trim();
725
+ // Fast path: stdout is a single JSON object (most common).
726
+ if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
727
+ try {
728
+ const parsed = JSON.parse(trimmed);
729
+ return normaliseEnvelope(parsed);
730
+ }
731
+ catch {
732
+ // fall through to multi-line scan
733
+ }
734
+ }
735
+ // Slow path: scan trailing lines for the last JSON-looking line.
736
+ const lines = trimmed.split('\n');
737
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
738
+ const line = lines[i]?.trim();
739
+ if (!line || !line.startsWith('{') || !line.endsWith('}'))
740
+ continue;
741
+ try {
742
+ const parsed = JSON.parse(line);
743
+ return normaliseEnvelope(parsed);
744
+ }
745
+ catch {
746
+ // try the next line up
747
+ }
748
+ }
749
+ return null;
750
+ }
751
+ function normaliseEnvelope(raw) {
752
+ if (typeof raw['status'] !== 'string')
753
+ return null;
754
+ const result = { status: raw['status'] };
755
+ if (typeof raw['verified'] === 'boolean')
756
+ result.verified = raw['verified'];
757
+ if (Array.isArray(raw['verificationCommands'])) {
758
+ result.verificationCommands = raw['verificationCommands'].filter((item) => typeof item === 'string');
759
+ }
760
+ if (Array.isArray(raw['verificationFailures'])) {
761
+ const failures = [];
762
+ for (const item of raw['verificationFailures']) {
763
+ if (item && typeof item === 'object') {
764
+ const r = item;
765
+ if (typeof r['command'] === 'string' &&
766
+ typeof r['exitCode'] === 'number') {
767
+ failures.push({
768
+ command: r['command'],
769
+ exitCode: r['exitCode'],
770
+ tailStderr: typeof r['tailStderr'] === 'string' ? r['tailStderr'] : '',
771
+ });
772
+ }
773
+ }
774
+ }
775
+ result.verificationFailures = failures;
776
+ }
777
+ if (typeof raw['unverifiedReason'] === 'string') {
778
+ result.unverifiedReason = raw['unverifiedReason'];
779
+ }
780
+ if (typeof raw['regressionOwnershipDispute'] === 'boolean') {
781
+ result.regressionOwnershipDispute = raw['regressionOwnershipDispute'];
782
+ }
783
+ return result;
784
+ }
785
+ /**
786
+ * Honest exit code derivation from the parsed envelope. Mirrors
787
+ * `resolveEngineExitCode` in `cli.ts` so the MCP wrapper's
788
+ * propagation matches what the child CLI actually exits with — a
789
+ * test can assert on either surface and see consistent codes.
790
+ */
791
+ export function resolveDispatchExitCode(envelope) {
792
+ if (envelope === null)
793
+ return 0;
794
+ if (envelope.status === 'needs_verification')
795
+ return 2;
796
+ if (envelope.unverifiedReason === 'verification_command_failed')
797
+ return 1;
798
+ if (envelope.status === 'done')
799
+ return 0;
800
+ if (envelope.status === 'failed')
801
+ return 1;
802
+ if (envelope.status === 'blocked')
803
+ return 1;
804
+ return 1;
805
+ }
806
+ //# sourceMappingURL=orchestrator-tools.js.map