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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/deploy.js +40 -40
  6. package/dist/commands/flatten.js +191 -0
  7. package/dist/commands/jobs-watch.js +201 -0
  8. package/dist/commands/jobs.js +42 -27
  9. package/dist/commands/smoke.js +133 -0
  10. package/dist/core/agent-progress/cleanup.js +134 -0
  11. package/dist/core/agent-progress/schema.js +144 -0
  12. package/dist/core/agent-progress/writer.js +101 -0
  13. package/dist/core/agents/adaptive-router.js +330 -0
  14. package/dist/core/agents/query-decomposer.js +297 -0
  15. package/dist/core/agents/registry.js +2 -2
  16. package/dist/core/approvals/shortcut-resolver.js +98 -0
  17. package/dist/core/artifact-chain/dispatcher.js +148 -0
  18. package/dist/core/artifact-chain/exporter.js +164 -0
  19. package/dist/core/artifact-chain/state.js +243 -0
  20. package/dist/core/artifact-chain/steps.js +169 -0
  21. package/dist/core/ask-user/question.js +92 -0
  22. package/dist/core/audit/audit-trail.js +275 -0
  23. package/dist/core/auth/ensure-authenticated.js +129 -0
  24. package/dist/core/auth/env-provider.js +238 -0
  25. package/dist/core/auto-open-browser.js +4 -4
  26. package/dist/core/auto-update/channels.js +122 -0
  27. package/dist/core/auto-update/checker.js +241 -0
  28. package/dist/core/auto-update/state.js +235 -0
  29. package/dist/core/bare-mode/index.js +107 -0
  30. package/dist/core/bash/redirect.js +281 -0
  31. package/dist/core/bash-classifier.js +436 -40
  32. package/dist/core/checkpoint/resumer.js +149 -0
  33. package/dist/core/checkpoint/rewinder.js +291 -0
  34. package/dist/core/checkpoints/shadow-git.js +670 -0
  35. package/dist/core/citations/parser.js +109 -0
  36. package/dist/core/classifier/yolo-classifier.js +88 -0
  37. package/dist/core/codegraph/decision-store.js +248 -0
  38. package/dist/core/codegraph/detect-repo.js +459 -0
  39. package/dist/core/codegraph/install.js +134 -0
  40. package/dist/core/codegraph/offer-hook.js +220 -0
  41. package/dist/core/compact/auto-trigger.js +96 -0
  42. package/dist/core/compact/buffer-rewriter.js +115 -0
  43. package/dist/core/compact/summarizer.js +208 -0
  44. package/dist/core/compact/token-counter.js +108 -0
  45. package/dist/core/consensus/anvil-fanout.js +25 -25
  46. package/dist/core/consensus/diff-capture.js +121 -12
  47. package/dist/core/consensus/rubric.js +21 -21
  48. package/dist/core/context/builder.js +6 -6
  49. package/dist/core/context/compaction-events.js +8 -8
  50. package/dist/core/context/compaction.js +31 -31
  51. package/dist/core/context/index.js +15 -8
  52. package/dist/core/context/invariants.js +51 -51
  53. package/dist/core/context/markdown-loader.js +28 -10
  54. package/dist/core/context/markdown-traverse.js +255 -0
  55. package/dist/core/context/pugiignore.js +41 -41
  56. package/dist/core/context/repo-skeleton.js +37 -37
  57. package/dist/core/context/tool-eviction.js +55 -0
  58. package/dist/core/context/watcher.js +32 -32
  59. package/dist/core/context/working-set.js +23 -23
  60. package/dist/core/coordinator/agent-tools.js +77 -0
  61. package/dist/core/coordinator/agent-toolset.js +65 -0
  62. package/dist/core/coordinator/fsm.js +73 -0
  63. package/dist/core/coordinator/mode-fsm.js +70 -0
  64. package/dist/core/cost/rate-card.js +129 -0
  65. package/dist/core/cost/tracker.js +221 -0
  66. package/dist/core/credentials.js +12 -12
  67. package/dist/core/cron/scheduler.js +138 -0
  68. package/dist/core/denial-tracking/index.js +8 -0
  69. package/dist/core/denial-tracking/state.js +264 -0
  70. package/dist/core/diagnostics/probe-runner.js +93 -0
  71. package/dist/core/diagnostics/probes/api.js +46 -0
  72. package/dist/core/diagnostics/probes/auth.js +93 -0
  73. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  74. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  75. package/dist/core/diagnostics/probes/config.js +72 -0
  76. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  77. package/dist/core/diagnostics/probes/disk.js +81 -0
  78. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  79. package/dist/core/diagnostics/probes/git.js +65 -0
  80. package/dist/core/diagnostics/probes/hooks.js +118 -0
  81. package/dist/core/diagnostics/probes/mcp.js +75 -0
  82. package/dist/core/diagnostics/probes/node.js +59 -0
  83. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  84. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  85. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  86. package/dist/core/diagnostics/probes/session.js +74 -0
  87. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  88. package/dist/core/diagnostics/probes/workspace.js +63 -0
  89. package/dist/core/diagnostics/types.js +70 -0
  90. package/dist/core/dispatch/cache-cleanup.js +197 -0
  91. package/dist/core/dispatch/cache-handoff.js +295 -0
  92. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  93. package/dist/core/edits/dispatch.js +293 -7
  94. package/dist/core/edits/format-matrix.js +26 -0
  95. package/dist/core/edits/fuzzy-ladder.js +650 -0
  96. package/dist/core/edits/index.js +3 -1
  97. package/dist/core/edits/journal.js +199 -0
  98. package/dist/core/edits/layer-a-apply.js +15 -15
  99. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  100. package/dist/core/edits/layer-b-apply.js +9 -9
  101. package/dist/core/edits/layer-c-apply.js +6 -6
  102. package/dist/core/edits/layer-d-ast.js +557 -14
  103. package/dist/core/edits/marker-parser.js +12 -12
  104. package/dist/core/edits/security-gate.js +27 -27
  105. package/dist/core/edits/verify-hook.js +273 -0
  106. package/dist/core/edits/worktree.js +322 -0
  107. package/dist/core/engine/anvil-client.js +140 -26
  108. package/dist/core/engine/auto-compact.js +179 -0
  109. package/dist/core/engine/budgets.js +186 -0
  110. package/dist/core/engine/context-prefix.js +155 -0
  111. package/dist/core/engine/index.js +1 -1
  112. package/dist/core/engine/intensity.js +158 -0
  113. package/dist/core/engine/intent.js +260 -0
  114. package/dist/core/engine/native-pugi.js +1295 -227
  115. package/dist/core/engine/prompts.js +134 -16
  116. package/dist/core/engine/strip-internal-fields.js +124 -0
  117. package/dist/core/engine/tool-bridge.js +1295 -59
  118. package/dist/core/evaluation/golden-dataset.js +293 -0
  119. package/dist/core/feedback/queue.js +177 -0
  120. package/dist/core/feedback/submitter.js +145 -0
  121. package/dist/core/file-cache.js +113 -1
  122. package/dist/core/flatten/flatten-repo.js +439 -0
  123. package/dist/core/format/osc8-link.js +28 -0
  124. package/dist/core/hook-chains.js +392 -0
  125. package/dist/core/hooks/citation-verify-hook.js +138 -0
  126. package/dist/core/hooks/citation-verify.js +112 -0
  127. package/dist/core/hooks/events.js +44 -0
  128. package/dist/core/hooks/index.js +15 -0
  129. package/dist/core/hooks/registry.js +213 -0
  130. package/dist/core/hooks/runner.js +236 -0
  131. package/dist/core/hooks/v2/event-emitter.js +115 -0
  132. package/dist/core/hooks/v2/executor.js +282 -0
  133. package/dist/core/hooks/v2/index.js +25 -0
  134. package/dist/core/hooks/v2/lifecycle.js +104 -0
  135. package/dist/core/hooks/v2/loader.js +216 -0
  136. package/dist/core/hooks/v2/matcher.js +125 -0
  137. package/dist/core/hooks/v2/trust.js +143 -0
  138. package/dist/core/hooks/v2/types.js +86 -0
  139. package/dist/core/image/renderer.js +71 -0
  140. package/dist/core/init/detector.js +582 -0
  141. package/dist/core/init/template-renderer.js +242 -0
  142. package/dist/core/jobs/registry.js +18 -18
  143. package/dist/core/ledger/results-tsv.js +142 -0
  144. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  145. package/dist/core/lsp/cache.js +105 -0
  146. package/dist/core/lsp/client.js +776 -0
  147. package/dist/core/lsp/language-detect.js +66 -0
  148. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  149. package/dist/core/lsp/symbol-tools.js +372 -0
  150. package/dist/core/mcp/client.js +97 -28
  151. package/dist/core/mcp/http-server.js +553 -0
  152. package/dist/core/mcp/orchestrator-tools.js +662 -0
  153. package/dist/core/mcp/permission.js +190 -0
  154. package/dist/core/mcp/registry.js +39 -17
  155. package/dist/core/mcp/server-tools.js +219 -0
  156. package/dist/core/mcp/server.js +397 -0
  157. package/dist/core/mcp/trust.js +10 -10
  158. package/dist/core/memory/dual-write.js +416 -0
  159. package/dist/core/memory/passive-extract.js +130 -0
  160. package/dist/core/memory/phase1-kinds.js +20 -0
  161. package/dist/core/memory/secret-scanner.js +304 -0
  162. package/dist/core/memory-sync/queue.js +170 -0
  163. package/dist/core/metrics/extract.js +113 -0
  164. package/dist/core/modes/roo-modes.js +68 -0
  165. package/dist/core/onboarding/ensure-initialized.js +133 -0
  166. package/dist/core/onboarding/marker.js +111 -0
  167. package/dist/core/onboarding/telemetry-state.js +108 -0
  168. package/dist/core/output-style/presets.js +176 -0
  169. package/dist/core/output-style/state.js +185 -0
  170. package/dist/core/path-security.js +287 -5
  171. package/dist/core/permission.js +82 -22
  172. package/dist/core/permissions/auto-classifier.js +124 -0
  173. package/dist/core/permissions/bash-parser.js +371 -0
  174. package/dist/core/permissions/circuit-breaker.js +83 -0
  175. package/dist/core/permissions/constrained-edit.js +91 -0
  176. package/dist/core/permissions/gate.js +278 -0
  177. package/dist/core/permissions/index.js +20 -0
  178. package/dist/core/permissions/mode.js +174 -0
  179. package/dist/core/permissions/network-egress.js +137 -0
  180. package/dist/core/permissions/state.js +241 -0
  181. package/dist/core/permissions/tool-class.js +93 -0
  182. package/dist/core/plan-mode/ui-state.js +51 -0
  183. package/dist/core/plans/plan-artifact.js +721 -0
  184. package/dist/core/policy-limits/etag-store.js +122 -0
  185. package/dist/core/prd-check/parser.js +215 -0
  186. package/dist/core/prd-check/reporter.js +127 -0
  187. package/dist/core/prd-check/session-review.js +557 -0
  188. package/dist/core/prd-check/verifiers.js +223 -0
  189. package/dist/core/prompt-cache/client-cache.js +99 -0
  190. package/dist/core/prompts/assembly.js +29 -0
  191. package/dist/core/prompts/registry.js +364 -0
  192. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  193. package/dist/core/pugi-md/context-injector.js +76 -0
  194. package/dist/core/pugi-md/walk-up.js +207 -0
  195. package/dist/core/python/uv-installer.js +270 -0
  196. package/dist/core/python/uv-resolver.js +83 -0
  197. package/dist/core/rate-limit/narrator.js +146 -0
  198. package/dist/core/recipes/cli-types.js +20 -0
  199. package/dist/core/recipes/loader.js +103 -0
  200. package/dist/core/recipes/runner.js +345 -0
  201. package/dist/core/recipes/schema.js +587 -0
  202. package/dist/core/release-notes/parser.js +241 -0
  203. package/dist/core/release-notes/state.js +116 -0
  204. package/dist/core/repl/ask.js +37 -37
  205. package/dist/core/repl/cancellation.js +26 -26
  206. package/dist/core/repl/cap-warning.js +4 -4
  207. package/dist/core/repl/clipboard-read.js +11 -11
  208. package/dist/core/repl/dispatch-fsm.js +12 -12
  209. package/dist/core/repl/history-search.js +15 -15
  210. package/dist/core/repl/history.js +28 -18
  211. package/dist/core/repl/kill-ring.js +5 -5
  212. package/dist/core/repl/model-pricing.js +135 -0
  213. package/dist/core/repl/privacy-banner.js +22 -22
  214. package/dist/core/repl/session.js +2157 -214
  215. package/dist/core/repl/slash-commands.js +533 -40
  216. package/dist/core/repl/store/index.js +1 -1
  217. package/dist/core/repl/store/jsonl-log.js +22 -22
  218. package/dist/core/repl/store/lockfile.js +10 -10
  219. package/dist/core/repl/store/session-store.js +136 -107
  220. package/dist/core/repl/store/types.js +15 -15
  221. package/dist/core/repl/store/uuid-v7.js +12 -12
  222. package/dist/core/repl/workspace-context.js +43 -21
  223. package/dist/core/repo-map/build.js +125 -0
  224. package/dist/core/repo-map/cache.js +185 -0
  225. package/dist/core/repo-map/extractor.js +254 -0
  226. package/dist/core/repo-map/formatter.js +145 -0
  227. package/dist/core/repo-map/page-rank.js +105 -0
  228. package/dist/core/repo-map/scanner.js +211 -0
  229. package/dist/core/retry-budget/budget.js +284 -0
  230. package/dist/core/retry-budget/index.js +5 -0
  231. package/dist/core/retry-budget/retry-cap.js +74 -0
  232. package/dist/core/routing/lead-worker.js +43 -0
  233. package/dist/core/routing/pre-flight-estimator.js +108 -0
  234. package/dist/core/runs/run-tree.js +103 -0
  235. package/dist/core/security/injection-scanner.js +367 -0
  236. package/dist/core/security/output-filter.js +418 -0
  237. package/dist/core/session/env-file.js +105 -0
  238. package/dist/core/session/section-budgets.js +140 -0
  239. package/dist/core/session.js +92 -0
  240. package/dist/core/settings.js +286 -5
  241. package/dist/core/share/formatter.js +271 -0
  242. package/dist/core/share/redactor.js +221 -0
  243. package/dist/core/share/uploader.js +267 -0
  244. package/dist/core/skills/defaults.js +457 -0
  245. package/dist/core/skills/loader.js +22 -22
  246. package/dist/core/skills/sources.js +27 -27
  247. package/dist/core/smoke/headless-driver.js +174 -0
  248. package/dist/core/smoke/orchestrator.js +194 -0
  249. package/dist/core/smoke/runner.js +238 -0
  250. package/dist/core/smoke/scenario-parser.js +316 -0
  251. package/dist/core/statusline.js +99 -0
  252. package/dist/core/subagents/dispatcher-real.js +600 -0
  253. package/dist/core/subagents/dispatcher.js +132 -43
  254. package/dist/core/subagents/index.js +19 -6
  255. package/dist/core/subagents/isolation-matrix.js +213 -0
  256. package/dist/core/subagents/spawn.js +19 -4
  257. package/dist/core/telemetry/emitter.js +229 -0
  258. package/dist/core/telemetry/queue.js +251 -0
  259. package/dist/core/theme/context.js +91 -0
  260. package/dist/core/theme/presets.js +228 -0
  261. package/dist/core/theme/state.js +181 -0
  262. package/dist/core/todos/invariant.js +10 -0
  263. package/dist/core/todos/state.js +177 -0
  264. package/dist/core/tool-schema/compressor.js +89 -0
  265. package/dist/core/transport/version-interceptor.js +166 -0
  266. package/dist/core/trust.js +2 -2
  267. package/dist/core/tui/thinking-block.js +64 -0
  268. package/dist/core/vim/keymap.js +288 -0
  269. package/dist/core/vim/state.js +92 -0
  270. package/dist/core/watch-markers/marker-watcher.js +133 -0
  271. package/dist/core/worktree-manager/cleanup.js +123 -0
  272. package/dist/core/worktree-manager/manager.js +303 -0
  273. package/dist/index.js +28 -0
  274. package/dist/runtime/bootstrap.js +190 -0
  275. package/dist/runtime/cli.js +4151 -489
  276. package/dist/runtime/commands/agents.js +30 -30
  277. package/dist/runtime/commands/budget.js +5 -5
  278. package/dist/runtime/commands/cancel.js +231 -0
  279. package/dist/runtime/commands/chain.js +489 -0
  280. package/dist/runtime/commands/codegraph-status.js +227 -0
  281. package/dist/runtime/commands/compact.js +297 -0
  282. package/dist/runtime/commands/config.js +32 -32
  283. package/dist/runtime/commands/cost.js +199 -0
  284. package/dist/runtime/commands/delegate.js +244 -13
  285. package/dist/runtime/commands/dispatch.js +126 -0
  286. package/dist/runtime/commands/doctor.js +579 -0
  287. package/dist/runtime/commands/feedback.js +184 -0
  288. package/dist/runtime/commands/hooks.js +184 -0
  289. package/dist/runtime/commands/init.js +254 -0
  290. package/dist/runtime/commands/lsp.js +368 -0
  291. package/dist/runtime/commands/mcp.js +879 -0
  292. package/dist/runtime/commands/memory.js +582 -0
  293. package/dist/runtime/commands/model.js +237 -0
  294. package/dist/runtime/commands/onboarding.js +275 -0
  295. package/dist/runtime/commands/patch.js +128 -0
  296. package/dist/runtime/commands/permissions.js +112 -0
  297. package/dist/runtime/commands/plan.js +143 -0
  298. package/dist/runtime/commands/prd-check.js +285 -0
  299. package/dist/runtime/commands/privacy.js +17 -17
  300. package/dist/runtime/commands/recipe.js +325 -0
  301. package/dist/runtime/commands/redo-blob-store.js +92 -0
  302. package/dist/runtime/commands/redo.js +361 -0
  303. package/dist/runtime/commands/release-notes.js +229 -0
  304. package/dist/runtime/commands/repo-map.js +95 -0
  305. package/dist/runtime/commands/report.js +299 -0
  306. package/dist/runtime/commands/resume.js +118 -0
  307. package/dist/runtime/commands/review-consensus.js +68 -53
  308. package/dist/runtime/commands/rewind.js +333 -0
  309. package/dist/runtime/commands/roster.js +14 -14
  310. package/dist/runtime/commands/sessions.js +163 -0
  311. package/dist/runtime/commands/share.js +316 -0
  312. package/dist/runtime/commands/skills.js +31 -31
  313. package/dist/runtime/commands/status.js +186 -0
  314. package/dist/runtime/commands/stickers.js +82 -0
  315. package/dist/runtime/commands/style.js +194 -0
  316. package/dist/runtime/commands/theme.js +196 -0
  317. package/dist/runtime/commands/undo.js +54 -22
  318. package/dist/runtime/commands/update.js +289 -0
  319. package/dist/runtime/commands/vim.js +140 -0
  320. package/dist/runtime/commands/worktree.js +177 -0
  321. package/dist/runtime/commands/worktrees.js +155 -0
  322. package/dist/runtime/headless-repl.js +195 -0
  323. package/dist/runtime/headless.js +543 -0
  324. package/dist/runtime/load-hooks-or-exit.js +71 -0
  325. package/dist/runtime/plan-decompose.js +531 -0
  326. package/dist/runtime/update-check.js +28 -28
  327. package/dist/runtime/version.js +65 -0
  328. package/dist/skills/bundled/batch.js +617 -0
  329. package/dist/skills/bundled/index.js +45 -0
  330. package/dist/skills/bundled/loop.js +358 -0
  331. package/dist/skills/bundled/remember.js +383 -0
  332. package/dist/skills/bundled/simplify.js +289 -0
  333. package/dist/skills/bundled/skillify.js +373 -0
  334. package/dist/skills/bundled/stuck.js +558 -0
  335. package/dist/skills/bundled/verify.js +439 -0
  336. package/dist/testing/vcr.js +486 -0
  337. package/dist/tools/agent-tool.js +229 -0
  338. package/dist/tools/apply-patch.js +556 -0
  339. package/dist/tools/ask-user-question.js +222 -0
  340. package/dist/tools/ask-user.js +115 -0
  341. package/dist/tools/bash.js +623 -45
  342. package/dist/tools/brief.js +224 -0
  343. package/dist/tools/enter-worktree.js +250 -0
  344. package/dist/tools/exit-worktree.js +147 -0
  345. package/dist/tools/file-tools.js +161 -44
  346. package/dist/tools/lsp-tools.js +189 -0
  347. package/dist/tools/mcp-tool.js +260 -0
  348. package/dist/tools/multi-edit.js +361 -0
  349. package/dist/tools/powershell.js +268 -0
  350. package/dist/tools/registry.js +85 -0
  351. package/dist/tools/skill-tool.js +96 -0
  352. package/dist/tools/sleep.js +99 -0
  353. package/dist/tools/synthetic-output.js +133 -0
  354. package/dist/tools/tasks.js +208 -0
  355. package/dist/tools/todo-write.js +184 -0
  356. package/dist/tools/verify-plan-execution.js +295 -0
  357. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  358. package/dist/tools/web-fetch.js +195 -10
  359. package/dist/tools/web-search.js +458 -0
  360. package/dist/tui/agent-progress-card.js +111 -0
  361. package/dist/tui/agent-tree.js +11 -1
  362. package/dist/tui/ask-modal.js +14 -14
  363. package/dist/tui/ask-user-question-prompt.js +203 -0
  364. package/dist/tui/compact-banner.js +81 -0
  365. package/dist/tui/conversation-pane.js +85 -11
  366. package/dist/tui/cost-table.js +111 -0
  367. package/dist/tui/device-flow.js +2 -2
  368. package/dist/tui/doctor-table.js +46 -0
  369. package/dist/tui/feedback-prompt.js +156 -0
  370. package/dist/tui/input-box.js +247 -32
  371. package/dist/tui/login-picker.js +3 -3
  372. package/dist/tui/markdown-render.js +6 -6
  373. package/dist/tui/onboarding-wizard.js +240 -0
  374. package/dist/tui/permissions-picker.js +86 -0
  375. package/dist/tui/render.js +35 -0
  376. package/dist/tui/repl-render.js +332 -54
  377. package/dist/tui/repl-splash-art.js +16 -16
  378. package/dist/tui/repl-splash-mascot.js +48 -24
  379. package/dist/tui/repl-splash.js +22 -22
  380. package/dist/tui/repl.js +124 -44
  381. package/dist/tui/slash-palette.js +6 -6
  382. package/dist/tui/splash.js +2 -2
  383. package/dist/tui/status-bar.js +109 -31
  384. package/dist/tui/status-table.js +7 -0
  385. package/dist/tui/stickers-art.js +136 -0
  386. package/dist/tui/style-table.js +28 -0
  387. package/dist/tui/theme-table.js +29 -0
  388. package/dist/tui/thinking-spinner.js +123 -0
  389. package/dist/tui/tool-stream-pane.js +53 -4
  390. package/dist/tui/update-banner.js +27 -2
  391. package/dist/tui/vim-input.js +267 -0
  392. package/dist/tui/welcome-banner.js +107 -0
  393. package/dist/tui/welcome-data.js +293 -0
  394. package/dist/tui/workspace-context.js +2 -2
  395. package/docs/examples/codegraph.mcp.json +10 -0
  396. package/package.json +23 -6
  397. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  398. package/test/scenarios/compact-force.scenario.txt +11 -0
  399. package/test/scenarios/identity.scenario.txt +11 -0
  400. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  401. package/test/scenarios/walkback.scenario.txt +12 -0
  402. package/dist/core/engine/compaction-hook.js +0 -154
@@ -1,34 +1,41 @@
1
+ // PR-CLI-SERVER-VERSION-HANDSHAKE . The interceptor stamps the
2
+ // outbound X-Pugi-Cli-Version header, inspects the inbound recommended/
3
+ // server-version headers, and throws PugiCliUpgradeRequiredError on a
4
+ // 426 server response. The top-level catch in `runtime/cli.ts` /
5
+ // `index.ts` renders the upgrade message and exits 1.
6
+ import { assertNotUpgradeRequired, injectClientVersionHeader, inspectVersionResponse, } from '../transport/version-interceptor.js';
7
+ import { PUGI_CLI_VERSION } from '../../runtime/version.js';
1
8
  /**
2
9
  * Anvil-backed engine loop client.
3
10
  *
4
11
  * Wire format: OpenAI-compatible `/v1/chat/completions` shape proxied
5
12
  * through the admin-api Pugi runtime endpoint. The CLI POSTs:
6
13
  *
7
- * POST {apiUrl}/api/pugi/engine
8
- * Authorization: Bearer {apiKey}
9
- * {
10
- * "personaSlug": "oes-dev",
11
- * "messages": [...],
12
- * "tools": [...],
13
- * "maxTokens": 4096,
14
- * "temperature": 0.2
15
- * }
14
+ * POST {apiUrl}/api/pugi/engine
15
+ * Authorization: Bearer {apiKey}
16
+ * {
17
+ * "personaSlug": "oes-dev",
18
+ * "messages": [...],
19
+ * "tools": [...],
20
+ * "maxTokens": 4096,
21
+ * "temperature": 0.2
22
+ * }
16
23
  *
17
24
  * and expects:
18
25
  *
19
- * 200 OK
20
- * {
21
- * "stop": "tool_use" | "text",
22
- * "content": "...", // present when stop=text
23
- * "toolCalls": [{id, name, arguments}], // present when stop=tool_use
24
- * "tokensUsed": 1234,
25
- * "model": "deepseek-chat-v3.1"
26
- * }
26
+ * 200 OK
27
+ * {
28
+ * "stop": "tool_use" | "text",
29
+ * "content": "...", // present when stop=text
30
+ * "toolCalls": [{id, name, arguments}], // present when stop=tool_use
31
+ * "tokensUsed": 1234,
32
+ * "model": "deepseek-chat-v3.1"
33
+ * }
27
34
  *
28
- * 401/403 -> auth_missing
29
- * 404 -> endpoint_missing
30
- * 429 -> rate_limited
31
- * other -> failed
35
+ * 401/403 -> auth_missing
36
+ * 404 -> endpoint_missing
37
+ * 429 -> rate_limited
38
+ * other -> failed
32
39
  *
33
40
  * The endpoint itself ships in Sprint 2 (Track 2A). Until then the CLI
34
41
  * surfaces `endpoint_missing` cleanly and the operator runs `pugi code`
@@ -54,23 +61,81 @@ export class AnvilEngineLoopClient {
54
61
  options.signal.addEventListener('abort', onAbort);
55
62
  const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);
56
63
  try {
64
+ // PR-CLI-SERVER-VERSION-HANDSHAKE . Stamp the outbound
65
+ // X-Pugi-Cli-Version header so the admin-api middleware can
66
+ // decide whether to honour, soft-warn, or 426 this request.
67
+ const outboundHeaders = injectClientVersionHeader({
68
+ 'content-type': 'application/json',
69
+ authorization: `Bearer ${this.config.apiKey}`,
70
+ 'user-agent': 'pugi-cli/0.0.1',
71
+ }, PUGI_CLI_VERSION);
57
72
  const res = await fetch(url, {
58
73
  method: 'POST',
59
- headers: {
60
- 'content-type': 'application/json',
61
- authorization: `Bearer ${this.config.apiKey}`,
62
- 'user-agent': 'pugi-cli/0.0.1',
63
- },
74
+ headers: outboundHeaders,
64
75
  body: JSON.stringify({
65
76
  personaSlug: options.personaSlug,
66
77
  messages,
67
78
  tools,
68
79
  maxTokens: options.maxTokens,
69
80
  temperature: options.temperature,
81
+ // β1 (audit E2): the admin-api `EngineRequestDto` accepts
82
+ // these optional fields (see `pugi-engine.controller.ts:230`
83
+ // EngineRequestDto schema). Before this fix the CLI dropped
84
+ // them, which forced the controller to fall back to legacy
85
+ // per-persona resolution + emit `command="(none)"` in its
86
+ // structured logs. `undefined` keys are stripped by
87
+ // `JSON.stringify` so the payload stays clean for fixture
88
+ // clients that exact-match the body shape.
89
+ command: options.command,
90
+ // β1a r1: `tag` is `EngineDispatchTag` object shape now —
91
+ // `JSON.stringify` serialises it as `{tag, priority?,
92
+ // budget_hint?}` matching `EngineDispatchTagDto`. Previously
93
+ // this was a bare string and the server's `IsIn` validator
94
+ // rejected every payload with HTTP 400.
95
+ tag: options.tag,
96
+ model: options.model,
97
+ // Task: server-side tier gate. Stripped by JSON.stringify
98
+ // when undefined so older runtimes без the contextTier DTO
99
+ // field never see an unknown key.
100
+ contextTier: options.contextTier,
70
101
  }),
71
102
  signal: controller.signal,
72
103
  });
73
104
  const text = await res.text();
105
+ // PR-CLI-SERVER-VERSION-HANDSHAKE: cache server-recommended +
106
+ // server-version headers so UpdateBanner / `pugi doctor` can
107
+ // surface them, then short-circuit on 426 by throwing
108
+ // PugiCliUpgradeRequiredError. The throw bubbles to the
109
+ // top-level catch in index.ts which renders the upgrade banner.
110
+ // The getter shim handles both real `Response` (`.headers.get`)
111
+ // and minimal fixture/stub responses (`.headers?.[name]`) so
112
+ // existing transport tests that mock `fetch` with `{status, text}`
113
+ // don't need to grow a Headers polyfill just to keep passing.
114
+ //
115
+ // Cache poison guard: skip the inspect step on 426. A hostile
116
+ // upstream (proxy with a compromised cert pin, or a transient MITM
117
+ // on a coffee-shop network) could otherwise forge an
118
+ // `X-Pugi-Cli-Upgrade-Recommended` header alongside a 426 status
119
+ // and poison `cachedServerRecommendation` for the rest of the REPL
120
+ // session — `UpdateBanner` would then surface attacker-chosen
121
+ // version strings to the operator. The 426 body still carries the
122
+ // legitimate `recommendedVersion` field, which assertNotUpgrade-
123
+ // Required parses + throws with, so the operator-facing banner
124
+ // remains accurate via the error path.
125
+ if (res.status !== 426) {
126
+ inspectVersionResponse((name) => {
127
+ const h = res.headers;
128
+ if (h && typeof h.get === 'function') {
129
+ return h.get(name);
130
+ }
131
+ if (h && typeof h === 'object') {
132
+ const lowered = h[name.toLowerCase()];
133
+ return lowered ?? null;
134
+ }
135
+ return null;
136
+ });
137
+ }
138
+ assertNotUpgradeRequired(res.status, text, PUGI_CLI_VERSION);
74
139
  if (res.status === 200) {
75
140
  try {
76
141
  const json = JSON.parse(text);
@@ -119,6 +184,55 @@ export class AnvilEngineLoopClient {
119
184
  };
120
185
  }
121
186
  if (res.status === 401 || res.status === 403) {
187
+ // 403 has two distinct causes:
188
+ // 1. genuinely invalid / expired token (auth_missing) — the
189
+ // old default.
190
+ // 2. tenant authenticated successfully but the privacy mode
191
+ // (strict / balanced policy) refused upstream LLM dispatch.
192
+ // The admin-api returns
193
+ // `{ code: 'privacy_strict_upstream_blocked', mode, model,
194
+ // message: '...switch via pugi config set privacy=...' }`.
195
+ // Reported as `auth_missing` the user runs `pugi login`
196
+ // again, which does nothing — the actual fix is a privacy-
197
+ // mode change. Parse the body and route accordingly.
198
+ // (2026-05-27 P0.3 — dogfood surfaced this on /api/pugi/engine
199
+ // for a strict-mode tenant; see memory feedback_no_fake_dispatch_promises
200
+ // for the broader "misleading error" pattern.)
201
+ try {
202
+ const parsed = text ? JSON.parse(text) : null;
203
+ // dogfood cycle 2: distinct error code for the
204
+ // infra-side "PII scrubber down" case. Previously the engine
205
+ // server returned `privacy_strict_upstream_blocked` here even
206
+ // when the tenant was on BALANCED (the scrubber crash forced
207
+ // a fail-closed). Operators chased the wrong fix ("switch
208
+ // privacy") for hours. Server now emits
209
+ // `pii_scrubber_unavailable` — surface a distinct remediation
210
+ // that points at the infra side, not the operator's privacy
211
+ // posture.
212
+ if (parsed?.code === 'pii_scrubber_unavailable') {
213
+ return {
214
+ stop: 'error',
215
+ code: 'privacy_blocked',
216
+ message: parsed.message ?? 'PII scrubber unavailable; privacy filter refused dispatch.',
217
+ remediation: 'Infra-side issue (not your tenant privacy mode). Wait for ops to restore ' +
218
+ 'the PiiScrubberService, OR temporarily switch your tenant to permissive via ' +
219
+ '`pugi config set privacy=permissive`.',
220
+ };
221
+ }
222
+ if (parsed?.code === 'privacy_strict_upstream_blocked' || parsed?.code === 'privacy_blocked') {
223
+ return {
224
+ stop: 'error',
225
+ code: 'privacy_blocked',
226
+ message: parsed.message ?? 'Tenant privacy mode forbids upstream LLM dispatch.',
227
+ remediation: 'pugi config set privacy=balanced — OR configure a self-hosted Anvil model.',
228
+ };
229
+ }
230
+ }
231
+ catch {
232
+ // Body not JSON — fall through to the generic auth_missing
233
+ // branch below; the 200-char text echo on `failed` will at
234
+ // least give the operator the raw response to triage.
235
+ }
122
236
  return {
123
237
  stop: 'error',
124
238
  code: 'auth_missing',
@@ -0,0 +1,179 @@
1
+ /**
2
+ * Crude token-count heuristic mirroring `runEngineLoop`'s fallback
3
+ * accounting (transcript char count / 4). The CLI does not have access
4
+ * to a real tokenizer pre-flight — the runtime returns `usage.totalTokens`
5
+ * only on the server response, which is too late for our pre-turn gate.
6
+ * char/4 is in the right order of magnitude for English/TS and matches
7
+ * what the loop's own fallback uses on `tokensUsed === 0` upstream.
8
+ */
9
+ export function estimateTranscriptTokens(messages) {
10
+ let chars = 0;
11
+ for (const m of messages) {
12
+ chars += m.content.length;
13
+ const calls = m.toolCalls ?? [];
14
+ for (const c of calls) {
15
+ chars += c.name.length + c.arguments.length;
16
+ }
17
+ }
18
+ return Math.ceil(chars / 4);
19
+ }
20
+ const FILE_TOOL_NAMES = new Set([
21
+ 'read',
22
+ 'write',
23
+ 'edit',
24
+ 'multi_edit',
25
+ 'multiEdit',
26
+ ]);
27
+ /**
28
+ * Walk the dropped slice and pull out tool-call metadata. We parse the
29
+ * `arguments` JSON best-effort — a bad parse is harmless here because
30
+ * the executor surfaced the canonical error to the model already; the
31
+ * gist just under-counts that one call.
32
+ */
33
+ export function summarizeDroppedTurns(dropped) {
34
+ let toolCalls = 0;
35
+ let bashCalls = 0;
36
+ const files = new Set();
37
+ for (const m of dropped) {
38
+ if (m.role === 'assistant') {
39
+ const calls = m.toolCalls ?? [];
40
+ toolCalls += calls.length;
41
+ for (const c of calls) {
42
+ if (c.name === 'bash') {
43
+ bashCalls += 1;
44
+ continue;
45
+ }
46
+ if (FILE_TOOL_NAMES.has(c.name)) {
47
+ const p = extractPath(c.arguments);
48
+ if (p)
49
+ files.add(p);
50
+ }
51
+ }
52
+ }
53
+ }
54
+ return {
55
+ toolCalls,
56
+ fileCount: files.size,
57
+ bashCalls,
58
+ messagesDropped: dropped.length,
59
+ };
60
+ }
61
+ function extractPath(rawArgs) {
62
+ if (!rawArgs)
63
+ return null;
64
+ try {
65
+ const parsed = JSON.parse(rawArgs);
66
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
67
+ const obj = parsed;
68
+ const path = obj['path'] ?? obj['filePath'];
69
+ if (typeof path === 'string' && path.length > 0)
70
+ return path;
71
+ }
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ return null;
77
+ }
78
+ /**
79
+ * Format the deterministic gist string spliced into the synthetic
80
+ * system message. Stable shape so spec assertions and operator
81
+ * logs do not drift turn-over-turn.
82
+ */
83
+ export function renderAutoCompactSentinel(stats) {
84
+ return (`[auto-compact] Earlier turns ` +
85
+ `(${stats.toolCalls} tool calls, ${stats.fileCount} files read, ${stats.bashCalls} bash commands) ` +
86
+ `summarized to free transcript headroom. ` +
87
+ `Recent turns and the original task remain in context; ` +
88
+ `re-read any earlier file by name if you need its contents again.`);
89
+ }
90
+ /**
91
+ * Minimum transcript length (in messages) before compact is allowed.
92
+ * We always retain `system + user` (the first 2) + the last 2 turns,
93
+ * so anything <= 4 messages has nothing in the middle to drop.
94
+ * Compacting на 4-message transcript would either be a no-op or
95
+ * accidentally drop the user's original task.
96
+ */
97
+ export const MIN_COMPACT_TRANSCRIPT_LENGTH = 5;
98
+ /**
99
+ * Pure gate. Returns `compact` when ALL of:
100
+ * - `config.enabled` is true
101
+ * - estimated transcript tokens >= `thresholdRatio * maxTokens`
102
+ * - transcript length >= 5 (need history to drop)
103
+ */
104
+ export function evaluateAutoCompactDecision(input) {
105
+ const usedTokens = estimateTranscriptTokens(input.transcript);
106
+ if (!input.config.enabled) {
107
+ return { kind: 'skip', reason: 'disabled', usedTokens };
108
+ }
109
+ if (input.transcript.length < MIN_COMPACT_TRANSCRIPT_LENGTH) {
110
+ return { kind: 'skip', reason: 'transcript-too-short', usedTokens };
111
+ }
112
+ const thresholdTokens = Math.floor(input.config.thresholdRatio * input.maxTokens);
113
+ if (usedTokens < thresholdTokens) {
114
+ return { kind: 'skip', reason: 'below-threshold', usedTokens };
115
+ }
116
+ return { kind: 'compact', usedTokens, thresholdTokens };
117
+ }
118
+ /**
119
+ * Rewrite the transcript: keep the first two messages (system + user
120
+ * task), drop the middle (assistant + tool turns), insert a synthetic
121
+ * system sentinel summarizing what was dropped, then re-append the
122
+ * last 2 messages so the model has the most-recent tool result + its
123
+ * own last reply in full fidelity.
124
+ *
125
+ * Precondition: caller has already checked the decision is `compact`
126
+ * (length >= MIN_COMPACT_TRANSCRIPT_LENGTH). The function still guards
127
+ * with a defensive identity-return on shorter transcripts so a careless
128
+ * caller cannot corrupt the prefix.
129
+ */
130
+ export function compactTranscript(transcript) {
131
+ const preUsedTokens = estimateTranscriptTokens(transcript);
132
+ if (transcript.length < MIN_COMPACT_TRANSCRIPT_LENGTH) {
133
+ return {
134
+ transcript: transcript.slice(),
135
+ droppedCount: 0,
136
+ gist: '',
137
+ stats: { toolCalls: 0, fileCount: 0, bashCalls: 0, messagesDropped: 0 },
138
+ preUsedTokens,
139
+ postUsedTokens: preUsedTokens,
140
+ };
141
+ }
142
+ // Always retain: index 0 (system) + index 1 (original user task) +
143
+ // last 2 messages. The middle slice is what gets summarised.
144
+ const head = transcript.slice(0, 2);
145
+ const tail = transcript.slice(-2);
146
+ const middle = transcript.slice(2, -2);
147
+ const stats = summarizeDroppedTurns(middle);
148
+ const gist = renderAutoCompactSentinel(stats);
149
+ const sentinelMessage = {
150
+ role: 'system',
151
+ content: gist,
152
+ };
153
+ const next = [...head, sentinelMessage, ...tail];
154
+ const postUsedTokens = estimateTranscriptTokens(next);
155
+ return {
156
+ transcript: next,
157
+ droppedCount: middle.length,
158
+ gist,
159
+ stats,
160
+ preUsedTokens,
161
+ postUsedTokens,
162
+ };
163
+ }
164
+ /**
165
+ * Convenience composer used by `runEngineLoop`: evaluate → compact in
166
+ * one shot. Returns `null` when the decision was `skip` so the loop
167
+ * driver can branch cheaply без destructuring two layers of records.
168
+ */
169
+ export function maybeCompact(transcript, maxTokens, config) {
170
+ const decision = evaluateAutoCompactDecision({
171
+ transcript,
172
+ maxTokens,
173
+ config,
174
+ });
175
+ if (decision.kind === 'skip')
176
+ return null;
177
+ return compactTranscript(transcript);
178
+ }
179
+ //# sourceMappingURL=auto-compact.js.map
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Auto-compact (mid-loop transcript summarization) default trip point as
3
+ * a fraction of the per-command `maxTokens` envelope. CEO P1 #14 (CC
4
+ * parity): when transcript char-count tokens cross 75% of the budget,
5
+ * the engine loop drops the middle turns and inserts a deterministic
6
+ * `[auto-compact]` sentinel so the loop can continue без the model
7
+ * tripping the `budget_exhausted` terminal status mid-build.
8
+ *
9
+ * Empirically — `pugi code "big refactor"` hits the 80k cap on turn 4-5
10
+ * and refuses to finish; `pugi fix` does the same at 50k. Auto-compact
11
+ * keeps the recent N turns + a one-line gist of the dropped tool calls
12
+ * so the model retains the most recent state without paying for the
13
+ * full prefix.
14
+ *
15
+ * Operators can opt out / retune via `.pugi/settings.json`:
16
+ *
17
+ * {
18
+ * "autoCompact": { "enabled": true, "thresholdRatio": 0.75 }
19
+ * }
20
+ *
21
+ * Bad values fall back silently to the default — the engine loop never
22
+ * crashes on a malformed settings field (mirrors `resolveBudget`).
23
+ */
24
+ export const AUTO_COMPACT_THRESHOLD_RATIO = 0.75;
25
+ export const DEFAULT_AUTO_COMPACT_CONFIG = {
26
+ enabled: true,
27
+ thresholdRatio: AUTO_COMPACT_THRESHOLD_RATIO,
28
+ };
29
+ /**
30
+ * Pull the auto-compact override from `.pugi/settings.json`. Uses the
31
+ * same defensive-cast pattern as `readSettingsBudget` so an unknown
32
+ * field shape silently falls back к defaults (the gate is a comfort
33
+ * feature; a malformed settings line must not break the engine loop).
34
+ *
35
+ * Returns the merged config — caller never sees `undefined`.
36
+ */
37
+ export function resolveAutoCompactConfig(settings) {
38
+ if (!settings)
39
+ return DEFAULT_AUTO_COMPACT_CONFIG;
40
+ const root = settings.autoCompact;
41
+ if (!root || typeof root !== 'object' || Array.isArray(root)) {
42
+ return DEFAULT_AUTO_COMPACT_CONFIG;
43
+ }
44
+ const r = root;
45
+ const enabledRaw = r['enabled'];
46
+ const thresholdRaw = r['thresholdRatio'];
47
+ const enabled = typeof enabledRaw === 'boolean'
48
+ ? enabledRaw
49
+ : DEFAULT_AUTO_COMPACT_CONFIG.enabled;
50
+ let thresholdRatio = DEFAULT_AUTO_COMPACT_CONFIG.thresholdRatio;
51
+ if (typeof thresholdRaw === 'number' && Number.isFinite(thresholdRaw)) {
52
+ if (thresholdRaw > 0 && thresholdRaw <= 1) {
53
+ thresholdRatio = thresholdRaw;
54
+ }
55
+ }
56
+ return { enabled, thresholdRatio };
57
+ }
58
+ /**
59
+ * β1 defaults. Source of truth for the per-command budget envelope.
60
+ * The runtime is allowed to look these up directly (no need to round
61
+ * trip through settings.json when no override is in play).
62
+ *
63
+ * bump (post-Wave-7 hooks-v2 + 6-perm-modes + auto-classifier
64
+ * added ~12K tokens of system-prompt + tools-schema overhead per turn):
65
+ * `code` 30k → 80k и `fix` 30k → 50k so a single-file refactor on a
66
+ * 1000-line source file no longer exhausts the budget on turn 2.
67
+ * Empirical: smoke `pugi code "сделай snake.html"` on beta.37 burned
68
+ * 36k/30k after 2 tool calls; beta.36 same task closed in 8k. The Wave-7
69
+ * additions are good (the upstream tool parity), but the budget cap did not move with
70
+ * them. the upstream tool's `code` default is ~80k; matching that restores headroom.
71
+ */
72
+ export const beta1DefaultBudgets = {
73
+ // bump (CEO #44, post-auto-compact #14 + prompt-cache #15):
74
+ // auto-compact at 75% threshold + cache_control on stable prefix mean
75
+ // real per-call token use is ~30-40% lower than legacy. Bump headroom
76
+ // so multi-file refactors no longer trip the cap. Anvil clamps per-call
77
+ // max_tokens to 128k (PR) so the engine envelope still safe.
78
+ fix: { maxTokens: 80_000, maxToolCalls: 20 },
79
+ code: { maxTokens: 120_000, maxToolCalls: 25 },
80
+ build: { maxTokens: 200_000, maxToolCalls: 30 },
81
+ plan: { maxTokens: 200_000, maxToolCalls: 8 },
82
+ explain: { maxTokens: 60_000, maxToolCalls: 10 },
83
+ review_triple: { maxTokens: 100_000, maxToolCalls: 10 },
84
+ };
85
+ /**
86
+ * Hard upper bounds. Anything above this is treated as user error
87
+ * (likely a typo or misplaced decimal) and rejected by
88
+ * `assertBudgetWithinTier`. Stops a careless settings.json edit from
89
+ * silently authorising a 100M-token run.
90
+ */
91
+ export const HARD_MAX_TOKENS = 5_000_000;
92
+ export const HARD_MAX_TOOL_CALLS = 500;
93
+ /**
94
+ * Compute the effective budget for a given command, applying:
95
+ * 1. β1 defaults
96
+ * 2. settings.json `budgets.<command>` partial overrides
97
+ * 3. task-level override (caller-provided, e.g. CLI `--max-tokens`,
98
+ * `--max-turns`, or the resolved intensity profile)
99
+ *
100
+ * Throws `BudgetConfigError` when the resolved budget exceeds the
101
+ * HARD_MAX_* caps so misconfigured settings.json fails fast.
102
+ *
103
+ * Triple-review P1 follow-up : `maxTurns` propagates through this
104
+ * resolver so the SDK's `runEngineLoop` actually receives the per-tier
105
+ * cap that the intensity dial advertises. Override precedence matches
106
+ * the other knobs: explicit task override > settings.json > undefined
107
+ * (legacy behaviour: no turn cap, only tokens + tool-calls).
108
+ */
109
+ export function resolveBudget(command, settings, override) {
110
+ const base = beta1DefaultBudgets[command];
111
+ const settingsBudget = readSettingsBudget(settings, command);
112
+ const resolvedMaxTurns = override?.maxTurns ?? settingsBudget?.maxTurns ?? undefined;
113
+ const resolved = {
114
+ maxTokens: override?.maxTokens ??
115
+ settingsBudget?.maxTokens ??
116
+ base.maxTokens,
117
+ maxToolCalls: override?.maxToolCalls ??
118
+ settingsBudget?.maxToolCalls ??
119
+ base.maxToolCalls,
120
+ ...(resolvedMaxTurns !== undefined ? { maxTurns: resolvedMaxTurns } : {}),
121
+ };
122
+ assertBudgetWithinTier(command, resolved);
123
+ return resolved;
124
+ }
125
+ export class BudgetConfigError extends Error {
126
+ constructor(message) {
127
+ super(message);
128
+ this.name = 'BudgetConfigError';
129
+ }
130
+ }
131
+ /**
132
+ * Hard upper bound on per-task LLM turns. Above this we reject the
133
+ * configuration as a likely typo. The marathon intensity preset is 200,
134
+ * so 1000 buys an order of magnitude of headroom for power-user overrides
135
+ * without authorising runaway loops.
136
+ */
137
+ export const HARD_MAX_TURNS = 1000;
138
+ export function assertBudgetWithinTier(command, budget) {
139
+ if (!Number.isFinite(budget.maxTokens) || budget.maxTokens <= 0) {
140
+ throw new BudgetConfigError(`budget[${command}].maxTokens must be a positive number, got ${budget.maxTokens}`);
141
+ }
142
+ if (!Number.isFinite(budget.maxToolCalls) || budget.maxToolCalls <= 0) {
143
+ throw new BudgetConfigError(`budget[${command}].maxToolCalls must be a positive number, got ${budget.maxToolCalls}`);
144
+ }
145
+ if (budget.maxTokens > HARD_MAX_TOKENS) {
146
+ throw new BudgetConfigError(`budget[${command}].maxTokens=${budget.maxTokens} exceeds hard cap ${HARD_MAX_TOKENS}`);
147
+ }
148
+ if (budget.maxToolCalls > HARD_MAX_TOOL_CALLS) {
149
+ throw new BudgetConfigError(`budget[${command}].maxToolCalls=${budget.maxToolCalls} exceeds hard cap ${HARD_MAX_TOOL_CALLS}`);
150
+ }
151
+ if (budget.maxTurns !== undefined) {
152
+ if (!Number.isFinite(budget.maxTurns) || budget.maxTurns <= 0) {
153
+ throw new BudgetConfigError(`budget[${command}].maxTurns must be a positive number, got ${budget.maxTurns}`);
154
+ }
155
+ if (budget.maxTurns > HARD_MAX_TURNS) {
156
+ throw new BudgetConfigError(`budget[${command}].maxTurns=${budget.maxTurns} exceeds hard cap ${HARD_MAX_TURNS}`);
157
+ }
158
+ }
159
+ }
160
+ /**
161
+ * Pull a settings.json budget override for the given command, with
162
+ * defensive typing. `PugiSettings` does not yet declare `budgets`
163
+ * formally (β1 is the first sprint to land it) so we cast via unknown
164
+ * and validate each field at the boundary.
165
+ */
166
+ function readSettingsBudget(settings, command) {
167
+ if (!settings)
168
+ return undefined;
169
+ const root = settings.budgets;
170
+ if (!root || typeof root !== 'object' || Array.isArray(root))
171
+ return undefined;
172
+ const map = root;
173
+ const entry = map[command];
174
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
175
+ return undefined;
176
+ const e = entry;
177
+ const out = {};
178
+ if (typeof e['maxTokens'] === 'number')
179
+ out.maxTokens = e['maxTokens'];
180
+ if (typeof e['maxToolCalls'] === 'number')
181
+ out.maxToolCalls = e['maxToolCalls'];
182
+ if (typeof e['maxTurns'] === 'number')
183
+ out.maxTurns = e['maxTurns'];
184
+ return out;
185
+ }
186
+ //# sourceMappingURL=budgets.js.map