@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,224 @@
1
+ /**
2
+ * brief tool — operator-facing progress brief (tool gap pack).
3
+ *
4
+ * Emits a short, structured status note from the engine to the operator
5
+ * without consuming an assistant turn for narrative. The model calls
6
+ * this tool when it wants to surface "what I am doing right now" — a
7
+ * planning sketch, a blocker reason, a final wrap — in a form the
8
+ * operator can scan at-a-glance.
9
+ *
10
+ * Wire shape:
11
+ * args: { headline: string, status: 'planning'|'working'|'blocked'|'done',
12
+ * detail?: string }
13
+ * side-effect: append one JSON line to `.pugi/briefs/<sessionId>.jsonl`,
14
+ * via the atomic tmp+rename pattern (see writeBriefLine).
15
+ * return: one-line text envelope the engine echoes back to the
16
+ * operator's stdout (the engine adapter renders it as a
17
+ * neutral system line above the next model output).
18
+ *
19
+ * Why JSONL (one record per line) instead of a single rewritten JSON
20
+ * document: briefs accumulate across a session and the operator may
21
+ * want to scroll back through them; a JSONL tail is the natural shape
22
+ * for "give me the last N briefs" — same convention `.pugi/sessions/`
23
+ * already uses for tool-call events.
24
+ *
25
+ * Why atomic tmp+rename for append: a concurrent reader (`pugi briefs
26
+ * --tail`, future TUI surface) must never see a half-written line. The
27
+ * pattern: read existing body -> append the new line -> write the
28
+ * combined body to a sibling tmp file -> rename. Atomic on the same
29
+ * filesystem and POSIX-portable. The overhead (re-write the whole file
30
+ * on each append) is acceptable because briefs are short and capped to
31
+ * a small per-session budget by upstream rate limits.
32
+ *
33
+ * Brand voice: English only, no emoji, no banned words.
34
+ */
35
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
36
+ import { dirname, join, resolve } from 'node:path';
37
+ /** Maximum header length. Mirrors the agent-progress card cap so the
38
+ * TUI can render briefs and progress cards in the same visual lane. */
39
+ export const BRIEF_HEADLINE_MAX = 120;
40
+ /** Maximum detail length. Keeps a single brief well below the 4 KiB
41
+ * line limit some tooling assumes for JSONL files. */
42
+ export const BRIEF_DETAIL_MAX = 2_000;
43
+ /** Hard cap on total bytes per brief line. Defense-in-depth gate so a
44
+ * pathological detail-with-headline-with-status combo cannot blow the
45
+ * tail consumer's per-line buffer. */
46
+ export const BRIEF_LINE_MAX_BYTES = 4_096;
47
+ /** Canonical brief statuses. The set is intentionally tiny — the goal
48
+ * is "what is the agent doing" at a glance, not a free-form taxonomy. */
49
+ export const BRIEF_STATUSES = ['planning', 'working', 'blocked', 'done'];
50
+ /** Sentinel returned when the input fails schema validation. The
51
+ * dispatcher pattern-matches on the prefix for retry-budget bookkeeping
52
+ * and the model self-corrects from the issue list that follows. */
53
+ export const BRIEF_INVALID_ARGS = 'BRIEF_INVALID_ARGS';
54
+ /** Sentinel returned when the encoded line would exceed the per-line
55
+ * byte cap. Distinct from the args-schema failure so the model knows it
56
+ * needs to shorten the detail / headline, not change the shape. */
57
+ export const BRIEF_LINE_TOO_LARGE = 'BRIEF_LINE_TOO_LARGE';
58
+ /**
59
+ * Validate the raw arguments. Returns the typed value on success or a
60
+ * `BRIEF_INVALID_ARGS: ...` sentinel string the dispatcher surfaces back
61
+ * to the model.
62
+ */
63
+ export function parseBriefArgs(raw) {
64
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
65
+ return `${BRIEF_INVALID_ARGS}: arguments must be a JSON object`;
66
+ }
67
+ const obj = raw;
68
+ const issues = [];
69
+ const headline = obj['headline'];
70
+ if (typeof headline !== 'string') {
71
+ issues.push('headline: must be a string');
72
+ }
73
+ else if (headline.trim().length === 0) {
74
+ issues.push('headline: must be non-empty');
75
+ }
76
+ else if (headline.length > BRIEF_HEADLINE_MAX) {
77
+ issues.push(`headline: must be <= ${BRIEF_HEADLINE_MAX} chars`);
78
+ }
79
+ const status = obj['status'];
80
+ if (typeof status !== 'string') {
81
+ issues.push('status: must be a string');
82
+ }
83
+ else if (!BRIEF_STATUSES.includes(status)) {
84
+ issues.push(`status: must be one of ${BRIEF_STATUSES.join('|')}`);
85
+ }
86
+ let detail;
87
+ if (obj['detail'] !== undefined && obj['detail'] !== null) {
88
+ if (typeof obj['detail'] !== 'string') {
89
+ issues.push('detail: must be a string when present');
90
+ }
91
+ else if (obj['detail'].length > BRIEF_DETAIL_MAX) {
92
+ issues.push(`detail: must be <= ${BRIEF_DETAIL_MAX} chars`);
93
+ }
94
+ else {
95
+ detail = obj['detail'];
96
+ }
97
+ }
98
+ if (issues.length > 0) {
99
+ return `${BRIEF_INVALID_ARGS}: ${issues.join('; ')}`;
100
+ }
101
+ const result = detail !== undefined
102
+ ? { headline: headline, status: status, detail }
103
+ : { headline: headline, status: status };
104
+ return result;
105
+ }
106
+ /**
107
+ * Compute the on-disk path for a session's brief log. Public so a
108
+ * future tail consumer (`pugi briefs --tail <session>`) can share the
109
+ * exact resolution rules without duplicating layout knowledge.
110
+ */
111
+ export function briefLogPath(ctx) {
112
+ const safe = sanitizeSessionId(ctx.sessionId);
113
+ return join(resolve(ctx.workspaceRoot), '.pugi', 'briefs', `${safe}.jsonl`);
114
+ }
115
+ /**
116
+ * Sanitise a session id for use as a filename. Restricts to the same
117
+ * character set agent-progress uses (`[A-Za-z0-9_-]+`) so the basename
118
+ * cannot escape the briefs directory via a `..` segment. Collapses any
119
+ * disallowed character to `_`; an empty result falls back to `session`.
120
+ *
121
+ * NOT exported as a generic helper because the only caller is this
122
+ * module — keep the surface area minimal.
123
+ */
124
+ function sanitizeSessionId(id) {
125
+ if (typeof id !== 'string' || id.length === 0)
126
+ return 'session';
127
+ const collapsed = id.replace(/[^A-Za-z0-9_-]/g, '_');
128
+ const trimmed = collapsed.slice(0, 64);
129
+ return trimmed.length > 0 ? trimmed : 'session';
130
+ }
131
+ /**
132
+ * Dispatch entry point. Validates input, persists the brief line, and
133
+ * returns the operator-visible echo envelope. Returns sentinels on
134
+ * recoverable failures so the engine loop can surface them as tool
135
+ * results without tearing down on a throw.
136
+ */
137
+ export function dispatchBrief(ctx, raw) {
138
+ const parsed = parseBriefArgs(raw);
139
+ if (typeof parsed === 'string') {
140
+ return parsed;
141
+ }
142
+ const ts = (ctx.now ? ctx.now() : new Date()).toISOString();
143
+ const record = parsed.detail !== undefined
144
+ ? { ts, status: parsed.status, headline: parsed.headline, detail: parsed.detail }
145
+ : { ts, status: parsed.status, headline: parsed.headline };
146
+ const line = JSON.stringify(record);
147
+ // Defense-in-depth byte cap. The earlier char-length checks bound the
148
+ // best case, but multi-byte UTF-8 input can still push the encoded
149
+ // line over the limit. Surface a distinct sentinel so the model knows
150
+ // to shorten the payload rather than change the shape.
151
+ if (Buffer.byteLength(line, 'utf8') > BRIEF_LINE_MAX_BYTES) {
152
+ return `${BRIEF_LINE_TOO_LARGE}: encoded line exceeds ${BRIEF_LINE_MAX_BYTES} bytes`;
153
+ }
154
+ const path = briefLogPath(ctx);
155
+ appendBriefLine(path, line);
156
+ return formatEcho(record, path);
157
+ }
158
+ /**
159
+ * Atomic append. Read existing body -> append the new line -> write to
160
+ * sibling tmp -> rename. The rename is atomic on the same filesystem
161
+ * so a concurrent tail consumer never reads a half-written line.
162
+ *
163
+ * Append-only via full rewrite is acceptable because brief logs are
164
+ * tiny (one short JSON object per call, sub-kilobyte each, capped by
165
+ * upstream rate limits to a few-hundred per session at most).
166
+ */
167
+ function appendBriefLine(finalPath, line) {
168
+ const dir = dirname(finalPath);
169
+ if (!existsSync(dir)) {
170
+ mkdirSync(dir, { recursive: true });
171
+ }
172
+ const prior = existsSync(finalPath) ? readFileSync(finalPath, 'utf8') : '';
173
+ const tail = prior.length > 0 && !prior.endsWith('\n') ? '\n' : '';
174
+ const next = `${prior}${tail}${line}\n`;
175
+ const tmpPath = `${finalPath}.tmp-${process.pid}-${briefSequence++}`;
176
+ writeFileSync(tmpPath, next, { encoding: 'utf8', mode: 0o644 });
177
+ renameSync(tmpPath, finalPath);
178
+ }
179
+ let briefSequence = 0;
180
+ /**
181
+ * Render the one-line operator echo. The format is stable so a future
182
+ * REPL renderer can pattern-match the prefix for colouring.
183
+ */
184
+ function formatEcho(record, path) {
185
+ const detail = record.detail ? ` -- ${truncate(record.detail, 240)}` : '';
186
+ return `[brief ${record.status}] ${record.headline}${detail} (logged to ${path})`;
187
+ }
188
+ function truncate(value, max) {
189
+ if (value.length <= max)
190
+ return value;
191
+ return `${value.slice(0, max - 1)}…`;
192
+ }
193
+ /**
194
+ * JSON-Schema fragment the schema builder advertises to the model.
195
+ * Hand-written to mirror the `parseBriefArgs` checks 1:1 — same
196
+ * convention `todo_write` and `ask_user_question` use because the
197
+ * runtime engine wires OpenAI-shape JSON Schema and we have not
198
+ * greenlit a zod-to-json-schema dependency.
199
+ */
200
+ export const briefJsonSchema = {
201
+ type: 'object',
202
+ additionalProperties: false,
203
+ required: ['headline', 'status'],
204
+ properties: {
205
+ headline: {
206
+ type: 'string',
207
+ minLength: 1,
208
+ maxLength: BRIEF_HEADLINE_MAX,
209
+ description: `Short one-line summary, <= ${BRIEF_HEADLINE_MAX} chars.`,
210
+ },
211
+ status: {
212
+ type: 'string',
213
+ enum: [...BRIEF_STATUSES],
214
+ description: 'Lifecycle state: planning (deciding what to do), working (in progress), ' +
215
+ 'blocked (waiting on operator), done (final wrap).',
216
+ },
217
+ detail: {
218
+ type: 'string',
219
+ maxLength: BRIEF_DETAIL_MAX,
220
+ description: `Optional context, <= ${BRIEF_DETAIL_MAX} chars.`,
221
+ },
222
+ },
223
+ };
224
+ //# sourceMappingURL=brief.js.map
@@ -0,0 +1,250 @@
1
+ /**
2
+ * enter_worktree tool — scratch git worktree primitive (tool gap pack
3
+ *).
4
+ *
5
+ * Spawns `git worktree add <path> [<baseRef>]` at
6
+ * `.pugi/worktrees/<taskId>/` so a long-running agent task can land
7
+ * its edits into an isolated scratch tree. The matching `exit_worktree`
8
+ * tool is the cleanup primitive. The actual fan-out (one worktree per
9
+ * task in a backlog batch) lives behind a separate task — this tool is
10
+ * the primitive both call paths share.
11
+ *
12
+ * Wire shape:
13
+ * args: { taskId: string, baseRef?: string }
14
+ * - taskId: lowercase slug, `[a-z0-9][a-z0-9-]{0,63}`. The
15
+ * pattern bans path-traversal segments (`..`, `/`, `\\`) so
16
+ * the basename cannot escape `.pugi/worktrees/`.
17
+ * - baseRef: optional git ref (branch / tag / SHA). Defaults
18
+ * to `main` so the standard case is "fork from main and go".
19
+ * The CLI validates the ref shape only loosely — git
20
+ * rejects unknown refs upstream and that error message is
21
+ * surfaced verbatim.
22
+ * return: { ok: true, worktreePath, branchName } serialised JSON.
23
+ *
24
+ * Spawn discipline: every git invocation uses `spawnSync` with
25
+ * `shell: false` and a string-array argv so an unsanitised character
26
+ * inside a ref name cannot inject shell metacharacters. The taskId is
27
+ * gated by a strict regex BEFORE the spawn so the path argument is
28
+ * trusted.
29
+ *
30
+ * Path discipline: the worktree path is computed locally
31
+ * (`path.resolve(workspaceRoot, '.pugi/worktrees', taskId)`) and then
32
+ * containment-checked against the workspace root and the
33
+ * `.pugi/worktrees` subtree. Anything else throws — a deformed taskId
34
+ * that survived the regex (it should not) cannot pull the basename
35
+ * outside the expected subtree.
36
+ *
37
+ * Brand voice: English only, no emoji, no banned words.
38
+ */
39
+ import { spawnSync } from 'node:child_process';
40
+ import { existsSync, mkdirSync } from 'node:fs';
41
+ import { join, resolve, sep } from 'node:path';
42
+ /** Strict taskId pattern. Lowercase alphanumeric + hyphen, must start
43
+ * with a lowercase letter or digit, length 1..64. The first-character
44
+ * constraint stops a leading hyphen from being interpreted as a flag
45
+ * by `git worktree add` (defense in depth — argv form already protects
46
+ * us, but a leading hyphen is still a smell). */
47
+ export const TASK_ID_PATTERN = /^[a-z0-9][a-z0-9-]{0,63}$/;
48
+ /** Default base ref when the caller does not supply one. */
49
+ export const DEFAULT_BASE_REF = 'main';
50
+ /** Sentinels. Distinct prefixes so the model / dispatcher can
51
+ * pattern-match without parsing the trailing message. */
52
+ export const ENTER_WORKTREE_INVALID_ARGS = 'ENTER_WORKTREE_INVALID_ARGS';
53
+ export const ENTER_WORKTREE_GIT_FAILED = 'ENTER_WORKTREE_GIT_FAILED';
54
+ export const ENTER_WORKTREE_ALREADY_EXISTS = 'ENTER_WORKTREE_ALREADY_EXISTS';
55
+ export const ENTER_WORKTREE_PATH_ESCAPED = 'ENTER_WORKTREE_PATH_ESCAPED';
56
+ /**
57
+ * Validate the raw arguments. Returns the typed value on success or a
58
+ * `ENTER_WORKTREE_INVALID_ARGS: ...` sentinel string.
59
+ */
60
+ /**
61
+ * Permissive git ref-name validation. We do not aim to fully implement
62
+ * `git check-ref-format` — instead reject anything outside a safe
63
+ * character class plus a leading-dash check. The leading-dash rule is
64
+ * the real defense: it stops an operator-controlled `baseRef` from
65
+ * being reinterpreted as a CLI flag (e.g. `--detach`, `--force`).
66
+ */
67
+ function isValidGitRef(s) {
68
+ if (s.startsWith('-'))
69
+ return false;
70
+ return /^[A-Za-z0-9._/-]+$/.test(s);
71
+ }
72
+ export function parseEnterWorktreeArgs(raw) {
73
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
74
+ return `${ENTER_WORKTREE_INVALID_ARGS}: arguments must be a JSON object`;
75
+ }
76
+ const obj = raw;
77
+ const issues = [];
78
+ const taskId = obj['taskId'];
79
+ if (typeof taskId !== 'string') {
80
+ issues.push('taskId: must be a string');
81
+ }
82
+ else if (!TASK_ID_PATTERN.test(taskId)) {
83
+ issues.push(`taskId: must match ${TASK_ID_PATTERN}`);
84
+ }
85
+ let baseRef;
86
+ if (obj['baseRef'] !== undefined && obj['baseRef'] !== null) {
87
+ if (typeof obj['baseRef'] !== 'string') {
88
+ issues.push('baseRef: must be a string when present');
89
+ }
90
+ else if (obj['baseRef'].length === 0) {
91
+ issues.push('baseRef: must be non-empty when present');
92
+ }
93
+ else if (obj['baseRef'].length > 250) {
94
+ // Git imposes its own limits; we cut off well below them to keep
95
+ // error messages and audit lines bounded.
96
+ issues.push('baseRef: must be <= 250 chars when present');
97
+ }
98
+ else if (!isValidGitRef(obj['baseRef'])) {
99
+ // Permissive ref-chars regex that also rejects leading `-` so
100
+ // an operator-controlled `baseRef` cannot be reinterpreted as
101
+ // a git flag (e.g. `--detach`, `--force`). Combined with the
102
+ // `--` separator in argv this is belt-and-suspenders against
103
+ // arg-injection. Triple-review Claude P2,.
104
+ issues.push('baseRef: must match [A-Za-z0-9._/-] and not start with `-`');
105
+ }
106
+ else {
107
+ baseRef = obj['baseRef'];
108
+ }
109
+ }
110
+ if (issues.length > 0) {
111
+ return `${ENTER_WORKTREE_INVALID_ARGS}: ${issues.join('; ')}`;
112
+ }
113
+ const out = baseRef !== undefined
114
+ ? { taskId: taskId, baseRef }
115
+ : { taskId: taskId };
116
+ return out;
117
+ }
118
+ /**
119
+ * Compute the absolute on-disk path for a task's worktree. Public so
120
+ * `exit_worktree` and any future TUI surface can share the layout.
121
+ */
122
+ export function worktreePathFor(workspaceRoot, taskId) {
123
+ return join(resolve(workspaceRoot), '.pugi', 'worktrees', taskId);
124
+ }
125
+ /**
126
+ * Compute the canonical worktree root (`<workspaceRoot>/.pugi/worktrees`).
127
+ * Used by `exit_worktree` for the containment check.
128
+ */
129
+ export function worktreesRoot(workspaceRoot) {
130
+ return join(resolve(workspaceRoot), '.pugi', 'worktrees');
131
+ }
132
+ /**
133
+ * Dispatch entry point. Validates input, ensures the parent directory
134
+ * exists, then spawns `git worktree add` with the strict argv form.
135
+ * Returns the structured result envelope as JSON, or a sentinel string
136
+ * on a recoverable failure.
137
+ */
138
+ export function dispatchEnterWorktree(ctx, raw) {
139
+ const parsed = parseEnterWorktreeArgs(raw);
140
+ if (typeof parsed === 'string') {
141
+ return parsed;
142
+ }
143
+ const baseRef = parsed.baseRef ?? DEFAULT_BASE_REF;
144
+ const root = resolve(ctx.workspaceRoot);
145
+ const targetPath = worktreePathFor(root, parsed.taskId);
146
+ const subtreeRoot = worktreesRoot(root);
147
+ // Defense in depth: re-check that the resolved target stays inside
148
+ // `<workspaceRoot>/.pugi/worktrees/`. The regex on taskId already
149
+ // bans `..` and `/`, so a successful escape here would indicate a
150
+ // catastrophic node:path regression — we still treat it as a fatal
151
+ // refusal rather than trusting the upstream gate alone.
152
+ if (!isContainedIn(targetPath, subtreeRoot)) {
153
+ return `${ENTER_WORKTREE_PATH_ESCAPED}: ${targetPath} is not under ${subtreeRoot}`;
154
+ }
155
+ if (existsSync(targetPath)) {
156
+ return `${ENTER_WORKTREE_ALREADY_EXISTS}: ${targetPath}`;
157
+ }
158
+ // Ensure `.pugi/worktrees/` exists; git worktree add will not create
159
+ // the parent for us.
160
+ if (!existsSync(subtreeRoot)) {
161
+ mkdirSync(subtreeRoot, { recursive: true });
162
+ }
163
+ // Compose the branch name we hand to git. The convention pairs
164
+ // 1:1 with taskId so `exit_worktree` does not need a separate
165
+ // lookup. `-b <branch>` creates the branch as a side effect of the
166
+ // worktree-add; without it git would refuse a worktree that points
167
+ // to an existing branch already checked out elsewhere.
168
+ const branchName = `pugi/worktree/${parsed.taskId}`;
169
+ // `--` separator forces git to treat everything after it as strict
170
+ // positionals. Belt-and-suspenders against arg-injection via
171
+ // `baseRef` (the regex above already rejects leading `-`).
172
+ const argv = [
173
+ 'worktree',
174
+ 'add',
175
+ '-b',
176
+ branchName,
177
+ '--',
178
+ targetPath,
179
+ baseRef,
180
+ ];
181
+ const runner = ctx.runGit ?? defaultGitRunner;
182
+ const result = runner(argv, root);
183
+ if (result.status !== 0) {
184
+ const detail = (result.stderr || result.stdout || `exit=${result.status}`).trim();
185
+ return `${ENTER_WORKTREE_GIT_FAILED}: ${detail}`;
186
+ }
187
+ const out = {
188
+ ok: true,
189
+ worktreePath: targetPath,
190
+ branchName,
191
+ baseRef,
192
+ };
193
+ return JSON.stringify(out);
194
+ }
195
+ /**
196
+ * Default spawnSync wrapper. `shell: false` is non-negotiable — every
197
+ * argv element is forwarded verbatim к the child without shell
198
+ * interpretation. The stdout/stderr captures are forced to utf-8 so the
199
+ * caller works with strings without manual decoding.
200
+ */
201
+ function defaultGitRunner(argv, cwd) {
202
+ const result = spawnSync('git', [...argv], {
203
+ cwd,
204
+ shell: false,
205
+ encoding: 'utf8',
206
+ timeout: 30_000,
207
+ });
208
+ return {
209
+ status: result.status,
210
+ stdout: typeof result.stdout === 'string' ? result.stdout : '',
211
+ stderr: typeof result.stderr === 'string' ? result.stderr : '',
212
+ };
213
+ }
214
+ /**
215
+ * Containment check. Resolves both paths and asserts the target lives
216
+ * under the root via a separator-aware prefix match. The separator
217
+ * append on the root protects against the `/foo/bar2` vs `/foo/bar`
218
+ * partial-prefix bug.
219
+ */
220
+ function isContainedIn(target, root) {
221
+ const rResolved = resolve(root);
222
+ const tResolved = resolve(target);
223
+ if (tResolved === rResolved)
224
+ return false;
225
+ const rootWithSep = rResolved.endsWith(sep) ? rResolved : `${rResolved}${sep}`;
226
+ return tResolved.startsWith(rootWithSep);
227
+ }
228
+ /**
229
+ * JSON-Schema fragment the schema builder advertises to the model.
230
+ */
231
+ export const enterWorktreeJsonSchema = {
232
+ type: 'object',
233
+ additionalProperties: false,
234
+ required: ['taskId'],
235
+ properties: {
236
+ taskId: {
237
+ type: 'string',
238
+ pattern: '^[a-z0-9][a-z0-9-]{0,63}$',
239
+ description: 'Slug used as the worktree basename. ' +
240
+ 'Lowercase alphanumeric + hyphen, must start with letter/digit, 1..64 chars.',
241
+ },
242
+ baseRef: {
243
+ type: 'string',
244
+ minLength: 1,
245
+ maxLength: 250,
246
+ description: `Optional git base ref (branch / tag / SHA). Defaults to "${DEFAULT_BASE_REF}".`,
247
+ },
248
+ },
249
+ };
250
+ //# sourceMappingURL=enter-worktree.js.map
@@ -0,0 +1,147 @@
1
+ /**
2
+ * exit_worktree tool — opposite of `enter_worktree` (tool gap pack
3
+ *).
4
+ *
5
+ * Tears down a scratch git worktree at the supplied path. The path is
6
+ * STRICTLY validated к live under `<workspaceRoot>/.pugi/worktrees/`
7
+ * — anything outside that subtree refuses with a sentinel and zero
8
+ * filesystem mutation. This is a destructive primitive, so the
9
+ * containment check is the load-bearing safety boundary.
10
+ *
11
+ * Procedure:
12
+ * 1. Validate the argument shape.
13
+ * 2. Resolve the input path and assert it lives under
14
+ * `<workspaceRoot>/.pugi/worktrees/`. Refuse otherwise.
15
+ * 3. Run `git worktree remove --force <path>` so git's bookkeeping
16
+ * drops the worktree. `--force` is required because the model may
17
+ * have left dirty edits inside the scratch tree — that is the
18
+ * normal case for a refactor that the operator is about to merge
19
+ * via a different tool. Best-effort: a non-zero exit from git is
20
+ * logged in the return envelope but does NOT abort the disk
21
+ * cleanup. Git may have already cleaned the entry when the dir
22
+ * itself was removed manually.
23
+ * 4. `rmSync(path, { recursive: true, force: true })` for any
24
+ * leftover files.
25
+ *
26
+ * Wire shape:
27
+ * args: { worktreePath: string }
28
+ * return: { ok: true, removed: boolean, gitStatus: number|null,
29
+ * gitDetail?: string } serialised JSON.
30
+ *
31
+ * Idempotent: calling on a path that no longer exists returns
32
+ * `{ ok: true, removed: false }` with no error. This matches the
33
+ * semantics agent-progress / cleanup operations already follow elsewhere
34
+ * in the CLI.
35
+ *
36
+ * Brand voice: English only, no emoji, no banned words.
37
+ */
38
+ import { spawnSync } from 'node:child_process';
39
+ import { existsSync, rmSync } from 'node:fs';
40
+ import { resolve, sep } from 'node:path';
41
+ import { worktreesRoot } from './enter-worktree.js';
42
+ /** Sentinels. Distinct prefixes so the model / dispatcher can
43
+ * pattern-match. */
44
+ export const EXIT_WORKTREE_INVALID_ARGS = 'EXIT_WORKTREE_INVALID_ARGS';
45
+ export const EXIT_WORKTREE_PATH_ESCAPED = 'EXIT_WORKTREE_PATH_ESCAPED';
46
+ /**
47
+ * Validate the raw arguments. Returns the typed value on success or a
48
+ * `EXIT_WORKTREE_INVALID_ARGS: ...` sentinel string.
49
+ */
50
+ export function parseExitWorktreeArgs(raw) {
51
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
52
+ return `${EXIT_WORKTREE_INVALID_ARGS}: arguments must be a JSON object`;
53
+ }
54
+ const obj = raw;
55
+ const worktreePath = obj['worktreePath'];
56
+ if (typeof worktreePath !== 'string') {
57
+ return `${EXIT_WORKTREE_INVALID_ARGS}: worktreePath: must be a string`;
58
+ }
59
+ if (worktreePath.length === 0) {
60
+ return `${EXIT_WORKTREE_INVALID_ARGS}: worktreePath: must be non-empty`;
61
+ }
62
+ return { worktreePath };
63
+ }
64
+ /**
65
+ * Dispatch entry point. Validates input, asserts the path lives under
66
+ * the allowed root, then runs `git worktree remove --force` followed
67
+ * by a filesystem removal for any leftovers. Returns the structured
68
+ * envelope as JSON.
69
+ *
70
+ * Returns sentinel strings on validation failures or a containment
71
+ * refusal — no throw — so the engine adapter surfaces a recoverable
72
+ * tool result and the model can adapt.
73
+ */
74
+ export function dispatchExitWorktree(ctx, raw) {
75
+ const parsed = parseExitWorktreeArgs(raw);
76
+ if (typeof parsed === 'string') {
77
+ return parsed;
78
+ }
79
+ const root = resolve(ctx.workspaceRoot);
80
+ const subtreeRoot = worktreesRoot(root);
81
+ const target = resolve(parsed.worktreePath);
82
+ if (!isContainedIn(target, subtreeRoot)) {
83
+ return `${EXIT_WORKTREE_PATH_ESCAPED}: ${target} is not under ${subtreeRoot}`;
84
+ }
85
+ // git worktree remove. Best-effort. Non-zero exit is captured in the
86
+ // return envelope but does not block the rmTree step — a half-cleaned
87
+ // worktree (entry gone but dir survived) is a common operator-side
88
+ // mistake we want to recover from.
89
+ const runner = ctx.runGit ?? defaultGitRunner;
90
+ const gitResult = runner(['worktree', 'remove', '--force', target], root);
91
+ const existedBefore = existsSync(target);
92
+ const rmTree = ctx.rmTree ?? defaultRmTree;
93
+ if (existedBefore) {
94
+ rmTree(target);
95
+ }
96
+ const removed = existedBefore && !existsSync(target);
97
+ const envelope = gitResult.status === 0
98
+ ? { ok: true, removed, gitStatus: gitResult.status }
99
+ : {
100
+ ok: true,
101
+ removed,
102
+ gitStatus: gitResult.status,
103
+ gitDetail: (gitResult.stderr || gitResult.stdout || '').trim(),
104
+ };
105
+ return JSON.stringify(envelope);
106
+ }
107
+ function defaultGitRunner(argv, cwd) {
108
+ const result = spawnSync('git', [...argv], {
109
+ cwd,
110
+ shell: false,
111
+ encoding: 'utf8',
112
+ timeout: 30_000,
113
+ });
114
+ return {
115
+ status: result.status,
116
+ stdout: typeof result.stdout === 'string' ? result.stdout : '',
117
+ stderr: typeof result.stderr === 'string' ? result.stderr : '',
118
+ };
119
+ }
120
+ function defaultRmTree(path) {
121
+ rmSync(path, { recursive: true, force: true });
122
+ }
123
+ function isContainedIn(target, root) {
124
+ const rResolved = resolve(root);
125
+ const tResolved = resolve(target);
126
+ if (tResolved === rResolved)
127
+ return false;
128
+ const rootWithSep = rResolved.endsWith(sep) ? rResolved : `${rResolved}${sep}`;
129
+ return tResolved.startsWith(rootWithSep);
130
+ }
131
+ /**
132
+ * JSON-Schema fragment the schema builder advertises to the model.
133
+ */
134
+ export const exitWorktreeJsonSchema = {
135
+ type: 'object',
136
+ additionalProperties: false,
137
+ required: ['worktreePath'],
138
+ properties: {
139
+ worktreePath: {
140
+ type: 'string',
141
+ minLength: 1,
142
+ description: 'Absolute path to the worktree to tear down. Must live under ' +
143
+ '<workspaceRoot>/.pugi/worktrees/ — anything else refuses.',
144
+ },
145
+ },
146
+ };
147
+ //# sourceMappingURL=exit-worktree.js.map