@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
@@ -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,92 @@ 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
+ // PUGI-260: also stamp `X-Pugi-Context-Tier: 1m` when the
68
+ // operator opted into the long-context lane. The server reads
69
+ // either the body's `contextTier` field OR this header (header is
70
+ // a fallback for older runtimes / non-CLI clients), so emitting
71
+ // both is harmless и belt-and-suspenders. Only emitted for the
72
+ // `'1m'` value — the absence of the header is wire-equivalent к
73
+ // `'standard'`, keeping the default-lane path header-free.
74
+ const baseHeaders = {
75
+ 'content-type': 'application/json',
76
+ authorization: `Bearer ${this.config.apiKey}`,
77
+ 'user-agent': 'pugi-cli/0.0.1',
78
+ };
79
+ if (options.contextTier === '1m') {
80
+ baseHeaders['x-pugi-context-tier'] = '1m';
81
+ }
82
+ const outboundHeaders = injectClientVersionHeader(baseHeaders, PUGI_CLI_VERSION);
57
83
  const res = await fetch(url, {
58
84
  method: 'POST',
59
- headers: {
60
- 'content-type': 'application/json',
61
- authorization: `Bearer ${this.config.apiKey}`,
62
- 'user-agent': 'pugi-cli/0.0.1',
63
- },
85
+ headers: outboundHeaders,
64
86
  body: JSON.stringify({
65
87
  personaSlug: options.personaSlug,
66
88
  messages,
67
89
  tools,
68
90
  maxTokens: options.maxTokens,
69
91
  temperature: options.temperature,
92
+ // β1 (audit E2): the admin-api `EngineRequestDto` accepts
93
+ // these optional fields (see `pugi-engine.controller.ts:230`
94
+ // EngineRequestDto schema). Before this fix the CLI dropped
95
+ // them, which forced the controller to fall back to legacy
96
+ // per-persona resolution + emit `command="(none)"` in its
97
+ // structured logs. `undefined` keys are stripped by
98
+ // `JSON.stringify` so the payload stays clean for fixture
99
+ // clients that exact-match the body shape.
100
+ command: options.command,
101
+ // β1a r1: `tag` is `EngineDispatchTag` object shape now —
102
+ // `JSON.stringify` serialises it as `{tag, priority?,
103
+ // budget_hint?}` matching `EngineDispatchTagDto`. Previously
104
+ // this was a bare string and the server's `IsIn` validator
105
+ // rejected every payload with HTTP 400.
106
+ tag: options.tag,
107
+ model: options.model,
108
+ // Task: server-side tier gate. Stripped by JSON.stringify
109
+ // when undefined so older runtimes без the contextTier DTO
110
+ // field never see an unknown key.
111
+ contextTier: options.contextTier,
70
112
  }),
71
113
  signal: controller.signal,
72
114
  });
73
115
  const text = await res.text();
116
+ // PR-CLI-SERVER-VERSION-HANDSHAKE: cache server-recommended +
117
+ // server-version headers so UpdateBanner / `pugi doctor` can
118
+ // surface them, then short-circuit on 426 by throwing
119
+ // PugiCliUpgradeRequiredError. The throw bubbles to the
120
+ // top-level catch in index.ts which renders the upgrade banner.
121
+ // The getter shim handles both real `Response` (`.headers.get`)
122
+ // and minimal fixture/stub responses (`.headers?.[name]`) so
123
+ // existing transport tests that mock `fetch` with `{status, text}`
124
+ // don't need to grow a Headers polyfill just to keep passing.
125
+ //
126
+ // Cache poison guard: skip the inspect step on 426. A hostile
127
+ // upstream (proxy with a compromised cert pin, or a transient MITM
128
+ // on a coffee-shop network) could otherwise forge an
129
+ // `X-Pugi-Cli-Upgrade-Recommended` header alongside a 426 status
130
+ // and poison `cachedServerRecommendation` for the rest of the REPL
131
+ // session — `UpdateBanner` would then surface attacker-chosen
132
+ // version strings to the operator. The 426 body still carries the
133
+ // legitimate `recommendedVersion` field, which assertNotUpgrade-
134
+ // Required parses + throws with, so the operator-facing banner
135
+ // remains accurate via the error path.
136
+ if (res.status !== 426) {
137
+ inspectVersionResponse((name) => {
138
+ const h = res.headers;
139
+ if (h && typeof h.get === 'function') {
140
+ return h.get(name);
141
+ }
142
+ if (h && typeof h === 'object') {
143
+ const lowered = h[name.toLowerCase()];
144
+ return lowered ?? null;
145
+ }
146
+ return null;
147
+ });
148
+ }
149
+ assertNotUpgradeRequired(res.status, text, PUGI_CLI_VERSION);
74
150
  if (res.status === 200) {
75
151
  try {
76
152
  const json = JSON.parse(text);
@@ -119,6 +195,55 @@ export class AnvilEngineLoopClient {
119
195
  };
120
196
  }
121
197
  if (res.status === 401 || res.status === 403) {
198
+ // 403 has two distinct causes:
199
+ // 1. genuinely invalid / expired token (auth_missing) — the
200
+ // old default.
201
+ // 2. tenant authenticated successfully but the privacy mode
202
+ // (strict / balanced policy) refused upstream LLM dispatch.
203
+ // The admin-api returns
204
+ // `{ code: 'privacy_strict_upstream_blocked', mode, model,
205
+ // message: '...switch via pugi config set privacy=...' }`.
206
+ // Reported as `auth_missing` the user runs `pugi login`
207
+ // again, which does nothing — the actual fix is a privacy-
208
+ // mode change. Parse the body and route accordingly.
209
+ // (2026-05-27 P0.3 — dogfood surfaced this on /api/pugi/engine
210
+ // for a strict-mode tenant; see memory feedback_no_fake_dispatch_promises
211
+ // for the broader "misleading error" pattern.)
212
+ try {
213
+ const parsed = text ? JSON.parse(text) : null;
214
+ // dogfood cycle 2: distinct error code for the
215
+ // infra-side "PII scrubber down" case. Previously the engine
216
+ // server returned `privacy_strict_upstream_blocked` here even
217
+ // when the tenant was on BALANCED (the scrubber crash forced
218
+ // a fail-closed). Operators chased the wrong fix ("switch
219
+ // privacy") for hours. Server now emits
220
+ // `pii_scrubber_unavailable` — surface a distinct remediation
221
+ // that points at the infra side, not the operator's privacy
222
+ // posture.
223
+ if (parsed?.code === 'pii_scrubber_unavailable') {
224
+ return {
225
+ stop: 'error',
226
+ code: 'privacy_blocked',
227
+ message: parsed.message ?? 'PII scrubber unavailable; privacy filter refused dispatch.',
228
+ remediation: 'Infra-side issue (not your tenant privacy mode). Wait for ops to restore ' +
229
+ 'the PiiScrubberService, OR temporarily switch your tenant to permissive via ' +
230
+ '`pugi config set privacy=permissive`.',
231
+ };
232
+ }
233
+ if (parsed?.code === 'privacy_strict_upstream_blocked' || parsed?.code === 'privacy_blocked') {
234
+ return {
235
+ stop: 'error',
236
+ code: 'privacy_blocked',
237
+ message: parsed.message ?? 'Tenant privacy mode forbids upstream LLM dispatch.',
238
+ remediation: 'pugi config set privacy=balanced — OR configure a self-hosted Anvil model.',
239
+ };
240
+ }
241
+ }
242
+ catch {
243
+ // Body not JSON — fall through to the generic auth_missing
244
+ // branch below; the 200-char text echo on `failed` will at
245
+ // least give the operator the raw response to triage.
246
+ }
122
247
  return {
123
248
  stop: 'error',
124
249
  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