@pugi/cli 0.1.0-beta.7 → 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 +4162 -488
  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
@@ -0,0 +1,418 @@
1
+ /**
2
+ * Output filter (primitive #2 of the 9-layer RAG architecture).
3
+ *
4
+ * Composes four independent safety checks into a single perimeter that
5
+ * runs against text the engine is ABOUT to emit to the operator. Each
6
+ * sub-check is a private function inside this module; the public entry
7
+ * point `filterOutput` runs them in a deterministic order and returns
8
+ * the union of their findings.
9
+ *
10
+ * The intent is "one chokepoint" — the engine wire-up (out of scope for
11
+ * this PR) will call `filterOutput` once per assistant turn and refuse
12
+ * to surface text whose `allowed` flag is false. Operators get a single
13
+ * structured violations list to triage.
14
+ *
15
+ * # Composed checks (deterministic order)
16
+ *
17
+ * 1. citation-verify — `[file:line]` / `(file:line)` patterns must
18
+ * resolve to a real file inside `workspaceRoot`.
19
+ * Path containment uses the existing workspace
20
+ * boundary check pattern from `path-security.ts`.
21
+ * 2. secret-scan — defers to `core/memory/secret-scanner.ts`
22
+ * (backlog). When `redactSecrets=true`
23
+ * (default) the filter substitutes detected
24
+ * secrets with `[SECRET:<kind>]` placeholders
25
+ * AND reports the violations.
26
+ * 3. numeric-claim — standalone numbers (≥3 digits OR percentages
27
+ * OR currency) must have a citation within
28
+ * 80 chars. When `allowNumericClaims=true` the
29
+ * check is skipped entirely.
30
+ * 4. malicious-url — every URL found in the text must intersect
31
+ * `allowedURLs`. Empty allowlist + non-empty URL
32
+ * set = all URLs flagged.
33
+ * 5. injection-echo — surface obvious prompt-injection fragments
34
+ * that look like upstream content leaked into
35
+ * the model's output (`<system-reminder>`,
36
+ * ChatML tags, "ignore previous instructions",
37
+ * etc.).
38
+ *
39
+ * # No early exit
40
+ *
41
+ * The filter walks every check on every call and accumulates every
42
+ * violation. A text with three secrets, two bad URLs and one bogus
43
+ * citation surfaces six violations — operators see the whole picture in
44
+ * one pass, not piecemeal across retries.
45
+ *
46
+ * # What this is NOT
47
+ *
48
+ * - A blocking guard. The caller decides what to do with
49
+ * `allowed=false`. We never throw on a violation — throwing would
50
+ * deny the caller the redacted text + structured findings.
51
+ * - A locale-aware numeric extractor. Only English thousands
52
+ * separators are recognised for now (`1,000`, `1000`, `1,000.50`).
53
+ * Locale variants (e.g. `1.000,50` in DE) are intentionally out of
54
+ * scope per the spec.
55
+ * - An ML-based harm classifier. Regex tier only; harm-pattern
56
+ * expansion is deferred.
57
+ *
58
+ * # Existing modules composed against
59
+ *
60
+ * - `core/memory/secret-scanner.ts` — secret-scan delegate.
61
+ * - `core/path-security.ts` — workspace boundary check
62
+ * pattern (containment +
63
+ * realpath).
64
+ *
65
+ * # Follow-ups (tracked, NOT shipped here)
66
+ *
67
+ * - Engine wire-up — consumer pulls `filterOutput` before emitting
68
+ * to the operator. Out of scope; touches `core/engine/` only.
69
+ * - Shared secret-scanner consolidation — depends on ship.
70
+ * - Locale-aware numeric extraction.
71
+ * - LLM-driven harm classifier (regex tier only today).
72
+ */
73
+ import { existsSync, realpathSync } from 'node:fs';
74
+ import { isAbsolute, relative, resolve } from 'node:path';
75
+ import { redactSecrets as redactSecretsImpl, scanForSecrets, } from '../memory/secret-scanner.js';
76
+ // ---------------------------------------------------------------------------
77
+ // Pattern catalog
78
+ // ---------------------------------------------------------------------------
79
+ /**
80
+ * Inline citation patterns. Accepts:
81
+ * - `[file:line]` e.g. `[src/foo.ts:42]`
82
+ * - `(file:line)` e.g. `(src/foo.ts:42)`
83
+ *
84
+ * The path must contain a `.` (file extension hint) to keep prose like
85
+ * `[chapter:1]` from being misread as a code citation. Line is a
86
+ * 1+ digit positive integer.
87
+ *
88
+ * Anchored with bounded character classes — no `.*` and no nested
89
+ * alternations, defending against catastrophic backtracking.
90
+ */
91
+ const INLINE_CITATION_RE = /[\[(]([^\]\s)]+\.[A-Za-z0-9]+):(\d+)[\])]/g;
92
+ /**
93
+ * URL extraction. Matches `http://` or `https://` followed by host +
94
+ * optional path. Bounded path char class.
95
+ */
96
+ const URL_RE = /\bhttps?:\/\/[A-Za-z0-9.\-]+(?::\d+)?(?:\/[A-Za-z0-9._~:/?#@!$&'()*+,;=%\-]*)?/g;
97
+ /**
98
+ * Numeric-claim patterns. We accumulate matches from THREE shapes:
99
+ * - percentage: `42%`, `0.5%`, `100.0%`
100
+ * - currency: `$1`, `$1,000`, `$1.50`, `€42`, `£99.99` (any 1+)
101
+ * - bare number: `123`, `1,000`, `42.5` (≥3 significant digits)
102
+ *
103
+ * "Significant digits" for the bare-number rule means ≥3 digit chars
104
+ * total ignoring separators. `99` (2 digits) is NOT flagged; `100` is.
105
+ * This dodges the false-positive trap of years / common small numbers
106
+ * appearing in prose without a citation needing to back them up.
107
+ */
108
+ const PERCENT_RE = /\b\d+(?:\.\d+)?%/g;
109
+ const CURRENCY_RE = /[$€£¥₹]\s?\d{1,3}(?:,\d{3})*(?:\.\d+)?/g;
110
+ const BARE_NUMBER_RE = /\b\d{1,3}(?:,\d{3})+(?:\.\d+)?\b|\b\d{3,}(?:\.\d+)?\b/g;
111
+ /**
112
+ * Injection-echo fragments. These are markers we never expect a model
113
+ * to legitimately produce in its output; their appearance suggests
114
+ * upstream context (a user-supplied document, a tool result, a leaked
115
+ * system prompt) has bled into the generation surface.
116
+ *
117
+ * Kept literal-prefix-anchored to keep false-positives low. A model
118
+ * legitimately discussing prompt-injection education ("attackers might
119
+ * say `ignore previous instructions`") will trip the warning — the
120
+ * caller can downgrade via `allowed` post-hoc, but the violation must
121
+ * still surface so reviewers see it.
122
+ */
123
+ const INJECTION_PATTERNS = [
124
+ { id: 'system-reminder-tag', re: /<system-reminder\b/gi },
125
+ { id: 'system-tag', re: /<\|im_start\|>/g },
126
+ { id: 'endoftext-tag', re: /<\|endoftext\|>/g },
127
+ { id: 'inst-marker', re: /\[INST\]/g },
128
+ { id: 'inst-close', re: /\[\/INST\]/g },
129
+ { id: 'ignore-previous', re: /\b(?:important[^a-z]*)?ignore\s+(?:all\s+|the\s+)?previous\s+(?:instructions|prompts?)/gi },
130
+ { id: 'disregard-above', re: /\bdisregard\s+(?:all|prior|the)?\s*above/gi },
131
+ { id: 'anthropic-human-tag', re: /\n\s*Human:\s*/g },
132
+ { id: 'anthropic-assistant-tag', re: /\n\s*Assistant:\s*/g },
133
+ ];
134
+ // ---------------------------------------------------------------------------
135
+ // Public entry point
136
+ // ---------------------------------------------------------------------------
137
+ export function filterOutput(text, options = {}) {
138
+ if (typeof text !== 'string' || text.length === 0) {
139
+ return { allowed: true, violations: [] };
140
+ }
141
+ const violations = [];
142
+ // Check order is deterministic — locked by the spec. Each check
143
+ // pushes its findings to `violations` and never throws.
144
+ // 1. Citation verify.
145
+ pushCitationViolations(text, options, violations);
146
+ // 2. Secret scan. We always scan; redaction is gated by the flag.
147
+ const redact = options.redactSecrets !== false;
148
+ const secretMatches = scanForSecrets(text);
149
+ for (const match of secretMatches) {
150
+ violations.push({
151
+ kind: 'secret-leak',
152
+ location: lineColumnFor(text, match.offset),
153
+ detail: `Secret pattern '${match.pattern}' (${match.confidence}) detected`,
154
+ });
155
+ }
156
+ let redactedText;
157
+ if (redact && secretMatches.length > 0) {
158
+ const result = redactSecretsImpl(text);
159
+ redactedText = result.redacted;
160
+ }
161
+ // 3. Numeric claim guard. Skipped when allowNumericClaims=true.
162
+ if (options.allowNumericClaims !== true) {
163
+ pushNumericClaimViolations(text, violations);
164
+ }
165
+ // 4. Malicious URL.
166
+ pushUrlViolations(text, options.allowedURLs ?? [], violations);
167
+ // 5. Injection echo.
168
+ pushInjectionEchoViolations(text, violations);
169
+ return {
170
+ allowed: violations.length === 0,
171
+ redactedText,
172
+ violations,
173
+ };
174
+ }
175
+ function collectInlineCitations(text) {
176
+ const out = [];
177
+ for (const m of text.matchAll(INLINE_CITATION_RE)) {
178
+ if (m.index === undefined)
179
+ continue;
180
+ const path = m[1];
181
+ const lineStr = m[2];
182
+ if (path === undefined || lineStr === undefined)
183
+ continue;
184
+ const lineNum = Number.parseInt(lineStr, 10);
185
+ if (!Number.isFinite(lineNum) || lineNum <= 0)
186
+ continue;
187
+ out.push({
188
+ raw: m[0],
189
+ path,
190
+ line: lineNum,
191
+ offset: m.index,
192
+ });
193
+ }
194
+ return out;
195
+ }
196
+ function pushCitationViolations(text, options, violations) {
197
+ const inline = collectInlineCitations(text);
198
+ const explicit = options.citations ?? [];
199
+ // Nothing to check.
200
+ if (inline.length === 0 && explicit.length === 0)
201
+ return;
202
+ const root = options.workspaceRoot;
203
+ if (root === undefined) {
204
+ // Fail-closed: cannot verify without a root, but we still surface
205
+ // the citations so the caller sees them.
206
+ for (const cite of inline) {
207
+ violations.push({
208
+ kind: 'invalid-citation',
209
+ location: lineColumnFor(text, cite.offset),
210
+ detail: `Citation '${cite.raw}' cannot be verified (workspaceRoot missing)`,
211
+ });
212
+ }
213
+ for (const cite of explicit) {
214
+ violations.push({
215
+ kind: 'invalid-citation',
216
+ detail: `Citation '${cite.path}:${cite.line}' cannot be verified (workspaceRoot missing)`,
217
+ });
218
+ }
219
+ return;
220
+ }
221
+ // Canonicalise the workspace root once. realpath fail = pass-through;
222
+ // the containment check below will still reject any traversal.
223
+ let canonicalRoot;
224
+ try {
225
+ canonicalRoot = realpathSync.native(root);
226
+ }
227
+ catch {
228
+ canonicalRoot = resolve(root);
229
+ }
230
+ for (const cite of inline) {
231
+ if (!isCitationContained(cite.path, canonicalRoot)) {
232
+ violations.push({
233
+ kind: 'invalid-citation',
234
+ location: lineColumnFor(text, cite.offset),
235
+ detail: `Citation '${cite.raw}' resolves outside workspace`,
236
+ });
237
+ continue;
238
+ }
239
+ const abs = resolve(canonicalRoot, cite.path);
240
+ if (!existsSync(abs)) {
241
+ violations.push({
242
+ kind: 'invalid-citation',
243
+ location: lineColumnFor(text, cite.offset),
244
+ detail: `Citation '${cite.raw}' references missing file`,
245
+ });
246
+ }
247
+ }
248
+ for (const cite of explicit) {
249
+ if (!isCitationContained(cite.path, canonicalRoot)) {
250
+ violations.push({
251
+ kind: 'invalid-citation',
252
+ detail: `Citation '${cite.path}:${cite.line}' resolves outside workspace`,
253
+ });
254
+ continue;
255
+ }
256
+ const abs = resolve(canonicalRoot, cite.path);
257
+ if (!existsSync(abs)) {
258
+ violations.push({
259
+ kind: 'invalid-citation',
260
+ detail: `Citation '${cite.path}:${cite.line}' references missing file`,
261
+ });
262
+ }
263
+ }
264
+ }
265
+ function isCitationContained(citationPath, canonicalRoot) {
266
+ if (isAbsolute(citationPath))
267
+ return false; // absolute paths in citations are never trusted
268
+ // Reject `..` segments before any FS work (mirror path-security.ts step #1).
269
+ const segments = citationPath.split(/[/\\]/);
270
+ if (segments.some((seg) => seg === '..'))
271
+ return false;
272
+ // Resolve relative to the canonical root and reverify containment.
273
+ const target = resolve(canonicalRoot, citationPath);
274
+ const rel = relative(canonicalRoot, target);
275
+ if (!rel)
276
+ return true;
277
+ return !rel.startsWith('..');
278
+ }
279
+ function collectNumericHits(text) {
280
+ const out = [];
281
+ for (const re of [PERCENT_RE, CURRENCY_RE, BARE_NUMBER_RE]) {
282
+ for (const m of text.matchAll(re)) {
283
+ if (m.index === undefined)
284
+ continue;
285
+ out.push({ value: m[0], offset: m.index });
286
+ }
287
+ }
288
+ // Deduplicate overlapping hits — `100%` matches PERCENT_RE and the
289
+ // bare-number rule would not (anchored to no trailing `%`), but a
290
+ // currency `$100` could overlap a bare `100`. Sort by offset, keep
291
+ // the longest match at each anchor.
292
+ out.sort((a, b) => a.offset - b.offset || b.value.length - a.value.length);
293
+ const deduped = [];
294
+ let lastEnd = -1;
295
+ for (const hit of out) {
296
+ if (hit.offset < lastEnd)
297
+ continue;
298
+ deduped.push(hit);
299
+ lastEnd = hit.offset + hit.value.length;
300
+ }
301
+ return deduped;
302
+ }
303
+ function pushNumericClaimViolations(text, violations) {
304
+ const numbers = collectNumericHits(text);
305
+ if (numbers.length === 0)
306
+ return;
307
+ // Compute citation offsets ONCE — every inline citation in the text
308
+ // counts as anchorage for nearby numbers regardless of validity.
309
+ // (Validity is checked separately in pushCitationViolations.)
310
+ const citationOffsets = [];
311
+ for (const m of text.matchAll(INLINE_CITATION_RE)) {
312
+ if (m.index !== undefined)
313
+ citationOffsets.push(m.index);
314
+ }
315
+ for (const hit of numbers) {
316
+ if (!hasCitationWithin(hit.offset, hit.value.length, citationOffsets, 80)) {
317
+ violations.push({
318
+ kind: 'numeric-without-citation',
319
+ location: lineColumnFor(text, hit.offset),
320
+ detail: `Numeric claim '${hit.value}' has no citation within 80 chars`,
321
+ });
322
+ }
323
+ }
324
+ }
325
+ function hasCitationWithin(hitOffset, hitLength, citationOffsets, window) {
326
+ const lo = hitOffset - window;
327
+ const hi = hitOffset + hitLength + window;
328
+ for (const cOff of citationOffsets) {
329
+ if (cOff >= lo && cOff <= hi)
330
+ return true;
331
+ }
332
+ return false;
333
+ }
334
+ // ---------------------------------------------------------------------------
335
+ // Malicious URL
336
+ // ---------------------------------------------------------------------------
337
+ function pushUrlViolations(text, allowed, violations) {
338
+ const normalisedAllowed = allowed.map((u) => stripTrailingSlash(u.toLowerCase()));
339
+ for (const m of text.matchAll(URL_RE)) {
340
+ if (m.index === undefined)
341
+ continue;
342
+ const url = m[0];
343
+ if (!isUrlAllowed(url, normalisedAllowed)) {
344
+ violations.push({
345
+ kind: 'malicious-url',
346
+ location: lineColumnFor(text, m.index),
347
+ detail: `URL '${url}' is not in the allowlist`,
348
+ });
349
+ }
350
+ }
351
+ }
352
+ function stripTrailingSlash(s) {
353
+ return s.endsWith('/') ? s.slice(0, -1) : s;
354
+ }
355
+ function isUrlAllowed(url, allowlist) {
356
+ const lower = stripTrailingSlash(url.toLowerCase());
357
+ for (const allowed of allowlist) {
358
+ if (lower === allowed)
359
+ return true;
360
+ // Prefix-with-`/` match prevents `https://example.com.evil` from
361
+ // satisfying an `https://example.com` entry. We also accept the
362
+ // bare host without path.
363
+ if (lower.startsWith(`${allowed}/`))
364
+ return true;
365
+ if (lower.startsWith(`${allowed}?`))
366
+ return true;
367
+ if (lower.startsWith(`${allowed}#`))
368
+ return true;
369
+ }
370
+ return false;
371
+ }
372
+ // ---------------------------------------------------------------------------
373
+ // Injection echo
374
+ // ---------------------------------------------------------------------------
375
+ function pushInjectionEchoViolations(text, violations) {
376
+ for (const pattern of INJECTION_PATTERNS) {
377
+ // Clone each regex so concurrent calls do not share `lastIndex`.
378
+ const rx = new RegExp(pattern.re.source, pattern.re.flags);
379
+ for (const m of text.matchAll(rx)) {
380
+ if (m.index === undefined)
381
+ continue;
382
+ violations.push({
383
+ kind: 'injection-echo',
384
+ location: lineColumnFor(text, m.index),
385
+ detail: `Injection-echo pattern '${pattern.id}' matched (${truncateMatch(m[0])})`,
386
+ });
387
+ }
388
+ }
389
+ }
390
+ function truncateMatch(s) {
391
+ if (s.length <= 64)
392
+ return s;
393
+ return `${s.slice(0, 64)}...`;
394
+ }
395
+ // ---------------------------------------------------------------------------
396
+ // Helpers
397
+ // ---------------------------------------------------------------------------
398
+ /**
399
+ * Compute 1-based line + column for a 0-based byte (UTF-16 code unit)
400
+ * offset within `text`. Mirrors the helper in `secret-scanner.ts` but
401
+ * returns column too — the spec demands a `{ line, column }` shape.
402
+ */
403
+ function lineColumnFor(text, offset) {
404
+ let line = 1;
405
+ let column = 1;
406
+ const cap = Math.min(offset, text.length);
407
+ for (let i = 0; i < cap; i += 1) {
408
+ if (text.charCodeAt(i) === 10) {
409
+ line += 1;
410
+ column = 1;
411
+ }
412
+ else {
413
+ column += 1;
414
+ }
415
+ }
416
+ return { line, column };
417
+ }
418
+ //# sourceMappingURL=output-filter.js.map
@@ -0,0 +1,105 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ const PUGI_PREFIX = 'PUGI_';
4
+ function isPugiKey(key) {
5
+ return key.startsWith(PUGI_PREFIX);
6
+ }
7
+ function filterPugiVars(env) {
8
+ const out = {};
9
+ for (const [k, v] of Object.entries(env)) {
10
+ if (isPugiKey(k) && typeof v === 'string')
11
+ out[k] = v;
12
+ }
13
+ return out;
14
+ }
15
+ function isValidState(value) {
16
+ if (!value || typeof value !== 'object')
17
+ return false;
18
+ const v = value;
19
+ if (typeof v.cwd !== 'string')
20
+ return false;
21
+ if (!v.environment || typeof v.environment !== 'object' || Array.isArray(v.environment))
22
+ return false;
23
+ if (typeof v.updatedAt !== 'string')
24
+ return false;
25
+ if (v.personaSlug !== undefined && typeof v.personaSlug !== 'string')
26
+ return false;
27
+ for (const [k, entry] of Object.entries(v.environment)) {
28
+ if (typeof k !== 'string' || typeof entry !== 'string')
29
+ return false;
30
+ }
31
+ return true;
32
+ }
33
+ function warn(message) {
34
+ process.stderr.write(`[pugi:env-file] ${message}\n`);
35
+ }
36
+ export async function loadEnvState(opts) {
37
+ let raw;
38
+ try {
39
+ raw = await fs.readFile(opts.envFilePath, 'utf8');
40
+ }
41
+ catch (err) {
42
+ const code = err.code;
43
+ if (code === 'ENOENT')
44
+ return null;
45
+ warn(`failed to read ${opts.envFilePath}: ${err.message}`);
46
+ return null;
47
+ }
48
+ let parsed;
49
+ try {
50
+ parsed = JSON.parse(raw);
51
+ }
52
+ catch (err) {
53
+ warn(`malformed JSON at ${opts.envFilePath}: ${err.message}`);
54
+ return null;
55
+ }
56
+ if (!isValidState(parsed)) {
57
+ warn(`invalid env state shape at ${opts.envFilePath}`);
58
+ return null;
59
+ }
60
+ const state = {
61
+ cwd: parsed.cwd,
62
+ environment: filterPugiVars(parsed.environment),
63
+ updatedAt: parsed.updatedAt,
64
+ };
65
+ if (parsed.personaSlug !== undefined)
66
+ state.personaSlug = parsed.personaSlug;
67
+ return state;
68
+ }
69
+ export async function saveEnvState(state, opts) {
70
+ const dir = dirname(opts.envFilePath);
71
+ await fs.mkdir(dir, { recursive: true });
72
+ const finalState = {
73
+ cwd: state.cwd,
74
+ environment: filterPugiVars(state.environment ?? {}),
75
+ updatedAt: new Date().toISOString(),
76
+ };
77
+ if (state.personaSlug !== undefined)
78
+ finalState.personaSlug = state.personaSlug;
79
+ const payload = JSON.stringify(finalState, null, 2);
80
+ const tmpSuffix = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
81
+ const tmpPath = `${opts.envFilePath}.tmp-${tmpSuffix}`;
82
+ await fs.writeFile(tmpPath, payload, { encoding: 'utf8', mode: 0o600 });
83
+ try {
84
+ await fs.rename(tmpPath, opts.envFilePath);
85
+ }
86
+ catch (err) {
87
+ await fs.rm(tmpPath, { force: true }).catch(() => { });
88
+ throw err;
89
+ }
90
+ }
91
+ export function mergeIntoEnv(state, processEnv) {
92
+ const out = {};
93
+ if (state) {
94
+ for (const [k, v] of Object.entries(state.environment)) {
95
+ if (isPugiKey(k) && typeof v === 'string')
96
+ out[k] = v;
97
+ }
98
+ }
99
+ for (const [k, v] of Object.entries(processEnv)) {
100
+ if (v !== undefined)
101
+ out[k] = v;
102
+ }
103
+ return out;
104
+ }
105
+ //# sourceMappingURL=env-file.js.map
@@ -0,0 +1,140 @@
1
+ import { estimateTokens } from '../compact/token-counter.js';
2
+ export const TRUNCATION_MARKER = '\n[…truncated…]\n';
3
+ const DEFAULT_PRIORITY = 0.5;
4
+ const SHAVE_STEP_RATIO = 0.1;
5
+ const SHAVE_SAFETY_BOUND = 10_000;
6
+ function validateInputs(sections) {
7
+ const seen = new Set();
8
+ for (const s of sections) {
9
+ if (typeof s.name !== 'string' || s.name.length === 0) {
10
+ throw new TypeError('section name must be a non-empty string');
11
+ }
12
+ if (seen.has(s.name)) {
13
+ throw new TypeError(`duplicate section name: ${s.name}`);
14
+ }
15
+ seen.add(s.name);
16
+ if (!Number.isFinite(s.budget) || s.budget < 0) {
17
+ throw new RangeError(`section ${s.name}: budget must be a finite number >= 0`);
18
+ }
19
+ if (s.priority !== undefined) {
20
+ if (!Number.isFinite(s.priority) || s.priority < 0 || s.priority > 1) {
21
+ throw new RangeError(`section ${s.name}: priority must be in [0, 1]`);
22
+ }
23
+ }
24
+ }
25
+ }
26
+ function buildTruncated(content, keepChars, strategy) {
27
+ if (keepChars <= 0)
28
+ return '';
29
+ if (strategy === 'head') {
30
+ const start = Math.max(0, content.length - keepChars);
31
+ return TRUNCATION_MARKER + content.slice(start);
32
+ }
33
+ if (strategy === 'tail') {
34
+ return content.slice(0, keepChars) + TRUNCATION_MARKER;
35
+ }
36
+ const headChars = Math.floor(keepChars / 2);
37
+ const tailChars = keepChars - headChars;
38
+ const head = content.slice(0, headChars);
39
+ const tailStart = Math.max(headChars, content.length - tailChars);
40
+ const tail = content.slice(tailStart);
41
+ return head + TRUNCATION_MARKER + tail;
42
+ }
43
+ function truncateSection(name, content, budget, strategy) {
44
+ const originalTokens = estimateTokens(content);
45
+ if (originalTokens <= budget) {
46
+ return { content, tokens: originalTokens, truncated: false, droppedTokens: 0 };
47
+ }
48
+ if (strategy === 'none') {
49
+ throw new RangeError(`section ${name}: content (${originalTokens} tokens) exceeds budget (${budget}) and strategy='none'`);
50
+ }
51
+ if (budget === 0) {
52
+ return { content: '', tokens: 0, truncated: true, droppedTokens: originalTokens };
53
+ }
54
+ const markerTokens = estimateTokens(TRUNCATION_MARKER);
55
+ if (markerTokens >= budget) {
56
+ return { content: '', tokens: 0, truncated: true, droppedTokens: originalTokens };
57
+ }
58
+ const available = budget - markerTokens;
59
+ let keepChars = Math.max(0, available * 4);
60
+ let next = buildTruncated(content, keepChars, strategy);
61
+ let tokens = estimateTokens(next);
62
+ while (tokens > budget && keepChars > 0) {
63
+ keepChars = Math.max(0, keepChars - 4);
64
+ if (keepChars === 0) {
65
+ next = '';
66
+ tokens = 0;
67
+ break;
68
+ }
69
+ next = buildTruncated(content, keepChars, strategy);
70
+ tokens = estimateTokens(next);
71
+ }
72
+ return {
73
+ content: next,
74
+ tokens,
75
+ truncated: true,
76
+ droppedTokens: originalTokens - tokens,
77
+ };
78
+ }
79
+ export function applySectionBudgets(sections, options) {
80
+ validateInputs(sections);
81
+ const sumBudgets = sections.reduce((sum, s) => sum + s.budget, 0);
82
+ const totalBudget = options?.totalBudget ?? sumBudgets;
83
+ const working = sections.map((s) => {
84
+ const r = truncateSection(s.name, s.content, s.budget, s.strategy);
85
+ return {
86
+ input: s,
87
+ currentBudget: s.budget,
88
+ content: r.content,
89
+ tokens: r.tokens,
90
+ truncated: r.truncated,
91
+ droppedTokens: r.droppedTokens,
92
+ };
93
+ });
94
+ let totalTokens = working.reduce((sum, w) => sum + w.tokens, 0);
95
+ if (totalTokens > totalBudget) {
96
+ const shrinkable = working
97
+ .map((w, idx) => ({ w, idx, priority: w.input.priority ?? DEFAULT_PRIORITY }))
98
+ .filter((x) => x.w.input.strategy !== 'none')
99
+ .sort((a, b) => a.priority - b.priority || a.idx - b.idx);
100
+ let safety = 0;
101
+ while (totalTokens > totalBudget && safety < SHAVE_SAFETY_BOUND) {
102
+ safety++;
103
+ let shaved = false;
104
+ for (const x of shrinkable) {
105
+ if (x.w.currentBudget === 0 && x.w.tokens === 0)
106
+ continue;
107
+ const step = Math.max(1, Math.ceil(x.w.input.budget * SHAVE_STEP_RATIO));
108
+ const newBudget = Math.max(0, x.w.currentBudget - step);
109
+ if (newBudget === x.w.currentBudget)
110
+ continue;
111
+ x.w.currentBudget = newBudget;
112
+ const r = truncateSection(x.w.input.name, x.w.input.content, newBudget, x.w.input.strategy);
113
+ const delta = x.w.tokens - r.tokens;
114
+ x.w.content = r.content;
115
+ x.w.tokens = r.tokens;
116
+ x.w.truncated = r.truncated;
117
+ x.w.droppedTokens = r.droppedTokens;
118
+ totalTokens -= delta;
119
+ shaved = true;
120
+ break;
121
+ }
122
+ if (!shaved)
123
+ break;
124
+ }
125
+ }
126
+ const finalSections = working.map((w) => ({
127
+ name: w.input.name,
128
+ content: w.content,
129
+ tokens: w.tokens,
130
+ truncated: w.truncated,
131
+ droppedTokens: w.droppedTokens,
132
+ }));
133
+ return {
134
+ sections: finalSections,
135
+ totalTokens,
136
+ totalBudget,
137
+ overBudget: totalTokens > totalBudget,
138
+ };
139
+ }
140
+ //# sourceMappingURL=section-budgets.js.map