@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
@@ -1,17 +1,17 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * Office-hours forcing questions + plan-review modals - Sprint α6.3.
3
+ * Office-hours forcing questions + plan-review modals - Sprint .
4
4
  *
5
5
  * Two Ink components feed by the parsed `<pugi-ask>` / `<pugi-plan-review>`
6
6
  * tag records the session module extracts from persona output.
7
7
  *
8
- * - <AskModal />: numbered options (1-4) + optional "Other" custom-input
9
- * fallback. Operator types `1`, `2`, `3`, `4`, or `o <free text>`.
8
+ * - <AskModal />: numbered options (1-4) + optional "Other" custom-input
9
+ * fallback. Operator types `1`, `2`, `3`, `4`, or `o <free text>`.
10
10
  *
11
- * - <PlanReviewModal />: numbered steps + optional risk callout +
12
- * three-way verdict `[a] approve · [m] modify · [c] cancel`.
13
- * Operator types `a`, `m`, or `c`. `m` opens a free-text editor;
14
- * the operator's edited text is returned verbatim to the session.
11
+ * - <PlanReviewModal />: numbered steps + optional risk callout +
12
+ * three-way verdict `[a] approve · [m] modify · [c] cancel`.
13
+ * Operator types `a`, `m`, or `c`. `m` opens a free-text editor;
14
+ * the operator's edited text is returned verbatim to the session.
15
15
  *
16
16
  * Both components are PURE in the Ink sense — they read props + own
17
17
  * useState for the input buffer, and emit one final `onResolve` callback
@@ -85,7 +85,7 @@ export function AskModal(props) {
85
85
  setBuffer((prev) => prev + input);
86
86
  }
87
87
  }, { isActive: props.inert !== true });
88
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "yellow", children: '? ' }), _jsx(Text, { bold: true, children: 'Need your call before I continue' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: props.tag.question }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [props.tag.options.map((opt, idx) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: ` ${idx + 1}. ` }), _jsx(Text, { children: opt.label })] }), opt.desc ? (_jsx(Box, { marginLeft: 5, children: _jsx(Text, { dimColor: true, children: opt.desc }) })) : null] }, opt.value))), _jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: ` ${props.tag.options.length + 1}. ` }), _jsx(Text, { dimColor: true, children: 'Other (type a custom answer)' })] })] }), mode === 'pick' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `Press 1-${props.tag.options.length + 1} to choose. Esc cancels.` }) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: '> ' }), _jsx(Text, { children: buffer }), _jsx(Text, { inverse: true, children: ' ' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: 'Type your custom answer. Enter submits. Esc cancels.' }) })] }))] }));
88
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "yellow", children: '? ' }), _jsx(Text, { bold: true, children: 'Need your call before I continue' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: props.tag.question }) }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [props.tag.options.map((opt, idx) => (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", bold: true, children: ` ${idx + 1}. ` }), _jsx(Text, { children: opt.label })] }), opt.desc ? (_jsx(Box, { marginLeft: 5, children: _jsx(Text, { dimColor: true, children: opt.desc }) })) : null] }, opt.value))), _jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", bold: true, children: ` ${props.tag.options.length + 1}. ` }), _jsx(Text, { dimColor: true, children: 'Other (type a custom answer)' })] })] }), mode === 'pick' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `Press 1-${props.tag.options.length + 1} to choose. Esc cancels.` }) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '> ' }), _jsx(Text, { children: buffer }), _jsx(Text, { inverse: true, children: ' ' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: 'Type your custom answer. Enter submits. Esc cancels.' }) })] }))] }));
89
89
  }
90
90
  export function PlanReviewModal(props) {
91
91
  const [mode, setMode] = useState('pick');
@@ -130,10 +130,10 @@ export function PlanReviewModal(props) {
130
130
  setBuffer((prev) => prev + input);
131
131
  }
132
132
  }, { isActive: props.inert !== true });
133
- return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "magenta", children: '? ' }), _jsx(Text, { bold: true, children: 'Plan review - approve before I execute' })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: 'Steps:' }), props.tag.steps.map((step, idx) => (_jsxs(Box, { children: [_jsx(Text, { color: "cyan", bold: true, children: ` ${idx + 1}. ` }), _jsx(Text, { children: step.text })] }, `step-${idx}`)))] }), props.tag.risk ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "red", children: 'Risk:' }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { children: props.tag.risk }) })] })) : null, mode === 'pick' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: ' [a] approve ' }), _jsx(Text, { color: "yellow", bold: true, children: '[m] modify ' }), _jsx(Text, { color: "red", bold: true, children: '[c] cancel' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: 'Press a, m, or c. Esc cancels.' }) })] })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: 'modify > ' }), _jsx(Text, { children: buffer }), _jsx(Text, { inverse: true, children: ' ' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: 'Type the revision. Enter submits. Esc cancels.' }) })] }))] }));
133
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "magenta", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "magenta", children: '? ' }), _jsx(Text, { bold: true, children: 'Plan review - approve before I execute' })] }), _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: 'Steps:' }), props.tag.steps.map((step, idx) => (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", bold: true, children: ` ${idx + 1}. ` }), _jsx(Text, { children: step.text })] }, `step-${idx}`)))] }), props.tag.risk ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { bold: true, color: "red", children: 'Risk:' }), _jsx(Box, { marginLeft: 2, children: _jsx(Text, { children: props.tag.risk }) })] })) : null, mode === 'pick' ? (_jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "green", bold: true, children: ' [a] approve ' }), _jsx(Text, { color: "yellow", bold: true, children: '[m] modify ' }), _jsx(Text, { color: "red", bold: true, children: '[c] cancel' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: 'Press a, m, or c. Esc cancels.' }) })] })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "yellow", children: 'modify > ' }), _jsx(Text, { children: buffer }), _jsx(Text, { inverse: true, children: ' ' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: 'Type the revision. Enter submits. Esc cancels.' }) })] }))] }));
134
134
  }
135
135
  /* ------------------------------------------------------------------ */
136
- /* Verdict serialisation */
136
+ /* Verdict serialisation */
137
137
  /* ------------------------------------------------------------------ */
138
138
  /**
139
139
  * Encode an ask-modal verdict into the literal string the session
@@ -142,14 +142,14 @@ export function PlanReviewModal(props) {
142
142
  * a side channel.
143
143
  *
144
144
  * Examples:
145
- * { value: 'vercel' } -> "[ASK-RESPONSE:vercel]"
146
- * { value: '', customInput: 'gcp'} -> "[ASK-RESPONSE:other] gcp"
147
- * { cancelled: true } -> "[ASK-RESPONSE:cancelled]"
145
+ * { value: 'vercel' } -> "[ASK-RESPONSE:vercel]"
146
+ * { value: '', customInput: 'gcp'} -> "[ASK-RESPONSE:other] gcp"
147
+ * { cancelled: true } -> "[ASK-RESPONSE:cancelled]"
148
148
  *
149
149
  * customInput is stripped of any leading `[ASK-RESPONSE:...]` /
150
150
  * `[PLAN-VERDICT:...]` pattern so a forged operator prefix cannot be
151
151
  * read as a different verdict by a prefix-greedy persona (Claude
152
- * triple-review P1, PR #375).
152
+ * triple-review P1, PR).
153
153
  */
154
154
  export function encodeAskVerdict(verdict) {
155
155
  if (verdict.cancelled)
@@ -0,0 +1,203 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * AskUserQuestionPrompt — Ink modal for the structured tool grammar
4
+ * (, ).
5
+ *
6
+ * Renders the four required elements of an standard clarifier:
7
+ * 1. A short "header" chip at the top (e.g. "Auth method"). Max 12
8
+ * chars by schema, fits one line at the standard 80-col REPL width.
9
+ * 2. The full question prose (must end "?"). Word-wrapped by Ink.
10
+ * 3. The 2-4 options as a selectable list with j/k navigation. Each
11
+ * option shows the `label` (bright) + `description` (dim).
12
+ * 4. An auto-appended "Other" row that the operator can pick to type
13
+ * a custom answer. The model NEVER emits this — the UI owns it.
14
+ *
15
+ * Multi-select mode: when `multiSelect=true`, space toggles the
16
+ * current row, Enter submits the toggled set. Selected rows are
17
+ * marked with a leading checkbox glyph. Single-select mode: Enter
18
+ * commits the highlighted row immediately.
19
+ *
20
+ * Resolver contract: `onResolve` receives either an `answers: string[]`
21
+ * (one or more picked labels), a `customInput: string` (Other path),
22
+ * or `cancelled: true` (Esc). Mirrors AskModal so the REPL wiring
23
+ * stays uniform.
24
+ *
25
+ * Brand voice gate: ASCII glyphs only. No em-dashes, no banned brand
26
+ * words. The copy is power-word neutral so a localised variant lands
27
+ * cleanly later.
28
+ */
29
+ import { useState } from 'react';
30
+ import { Box, Text, useInput } from 'ink';
31
+ export function AskUserQuestionPrompt(props) {
32
+ const multiSelect = props.multiSelect === true;
33
+ const [mode, setMode] = useState('pick');
34
+ const [cursor, setCursor] = useState(0);
35
+ // Used in multi-select mode: indices in `options` that the operator
36
+ // has toggled. Order preserved so the resolved answer list reflects
37
+ // selection order (the model's downstream reasoning often weights
38
+ // earlier picks higher — preserving order is cheap).
39
+ const [picked, setPicked] = useState([]);
40
+ const [buffer, setBuffer] = useState('');
41
+ const otherIndex = props.options.length; // 0-indexed slot for "Other"
42
+ const totalRows = props.options.length + 1; // options + Other
43
+ useInput((input, key) => {
44
+ // Esc cancels the modal in either mode.
45
+ if (key.escape) {
46
+ props.onResolve({ cancelled: true });
47
+ return;
48
+ }
49
+ if (mode === 'pick') {
50
+ // Numeric hotkeys: 1..N selects the matching option directly
51
+ // (single-select), or toggles it (multi-select). Convenience
52
+ // shortcut so a keyboard-only user does not need to j/k walk
53
+ // through 4 rows. Out-of-range keys fall through.
54
+ const numeric = Number.parseInt(input, 10);
55
+ if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= totalRows) {
56
+ const row = numeric - 1;
57
+ if (row === otherIndex) {
58
+ setMode('custom');
59
+ setBuffer('');
60
+ return;
61
+ }
62
+ if (multiSelect) {
63
+ togglePick(row);
64
+ return;
65
+ }
66
+ commitSinglePick(row);
67
+ return;
68
+ }
69
+ // Vim-style navigation: j down, k up. Arrow keys also work.
70
+ if (input === 'j' || key.downArrow) {
71
+ setCursor((c) => (c + 1) % totalRows);
72
+ return;
73
+ }
74
+ if (input === 'k' || key.upArrow) {
75
+ setCursor((c) => (c - 1 + totalRows) % totalRows);
76
+ return;
77
+ }
78
+ // 'o' hotkey: jump straight to Other (mirrors AskModal).
79
+ if (input === 'o' || input === 'O') {
80
+ setMode('custom');
81
+ setBuffer('');
82
+ return;
83
+ }
84
+ if (key.return) {
85
+ if (cursor === otherIndex) {
86
+ setMode('custom');
87
+ setBuffer('');
88
+ return;
89
+ }
90
+ if (multiSelect) {
91
+ // Enter in multi-select mode COMMITS the toggled set. If the
92
+ // current row is not yet toggled, fold it in first so the
93
+ // operator does not have to press space+enter for a single pick.
94
+ const finalPicks = picked.includes(cursor) ? picked : [...picked, cursor];
95
+ if (finalPicks.length === 0) {
96
+ // No picks + Enter = ignore; the footer hint nudges them.
97
+ return;
98
+ }
99
+ const answers = finalPicks.map((i) => props.options[i].label);
100
+ props.onResolve({ answers, cancelled: false });
101
+ return;
102
+ }
103
+ commitSinglePick(cursor);
104
+ return;
105
+ }
106
+ // Space toggles the current row in multi-select mode. Ignored in
107
+ // single-select (a single space could otherwise leak into a buffer).
108
+ if (multiSelect && input === ' ') {
109
+ togglePick(cursor);
110
+ return;
111
+ }
112
+ return;
113
+ }
114
+ // Custom-input mode: line editor.
115
+ if (key.return) {
116
+ if (buffer.trim().length === 0) {
117
+ // Empty buffer + Enter = bounce back to pick mode (mirrors AskModal).
118
+ setMode('pick');
119
+ setBuffer('');
120
+ return;
121
+ }
122
+ props.onResolve({
123
+ customInput: buffer.trim(),
124
+ cancelled: false,
125
+ });
126
+ return;
127
+ }
128
+ if (key.backspace || key.delete) {
129
+ setBuffer((prev) => prev.slice(0, -1));
130
+ return;
131
+ }
132
+ if (input && !key.meta && !key.ctrl) {
133
+ setBuffer((prev) => prev + input);
134
+ }
135
+ }, { isActive: props.inert !== true });
136
+ function togglePick(row) {
137
+ setPicked((prev) => prev.includes(row) ? prev.filter((i) => i !== row) : [...prev, row]);
138
+ }
139
+ function commitSinglePick(row) {
140
+ if (row < 0 || row >= props.options.length)
141
+ return;
142
+ const opt = props.options[row];
143
+ if (!opt)
144
+ return;
145
+ props.onResolve({ answers: [opt.label], cancelled: false });
146
+ }
147
+ // CC preview pattern: any option carrying `preview` flips к side-by-side
148
+ // layout — options column on the left, preview pane on the right.
149
+ // The currently-cursored option's preview is shown; rows без preview
150
+ // simply render an empty pane. Multi-select previews remain disabled
151
+ // by schema (validated в core/ask-user/question.ts) since side-by-side
152
+ // diffing across N picked rows is ambiguous UX.
153
+ const hasAnyPreview = props.options.some((opt) => typeof opt.preview === 'string' && opt.preview.length > 0);
154
+ const focusedPreview = hasAnyPreview && mode === 'pick' && cursor < props.options.length
155
+ ? props.options[cursor]?.preview ?? ''
156
+ : '';
157
+ const optionsColumn = (_jsxs(Box, { flexDirection: "column", marginTop: 1, flexBasis: hasAnyPreview ? '40%' : '100%', children: [props.options.map((opt, idx) => {
158
+ const isCursor = mode === 'pick' && cursor === idx;
159
+ const isPicked = multiSelect && picked.includes(idx);
160
+ const marker = multiSelect ? (isPicked ? '[x]' : '[ ]') : `${idx + 1}.`;
161
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: isCursor ? 'cyan' : '#3da9fc', bold: true, children: isCursor ? '> ' : ' ' }), _jsx(Text, { color: "#3da9fc", bold: true, children: `${marker} ` }), _jsx(Text, { bold: isCursor, children: opt.label })] }), _jsx(Box, { marginLeft: multiSelect ? 7 : 5, children: _jsx(Text, { dimColor: true, children: opt.description }) })] }, `${idx}-${opt.label}`));
162
+ }), _jsxs(Box, { children: [_jsx(Text, { color: mode === 'pick' && cursor === otherIndex ? 'cyan' : '#3da9fc', bold: true, children: mode === 'pick' && cursor === otherIndex ? '> ' : ' ' }), _jsx(Text, { color: "#3da9fc", bold: true, children: `${multiSelect ? '[*]' : `${totalRows}.`} ` }), _jsx(Text, { dimColor: true, children: 'Other (type a custom answer)' })] })] }));
163
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "yellow", children: '? ' }), _jsx(Text, { inverse: true, bold: true, color: "yellow", children: ` ${props.header} ` }), _jsx(Text, { bold: true, children: ' Need your call before I continue' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: props.question }) }), hasAnyPreview ? (_jsxs(Box, { flexDirection: "row", marginTop: 1, children: [optionsColumn, _jsx(Box, { flexDirection: "column", flexBasis: "60%", marginLeft: 2, paddingX: 1, borderStyle: "single", borderColor: "gray", children: focusedPreview.length > 0 ? (focusedPreview.split('\n').map((line, i) => (_jsx(Text, { children: line || ' ' }, `pv-${i}`)))) : (_jsx(Text, { dimColor: true, italic: true, children: '(no preview)' })) })] })) : (optionsColumn), mode === 'pick' ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: multiSelect
164
+ ? `j/k navigate. Space toggles. Enter commits. Esc cancels.`
165
+ : `1-${totalRows} or j/k navigate. Enter commits. Esc cancels.` }) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '> ' }), _jsx(Text, { children: buffer }), _jsx(Text, { inverse: true, children: ' ' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: 'Type your custom answer. Enter submits. Esc cancels.' }) })] }))] }));
166
+ }
167
+ /**
168
+ * Encode the prompt verdict into the literal turn-injection string. The
169
+ * persona prompt teaches the model to recognise this prefix; mirrors
170
+ * AskModal's `encodeAskVerdict` for consistency.
171
+ *
172
+ * Examples:
173
+ * { answers: ['Vercel'] } → "[ASK-USER-QUESTION:answered] Vercel"
174
+ * { answers: ['a', 'b'] } → "[ASK-USER-QUESTION:answered] a, b"
175
+ * { customInput: 'gcp' } → "[ASK-USER-QUESTION:other] gcp"
176
+ * { cancelled: true } → "[ASK-USER-QUESTION:cancelled]"
177
+ */
178
+ export function encodeAskUserQuestionVerdict(verdict) {
179
+ if (verdict.cancelled)
180
+ return '[ASK-USER-QUESTION:cancelled]';
181
+ if (verdict.answers && verdict.answers.length > 0) {
182
+ return `[ASK-USER-QUESTION:answered] ${verdict.answers.join(', ')}`;
183
+ }
184
+ if (verdict.customInput && verdict.customInput.trim().length > 0) {
185
+ // Strip any forged verdict header (mirrors AskModal sanitiser).
186
+ const cleaned = sanitiseVerdictText(verdict.customInput);
187
+ if (cleaned.length > 0) {
188
+ return `[ASK-USER-QUESTION:other] ${cleaned}`;
189
+ }
190
+ }
191
+ return '[ASK-USER-QUESTION:cancelled]';
192
+ }
193
+ function sanitiseVerdictText(raw) {
194
+ let cleaned = raw;
195
+ for (let i = 0; i < raw.length + 4; i += 1) {
196
+ const stripped = cleaned.replace(/^\s*\[(?:ASK-RESPONSE|PLAN-VERDICT|ASK-USER-QUESTION):[^\]]*\]\s*/u, '');
197
+ if (stripped === cleaned)
198
+ break;
199
+ cleaned = stripped;
200
+ }
201
+ return cleaned.trim();
202
+ }
203
+ //# sourceMappingURL=ask-user-question-prompt.js.map
@@ -0,0 +1,81 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Visible boundary marker for the conversation pane.
4
+ *
5
+ * Rendered inline in the transcript when the replay walker hits a
6
+ * `compaction` event. Conveys three facts at a glance:
7
+ *
8
+ * 1. A compaction happened (separator line).
9
+ * 2. How many turns were folded into the summary.
10
+ * 3. Whether it was manual or auto-triggered.
11
+ *
12
+ * The separator line uses U+2500 (BOX DRAWINGS LIGHT HORIZONTAL) so
13
+ * the visual weight matches the rest of the Ink chrome. Dim ink-color
14
+ * `gray` keeps the marker subdued — it is a navigation aid, not the
15
+ * conversation itself.
16
+ *
17
+ * Width-aware: when stdout columns are known we render the dashes to
18
+ * fill the line minus the centred label; on unknown width we fall
19
+ * back to a fixed 64-dash pad. The fallback is wide enough for any
20
+ * realistic terminal and narrow enough to not wrap on small ones.
21
+ */
22
+ import { Box, Text } from 'ink';
23
+ const FALLBACK_COLUMNS = 80;
24
+ /**
25
+ * Render the boundary line. The wrapping `<Box>` keeps the marker on
26
+ * its own row even when the surrounding flex container packs siblings.
27
+ */
28
+ export function CompactBanner(props) {
29
+ const columns = props.columns && props.columns > 20 ? props.columns : FALLBACK_COLUMNS;
30
+ const label = buildLabel(props);
31
+ const dashesEach = Math.max(3, Math.floor((columns - label.length - 2) / 2));
32
+ const dashes = '─'.repeat(dashesEach);
33
+ return (_jsx(Box, { children: _jsx(Text, { color: "gray", children: `${dashes} ${label} ${dashes}` }) }));
34
+ }
35
+ /**
36
+ * Build the centred label. Examples:
37
+ * "context compacted (12 turns → 1 summary, auto)"
38
+ * "context compacted (4 turns → 1 summary, manual · ~3.2k tokens)"
39
+ * "context compacted (12 turns → summary at 14:32, auto)"
40
+ * "context compacted (12 turns → 1 summary, manual) · 6 turns ago"
41
+ */
42
+ export function buildLabel(props) {
43
+ const trigger = props.trigger === 'auto' ? 'auto' : 'manual';
44
+ const turns = `${props.turnsBefore} ${props.turnsBefore === 1 ? 'turn' : 'turns'}`;
45
+ const summarySlot = typeof props.summaryAtEpochMs === 'number' && props.summaryAtEpochMs > 0
46
+ ? `summary at ${formatClock(props.summaryAtEpochMs)}`
47
+ : '1 summary';
48
+ const tokens = props.summaryTokenCount && props.summaryTokenCount > 0
49
+ ? ` · ~${formatTokens(props.summaryTokenCount)} tokens`
50
+ : '';
51
+ const base = `context compacted (${turns} → ${summarySlot}, ${trigger}${tokens})`;
52
+ // L29 history-replay suffix. Only render when the boundary has live
53
+ // turns following it — a fresh in-REPL `/compact` lands with
54
+ // `turnsAgo === 0` and we keep the label compact.
55
+ if (typeof props.turnsAgo === 'number' && props.turnsAgo > 0) {
56
+ const ago = `${props.turnsAgo} ${props.turnsAgo === 1 ? 'turn' : 'turns'} ago`;
57
+ return `${base} · ${ago}`;
58
+ }
59
+ return base;
60
+ }
61
+ /** Format token counts like 1234 → 1.2k, 950 → 950. */
62
+ function formatTokens(n) {
63
+ if (n < 1000)
64
+ return `${n}`;
65
+ return `${(n / 1000).toFixed(1)}k`;
66
+ }
67
+ /**
68
+ * Render an epoch-ms instant as a deterministic `HH:MM` clock in the
69
+ * operator's local timezone. Used only inside the centred label so a
70
+ * boundary that landed mid-session reads `summary at 14:32` instead of
71
+ * the generic `1 summary`. Pure (no side effects), constant-time.
72
+ */
73
+ function formatClock(epochMs) {
74
+ const d = new Date(epochMs);
75
+ if (Number.isNaN(d.getTime()))
76
+ return '--:--';
77
+ const hh = d.getHours().toString().padStart(2, '0');
78
+ const mm = d.getMinutes().toString().padStart(2, '0');
79
+ return `${hh}:${mm}`;
80
+ }
81
+ //# sourceMappingURL=compact-banner.js.map
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { MarkdownRender } from './markdown-render.js';
4
+ import { CompactBanner } from './compact-banner.js';
4
5
  const HUE_COLOR_BY_SLUG = {
5
- // Mira (Pug) - coordinator
6
+ // Pugi (Pug) - coordinator
6
7
  main: 'cyan',
7
8
  // Olivia (Honeybee) - release
8
9
  pm: 'yellow',
@@ -36,28 +37,101 @@ function PaneHeader({ count }) {
36
37
  function ConversationRow({ row, personaNames, }) {
37
38
  switch (row.source) {
38
39
  case 'operator':
39
- return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: '› ' }), _jsx(Text, { children: row.text })] }));
40
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "#3da9fc", children: '› ' }), _jsx(Text, { children: row.text })] }));
40
41
  case 'system':
41
42
  return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: '· ' }), _jsx(Text, { dimColor: true, children: row.text })] }));
43
+ case 'compact-boundary': {
44
+ // L29 : structured render. When the row carries the
45
+ // `compaction` payload, drive the Ink banner directly so the
46
+ // operator sees a width-aware gray separator with the canonical
47
+ // label. Fallback: a legacy boundary missing the structured
48
+ // payload (older replay events, hand-built rows) renders through
49
+ // the dim text path so the operator still sees the marker.
50
+ if (row.compaction) {
51
+ return (_jsx(CompactBanner, { turnsBefore: row.compaction.turnsBefore, trigger: row.compaction.trigger, summaryTokenCount: row.compaction.summaryTokenCount, turnsAgo: row.compaction.turnsAgo, summaryAtEpochMs: row.timestampEpochMs, columns: process.stdout?.columns }));
52
+ }
53
+ return (_jsx(Box, { children: _jsx(Text, { dimColor: true, children: row.text }) }));
54
+ }
42
55
  case 'persona': {
43
56
  const slug = row.personaSlug ?? '';
44
57
  const color = HUE_COLOR_BY_SLUG[slug] ?? 'white';
45
58
  const displayName = personaNames?.get(slug) ?? slug;
46
- // α6.12: persona bodies travel through MarkdownRender so code
47
- // fences, headings, and inline accents land correctly. A row that
48
- // carries no Markdown syntax renders as plain text under the same
49
- // path (the parser falls through to a single paragraph span), so
50
- // there is no regression for the simple "Mira shipped." baseline.
51
- const containsMarkdown = looksLikeMarkdown(row.text);
52
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: color, bold: true, children: `▸ ${displayName} ` }), containsMarkdown ? null : _jsx(Text, { children: row.text })] }), containsMarkdown ? (_jsx(Box, { marginLeft: 2, children: _jsx(MarkdownRender, { source: row.text }) })) : null] }));
59
+ // CEO live dogfood: a body that started with a
60
+ // Cyrillic letter (e.g. "Принял.") rendered as "PugiПринял." on
61
+ // the same line because the persona label sat at zero margin and
62
+ // the body fell flush behind it. The label is bold-coloured chrome,
63
+ // not text the operator is meant to read as a sentence stem.
64
+ // Split body to its own row and indent two columns so the
65
+ // transcript reads:
66
+ //
67
+ // ▸ Pugi
68
+ // Принял.
69
+ //
70
+ // matching the the upstream tool / Codex / Gemini baseline visually +
71
+ // preventing the "PugiПринял" glue regardless of the body's
72
+ // leading character.
73
+ //
74
+ // The render also strips a leading identity-intro phrase as a
75
+ // defense-in-depth complement to the backend output gate — when
76
+ // turnIndex > 1, the operator must NOT see "Я Pugi - твой
77
+ // инженерный напарник." opening every reply. This is belt-and-
78
+ // braces; the prompt + output-gate already block it upstream.
79
+ const stripped = stripLeadingIdentityIntro(row.text);
80
+ const containsMarkdown = looksLikeMarkdown(stripped);
81
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsx(Text, { color: color, bold: true, children: `▸ ${displayName}` }) }), containsMarkdown ? (_jsx(Box, { marginLeft: 2, children: _jsx(MarkdownRender, { source: stripped }) })) : (_jsx(Box, { marginLeft: 2, children: _jsx(Text, { children: stripped }) }))] }));
53
82
  }
54
83
  }
55
84
  }
85
+ /**
86
+ * Strip a leading identity-intro line ("I'm Pugi — your engineering
87
+ * copilot..." / "Я Pugi — твой инженерный напарник...") from the
88
+ * transcript body as a defense-in-depth complement to the backend
89
+ * output gate. The prompt + gate already block this upstream; the
90
+ * render guards against the rare case where a legacy model release
91
+ * sneaks the intro past both layers.
92
+ *
93
+ * The strip is intentionally conservative — we only remove the
94
+ * canonical phrasings the prompt teaches Pugi to emit. Any other
95
+ * prose containing the word "Pugi" passes through unchanged.
96
+ *
97
+ * Exported for spec.
98
+ */
99
+ export function stripLeadingIdentityIntro(text) {
100
+ if (!text || text.length === 0)
101
+ return text;
102
+ // Try each canonical intro shape in turn (RU + EN). We strip only
103
+ // the LEADING occurrence; a mid-body mention is intentional citation
104
+ // (e.g. "the Pugi codename..." in an explainer turn).
105
+ const introPatterns = [
106
+ // RU canonical — both masculine ("напарник/напарника/напарнику") and
107
+ // feminine ("напарница/напарницей/напарнице") declensions land on this
108
+ // strip. The prior pattern `напарни[ккк][аеу]?` was a character-class
109
+ // typo (three `к`s collapse to one) and accepted no feminine form, so
110
+ // a Pugi reply opening "Я Pugi — твоя инженерная напарница…" leaked
111
+ // past the strip. P1 reviewer fix PR .
112
+ /^(?:Я\s+Pugi\s*[—–-]\s*тв(?:ой|ё|оя)\s+инженерн[аыяий][хйеомя]*\s+напарни(?:к[аеу]?|ц(?:ей|а|ы|е|у))[.,!]?\s*)/u,
113
+ /^(?:Я\s+Pugi\s*[—–-]\s*координатор[.,!]?\s*)/u,
114
+ // EN canonical
115
+ /^(?:I'?m\s+Pugi\s*[—–-]\s*your\s+engineering\s+copilot[.,!]?\s*)/iu,
116
+ /^(?:Pugi\s+here[.,!]?\s*)/iu,
117
+ /^(?:This\s+is\s+Pugi[.,!]?\s*)/iu,
118
+ ];
119
+ let out = text;
120
+ for (const re of introPatterns) {
121
+ out = out.replace(re, '');
122
+ }
123
+ // If the body had ONLY the intro phrase, return the original — never
124
+ // hand the operator an empty bubble. The output gate would have
125
+ // logged the verbosity hit; the operator sees the unmodified text.
126
+ if (out.trim().length === 0)
127
+ return text;
128
+ return out;
129
+ }
56
130
  /**
57
131
  * Cheap heuristic for "this transcript row will benefit from Markdown
58
132
  * rendering". We only pay the parser cost when the row plausibly
59
133
  * contains a code fence, heading, list item, or inline accent. A bare
60
- * "Mira shipped." line takes the legacy fast path.
134
+ * "Pugi shipped." line takes the legacy fast path.
61
135
  *
62
136
  * The probe is intentionally generous - false positives just route
63
137
  * through the parser, which renders plain text identically.
@@ -67,7 +141,7 @@ function looksLikeMarkdown(text) {
67
141
  return false;
68
142
  if (text.includes('```'))
69
143
  return true;
70
- // Codex P2 PR #369: intro-plus-list shape ("Summary:\n- bullet")
144
+ // Codex P2 PR: intro-plus-list shape ("Summary:\n- bullet")
71
145
  // must route through renderer. Scan EVERY line, not just the first.
72
146
  const lines = text.split('\n');
73
147
  for (const raw of lines) {
@@ -0,0 +1,111 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { estimateUsd, formatTokensWithCommas, formatUsd, rateFor } from '../core/cost/rate-card.js';
4
+ /**
5
+ * Column widths chosen to match the L19 spec output. The model column is
6
+ * the widest because slug strings like `qwen3-coder-480b-instruct-fp8`
7
+ * run 31 chars. We pad/truncate inside the renderer so a TUI on a 80-col
8
+ * terminal does not wrap mid-table.
9
+ */
10
+ const COL_MODEL = 34;
11
+ const COL_IN = 8;
12
+ const COL_OUT = 9;
13
+ const COL_USD = 10;
14
+ /**
15
+ * Render one cost report. Stateless — re-rendering with the same view
16
+ * produces the same output by construction. Tests assert against
17
+ * `lastFrame()` from `ink-testing-library`.
18
+ */
19
+ export function CostTable({ view }) {
20
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: view.heading }), _jsx(Text, { children: "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550" }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [pad('MODEL', COL_MODEL), padLeft('IN_TOK', COL_IN), padLeft('OUT_TOK', COL_OUT), padLeft('$ EST', COL_USD)] }), view.rows.length === 0 ? (_jsx(Text, { dimColor: true, children: "No calls recorded yet \u2014 brief a persona to charge the meter." })) : (view.rows.map((row) => (_jsxs(Text, { children: [pad(row.model, COL_MODEL), padLeft(formatTokensWithCommas(row.inputTokens), COL_IN), padLeft(formatTokensWithCommas(row.outputTokens), COL_OUT), padLeft(formatUsd(row.usd), COL_USD), row.note ? ` (${row.note})` : ''] }, row.model)))), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["Total tokens: ", formatTokensWithCommas(view.totalInputTokens + view.totalOutputTokens), ' (in: ', formatTokensWithCommas(view.totalInputTokens), ', out: ', formatTokensWithCommas(view.totalOutputTokens), ')'] }), _jsxs(Text, { children: ["Total dollar estimate: ", formatUsd(view.totalUsd)] }), view.tier ? _jsxs(Text, { children: ["Tier: ", view.tier.tier, view.tier.quotaLine ? ` (${view.tier.quotaLine})` : ''] }) : null] }));
21
+ }
22
+ /**
23
+ * Build a `CostView` from a `SessionAggregate`. The function lives here
24
+ * (next to the renderer) so a future caller — the REPL slash, the CLI
25
+ * command, a JSON-only path — uses the same view-model shape and the
26
+ * same row-sort rule.
27
+ */
28
+ export function buildCostView(input) {
29
+ const rows = [];
30
+ for (const [model, entry] of Object.entries(input.aggregate.models)) {
31
+ const usd = estimateUsd(model, entry.input, entry.output);
32
+ const rate = rateFor(model);
33
+ rows.push({
34
+ model,
35
+ inputTokens: entry.input,
36
+ outputTokens: entry.output,
37
+ usd,
38
+ note: rate.note,
39
+ });
40
+ }
41
+ // Sort by USD descending first (most-expensive-first, matches the L19
42
+ // sample output where the Claude row leads). Ties break by total
43
+ // tokens so two free open-weight rows order deterministically.
44
+ rows.sort((a, b) => {
45
+ if (b.usd !== a.usd)
46
+ return b.usd - a.usd;
47
+ return (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens);
48
+ });
49
+ let totalIn = 0;
50
+ let totalOut = 0;
51
+ let totalUsd = 0;
52
+ for (const row of rows) {
53
+ totalIn += row.inputTokens;
54
+ totalOut += row.outputTokens;
55
+ totalUsd += row.usd;
56
+ }
57
+ return {
58
+ heading: input.heading,
59
+ rows,
60
+ totalInputTokens: totalIn,
61
+ totalOutputTokens: totalOut,
62
+ totalUsd,
63
+ tier: input.tier,
64
+ };
65
+ }
66
+ /**
67
+ * Plain-string renderer for non-TTY / `--json` callers. Produces the
68
+ * same table the Ink component would render — no ANSI / no color so it
69
+ * pipes cleanly into `less` or a JSON tool.
70
+ */
71
+ export function renderCostTableText(view) {
72
+ const lines = [];
73
+ lines.push(view.heading);
74
+ lines.push('════════════════════════════════════════════════');
75
+ lines.push('');
76
+ lines.push(pad('MODEL', COL_MODEL) +
77
+ padLeft('IN_TOK', COL_IN) +
78
+ padLeft('OUT_TOK', COL_OUT) +
79
+ padLeft('$ EST', COL_USD));
80
+ if (view.rows.length === 0) {
81
+ lines.push('No calls recorded yet — brief a persona to charge the meter.');
82
+ }
83
+ else {
84
+ for (const row of view.rows) {
85
+ const noteSuffix = row.note ? ` (${row.note})` : '';
86
+ lines.push(pad(row.model, COL_MODEL) +
87
+ padLeft(formatTokensWithCommas(row.inputTokens), COL_IN) +
88
+ padLeft(formatTokensWithCommas(row.outputTokens), COL_OUT) +
89
+ padLeft(formatUsd(row.usd), COL_USD) +
90
+ noteSuffix);
91
+ }
92
+ }
93
+ lines.push('');
94
+ lines.push(`Total tokens: ${formatTokensWithCommas(view.totalInputTokens + view.totalOutputTokens)} (in: ${formatTokensWithCommas(view.totalInputTokens)}, out: ${formatTokensWithCommas(view.totalOutputTokens)})`);
95
+ lines.push(`Total dollar estimate: ${formatUsd(view.totalUsd)}`);
96
+ if (view.tier) {
97
+ lines.push(`Tier: ${view.tier.tier}${view.tier.quotaLine ? ` (${view.tier.quotaLine})` : ''}`);
98
+ }
99
+ return lines.join('\n');
100
+ }
101
+ function pad(value, width) {
102
+ if (value.length >= width)
103
+ return value.slice(0, Math.max(0, width - 1)) + ' ';
104
+ return value + ' '.repeat(width - value.length);
105
+ }
106
+ function padLeft(value, width) {
107
+ if (value.length >= width)
108
+ return value.slice(0, width);
109
+ return ' '.repeat(width - value.length) + value;
110
+ }
111
+ //# sourceMappingURL=cost-table.js.map
@@ -5,7 +5,7 @@ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧',
5
5
  const ACCENT_CYAN = 'cyan';
6
6
  const ACCENT_RED = 'red';
7
7
  /**
8
- * P2-1 (triple-review 2026-05-24): both `userCode` and
8
+ * P2-1 (triple-review): both `userCode` and
9
9
  * `verificationUrl` originate from a server response and would render
10
10
  * verbatim into Ink `<Text>`. A hostile or compromised server could
11
11
  * embed ANSI escape sequences (clear screen, cursor moves, hyperlink
@@ -108,7 +108,7 @@ export function DeviceFlow(props) {
108
108
  * cabinet page.
109
109
  */
110
110
  function CodeChip({ code }) {
111
- return (_jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { bold: true, color: ACCENT_CYAN, children: ` ${code} ` }) }));
111
+ return (_jsx(Box, { marginTop: 1, justifyContent: "center", children: _jsx(Text, { bold: true, color: ACCENT_CYAN, children: ` ${code} ` }) }));
112
112
  }
113
113
  function BrowserHintRow({ opened, url, copied, }) {
114
114
  if (copied) {