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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (405) hide show
  1. package/CHANGELOG.md +132 -0
  2. package/LICENSE +1 -1
  3. package/THIRD_PARTY_NOTICES.md +40 -0
  4. package/assets/pugi-prozr2-mascot.ansi +9 -0
  5. package/bin/run.js +33 -1
  6. package/dist/commands/deploy.js +40 -40
  7. package/dist/commands/flatten.js +191 -0
  8. package/dist/commands/jobs-watch.js +201 -0
  9. package/dist/commands/jobs.js +42 -27
  10. package/dist/commands/smoke.js +133 -0
  11. package/dist/core/agent-progress/cleanup.js +134 -0
  12. package/dist/core/agent-progress/schema.js +144 -0
  13. package/dist/core/agent-progress/writer.js +101 -0
  14. package/dist/core/agents/adaptive-router.js +330 -0
  15. package/dist/core/agents/query-decomposer.js +297 -0
  16. package/dist/core/agents/registry.js +3 -3
  17. package/dist/core/approvals/shortcut-resolver.js +98 -0
  18. package/dist/core/artifact-chain/dispatcher.js +148 -0
  19. package/dist/core/artifact-chain/exporter.js +164 -0
  20. package/dist/core/artifact-chain/state.js +243 -0
  21. package/dist/core/artifact-chain/steps.js +169 -0
  22. package/dist/core/ask-user/question.js +92 -0
  23. package/dist/core/audit/audit-trail.js +275 -0
  24. package/dist/core/auth/ensure-authenticated.js +129 -0
  25. package/dist/core/auth/env-provider.js +238 -0
  26. package/dist/core/auto-open-browser.js +4 -4
  27. package/dist/core/auto-update/channels.js +122 -0
  28. package/dist/core/auto-update/checker.js +241 -0
  29. package/dist/core/auto-update/state.js +235 -0
  30. package/dist/core/bare-mode/index.js +107 -0
  31. package/dist/core/bash/redirect.js +281 -0
  32. package/dist/core/bash-classifier.js +436 -40
  33. package/dist/core/checkpoint/resumer.js +149 -0
  34. package/dist/core/checkpoint/rewinder.js +291 -0
  35. package/dist/core/checkpoints/shadow-git.js +670 -0
  36. package/dist/core/citations/parser.js +109 -0
  37. package/dist/core/classifier/yolo-classifier.js +88 -0
  38. package/dist/core/codegraph/decision-store.js +248 -0
  39. package/dist/core/codegraph/detect-repo.js +459 -0
  40. package/dist/core/codegraph/install.js +134 -0
  41. package/dist/core/codegraph/offer-hook.js +220 -0
  42. package/dist/core/compact/auto-trigger.js +96 -0
  43. package/dist/core/compact/buffer-rewriter.js +115 -0
  44. package/dist/core/compact/summarizer.js +208 -0
  45. package/dist/core/compact/token-counter.js +108 -0
  46. package/dist/core/consensus/anvil-fanout.js +25 -25
  47. package/dist/core/consensus/diff-capture.js +121 -12
  48. package/dist/core/consensus/rubric.js +21 -21
  49. package/dist/core/context/builder.js +6 -6
  50. package/dist/core/context/compaction-events.js +8 -8
  51. package/dist/core/context/compaction.js +31 -31
  52. package/dist/core/context/index.js +15 -8
  53. package/dist/core/context/invariants.js +51 -51
  54. package/dist/core/context/markdown-loader.js +28 -10
  55. package/dist/core/context/markdown-traverse.js +255 -0
  56. package/dist/core/context/pugiignore.js +41 -41
  57. package/dist/core/context/repo-skeleton.js +37 -37
  58. package/dist/core/context/tool-eviction.js +55 -0
  59. package/dist/core/context/watcher.js +32 -32
  60. package/dist/core/context/working-set.js +23 -23
  61. package/dist/core/coordinator/agent-tools.js +77 -0
  62. package/dist/core/coordinator/agent-toolset.js +65 -0
  63. package/dist/core/coordinator/fsm.js +73 -0
  64. package/dist/core/coordinator/mode-fsm.js +70 -0
  65. package/dist/core/cost/rate-card.js +129 -0
  66. package/dist/core/cost/tracker.js +221 -0
  67. package/dist/core/credentials.js +12 -12
  68. package/dist/core/cron/scheduler.js +138 -0
  69. package/dist/core/denial-tracking/index.js +8 -0
  70. package/dist/core/denial-tracking/state.js +264 -0
  71. package/dist/core/diagnostics/probe-runner.js +93 -0
  72. package/dist/core/diagnostics/probes/api.js +46 -0
  73. package/dist/core/diagnostics/probes/auth.js +93 -0
  74. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  75. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  76. package/dist/core/diagnostics/probes/config.js +72 -0
  77. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  78. package/dist/core/diagnostics/probes/disk.js +81 -0
  79. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  80. package/dist/core/diagnostics/probes/git.js +65 -0
  81. package/dist/core/diagnostics/probes/hooks.js +118 -0
  82. package/dist/core/diagnostics/probes/mcp.js +75 -0
  83. package/dist/core/diagnostics/probes/node.js +59 -0
  84. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  85. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  86. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  87. package/dist/core/diagnostics/probes/session.js +74 -0
  88. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  89. package/dist/core/diagnostics/probes/workspace.js +63 -0
  90. package/dist/core/diagnostics/types.js +70 -0
  91. package/dist/core/dispatch/cache-cleanup.js +197 -0
  92. package/dist/core/dispatch/cache-handoff.js +295 -0
  93. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  94. package/dist/core/edits/dispatch.js +293 -7
  95. package/dist/core/edits/format-matrix.js +26 -0
  96. package/dist/core/edits/fuzzy-ladder.js +650 -0
  97. package/dist/core/edits/index.js +3 -1
  98. package/dist/core/edits/journal.js +199 -0
  99. package/dist/core/edits/layer-a-apply.js +15 -15
  100. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  101. package/dist/core/edits/layer-b-apply.js +9 -9
  102. package/dist/core/edits/layer-c-apply.js +6 -6
  103. package/dist/core/edits/layer-d-ast.js +557 -14
  104. package/dist/core/edits/marker-parser.js +12 -12
  105. package/dist/core/edits/security-gate.js +27 -27
  106. package/dist/core/edits/verify-hook.js +273 -0
  107. package/dist/core/edits/worktree.js +322 -0
  108. package/dist/core/engine/anvil-client.js +151 -26
  109. package/dist/core/engine/auto-compact.js +179 -0
  110. package/dist/core/engine/budgets.js +186 -0
  111. package/dist/core/engine/context-prefix.js +155 -0
  112. package/dist/core/engine/index.js +1 -1
  113. package/dist/core/engine/intensity.js +158 -0
  114. package/dist/core/engine/intent.js +260 -0
  115. package/dist/core/engine/native-pugi.js +1295 -227
  116. package/dist/core/engine/prompts.js +134 -16
  117. package/dist/core/engine/strip-internal-fields.js +124 -0
  118. package/dist/core/engine/tool-bridge.js +1295 -59
  119. package/dist/core/evaluation/golden-dataset.js +293 -0
  120. package/dist/core/feedback/queue.js +177 -0
  121. package/dist/core/feedback/submitter.js +145 -0
  122. package/dist/core/file-cache.js +113 -1
  123. package/dist/core/flatten/flatten-repo.js +439 -0
  124. package/dist/core/format/osc8-link.js +28 -0
  125. package/dist/core/hook-chains.js +392 -0
  126. package/dist/core/hooks/citation-verify-hook.js +138 -0
  127. package/dist/core/hooks/citation-verify.js +112 -0
  128. package/dist/core/hooks/events.js +44 -0
  129. package/dist/core/hooks/index.js +15 -0
  130. package/dist/core/hooks/registry.js +213 -0
  131. package/dist/core/hooks/runner.js +236 -0
  132. package/dist/core/hooks/v2/event-emitter.js +115 -0
  133. package/dist/core/hooks/v2/executor.js +282 -0
  134. package/dist/core/hooks/v2/index.js +25 -0
  135. package/dist/core/hooks/v2/lifecycle.js +104 -0
  136. package/dist/core/hooks/v2/loader.js +216 -0
  137. package/dist/core/hooks/v2/matcher.js +125 -0
  138. package/dist/core/hooks/v2/trust.js +143 -0
  139. package/dist/core/hooks/v2/types.js +86 -0
  140. package/dist/core/image/renderer.js +71 -0
  141. package/dist/core/init/detector.js +582 -0
  142. package/dist/core/init/template-renderer.js +242 -0
  143. package/dist/core/jobs/registry.js +18 -18
  144. package/dist/core/ledger/results-tsv.js +142 -0
  145. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  146. package/dist/core/lsp/cache.js +105 -0
  147. package/dist/core/lsp/client.js +776 -0
  148. package/dist/core/lsp/language-detect.js +66 -0
  149. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  150. package/dist/core/lsp/symbol-tools.js +372 -0
  151. package/dist/core/mcp/client.js +97 -28
  152. package/dist/core/mcp/http-server.js +553 -0
  153. package/dist/core/mcp/orchestrator-tools.js +662 -0
  154. package/dist/core/mcp/permission.js +190 -0
  155. package/dist/core/mcp/registry.js +39 -17
  156. package/dist/core/mcp/server-tools.js +219 -0
  157. package/dist/core/mcp/server.js +397 -0
  158. package/dist/core/mcp/trust.js +10 -10
  159. package/dist/core/memory/dual-write.js +416 -0
  160. package/dist/core/memory/passive-extract.js +130 -0
  161. package/dist/core/memory/phase1-kinds.js +20 -0
  162. package/dist/core/memory/secret-scanner.js +304 -0
  163. package/dist/core/memory-sync/queue.js +170 -0
  164. package/dist/core/metrics/extract.js +113 -0
  165. package/dist/core/modes/roo-modes.js +68 -0
  166. package/dist/core/onboarding/ensure-initialized.js +133 -0
  167. package/dist/core/onboarding/marker.js +111 -0
  168. package/dist/core/onboarding/telemetry-state.js +108 -0
  169. package/dist/core/output-style/presets.js +176 -0
  170. package/dist/core/output-style/state.js +185 -0
  171. package/dist/core/path-security.js +287 -5
  172. package/dist/core/permission.js +82 -22
  173. package/dist/core/permissions/auto-classifier.js +124 -0
  174. package/dist/core/permissions/bash-parser.js +371 -0
  175. package/dist/core/permissions/circuit-breaker.js +83 -0
  176. package/dist/core/permissions/constrained-edit.js +91 -0
  177. package/dist/core/permissions/gate.js +278 -0
  178. package/dist/core/permissions/index.js +20 -0
  179. package/dist/core/permissions/mode.js +174 -0
  180. package/dist/core/permissions/network-egress.js +137 -0
  181. package/dist/core/permissions/state.js +241 -0
  182. package/dist/core/permissions/tool-class.js +93 -0
  183. package/dist/core/plan-mode/ui-state.js +51 -0
  184. package/dist/core/plans/plan-artifact.js +721 -0
  185. package/dist/core/policy-limits/etag-store.js +122 -0
  186. package/dist/core/prd-check/parser.js +215 -0
  187. package/dist/core/prd-check/reporter.js +127 -0
  188. package/dist/core/prd-check/session-review.js +557 -0
  189. package/dist/core/prd-check/verifiers.js +223 -0
  190. package/dist/core/prompt-cache/client-cache.js +99 -0
  191. package/dist/core/prompts/assembly.js +29 -0
  192. package/dist/core/prompts/registry.js +364 -0
  193. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  194. package/dist/core/pugi-md/context-injector.js +76 -0
  195. package/dist/core/pugi-md/walk-up.js +207 -0
  196. package/dist/core/python/uv-installer.js +270 -0
  197. package/dist/core/python/uv-resolver.js +83 -0
  198. package/dist/core/rate-limit/narrator.js +146 -0
  199. package/dist/core/recipes/cli-types.js +20 -0
  200. package/dist/core/recipes/loader.js +103 -0
  201. package/dist/core/recipes/runner.js +345 -0
  202. package/dist/core/recipes/schema.js +587 -0
  203. package/dist/core/release-notes/parser.js +241 -0
  204. package/dist/core/release-notes/state.js +116 -0
  205. package/dist/core/repl/ask.js +37 -37
  206. package/dist/core/repl/cancellation.js +26 -26
  207. package/dist/core/repl/cap-warning.js +4 -4
  208. package/dist/core/repl/clipboard-read.js +11 -11
  209. package/dist/core/repl/dispatch-fsm.js +12 -12
  210. package/dist/core/repl/history-search.js +15 -15
  211. package/dist/core/repl/history.js +28 -18
  212. package/dist/core/repl/kill-ring.js +5 -5
  213. package/dist/core/repl/model-pricing.js +135 -0
  214. package/dist/core/repl/privacy-banner.js +22 -22
  215. package/dist/core/repl/session.js +2157 -214
  216. package/dist/core/repl/slash-commands.js +533 -40
  217. package/dist/core/repl/store/index.js +1 -1
  218. package/dist/core/repl/store/jsonl-log.js +22 -22
  219. package/dist/core/repl/store/lockfile.js +10 -10
  220. package/dist/core/repl/store/session-store.js +136 -107
  221. package/dist/core/repl/store/types.js +15 -15
  222. package/dist/core/repl/store/uuid-v7.js +12 -12
  223. package/dist/core/repl/workspace-context.js +43 -21
  224. package/dist/core/repo-map/build.js +125 -0
  225. package/dist/core/repo-map/cache.js +185 -0
  226. package/dist/core/repo-map/extractor.js +254 -0
  227. package/dist/core/repo-map/formatter.js +145 -0
  228. package/dist/core/repo-map/page-rank.js +105 -0
  229. package/dist/core/repo-map/scanner.js +211 -0
  230. package/dist/core/retry-budget/budget.js +284 -0
  231. package/dist/core/retry-budget/index.js +5 -0
  232. package/dist/core/retry-budget/retry-cap.js +74 -0
  233. package/dist/core/routing/lead-worker.js +43 -0
  234. package/dist/core/routing/pre-flight-estimator.js +108 -0
  235. package/dist/core/runs/run-tree.js +103 -0
  236. package/dist/core/security/injection-scanner.js +367 -0
  237. package/dist/core/security/output-filter.js +418 -0
  238. package/dist/core/session/env-file.js +105 -0
  239. package/dist/core/session/section-budgets.js +140 -0
  240. package/dist/core/session.js +92 -0
  241. package/dist/core/settings.js +298 -5
  242. package/dist/core/share/formatter.js +271 -0
  243. package/dist/core/share/redactor.js +221 -0
  244. package/dist/core/share/uploader.js +267 -0
  245. package/dist/core/skills/defaults.js +457 -0
  246. package/dist/core/skills/loader.js +22 -22
  247. package/dist/core/skills/sources.js +27 -27
  248. package/dist/core/smoke/headless-driver.js +174 -0
  249. package/dist/core/smoke/orchestrator.js +194 -0
  250. package/dist/core/smoke/runner.js +238 -0
  251. package/dist/core/smoke/scenario-parser.js +316 -0
  252. package/dist/core/statusline.js +99 -0
  253. package/dist/core/subagents/dispatcher-real.js +600 -0
  254. package/dist/core/subagents/dispatcher.js +132 -43
  255. package/dist/core/subagents/index.js +19 -6
  256. package/dist/core/subagents/isolation-matrix.js +213 -0
  257. package/dist/core/subagents/spawn.js +19 -4
  258. package/dist/core/telemetry/emitter.js +229 -0
  259. package/dist/core/telemetry/queue.js +251 -0
  260. package/dist/core/theme/context.js +91 -0
  261. package/dist/core/theme/presets.js +228 -0
  262. package/dist/core/theme/state.js +181 -0
  263. package/dist/core/todos/invariant.js +10 -0
  264. package/dist/core/todos/state.js +177 -0
  265. package/dist/core/tool-schema/compressor.js +89 -0
  266. package/dist/core/transport/version-interceptor.js +166 -0
  267. package/dist/core/trust.js +2 -2
  268. package/dist/core/tui/thinking-block.js +64 -0
  269. package/dist/core/vim/keymap.js +288 -0
  270. package/dist/core/vim/state.js +92 -0
  271. package/dist/core/watch-markers/marker-watcher.js +133 -0
  272. package/dist/core/worktree-manager/cleanup.js +123 -0
  273. package/dist/core/worktree-manager/manager.js +303 -0
  274. package/dist/index.js +36 -0
  275. package/dist/runtime/bootstrap.js +190 -0
  276. package/dist/runtime/cli.js +4203 -493
  277. package/dist/runtime/commands/agents.js +30 -30
  278. package/dist/runtime/commands/budget.js +5 -5
  279. package/dist/runtime/commands/cancel.js +231 -0
  280. package/dist/runtime/commands/chain.js +489 -0
  281. package/dist/runtime/commands/codegraph-status.js +227 -0
  282. package/dist/runtime/commands/compact.js +297 -0
  283. package/dist/runtime/commands/config.js +73 -39
  284. package/dist/runtime/commands/cost.js +199 -0
  285. package/dist/runtime/commands/delegate.js +244 -13
  286. package/dist/runtime/commands/dispatch.js +126 -0
  287. package/dist/runtime/commands/doctor.js +579 -0
  288. package/dist/runtime/commands/feedback.js +184 -0
  289. package/dist/runtime/commands/hooks.js +184 -0
  290. package/dist/runtime/commands/init.js +254 -0
  291. package/dist/runtime/commands/lsp.js +368 -0
  292. package/dist/runtime/commands/mcp.js +879 -0
  293. package/dist/runtime/commands/memory.js +582 -0
  294. package/dist/runtime/commands/model.js +237 -0
  295. package/dist/runtime/commands/onboarding.js +275 -0
  296. package/dist/runtime/commands/patch.js +128 -0
  297. package/dist/runtime/commands/permissions.js +112 -0
  298. package/dist/runtime/commands/plan.js +143 -0
  299. package/dist/runtime/commands/prd-check.js +285 -0
  300. package/dist/runtime/commands/privacy.js +17 -17
  301. package/dist/runtime/commands/recipe.js +325 -0
  302. package/dist/runtime/commands/redo-blob-store.js +92 -0
  303. package/dist/runtime/commands/redo.js +361 -0
  304. package/dist/runtime/commands/release-notes.js +229 -0
  305. package/dist/runtime/commands/repo-map.js +95 -0
  306. package/dist/runtime/commands/report.js +299 -0
  307. package/dist/runtime/commands/resume.js +118 -0
  308. package/dist/runtime/commands/review-consensus.js +68 -53
  309. package/dist/runtime/commands/rewind.js +333 -0
  310. package/dist/runtime/commands/roster.js +14 -14
  311. package/dist/runtime/commands/sessions.js +163 -0
  312. package/dist/runtime/commands/share.js +316 -0
  313. package/dist/runtime/commands/skills.js +31 -31
  314. package/dist/runtime/commands/status.js +186 -0
  315. package/dist/runtime/commands/stickers.js +82 -0
  316. package/dist/runtime/commands/style.js +194 -0
  317. package/dist/runtime/commands/theme.js +196 -0
  318. package/dist/runtime/commands/undo.js +54 -22
  319. package/dist/runtime/commands/update.js +289 -0
  320. package/dist/runtime/commands/vim.js +140 -0
  321. package/dist/runtime/commands/worktree.js +177 -0
  322. package/dist/runtime/commands/worktrees.js +155 -0
  323. package/dist/runtime/headless-repl.js +195 -0
  324. package/dist/runtime/headless.js +543 -0
  325. package/dist/runtime/load-hooks-or-exit.js +71 -0
  326. package/dist/runtime/plan-decompose.js +531 -0
  327. package/dist/runtime/sigint-guard.js +272 -0
  328. package/dist/runtime/update-check.js +28 -28
  329. package/dist/runtime/version.js +65 -0
  330. package/dist/skills/bundled/batch.js +617 -0
  331. package/dist/skills/bundled/index.js +45 -0
  332. package/dist/skills/bundled/loop.js +358 -0
  333. package/dist/skills/bundled/remember.js +383 -0
  334. package/dist/skills/bundled/simplify.js +289 -0
  335. package/dist/skills/bundled/skillify.js +373 -0
  336. package/dist/skills/bundled/stuck.js +558 -0
  337. package/dist/skills/bundled/verify.js +439 -0
  338. package/dist/testing/vcr.js +486 -0
  339. package/dist/tools/agent-tool.js +229 -0
  340. package/dist/tools/apply-patch.js +556 -0
  341. package/dist/tools/ask-user-question.js +288 -0
  342. package/dist/tools/ask-user.js +115 -0
  343. package/dist/tools/bash.js +624 -46
  344. package/dist/tools/brief.js +224 -0
  345. package/dist/tools/enter-worktree.js +250 -0
  346. package/dist/tools/exit-worktree.js +147 -0
  347. package/dist/tools/file-tools.js +161 -44
  348. package/dist/tools/lsp-tools.js +189 -0
  349. package/dist/tools/mcp-tool.js +260 -0
  350. package/dist/tools/multi-edit.js +361 -0
  351. package/dist/tools/powershell.js +268 -0
  352. package/dist/tools/registry.js +85 -0
  353. package/dist/tools/skill-tool.js +96 -0
  354. package/dist/tools/sleep.js +99 -0
  355. package/dist/tools/synthetic-output.js +133 -0
  356. package/dist/tools/tasks.js +208 -0
  357. package/dist/tools/todo-write.js +184 -0
  358. package/dist/tools/verify-plan-execution.js +295 -0
  359. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  360. package/dist/tools/web-fetch.js +195 -10
  361. package/dist/tools/web-search.js +458 -0
  362. package/dist/tui/agent-progress-card.js +111 -0
  363. package/dist/tui/agent-tree.js +11 -1
  364. package/dist/tui/ask-modal.js +14 -14
  365. package/dist/tui/ask-user-question-chips.js +257 -0
  366. package/dist/tui/ask-user-question-prompt.js +203 -0
  367. package/dist/tui/compact-banner.js +81 -0
  368. package/dist/tui/conversation-pane.js +85 -11
  369. package/dist/tui/cost-table.js +111 -0
  370. package/dist/tui/device-flow.js +2 -2
  371. package/dist/tui/doctor-table.js +46 -0
  372. package/dist/tui/feedback-prompt.js +156 -0
  373. package/dist/tui/input-box.js +247 -32
  374. package/dist/tui/login-picker.js +3 -3
  375. package/dist/tui/markdown-render.js +6 -6
  376. package/dist/tui/onboarding-wizard.js +240 -0
  377. package/dist/tui/permissions-picker.js +86 -0
  378. package/dist/tui/render.js +35 -0
  379. package/dist/tui/repl-render.js +332 -54
  380. package/dist/tui/repl-splash-art.js +16 -16
  381. package/dist/tui/repl-splash-mascot.js +48 -24
  382. package/dist/tui/repl-splash.js +22 -22
  383. package/dist/tui/repl.js +124 -44
  384. package/dist/tui/slash-palette.js +6 -6
  385. package/dist/tui/splash.js +2 -2
  386. package/dist/tui/status-bar.js +109 -31
  387. package/dist/tui/status-table.js +7 -0
  388. package/dist/tui/stickers-art.js +136 -0
  389. package/dist/tui/style-table.js +28 -0
  390. package/dist/tui/theme-table.js +29 -0
  391. package/dist/tui/thinking-spinner.js +123 -0
  392. package/dist/tui/tool-stream-pane.js +53 -4
  393. package/dist/tui/update-banner.js +27 -2
  394. package/dist/tui/vim-input.js +267 -0
  395. package/dist/tui/welcome-banner.js +107 -0
  396. package/dist/tui/welcome-data.js +293 -0
  397. package/dist/tui/workspace-context.js +2 -2
  398. package/docs/examples/codegraph.mcp.json +10 -0
  399. package/package.json +25 -7
  400. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  401. package/test/scenarios/compact-force.scenario.txt +11 -0
  402. package/test/scenarios/identity.scenario.txt +11 -0
  403. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  404. package/test/scenarios/walkback.scenario.txt +12 -0
  405. package/dist/core/engine/compaction-hook.js +0 -154
@@ -1,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,257 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * AskUserQuestionChips — multi-question short-format chip renderer
4
+ * for the structured AskUserQuestion tool (PUGI-480).
5
+ *
6
+ * Renders up to 3 question chips side-by-side, each with up to ~5 short
7
+ * (≤ 5-word) option labels. Replaces paragraph-wall prompts с a compact
8
+ * keyboard-driven picker:
9
+ *
10
+ * Pugi: 2 quick choices
11
+ * ┌────────────────────────┐
12
+ * │ Stack │
13
+ * │ ▸ 1 Browser JS │
14
+ * │ 2 React │
15
+ * │ 3 Other... │
16
+ * └────────────────────────┘
17
+ * ┌────────────────────────┐
18
+ * │ Size │
19
+ * │ ▸ 1 9x9 (classic) │
20
+ * │ 2 16x16 │
21
+ * │ 3 Custom │
22
+ * └────────────────────────┘
23
+ * [Enter] use highlighted defaults · [↑↓] navigate · [s] skip
24
+ *
25
+ * Contract:
26
+ * - questions[].options[].label MUST be ≤ 5 words (renderer truncates
27
+ * overflow с ellipsis defensively; the schema validator rejects at
28
+ * the tool entry point so the model never produces overflow legally).
29
+ * - First option of each question is pre-highlighted (default).
30
+ * - Enter без movement applies defaults across ALL questions.
31
+ * - 1-9 jump к the matching option in the focused question.
32
+ * - ↑↓ navigate within the focused question.
33
+ * - ←→ (and Tab/Shift-Tab) move between questions.
34
+ * - [s] skips the focused question (uses its default) and advances to
35
+ * the next; [s] on the last question commits.
36
+ * - [Esc] cancels the entire dialog (user_cancelled).
37
+ *
38
+ * Non-TTY fallback: when the `forceFallback` prop is true, renders a
39
+ * static numbered text list and does NOT mount the Ink useInput hook.
40
+ * The component itself does NOT sniff `process.stdin.isTTY` — that gate
41
+ * lives in the upstream REPL wiring, which either mounts this chip
42
+ * component (TTY path) or emits the legacy `[user_input_required]`
43
+ * envelope (non-TTY path). Keeping the gate в the caller keeps the
44
+ * component testable under ink-testing-library (which mocks stdin but
45
+ * не sets isTTY).
46
+ *
47
+ * Brand voice gate: ASCII glyphs only, zero attribution to external AIs,
48
+ * no em-dashes. Power-word neutral so localised variants land cleanly.
49
+ */
50
+ import { useMemo, useState } from 'react';
51
+ import { Box, Text, useInput } from 'ink';
52
+ /**
53
+ * Hard truncation cap. The schema validator enforces ≤ 5 words per
54
+ * label, but the renderer still defends in depth: a malicious or
55
+ * legacy payload with a 14-word label MUST not blow the chip width.
56
+ */
57
+ export const ASK_CHIPS_LABEL_WORD_CAP = 5;
58
+ export const ASK_CHIPS_LABEL_CHAR_CAP = 22;
59
+ export const ASK_CHIPS_QUESTION_CAP = 3;
60
+ export const ASK_CHIPS_SKIP_LABEL = 'Skip — use defaults';
61
+ /**
62
+ * Truncate a label к the configured word / character caps. Always
63
+ * append "…" when truncation happens so the operator knows the chip
64
+ * is hiding content.
65
+ */
66
+ export function truncateLabel(raw) {
67
+ const trimmed = raw.trim().replace(/\s+/gu, ' ');
68
+ // Word cap (whitespace-delimited). 5-word rule is the canonical limit.
69
+ const words = trimmed.split(' ');
70
+ let candidate = trimmed;
71
+ if (words.length > ASK_CHIPS_LABEL_WORD_CAP) {
72
+ candidate = `${words.slice(0, ASK_CHIPS_LABEL_WORD_CAP).join(' ')}…`;
73
+ }
74
+ // Char cap (defense-in-depth — protects chip width even с short-word
75
+ // payloads like "AAAAAAAAAAAAAAAAAAAAA" that pass the word gate).
76
+ if (candidate.length > ASK_CHIPS_LABEL_CHAR_CAP) {
77
+ candidate = `${candidate.slice(0, ASK_CHIPS_LABEL_CHAR_CAP - 1)}…`;
78
+ }
79
+ return candidate;
80
+ }
81
+ /**
82
+ * The component renders в interactive mode by default. The non-TTY
83
+ * gate is the caller's responsibility — see ask-user-question-chips.tsx
84
+ * header docs. Production REPL wiring should check `process.stdin.isTTY`
85
+ * upstream и pass `forceFallback={true}` when stdin is not a TTY, OR
86
+ * skip mounting the chip component entirely и emit the legacy
87
+ * `[user_input_required]` envelope.
88
+ */
89
+ function shouldUseFallback(forceFallback) {
90
+ return forceFallback === true;
91
+ }
92
+ /**
93
+ * Build the default selections vector (all first options) so Enter
94
+ * без movement applies a coherent set, and so non-TTY fallback can
95
+ * commit immediately.
96
+ */
97
+ function buildDefaults(questions) {
98
+ return questions.map((q) => {
99
+ const first = q.options[0];
100
+ return {
101
+ header: q.header,
102
+ pickedIndex: 0,
103
+ pickedLabel: first ? truncateLabel(first.label) : '',
104
+ skipped: false,
105
+ };
106
+ });
107
+ }
108
+ export function AskUserQuestionChips(props) {
109
+ const useFallback = shouldUseFallback(props.forceFallback);
110
+ // Defensive cap: never render more than ASK_CHIPS_QUESTION_CAP chips.
111
+ // Schema enforces this at the tool boundary; the renderer still trims
112
+ // в case a legacy / malicious payload sneaks through.
113
+ const questions = useMemo(() => props.questions.slice(0, ASK_CHIPS_QUESTION_CAP), [props.questions]);
114
+ // Cursor state: which question is focused, and which option within
115
+ // each question is highlighted. Initialised к first option (default).
116
+ const [focusedQuestion, setFocusedQuestion] = useState(0);
117
+ const [cursorPerQuestion, setCursorPerQuestion] = useState(() => questions.map(() => 0));
118
+ // Per-question explicit skip flag. Skipped questions still emit their
119
+ // default index, but flagged `skipped: true` so downstream can audit.
120
+ const [skipped, setSkipped] = useState(() => questions.map(() => false));
121
+ function commit(skipMask = skipped) {
122
+ const selections = questions.map((q, qIdx) => {
123
+ const optionIdx = cursorPerQuestion[qIdx] ?? 0;
124
+ const opt = q.options[optionIdx] ?? q.options[0];
125
+ return {
126
+ header: q.header,
127
+ pickedIndex: optionIdx,
128
+ pickedLabel: opt ? truncateLabel(opt.label) : '',
129
+ skipped: skipMask[qIdx] === true,
130
+ };
131
+ });
132
+ props.onResolve({ selections, cancelled: false });
133
+ }
134
+ function cancel() {
135
+ props.onResolve({ selections: buildDefaults(questions), cancelled: true });
136
+ }
137
+ useInput((input, key) => {
138
+ // Esc cancels the entire AskUserQuestion (user_cancelled).
139
+ if (key.escape) {
140
+ cancel();
141
+ return;
142
+ }
143
+ // Enter commits the current selection (applies defaults across
144
+ // any questions the operator did not touch).
145
+ if (key.return) {
146
+ commit();
147
+ return;
148
+ }
149
+ // [s] skips the focused question (uses default), advances to next.
150
+ // [s] on the LAST question commits the whole set.
151
+ if (input === 's' || input === 'S') {
152
+ const nextSkipped = skipped.slice();
153
+ nextSkipped[focusedQuestion] = true;
154
+ if (focusedQuestion >= questions.length - 1) {
155
+ setSkipped(nextSkipped);
156
+ commit(nextSkipped);
157
+ return;
158
+ }
159
+ setSkipped(nextSkipped);
160
+ setFocusedQuestion((q) => Math.min(q + 1, questions.length - 1));
161
+ return;
162
+ }
163
+ // ↑/↓ navigate within the focused question.
164
+ if (key.upArrow) {
165
+ setCursorPerQuestion((prev) => {
166
+ const next = prev.slice();
167
+ const opts = questions[focusedQuestion]?.options ?? [];
168
+ const len = opts.length;
169
+ if (len === 0)
170
+ return prev;
171
+ const cur = next[focusedQuestion] ?? 0;
172
+ next[focusedQuestion] = (cur - 1 + len) % len;
173
+ return next;
174
+ });
175
+ return;
176
+ }
177
+ if (key.downArrow) {
178
+ setCursorPerQuestion((prev) => {
179
+ const next = prev.slice();
180
+ const opts = questions[focusedQuestion]?.options ?? [];
181
+ const len = opts.length;
182
+ if (len === 0)
183
+ return prev;
184
+ const cur = next[focusedQuestion] ?? 0;
185
+ next[focusedQuestion] = (cur + 1) % len;
186
+ return next;
187
+ });
188
+ return;
189
+ }
190
+ // ←/→ (or Tab/Shift-Tab) navigate between questions.
191
+ if (key.leftArrow || (key.shift && key.tab)) {
192
+ setFocusedQuestion((q) => (q - 1 + questions.length) % questions.length);
193
+ return;
194
+ }
195
+ if (key.rightArrow || key.tab) {
196
+ setFocusedQuestion((q) => (q + 1) % questions.length);
197
+ return;
198
+ }
199
+ // 1-9 jumps к the matching option in the focused question.
200
+ const numeric = Number.parseInt(input, 10);
201
+ if (!Number.isNaN(numeric) && numeric >= 1 && numeric <= 9) {
202
+ const opts = questions[focusedQuestion]?.options ?? [];
203
+ const target = numeric - 1;
204
+ if (target >= 0 && target < opts.length) {
205
+ setCursorPerQuestion((prev) => {
206
+ const next = prev.slice();
207
+ next[focusedQuestion] = target;
208
+ return next;
209
+ });
210
+ }
211
+ return;
212
+ }
213
+ }, { isActive: props.inert !== true && useFallback === false });
214
+ // ─── Non-TTY fallback ────────────────────────────────────────────────
215
+ // When stdin is not a TTY we still need to render SOMETHING — but
216
+ // we cannot drive keystrokes, so we emit a numbered text list per
217
+ // question and let the upstream envelope handler ask the operator.
218
+ if (useFallback) {
219
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsx(Text, { bold: true, children: `Pugi: ${questions.length} quick ${questions.length === 1 ? 'choice' : 'choices'}` }) }), questions.map((q, qIdx) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: `${q.header}` }), q.options.map((opt, oIdx) => (_jsx(Text, { children: ` ${oIdx + 1}. ${truncateLabel(opt.label)}${oIdx === 0 ? ' (default)' : ''}` }, `q-${qIdx}-o-${oIdx}`)))] }, `q-${qIdx}-${q.header}`))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, italic: true, children: `(non-interactive — defaults apply)` }) })] }));
220
+ }
221
+ // ─── Interactive Ink render ─────────────────────────────────────────
222
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: _jsx(Text, { bold: true, children: `Pugi: ${questions.length} quick ${questions.length === 1 ? 'choice' : 'choices'}` }) }), _jsx(Box, { flexDirection: "row", marginTop: 1, children: questions.map((q, qIdx) => {
223
+ const isFocused = qIdx === focusedQuestion;
224
+ const cursor = cursorPerQuestion[qIdx] ?? 0;
225
+ const isSkipped = skipped[qIdx] === true;
226
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: isFocused ? 'cyan' : 'gray', paddingX: 1, marginRight: 1, minWidth: 24, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: isFocused ? 'cyan' : undefined, children: q.header }), isSkipped ? (_jsx(Text, { dimColor: true, italic: true, children: ' (skipped)' })) : null] }), q.options.map((opt, oIdx) => {
227
+ const isHighlighted = isFocused && oIdx === cursor;
228
+ const label = truncateLabel(opt.label);
229
+ const isSkipOption = label === ASK_CHIPS_SKIP_LABEL ||
230
+ opt.label === ASK_CHIPS_SKIP_LABEL;
231
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isHighlighted ? 'cyan' : undefined, bold: isHighlighted, children: isHighlighted ? '▸ ' : ' ' }), _jsx(Text, { color: isHighlighted ? 'cyan' : undefined, children: `${oIdx + 1} ` }), isSkipOption ? (_jsx(Text, { dimColor: true, italic: true, children: label })) : (_jsx(Text, { bold: isHighlighted, children: label }))] }, `opt-${qIdx}-${oIdx}-${opt.label}`));
232
+ })] }, `chip-${qIdx}-${q.header}`));
233
+ }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `[Enter] use highlighted defaults · [↑↓] navigate · [←→] switch · [s] skip · [Esc] cancel` }) })] }));
234
+ }
235
+ /**
236
+ * Encode the chip verdict into the persona-recognisable turn-injection
237
+ * string. Mirrors `encodeAskUserQuestionVerdict` from the single-question
238
+ * prompt so the model sees a consistent grammar.
239
+ *
240
+ * Examples:
241
+ * - { cancelled: true } → "[ASK-USER-QUESTION:cancelled]"
242
+ * - All defaults → "[ASK-USER-QUESTION:answered] Stack=Browser JS; Size=9x9 (classic)"
243
+ * - One skipped → "[ASK-USER-QUESTION:answered] Stack=Browser JS; Size=(skipped: 9x9)"
244
+ */
245
+ export function encodeAskUserQuestionChipsVerdict(result) {
246
+ if (result.cancelled)
247
+ return '[ASK-USER-QUESTION:cancelled]';
248
+ if (result.selections.length === 0)
249
+ return '[ASK-USER-QUESTION:cancelled]';
250
+ const parts = result.selections.map((sel) => {
251
+ if (sel.skipped)
252
+ return `${sel.header}=(skipped: ${sel.pickedLabel})`;
253
+ return `${sel.header}=${sel.pickedLabel}`;
254
+ });
255
+ return `[ASK-USER-QUESTION:answered] ${parts.join('; ')}`;
256
+ }
257
+ //# sourceMappingURL=ask-user-question-chips.js.map
@@ -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