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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (402) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/deploy.js +40 -40
  6. package/dist/commands/flatten.js +191 -0
  7. package/dist/commands/jobs-watch.js +201 -0
  8. package/dist/commands/jobs.js +42 -27
  9. package/dist/commands/smoke.js +133 -0
  10. package/dist/core/agent-progress/cleanup.js +134 -0
  11. package/dist/core/agent-progress/schema.js +144 -0
  12. package/dist/core/agent-progress/writer.js +101 -0
  13. package/dist/core/agents/adaptive-router.js +330 -0
  14. package/dist/core/agents/query-decomposer.js +297 -0
  15. package/dist/core/agents/registry.js +2 -2
  16. package/dist/core/approvals/shortcut-resolver.js +98 -0
  17. package/dist/core/artifact-chain/dispatcher.js +148 -0
  18. package/dist/core/artifact-chain/exporter.js +164 -0
  19. package/dist/core/artifact-chain/state.js +243 -0
  20. package/dist/core/artifact-chain/steps.js +169 -0
  21. package/dist/core/ask-user/question.js +92 -0
  22. package/dist/core/audit/audit-trail.js +275 -0
  23. package/dist/core/auth/ensure-authenticated.js +129 -0
  24. package/dist/core/auth/env-provider.js +238 -0
  25. package/dist/core/auto-open-browser.js +4 -4
  26. package/dist/core/auto-update/channels.js +122 -0
  27. package/dist/core/auto-update/checker.js +241 -0
  28. package/dist/core/auto-update/state.js +235 -0
  29. package/dist/core/bare-mode/index.js +107 -0
  30. package/dist/core/bash/redirect.js +281 -0
  31. package/dist/core/bash-classifier.js +436 -40
  32. package/dist/core/checkpoint/resumer.js +149 -0
  33. package/dist/core/checkpoint/rewinder.js +291 -0
  34. package/dist/core/checkpoints/shadow-git.js +670 -0
  35. package/dist/core/citations/parser.js +109 -0
  36. package/dist/core/classifier/yolo-classifier.js +88 -0
  37. package/dist/core/codegraph/decision-store.js +248 -0
  38. package/dist/core/codegraph/detect-repo.js +459 -0
  39. package/dist/core/codegraph/install.js +134 -0
  40. package/dist/core/codegraph/offer-hook.js +220 -0
  41. package/dist/core/compact/auto-trigger.js +96 -0
  42. package/dist/core/compact/buffer-rewriter.js +115 -0
  43. package/dist/core/compact/summarizer.js +208 -0
  44. package/dist/core/compact/token-counter.js +108 -0
  45. package/dist/core/consensus/anvil-fanout.js +25 -25
  46. package/dist/core/consensus/diff-capture.js +121 -12
  47. package/dist/core/consensus/rubric.js +21 -21
  48. package/dist/core/context/builder.js +6 -6
  49. package/dist/core/context/compaction-events.js +8 -8
  50. package/dist/core/context/compaction.js +31 -31
  51. package/dist/core/context/index.js +15 -8
  52. package/dist/core/context/invariants.js +51 -51
  53. package/dist/core/context/markdown-loader.js +28 -10
  54. package/dist/core/context/markdown-traverse.js +255 -0
  55. package/dist/core/context/pugiignore.js +41 -41
  56. package/dist/core/context/repo-skeleton.js +37 -37
  57. package/dist/core/context/tool-eviction.js +55 -0
  58. package/dist/core/context/watcher.js +32 -32
  59. package/dist/core/context/working-set.js +23 -23
  60. package/dist/core/coordinator/agent-tools.js +77 -0
  61. package/dist/core/coordinator/agent-toolset.js +65 -0
  62. package/dist/core/coordinator/fsm.js +73 -0
  63. package/dist/core/coordinator/mode-fsm.js +70 -0
  64. package/dist/core/cost/rate-card.js +129 -0
  65. package/dist/core/cost/tracker.js +221 -0
  66. package/dist/core/credentials.js +12 -12
  67. package/dist/core/cron/scheduler.js +138 -0
  68. package/dist/core/denial-tracking/index.js +8 -0
  69. package/dist/core/denial-tracking/state.js +264 -0
  70. package/dist/core/diagnostics/probe-runner.js +93 -0
  71. package/dist/core/diagnostics/probes/api.js +46 -0
  72. package/dist/core/diagnostics/probes/auth.js +93 -0
  73. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  74. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  75. package/dist/core/diagnostics/probes/config.js +72 -0
  76. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  77. package/dist/core/diagnostics/probes/disk.js +81 -0
  78. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  79. package/dist/core/diagnostics/probes/git.js +65 -0
  80. package/dist/core/diagnostics/probes/hooks.js +118 -0
  81. package/dist/core/diagnostics/probes/mcp.js +75 -0
  82. package/dist/core/diagnostics/probes/node.js +59 -0
  83. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  84. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  85. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  86. package/dist/core/diagnostics/probes/session.js +74 -0
  87. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  88. package/dist/core/diagnostics/probes/workspace.js +63 -0
  89. package/dist/core/diagnostics/types.js +70 -0
  90. package/dist/core/dispatch/cache-cleanup.js +197 -0
  91. package/dist/core/dispatch/cache-handoff.js +295 -0
  92. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  93. package/dist/core/edits/dispatch.js +293 -7
  94. package/dist/core/edits/format-matrix.js +26 -0
  95. package/dist/core/edits/fuzzy-ladder.js +650 -0
  96. package/dist/core/edits/index.js +3 -1
  97. package/dist/core/edits/journal.js +199 -0
  98. package/dist/core/edits/layer-a-apply.js +15 -15
  99. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  100. package/dist/core/edits/layer-b-apply.js +9 -9
  101. package/dist/core/edits/layer-c-apply.js +6 -6
  102. package/dist/core/edits/layer-d-ast.js +557 -14
  103. package/dist/core/edits/marker-parser.js +12 -12
  104. package/dist/core/edits/security-gate.js +27 -27
  105. package/dist/core/edits/verify-hook.js +273 -0
  106. package/dist/core/edits/worktree.js +322 -0
  107. package/dist/core/engine/anvil-client.js +140 -26
  108. package/dist/core/engine/auto-compact.js +179 -0
  109. package/dist/core/engine/budgets.js +186 -0
  110. package/dist/core/engine/context-prefix.js +155 -0
  111. package/dist/core/engine/index.js +1 -1
  112. package/dist/core/engine/intensity.js +158 -0
  113. package/dist/core/engine/intent.js +260 -0
  114. package/dist/core/engine/native-pugi.js +1295 -227
  115. package/dist/core/engine/prompts.js +134 -16
  116. package/dist/core/engine/strip-internal-fields.js +124 -0
  117. package/dist/core/engine/tool-bridge.js +1295 -59
  118. package/dist/core/evaluation/golden-dataset.js +293 -0
  119. package/dist/core/feedback/queue.js +177 -0
  120. package/dist/core/feedback/submitter.js +145 -0
  121. package/dist/core/file-cache.js +113 -1
  122. package/dist/core/flatten/flatten-repo.js +439 -0
  123. package/dist/core/format/osc8-link.js +28 -0
  124. package/dist/core/hook-chains.js +392 -0
  125. package/dist/core/hooks/citation-verify-hook.js +138 -0
  126. package/dist/core/hooks/citation-verify.js +112 -0
  127. package/dist/core/hooks/events.js +44 -0
  128. package/dist/core/hooks/index.js +15 -0
  129. package/dist/core/hooks/registry.js +213 -0
  130. package/dist/core/hooks/runner.js +236 -0
  131. package/dist/core/hooks/v2/event-emitter.js +115 -0
  132. package/dist/core/hooks/v2/executor.js +282 -0
  133. package/dist/core/hooks/v2/index.js +25 -0
  134. package/dist/core/hooks/v2/lifecycle.js +104 -0
  135. package/dist/core/hooks/v2/loader.js +216 -0
  136. package/dist/core/hooks/v2/matcher.js +125 -0
  137. package/dist/core/hooks/v2/trust.js +143 -0
  138. package/dist/core/hooks/v2/types.js +86 -0
  139. package/dist/core/image/renderer.js +71 -0
  140. package/dist/core/init/detector.js +582 -0
  141. package/dist/core/init/template-renderer.js +242 -0
  142. package/dist/core/jobs/registry.js +18 -18
  143. package/dist/core/ledger/results-tsv.js +142 -0
  144. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  145. package/dist/core/lsp/cache.js +105 -0
  146. package/dist/core/lsp/client.js +776 -0
  147. package/dist/core/lsp/language-detect.js +66 -0
  148. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  149. package/dist/core/lsp/symbol-tools.js +372 -0
  150. package/dist/core/mcp/client.js +97 -28
  151. package/dist/core/mcp/http-server.js +553 -0
  152. package/dist/core/mcp/orchestrator-tools.js +662 -0
  153. package/dist/core/mcp/permission.js +190 -0
  154. package/dist/core/mcp/registry.js +39 -17
  155. package/dist/core/mcp/server-tools.js +219 -0
  156. package/dist/core/mcp/server.js +397 -0
  157. package/dist/core/mcp/trust.js +10 -10
  158. package/dist/core/memory/dual-write.js +416 -0
  159. package/dist/core/memory/passive-extract.js +130 -0
  160. package/dist/core/memory/phase1-kinds.js +20 -0
  161. package/dist/core/memory/secret-scanner.js +304 -0
  162. package/dist/core/memory-sync/queue.js +170 -0
  163. package/dist/core/metrics/extract.js +113 -0
  164. package/dist/core/modes/roo-modes.js +68 -0
  165. package/dist/core/onboarding/ensure-initialized.js +133 -0
  166. package/dist/core/onboarding/marker.js +111 -0
  167. package/dist/core/onboarding/telemetry-state.js +108 -0
  168. package/dist/core/output-style/presets.js +176 -0
  169. package/dist/core/output-style/state.js +185 -0
  170. package/dist/core/path-security.js +287 -5
  171. package/dist/core/permission.js +82 -22
  172. package/dist/core/permissions/auto-classifier.js +124 -0
  173. package/dist/core/permissions/bash-parser.js +371 -0
  174. package/dist/core/permissions/circuit-breaker.js +83 -0
  175. package/dist/core/permissions/constrained-edit.js +91 -0
  176. package/dist/core/permissions/gate.js +278 -0
  177. package/dist/core/permissions/index.js +20 -0
  178. package/dist/core/permissions/mode.js +174 -0
  179. package/dist/core/permissions/network-egress.js +137 -0
  180. package/dist/core/permissions/state.js +241 -0
  181. package/dist/core/permissions/tool-class.js +93 -0
  182. package/dist/core/plan-mode/ui-state.js +51 -0
  183. package/dist/core/plans/plan-artifact.js +721 -0
  184. package/dist/core/policy-limits/etag-store.js +122 -0
  185. package/dist/core/prd-check/parser.js +215 -0
  186. package/dist/core/prd-check/reporter.js +127 -0
  187. package/dist/core/prd-check/session-review.js +557 -0
  188. package/dist/core/prd-check/verifiers.js +223 -0
  189. package/dist/core/prompt-cache/client-cache.js +99 -0
  190. package/dist/core/prompts/assembly.js +29 -0
  191. package/dist/core/prompts/registry.js +364 -0
  192. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  193. package/dist/core/pugi-md/context-injector.js +76 -0
  194. package/dist/core/pugi-md/walk-up.js +207 -0
  195. package/dist/core/python/uv-installer.js +270 -0
  196. package/dist/core/python/uv-resolver.js +83 -0
  197. package/dist/core/rate-limit/narrator.js +146 -0
  198. package/dist/core/recipes/cli-types.js +20 -0
  199. package/dist/core/recipes/loader.js +103 -0
  200. package/dist/core/recipes/runner.js +345 -0
  201. package/dist/core/recipes/schema.js +587 -0
  202. package/dist/core/release-notes/parser.js +241 -0
  203. package/dist/core/release-notes/state.js +116 -0
  204. package/dist/core/repl/ask.js +37 -37
  205. package/dist/core/repl/cancellation.js +26 -26
  206. package/dist/core/repl/cap-warning.js +4 -4
  207. package/dist/core/repl/clipboard-read.js +11 -11
  208. package/dist/core/repl/dispatch-fsm.js +12 -12
  209. package/dist/core/repl/history-search.js +15 -15
  210. package/dist/core/repl/history.js +28 -18
  211. package/dist/core/repl/kill-ring.js +5 -5
  212. package/dist/core/repl/model-pricing.js +135 -0
  213. package/dist/core/repl/privacy-banner.js +22 -22
  214. package/dist/core/repl/session.js +2157 -214
  215. package/dist/core/repl/slash-commands.js +533 -40
  216. package/dist/core/repl/store/index.js +1 -1
  217. package/dist/core/repl/store/jsonl-log.js +22 -22
  218. package/dist/core/repl/store/lockfile.js +10 -10
  219. package/dist/core/repl/store/session-store.js +136 -107
  220. package/dist/core/repl/store/types.js +15 -15
  221. package/dist/core/repl/store/uuid-v7.js +12 -12
  222. package/dist/core/repl/workspace-context.js +43 -21
  223. package/dist/core/repo-map/build.js +125 -0
  224. package/dist/core/repo-map/cache.js +185 -0
  225. package/dist/core/repo-map/extractor.js +254 -0
  226. package/dist/core/repo-map/formatter.js +145 -0
  227. package/dist/core/repo-map/page-rank.js +105 -0
  228. package/dist/core/repo-map/scanner.js +211 -0
  229. package/dist/core/retry-budget/budget.js +284 -0
  230. package/dist/core/retry-budget/index.js +5 -0
  231. package/dist/core/retry-budget/retry-cap.js +74 -0
  232. package/dist/core/routing/lead-worker.js +43 -0
  233. package/dist/core/routing/pre-flight-estimator.js +108 -0
  234. package/dist/core/runs/run-tree.js +103 -0
  235. package/dist/core/security/injection-scanner.js +367 -0
  236. package/dist/core/security/output-filter.js +418 -0
  237. package/dist/core/session/env-file.js +105 -0
  238. package/dist/core/session/section-budgets.js +140 -0
  239. package/dist/core/session.js +92 -0
  240. package/dist/core/settings.js +286 -5
  241. package/dist/core/share/formatter.js +271 -0
  242. package/dist/core/share/redactor.js +221 -0
  243. package/dist/core/share/uploader.js +267 -0
  244. package/dist/core/skills/defaults.js +457 -0
  245. package/dist/core/skills/loader.js +22 -22
  246. package/dist/core/skills/sources.js +27 -27
  247. package/dist/core/smoke/headless-driver.js +174 -0
  248. package/dist/core/smoke/orchestrator.js +194 -0
  249. package/dist/core/smoke/runner.js +238 -0
  250. package/dist/core/smoke/scenario-parser.js +316 -0
  251. package/dist/core/statusline.js +99 -0
  252. package/dist/core/subagents/dispatcher-real.js +600 -0
  253. package/dist/core/subagents/dispatcher.js +132 -43
  254. package/dist/core/subagents/index.js +19 -6
  255. package/dist/core/subagents/isolation-matrix.js +213 -0
  256. package/dist/core/subagents/spawn.js +19 -4
  257. package/dist/core/telemetry/emitter.js +229 -0
  258. package/dist/core/telemetry/queue.js +251 -0
  259. package/dist/core/theme/context.js +91 -0
  260. package/dist/core/theme/presets.js +228 -0
  261. package/dist/core/theme/state.js +181 -0
  262. package/dist/core/todos/invariant.js +10 -0
  263. package/dist/core/todos/state.js +177 -0
  264. package/dist/core/tool-schema/compressor.js +89 -0
  265. package/dist/core/transport/version-interceptor.js +166 -0
  266. package/dist/core/trust.js +2 -2
  267. package/dist/core/tui/thinking-block.js +64 -0
  268. package/dist/core/vim/keymap.js +288 -0
  269. package/dist/core/vim/state.js +92 -0
  270. package/dist/core/watch-markers/marker-watcher.js +133 -0
  271. package/dist/core/worktree-manager/cleanup.js +123 -0
  272. package/dist/core/worktree-manager/manager.js +303 -0
  273. package/dist/index.js +28 -0
  274. package/dist/runtime/bootstrap.js +190 -0
  275. package/dist/runtime/cli.js +4162 -488
  276. package/dist/runtime/commands/agents.js +30 -30
  277. package/dist/runtime/commands/budget.js +5 -5
  278. package/dist/runtime/commands/cancel.js +231 -0
  279. package/dist/runtime/commands/chain.js +489 -0
  280. package/dist/runtime/commands/codegraph-status.js +227 -0
  281. package/dist/runtime/commands/compact.js +297 -0
  282. package/dist/runtime/commands/config.js +32 -32
  283. package/dist/runtime/commands/cost.js +199 -0
  284. package/dist/runtime/commands/delegate.js +244 -13
  285. package/dist/runtime/commands/dispatch.js +126 -0
  286. package/dist/runtime/commands/doctor.js +579 -0
  287. package/dist/runtime/commands/feedback.js +184 -0
  288. package/dist/runtime/commands/hooks.js +184 -0
  289. package/dist/runtime/commands/init.js +254 -0
  290. package/dist/runtime/commands/lsp.js +368 -0
  291. package/dist/runtime/commands/mcp.js +879 -0
  292. package/dist/runtime/commands/memory.js +582 -0
  293. package/dist/runtime/commands/model.js +237 -0
  294. package/dist/runtime/commands/onboarding.js +275 -0
  295. package/dist/runtime/commands/patch.js +128 -0
  296. package/dist/runtime/commands/permissions.js +112 -0
  297. package/dist/runtime/commands/plan.js +143 -0
  298. package/dist/runtime/commands/prd-check.js +285 -0
  299. package/dist/runtime/commands/privacy.js +17 -17
  300. package/dist/runtime/commands/recipe.js +325 -0
  301. package/dist/runtime/commands/redo-blob-store.js +92 -0
  302. package/dist/runtime/commands/redo.js +361 -0
  303. package/dist/runtime/commands/release-notes.js +229 -0
  304. package/dist/runtime/commands/repo-map.js +95 -0
  305. package/dist/runtime/commands/report.js +299 -0
  306. package/dist/runtime/commands/resume.js +118 -0
  307. package/dist/runtime/commands/review-consensus.js +68 -53
  308. package/dist/runtime/commands/rewind.js +333 -0
  309. package/dist/runtime/commands/roster.js +14 -14
  310. package/dist/runtime/commands/sessions.js +163 -0
  311. package/dist/runtime/commands/share.js +316 -0
  312. package/dist/runtime/commands/skills.js +31 -31
  313. package/dist/runtime/commands/status.js +186 -0
  314. package/dist/runtime/commands/stickers.js +82 -0
  315. package/dist/runtime/commands/style.js +194 -0
  316. package/dist/runtime/commands/theme.js +196 -0
  317. package/dist/runtime/commands/undo.js +54 -22
  318. package/dist/runtime/commands/update.js +289 -0
  319. package/dist/runtime/commands/vim.js +140 -0
  320. package/dist/runtime/commands/worktree.js +177 -0
  321. package/dist/runtime/commands/worktrees.js +155 -0
  322. package/dist/runtime/headless-repl.js +195 -0
  323. package/dist/runtime/headless.js +543 -0
  324. package/dist/runtime/load-hooks-or-exit.js +71 -0
  325. package/dist/runtime/plan-decompose.js +531 -0
  326. package/dist/runtime/update-check.js +28 -28
  327. package/dist/runtime/version.js +65 -0
  328. package/dist/skills/bundled/batch.js +617 -0
  329. package/dist/skills/bundled/index.js +45 -0
  330. package/dist/skills/bundled/loop.js +358 -0
  331. package/dist/skills/bundled/remember.js +383 -0
  332. package/dist/skills/bundled/simplify.js +289 -0
  333. package/dist/skills/bundled/skillify.js +373 -0
  334. package/dist/skills/bundled/stuck.js +558 -0
  335. package/dist/skills/bundled/verify.js +439 -0
  336. package/dist/testing/vcr.js +486 -0
  337. package/dist/tools/agent-tool.js +229 -0
  338. package/dist/tools/apply-patch.js +556 -0
  339. package/dist/tools/ask-user-question.js +222 -0
  340. package/dist/tools/ask-user.js +115 -0
  341. package/dist/tools/bash.js +623 -45
  342. package/dist/tools/brief.js +224 -0
  343. package/dist/tools/enter-worktree.js +250 -0
  344. package/dist/tools/exit-worktree.js +147 -0
  345. package/dist/tools/file-tools.js +161 -44
  346. package/dist/tools/lsp-tools.js +189 -0
  347. package/dist/tools/mcp-tool.js +260 -0
  348. package/dist/tools/multi-edit.js +361 -0
  349. package/dist/tools/powershell.js +268 -0
  350. package/dist/tools/registry.js +85 -0
  351. package/dist/tools/skill-tool.js +96 -0
  352. package/dist/tools/sleep.js +99 -0
  353. package/dist/tools/synthetic-output.js +133 -0
  354. package/dist/tools/tasks.js +208 -0
  355. package/dist/tools/todo-write.js +184 -0
  356. package/dist/tools/verify-plan-execution.js +295 -0
  357. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  358. package/dist/tools/web-fetch.js +195 -10
  359. package/dist/tools/web-search.js +458 -0
  360. package/dist/tui/agent-progress-card.js +111 -0
  361. package/dist/tui/agent-tree.js +11 -1
  362. package/dist/tui/ask-modal.js +14 -14
  363. package/dist/tui/ask-user-question-prompt.js +203 -0
  364. package/dist/tui/compact-banner.js +81 -0
  365. package/dist/tui/conversation-pane.js +85 -11
  366. package/dist/tui/cost-table.js +111 -0
  367. package/dist/tui/device-flow.js +2 -2
  368. package/dist/tui/doctor-table.js +46 -0
  369. package/dist/tui/feedback-prompt.js +156 -0
  370. package/dist/tui/input-box.js +247 -32
  371. package/dist/tui/login-picker.js +3 -3
  372. package/dist/tui/markdown-render.js +6 -6
  373. package/dist/tui/onboarding-wizard.js +240 -0
  374. package/dist/tui/permissions-picker.js +86 -0
  375. package/dist/tui/render.js +35 -0
  376. package/dist/tui/repl-render.js +332 -54
  377. package/dist/tui/repl-splash-art.js +16 -16
  378. package/dist/tui/repl-splash-mascot.js +48 -24
  379. package/dist/tui/repl-splash.js +22 -22
  380. package/dist/tui/repl.js +124 -44
  381. package/dist/tui/slash-palette.js +6 -6
  382. package/dist/tui/splash.js +2 -2
  383. package/dist/tui/status-bar.js +109 -31
  384. package/dist/tui/status-table.js +7 -0
  385. package/dist/tui/stickers-art.js +136 -0
  386. package/dist/tui/style-table.js +28 -0
  387. package/dist/tui/theme-table.js +29 -0
  388. package/dist/tui/thinking-spinner.js +123 -0
  389. package/dist/tui/tool-stream-pane.js +53 -4
  390. package/dist/tui/update-banner.js +27 -2
  391. package/dist/tui/vim-input.js +267 -0
  392. package/dist/tui/welcome-banner.js +107 -0
  393. package/dist/tui/welcome-data.js +293 -0
  394. package/dist/tui/workspace-context.js +2 -2
  395. package/docs/examples/codegraph.mcp.json +10 -0
  396. package/package.json +23 -6
  397. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  398. package/test/scenarios/compact-force.scenario.txt +11 -0
  399. package/test/scenarios/identity.scenario.txt +11 -0
  400. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  401. package/test/scenarios/walkback.scenario.txt +12 -0
  402. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,458 @@
1
+ /**
2
+ * web_search tool — β1b T4 .
3
+ *
4
+ * One-shot web search via the Anvil-proxied Brave Search API. Mirrors
5
+ * the gate + SSRF + sanitize + rate-limit posture of
6
+ * `tools/web-fetch.ts`. Returns the top-N {title, url, snippet}
7
+ * results wrapped in an `<untrusted-search-NONCE>` sentinel so the
8
+ * downstream prompt treats every snippet as data, never as
9
+ * instructions.
10
+ *
11
+ * Why an Anvil proxy and not a direct Brave API call:
12
+ * - The Brave Search subscription key is server-side. Shipping it in
13
+ * a customer CLI bundle would burn the rotation budget the first
14
+ * time the binary lands on a phishing repo.
15
+ * - The Anvil proxy already terminates the customer's `PUGI_API_KEY`
16
+ * for tenancy + per-tier rate-limiting + audit logging. Routing
17
+ * search through the same boundary keeps the security model
18
+ * uniform with `pugi engine` and `pugi sync`.
19
+ * - For local dev / offline testing the env var
20
+ * `PUGI_SEARCH_PROXY_URL` overrides the default proxy endpoint;
21
+ * unit tests inject `_setSearchFetchForTests()` so no network is
22
+ * touched.
23
+ *
24
+ * Gate: the tool refuses unless one of:
25
+ * 1. `settings.web.search.enabled === true` (persistent opt-in)
26
+ * 2. `allowSearch === true` (CLI flag `--allow-search`, single dispatch)
27
+ *
28
+ * SSRF: the proxy URL itself runs through `validateHostnameForFetch`
29
+ * from `web-fetch.ts` so a configured `PUGI_SEARCH_PROXY_URL=
30
+ * http://169.254.169.254/...` cannot ride to cloud metadata. We also
31
+ * apply the β1b #62 DNS-rebinding guard via the shared
32
+ * `pinnedAddressAgent` helper (web-fetch.ts) — the same SSRF window
33
+ * that fetch closes, search closes.
34
+ *
35
+ * Rate limit: in-memory session bucket — 5 requests per rolling 60 s
36
+ * keyed by `sessionId`. Burst above the cap returns
37
+ * `{ ok: false, error: 'rate_limited' }` immediately, no network
38
+ * dispatch. Per-session because a noisy session should not starve a
39
+ * quieter one in the same workstation process.
40
+ *
41
+ * Response size cap: hard 1 MiB ceiling on the proxy body. Anything
42
+ * larger drops the entire response with `oversized_response` — the
43
+ * proxy already normalizes Brave's payload to a small JSON shape, so
44
+ * 1 MiB is generous for the typical 10-result payload (~5 KiB).
45
+ *
46
+ * Sanitization: snippet text is stripped of HTML tags before being
47
+ * surfaced to the model. Brave returns raw HTML in `description` for
48
+ * highlight markup; we keep the visible text and drop every `<...>`
49
+ * span via a conservative regex. Cross-checked by the `script/style`
50
+ * spec which asserts a hostile `<script>alert(1)</script>` snippet
51
+ * lands as the inner text only.
52
+ *
53
+ * Brand voice: brief / dispatch / ship / sentinel only — brandbook §08.
54
+ */
55
+ import { request, Agent } from 'undici';
56
+ import { randomBytes } from 'node:crypto';
57
+ import { validateHostnameForFetch, validateIpLiteralForFetch } from './web-fetch.js';
58
+ let activeFetch = null;
59
+ /**
60
+ * Test seam — pass `null` to restore the production undici dispatcher.
61
+ * Mirrors the `_setLookupForTests` pattern from `web-fetch.ts`.
62
+ */
63
+ export function _setSearchFetchForTests(fn) {
64
+ activeFetch = fn;
65
+ }
66
+ const SEARCH_TIMEOUT_MS = 10_000;
67
+ const MAX_RESPONSE_BYTES = 1 * 1024 * 1024; // 1 MiB hard cap
68
+ const MAX_RESULTS = 10;
69
+ const DEFAULT_RESULTS = 10;
70
+ const MAX_QUERY_LEN = 256;
71
+ const RATE_LIMIT_WINDOW_MS = 60_000;
72
+ const RATE_LIMIT_MAX = 5;
73
+ const USER_AGENT = 'pugi-cli/0.1 (+https://pugi.dev)';
74
+ const DEFAULT_PROXY_URL = 'https://api.pugi.io/api/pugi/web-search';
75
+ export function isWebSearchEnabled(ctx) {
76
+ if (ctx.allowSearch === true)
77
+ return true;
78
+ return ctx.settings.web?.search?.enabled === true;
79
+ }
80
+ const rateBuckets = new Map();
81
+ /**
82
+ * β1b r1: Map sweep cap so a long-running engine session that recycles
83
+ * sessionIds (or a misconfigured caller that passes per-call random
84
+ * ids) cannot grow `rateBuckets` unbounded. 1024 is well above any
85
+ * realistic concurrent-session count for a single CLI process; when
86
+ * we cross the cap we sweep expired buckets first, and if the Map is
87
+ * still over the cap we evict the oldest-touched bucket. Pure LRU
88
+ * would need another timestamp; oldest-by-insertion (Map iteration
89
+ * order) is close enough for a defense-in-depth memory bound.
90
+ */
91
+ const RATE_BUCKETS_CAP = 1024;
92
+ /**
93
+ * Returns true when the dispatch is allowed and records the call;
94
+ * returns false when the per-session bucket is at cap. Pure
95
+ * in-process; CLI dispatch is short-lived so a Map is sufficient.
96
+ *
97
+ * Exported for spec reset between test cases.
98
+ */
99
+ export function _checkRateLimit(sessionId, now = Date.now()) {
100
+ if (!sessionId)
101
+ return true;
102
+ let bucket = rateBuckets.get(sessionId);
103
+ if (!bucket) {
104
+ // β1b r1: sweep before allocating a new bucket so we never grow
105
+ // past the cap. Sweep is O(n) but only runs when we are about to
106
+ // breach the cap, so per-call cost stays effectively constant.
107
+ if (rateBuckets.size >= RATE_BUCKETS_CAP) {
108
+ sweepRateBuckets(now);
109
+ }
110
+ bucket = { timestamps: [] };
111
+ rateBuckets.set(sessionId, bucket);
112
+ }
113
+ const cutoff = now - RATE_LIMIT_WINDOW_MS;
114
+ bucket.timestamps = bucket.timestamps.filter((ts) => ts > cutoff);
115
+ if (bucket.timestamps.length >= RATE_LIMIT_MAX)
116
+ return false;
117
+ bucket.timestamps.push(now);
118
+ return true;
119
+ }
120
+ /**
121
+ * Drop expired buckets (all timestamps older than the rate-limit
122
+ * window) and, if still over the cap, evict the oldest-inserted
123
+ * buckets until under cap. Memory-bound only — never affects rate
124
+ * limit correctness for live sessions.
125
+ */
126
+ function sweepRateBuckets(now) {
127
+ const cutoff = now - RATE_LIMIT_WINDOW_MS;
128
+ for (const [id, b] of rateBuckets) {
129
+ if (b.timestamps.every((ts) => ts <= cutoff)) {
130
+ rateBuckets.delete(id);
131
+ }
132
+ }
133
+ while (rateBuckets.size >= RATE_BUCKETS_CAP) {
134
+ const oldest = rateBuckets.keys().next();
135
+ if (oldest.done)
136
+ break;
137
+ rateBuckets.delete(oldest.value);
138
+ }
139
+ }
140
+ /** Test seam — clear rate buckets between specs. */
141
+ export function _resetRateLimitForTests() {
142
+ rateBuckets.clear();
143
+ }
144
+ /* ----------------------- sanitization ---------------------- */
145
+ /**
146
+ * Strip HTML tags + collapse whitespace. Brave's `description` carries
147
+ * highlight markup (`<strong>`); we keep the inner text only so the
148
+ * model never sees raw HTML it could mistake for a directive. The
149
+ * regex is conservative: `<[^>]*>` removes opening + closing tags +
150
+ * self-closing tags + comments + processing instructions. Script
151
+ * BODIES are removed via a second pass that drops the entire
152
+ * `<script>...</script>` and `<style>...</style>` block (case-
153
+ * insensitive) so even a server-rendered snippet cannot leak a
154
+ * `console.log` line into the model context.
155
+ */
156
+ export function sanitizeSnippet(input) {
157
+ if (!input)
158
+ return '';
159
+ // Drop full script + style blocks (with their bodies). The
160
+ // non-greedy `.*?` handles "..." with embedded newlines via the `s`
161
+ // flag.
162
+ const noScripts = input
163
+ .replace(/<script\b[^>]*>.*?<\/script>/gis, '')
164
+ .replace(/<style\b[^>]*>.*?<\/style>/gis, '');
165
+ // Drop any remaining tag.
166
+ const noTags = noScripts.replace(/<[^>]*>/g, '');
167
+ // Collapse whitespace runs. Brave already trims but defensive.
168
+ return noTags.replace(/\s+/g, ' ').trim();
169
+ }
170
+ /**
171
+ * HTML-escape for sentinel body — mirrors `escapeForSentinelBody` from
172
+ * web-fetch.ts. Kept local to avoid coupling the modules tighter than
173
+ * the SSRF helpers already do.
174
+ */
175
+ function escapeForSentinelBody(input) {
176
+ return input
177
+ .replace(/&/g, '&amp;')
178
+ .replace(/</g, '&lt;')
179
+ .replace(/>/g, '&gt;')
180
+ .replace(/"/g, '&quot;')
181
+ .replace(/'/g, '&#39;');
182
+ }
183
+ /* ----------------------- dispatcher ---------------------- */
184
+ /**
185
+ * Resolve the search proxy URL. `PUGI_SEARCH_PROXY_URL` env override
186
+ * lets local dev point at a fixture server; production hits
187
+ * `api.pugi.io`.
188
+ */
189
+ function resolveProxyUrl() {
190
+ const env = process.env.PUGI_SEARCH_PROXY_URL;
191
+ if (typeof env === 'string' && env.length > 0)
192
+ return env;
193
+ return DEFAULT_PROXY_URL;
194
+ }
195
+ /**
196
+ * Production fetch via undici with the β1b #62 DNS-rebinding guard
197
+ * applied. We resolve the proxy hostname, validate against the SSRF
198
+ * blocklist, then dispatch via an Agent pinned to the resolved IP so
199
+ * a hostile DNS server cannot answer a different address on the
200
+ * connect(2) than on the lookup.
201
+ */
202
+ async function productionFetch(url, init) {
203
+ // β1b #62 — pinned-address Dispatcher to close the lookup→connect
204
+ // race. The Agent's `connect.lookup` always returns the address we
205
+ // already validated, so DNS cannot flip between the SSRF guard and
206
+ // the socket creation. Agent is per-call (small object) so process
207
+ // teardown does not need to keep a long-lived dispatcher around.
208
+ const parsed = new URL(url);
209
+ const hostname = parsed.hostname.replace(/^\[|\]$/g, '');
210
+ const dnsGuard = await validateHostnameForFetch(hostname);
211
+ if (dnsGuard) {
212
+ throw new Error(`SSRF refused: ${dnsGuard}`);
213
+ }
214
+ const { lookup: dnsLookup } = await import('node:dns/promises');
215
+ const answers = await dnsLookup(hostname, { all: true, verbatim: true });
216
+ if (answers.length === 0) {
217
+ throw new Error(`DNS returned no answers for ${hostname}`);
218
+ }
219
+ // Pin the first answer of THIS lookup, then re-validate against the
220
+ // SSRF blocklist. β1b r1: closes the DNS rebinding window where a
221
+ // hostile resolver returns public IPs to `validateHostnameForFetch`
222
+ // above and private IPs here. The lookup that feeds the pin is the
223
+ // lookup whose answer the connect(2) will actually use; without a
224
+ // re-check on this literal the original guard's IP set can diverge
225
+ // from the IP we lock into `connect.lookup`.
226
+ const pinned = answers[0];
227
+ if (!pinned) {
228
+ throw new Error(`DNS returned no answers for ${hostname}`);
229
+ }
230
+ const ipCheck = validateIpLiteralForFetch(pinned.address, pinned.family);
231
+ if (ipCheck !== null) {
232
+ throw new Error(`SSRF refused: ssrf_pinned_address_blocked: ${ipCheck}`);
233
+ }
234
+ const agent = new Agent({
235
+ connect: {
236
+ // Force the connect path to use the pre-resolved address. undici
237
+ // accepts the standard Node `dns.LookupFunction` shape here.
238
+ lookup: (_h, _opts, cb) => {
239
+ cb(null, pinned.address, pinned.family);
240
+ },
241
+ },
242
+ });
243
+ try {
244
+ return await request(url, { ...init, dispatcher: agent });
245
+ }
246
+ finally {
247
+ await agent.close().catch(() => {
248
+ /* best-effort */
249
+ });
250
+ }
251
+ }
252
+ async function readBodyWithCap(body, controller) {
253
+ const chunks = [];
254
+ let total = 0;
255
+ try {
256
+ for await (const chunk of body) {
257
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
258
+ total += buf.length;
259
+ if (total > MAX_RESPONSE_BYTES) {
260
+ controller.abort();
261
+ try {
262
+ if (typeof body.destroy === 'function')
263
+ body.destroy();
264
+ }
265
+ catch {
266
+ /* ignore — already aborted */
267
+ }
268
+ return { ok: false, error: `oversized_response: > ${MAX_RESPONSE_BYTES} bytes` };
269
+ }
270
+ chunks.push(buf);
271
+ }
272
+ }
273
+ catch (error) {
274
+ const msg = error instanceof Error ? error.message : String(error);
275
+ return { ok: false, error: `body_read_failed: ${msg}` };
276
+ }
277
+ return { ok: true, buffer: Buffer.concat(chunks) };
278
+ }
279
+ /**
280
+ * Validate + clamp the optional `count` arg. Clamp into [1, MAX_RESULTS]
281
+ * and default to DEFAULT_RESULTS when missing. The bridge already
282
+ * validates the type — this enforces the bounds.
283
+ */
284
+ function resolveCount(raw) {
285
+ if (raw === undefined)
286
+ return DEFAULT_RESULTS;
287
+ if (raw < 1)
288
+ return 1;
289
+ if (raw > MAX_RESULTS)
290
+ return MAX_RESULTS;
291
+ return Math.floor(raw);
292
+ }
293
+ /**
294
+ * Sanitize the operator query before it crosses the proxy boundary.
295
+ * Trim, hard-cap at MAX_QUERY_LEN, reject empty after trim. The proxy
296
+ * also validates, but a CLI-side check keeps the model's mistake
297
+ * visible to the operator (the failed call shows up in
298
+ * `.pugi/events.jsonl`) rather than blamed on a 400 from the proxy.
299
+ */
300
+ function sanitizeQuery(raw) {
301
+ const trimmed = raw.trim();
302
+ if (trimmed.length === 0) {
303
+ throw new Error('empty_query');
304
+ }
305
+ if (trimmed.length > MAX_QUERY_LEN) {
306
+ return trimmed.slice(0, MAX_QUERY_LEN);
307
+ }
308
+ return trimmed;
309
+ }
310
+ export async function webSearchTool(input, ctx) {
311
+ if (!isWebSearchEnabled(ctx)) {
312
+ return {
313
+ ok: false,
314
+ error: 'web_search disabled. Enable with --allow-search or set web.search.enabled=true in .pugi/settings.json.',
315
+ };
316
+ }
317
+ let query;
318
+ try {
319
+ query = sanitizeQuery(input.query);
320
+ }
321
+ catch (error) {
322
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
323
+ }
324
+ if (!_checkRateLimit(ctx.sessionId)) {
325
+ return {
326
+ ok: false,
327
+ error: `rate_limited: max ${RATE_LIMIT_MAX} searches per ${RATE_LIMIT_WINDOW_MS / 1000}s per session`,
328
+ };
329
+ }
330
+ const count = resolveCount(input.count);
331
+ const proxyUrl = resolveProxyUrl();
332
+ // Validate the proxy URL parses + scheme + SSRF. Even though the
333
+ // env var is operator-controlled, a careless export of
334
+ // `http://localhost:8080` would otherwise let an unrelated local
335
+ // server intercept the call. Default URL is `api.pugi.io` and
336
+ // resolves clean.
337
+ let parsedProxy;
338
+ try {
339
+ parsedProxy = new URL(proxyUrl);
340
+ }
341
+ catch {
342
+ return { ok: false, error: `invalid_proxy_url: ${proxyUrl}` };
343
+ }
344
+ if (parsedProxy.protocol !== 'http:' && parsedProxy.protocol !== 'https:') {
345
+ return { ok: false, error: `unsupported_proxy_scheme: ${parsedProxy.protocol}` };
346
+ }
347
+ const proxyHost = parsedProxy.hostname.replace(/^\[|\]$/g, '');
348
+ // SSRF guard applies to the proxy hostname even when activeFetch is
349
+ // a test stub — keeps the spec contract honest. Tests that exercise
350
+ // the rate limiter use a stub that bypasses productionFetch but the
351
+ // guard still runs at the entry above test stubs.
352
+ const ssrfErr = await validateHostnameForFetch(proxyHost);
353
+ if (ssrfErr) {
354
+ return { ok: false, error: `SSRF refused: ${ssrfErr}` };
355
+ }
356
+ const controller = new AbortController();
357
+ const timer = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS);
358
+ // β1b: lifted PUGI_API_KEY from the runtime credential store would
359
+ // belong here for production — for the proxy call we forward whatever
360
+ // PUGI_API_KEY the operator has exported. Anonymous proxy access is
361
+ // permitted; the proxy treats it as the free-tier search quota.
362
+ const apiKey = process.env.PUGI_API_KEY ?? process.env.PUGI_LOGIN_TOKEN;
363
+ const headers = {
364
+ 'user-agent': USER_AGENT,
365
+ 'content-type': 'application/json',
366
+ accept: 'application/json',
367
+ };
368
+ if (typeof apiKey === 'string' && apiKey.length > 0) {
369
+ headers.authorization = `Bearer ${apiKey}`;
370
+ }
371
+ let response;
372
+ try {
373
+ const fetcher = activeFetch ?? (async (u, i) => (await productionFetch(u, i)));
374
+ response = (await fetcher(proxyUrl, {
375
+ method: 'POST',
376
+ headers,
377
+ body: JSON.stringify({ query, count }),
378
+ signal: controller.signal,
379
+ }));
380
+ }
381
+ catch (error) {
382
+ clearTimeout(timer);
383
+ const message = error instanceof Error ? error.message : String(error);
384
+ return { ok: false, error: `search_failed: ${message}` };
385
+ }
386
+ finally {
387
+ clearTimeout(timer);
388
+ }
389
+ if (response.statusCode < 200 || response.statusCode >= 300) {
390
+ // Drain the body so the socket can be reused / released.
391
+ try {
392
+ if (typeof response.body.destroy === 'function')
393
+ response.body.destroy();
394
+ }
395
+ catch {
396
+ /* ignore */
397
+ }
398
+ return { ok: false, error: `proxy_http_${response.statusCode}` };
399
+ }
400
+ const bodyResult = await readBodyWithCap(response.body, controller);
401
+ if (!bodyResult.ok)
402
+ return bodyResult;
403
+ let parsed;
404
+ try {
405
+ parsed = JSON.parse(bodyResult.buffer.toString('utf8'));
406
+ }
407
+ catch (error) {
408
+ const msg = error instanceof Error ? error.message : String(error);
409
+ return { ok: false, error: `proxy_malformed_json: ${msg}` };
410
+ }
411
+ // Expect the proxy to normalize Brave's payload to `{results: [{title, url, snippet}]}`.
412
+ // When the proxy is missing the `results` array (or it is not an array)
413
+ // we surface a clean error instead of returning empty.
414
+ const obj = parsed;
415
+ if (!obj || typeof obj !== 'object') {
416
+ return { ok: false, error: 'proxy_malformed_response: not an object' };
417
+ }
418
+ const rawResults = obj.results;
419
+ if (!Array.isArray(rawResults)) {
420
+ return { ok: false, error: 'proxy_malformed_response: missing results array' };
421
+ }
422
+ const results = [];
423
+ for (const r of rawResults.slice(0, count)) {
424
+ if (!r || typeof r !== 'object')
425
+ continue;
426
+ const row = r;
427
+ const title = typeof row.title === 'string' ? sanitizeSnippet(row.title) : '';
428
+ const url = typeof row.url === 'string' ? row.url : '';
429
+ const snippet = typeof row.snippet === 'string' ? sanitizeSnippet(row.snippet) : '';
430
+ if (!url)
431
+ continue;
432
+ results.push({ title, url, snippet });
433
+ }
434
+ // Per-call nonce for the sentinel. Mirrors `web-fetch.ts` so the
435
+ // downstream prompt's nonce-matching escape logic works uniformly.
436
+ const nonce = randomBytes(8).toString('hex');
437
+ const renderedLines = [`<untrusted-search-${nonce}>`, `Query: ${escapeForSentinelBody(query)}`, ''];
438
+ for (let i = 0; i < results.length; i += 1) {
439
+ const r = results[i];
440
+ if (!r)
441
+ continue;
442
+ renderedLines.push(`${i + 1}. ${escapeForSentinelBody(r.title || r.url)}`);
443
+ renderedLines.push(` ${escapeForSentinelBody(r.url)}`);
444
+ if (r.snippet)
445
+ renderedLines.push(` ${escapeForSentinelBody(r.snippet)}`);
446
+ renderedLines.push('');
447
+ }
448
+ renderedLines.push(`</untrusted-search-${nonce}>`);
449
+ const content_md = renderedLines.join('\n').trimEnd();
450
+ return {
451
+ ok: true,
452
+ query,
453
+ results,
454
+ content_md,
455
+ fetched_at: new Date().toISOString(),
456
+ };
457
+ }
458
+ //# sourceMappingURL=web-search.js.map
@@ -0,0 +1,111 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ /** Width of the progress bar in display cells. Tuned to fit comfortably
4
+ * inside an 80-col terminal alongside the percent label. */
5
+ export const PROGRESS_BAR_WIDTH = 24;
6
+ /** Max milestone rows the card renders before collapsing to the footer
7
+ * summary. Matches the CC `/compact` cutoff. */
8
+ export const MAX_VISIBLE_MILESTONES = 5;
9
+ const STATUS_GLYPH = {
10
+ done: '◼',
11
+ active: '▸',
12
+ pending: '◻',
13
+ };
14
+ const STATUS_COLOR = {
15
+ done: 'green',
16
+ active: 'yellow',
17
+ pending: 'gray',
18
+ };
19
+ const HEADER_DOT_COLOR = {
20
+ running: 'cyan',
21
+ completed: 'green',
22
+ failed: 'red',
23
+ };
24
+ /**
25
+ * Build the unicode progress bar. Exported для тесты — guarantees the
26
+ * filled/empty counts match the percent under all rounding edges.
27
+ */
28
+ export function renderProgressBarCells(percent, width = PROGRESS_BAR_WIDTH) {
29
+ const safePercent = Math.max(0, Math.min(100, percent));
30
+ const cells = Math.round((safePercent / 100) * width);
31
+ const clamped = Math.max(0, Math.min(width, cells));
32
+ return {
33
+ filled: '▰'.repeat(clamped),
34
+ empty: '▱'.repeat(width - clamped),
35
+ cells: clamped,
36
+ };
37
+ }
38
+ /**
39
+ * Format milliseconds as the `Hh Mm Ss` / `Mm Ss` / `Ss` label.
40
+ * Mirrors the rule used by status-bar elapsed slot.
41
+ */
42
+ export function formatElapsed(ms) {
43
+ const total = Math.max(0, Math.floor(ms / 1000));
44
+ const h = Math.floor(total / 3600);
45
+ const m = Math.floor((total % 3600) / 60);
46
+ const s = total % 60;
47
+ if (h > 0)
48
+ return `${h}h ${m}m ${s}s`;
49
+ if (m > 0)
50
+ return `${m}m ${s}s`;
51
+ return `${s}s`;
52
+ }
53
+ /**
54
+ * Format a raw token count as `21.7k` / `3.4M` / `812`. Mirrors the
55
+ * formatter in `core/repl/model-pricing.ts` so both surfaces stay
56
+ * visually consistent without coupling.
57
+ */
58
+ export function formatTokenCount(n) {
59
+ if (n === undefined)
60
+ return undefined;
61
+ if (n < 1_000)
62
+ return `${n}`;
63
+ if (n < 1_000_000) {
64
+ const k = n / 1_000;
65
+ return `${k >= 10 ? k.toFixed(1).replace(/\.0$/, '') : k.toFixed(1)}k`;
66
+ }
67
+ const m = n / 1_000_000;
68
+ return `${m >= 10 ? m.toFixed(1).replace(/\.0$/, '') : m.toFixed(1)}M`;
69
+ }
70
+ /**
71
+ * Compute the "… +N pending, M completed" footer counts. When the
72
+ * agent supplied rollups they win; otherwise we derive from the
73
+ * milestone array.
74
+ */
75
+ export function computeFooterCounts(milestones, visibleCount, rollup) {
76
+ const pending = rollup.pendingCount
77
+ ?? milestones.filter((m) => m.status === 'pending').length;
78
+ const completed = rollup.completedCount
79
+ ?? milestones.filter((m) => m.status === 'done').length;
80
+ const hidden = Math.max(0, milestones.length - visibleCount);
81
+ return { pending, completed, hidden };
82
+ }
83
+ function MilestoneRow({ milestone }) {
84
+ const glyph = STATUS_GLYPH[milestone.status];
85
+ const color = STATUS_COLOR[milestone.status];
86
+ // Truncate to 64 chars so a verbose label can't wrap and break the
87
+ // grid layout in the watcher.
88
+ const label = milestone.label.length > 64
89
+ ? `${milestone.label.slice(0, 63)}…`
90
+ : milestone.label;
91
+ return (_jsxs(Box, { children: [_jsx(Text, { children: ' ' }), _jsx(Text, { color: color, children: glyph }), _jsx(Text, { children: " " }), _jsx(Text, { color: color === 'gray' ? 'gray' : undefined, dimColor: milestone.status === 'pending', children: label })] }));
92
+ }
93
+ export function AgentProgressCard({ progress, nowEpochMs, }) {
94
+ // Re-derive elapsed from the wall clock when the parent supplied it;
95
+ // this is what makes the card tick once a second without the writer
96
+ // re-emitting JSON every tick.
97
+ const elapsed = nowEpochMs !== undefined
98
+ ? Math.max(progress.elapsedMs, nowEpochMs - Date.parse(progress.startedAt))
99
+ : progress.elapsedMs;
100
+ const bar = renderProgressBarCells(progress.percentComplete);
101
+ const percentLabel = `${Math.round(Math.max(0, Math.min(100, progress.percentComplete)))}%`;
102
+ const tokensLabel = formatTokenCount(progress.tokensUsed);
103
+ const dotColor = HEADER_DOT_COLOR[progress.status];
104
+ const visibleMilestones = progress.milestones.slice(0, MAX_VISIBLE_MILESTONES);
105
+ const footer = computeFooterCounts(progress.milestones, visibleMilestones.length, { pendingCount: progress.pendingCount, completedCount: progress.completedCount });
106
+ // CC compact pattern: header has a leading `· ` glyph + the task label.
107
+ // We append `…` only while running (matches CC's "Compacting…" verb form).
108
+ const headerVerb = progress.status === 'running' ? '…' : '';
109
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 0, marginBottom: 1, children: [_jsxs(Box, { children: [_jsx(Text, { color: dotColor, children: '· ' }), _jsx(Text, { bold: true, children: progress.agentType }), _jsx(Text, { children: " " }), _jsxs(Text, { children: [progress.task, headerVerb] }), _jsxs(Text, { dimColor: true, children: [' (', formatElapsed(elapsed), tokensLabel ? ` · ↑ ${tokensLabel} tokens` : '', ')'] })] }), _jsxs(Box, { children: [_jsx(Text, { children: ' ' }), _jsx(Text, { color: "cyan", children: bar.filled }), _jsx(Text, { dimColor: true, children: bar.empty }), _jsxs(Text, { children: [' ', percentLabel] })] }), progress.stepDescription ? (_jsxs(Box, { children: [_jsx(Text, { children: ' ' }), _jsxs(Text, { dimColor: true, children: ["step ", progress.currentStep, "/", progress.totalSteps, ": ", progress.stepDescription] })] })) : null, visibleMilestones.length > 0 ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { children: ' ' }), _jsx(Text, { dimColor: true, children: "\u23BF" })] }), visibleMilestones.map((m, i) => (_jsx(MilestoneRow, { milestone: m }, `${m.label}-${i}`))), footer.hidden > 0 ? (_jsxs(Box, { children: [_jsx(Text, { children: ' ' }), _jsxs(Text, { dimColor: true, children: ["\u2026 +", footer.pending, " pending, ", footer.completed, " completed"] })] })) : null] })) : null] }));
110
+ }
111
+ //# sourceMappingURL=agent-progress-card.js.map
@@ -16,7 +16,7 @@ function AgentRow({ agent, last, nowEpochMs, }) {
16
16
  const name = agent.personaName.padEnd(8, ' ');
17
17
  const role = agent.role.padEnd(10, ' ');
18
18
  const detail = agent.detail.length > 60 ? `${agent.detail.slice(0, 57)}…` : agent.detail;
19
- return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: ` ${branch} ` }), _jsx(Text, { bold: true, children: `${name}` }), _jsx(Text, { dimColor: true, children: ` ${role} ` }), _jsx(Text, { color: glyphColor, children: glyph }), _jsx(Text, { children: ` ${detail}` }), _jsx(Text, { dimColor: true, children: ` (${elapsed}${tokens ? ` · ↓ ${tokens}` : ''})` })] }));
19
+ return (_jsxs(Box, { children: [_jsx(Text, { dimColor: true, children: ` ${branch} ` }), _jsx(Text, { bold: true, children: `${name}` }), _jsx(Text, { dimColor: true, children: ` ${role} ` }), _jsx(Text, { color: glyphColor, children: glyph }), _jsx(Text, { children: ` ${detail}` }), _jsx(Text, { dimColor: true, children: ` (${elapsed}${tokens ? ` · ↓ ${tokens}` : ''})` })] }));
20
20
  }
21
21
  function statusGlyph(status) {
22
22
  switch (status) {
@@ -26,6 +26,13 @@ function statusGlyph(status) {
26
26
  return '⏳';
27
27
  case 'shipped':
28
28
  return '✓';
29
+ // — `replied` = orchestrator finished talking but no
30
+ // tool/delegate side-effect emitted. Distinct arrow glyph so the
31
+ // operator can tell at a glance that no real work shipped, even
32
+ // though the LLM completed cleanly. Defeats the fake-shipped
33
+ // anti-pattern (memory feedback_no_fake_dispatch_promises).
34
+ case 'replied':
35
+ return '→';
29
36
  case 'blocked':
30
37
  return '✗';
31
38
  case 'failed':
@@ -40,6 +47,9 @@ function statusColor(status) {
40
47
  return 'cyan';
41
48
  case 'shipped':
42
49
  return 'green';
50
+ // Dim/grey-ish — succeeded technically but no work was shipped.
51
+ case 'replied':
52
+ return 'gray';
43
53
  case 'blocked':
44
54
  return 'yellow';
45
55
  case 'failed':