@pugi/cli 0.1.0-beta.5 → 0.1.0-beta.51

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 (264) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -25
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/jobs-watch.js +201 -0
  6. package/dist/commands/jobs.js +15 -0
  7. package/dist/commands/smoke.js +133 -0
  8. package/dist/core/agent-progress/cleanup.js +134 -0
  9. package/dist/core/agent-progress/schema.js +144 -0
  10. package/dist/core/agent-progress/writer.js +101 -0
  11. package/dist/core/artifact-chain/dispatcher.js +148 -0
  12. package/dist/core/artifact-chain/exporter.js +164 -0
  13. package/dist/core/artifact-chain/state.js +243 -0
  14. package/dist/core/artifact-chain/steps.js +169 -0
  15. package/dist/core/auth/ensure-authenticated.js +129 -0
  16. package/dist/core/auth/env-provider.js +238 -0
  17. package/dist/core/auto-update/channels.js +122 -0
  18. package/dist/core/auto-update/checker.js +241 -0
  19. package/dist/core/auto-update/state.js +235 -0
  20. package/dist/core/bare-mode/index.js +107 -0
  21. package/dist/core/bash-classifier.js +400 -4
  22. package/dist/core/checkpoint/resumer.js +149 -0
  23. package/dist/core/checkpoint/rewinder.js +291 -0
  24. package/dist/core/codegraph/decision-store.js +248 -0
  25. package/dist/core/codegraph/detect-repo.js +459 -0
  26. package/dist/core/codegraph/install.js +134 -0
  27. package/dist/core/codegraph/offer-hook.js +220 -0
  28. package/dist/core/compact/auto-trigger.js +96 -0
  29. package/dist/core/compact/buffer-rewriter.js +115 -0
  30. package/dist/core/compact/summarizer.js +208 -0
  31. package/dist/core/compact/token-counter.js +108 -0
  32. package/dist/core/consensus/diff-capture.js +112 -3
  33. package/dist/core/context/index.js +7 -0
  34. package/dist/core/context/markdown-traverse.js +255 -0
  35. package/dist/core/cost/rate-card.js +129 -0
  36. package/dist/core/cost/tracker.js +221 -0
  37. package/dist/core/denial-tracking/index.js +8 -0
  38. package/dist/core/denial-tracking/state.js +264 -0
  39. package/dist/core/diagnostics/probe-runner.js +93 -0
  40. package/dist/core/diagnostics/probes/api.js +46 -0
  41. package/dist/core/diagnostics/probes/auth.js +86 -0
  42. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  43. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  44. package/dist/core/diagnostics/probes/config.js +72 -0
  45. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  46. package/dist/core/diagnostics/probes/disk.js +81 -0
  47. package/dist/core/diagnostics/probes/git.js +65 -0
  48. package/dist/core/diagnostics/probes/hooks.js +118 -0
  49. package/dist/core/diagnostics/probes/mcp.js +75 -0
  50. package/dist/core/diagnostics/probes/node.js +59 -0
  51. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  52. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  53. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  54. package/dist/core/diagnostics/probes/session.js +74 -0
  55. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  56. package/dist/core/diagnostics/probes/workspace.js +63 -0
  57. package/dist/core/diagnostics/types.js +70 -0
  58. package/dist/core/dispatch/cache-cleanup.js +197 -0
  59. package/dist/core/dispatch/cache-handoff.js +295 -0
  60. package/dist/core/edits/dispatch.js +218 -2
  61. package/dist/core/edits/journal.js +199 -0
  62. package/dist/core/edits/layer-d-ast.js +557 -14
  63. package/dist/core/edits/verify-hook.js +273 -0
  64. package/dist/core/edits/worktree.js +322 -0
  65. package/dist/core/engine/anvil-client.js +115 -5
  66. package/dist/core/engine/auto-compact.js +179 -0
  67. package/dist/core/engine/budgets.js +155 -0
  68. package/dist/core/engine/context-prefix.js +155 -0
  69. package/dist/core/engine/intent.js +260 -0
  70. package/dist/core/engine/native-pugi.js +897 -211
  71. package/dist/core/engine/prompts.js +88 -2
  72. package/dist/core/engine/strip-internal-fields.js +124 -0
  73. package/dist/core/engine/tool-bridge.js +1045 -36
  74. package/dist/core/feedback/queue.js +177 -0
  75. package/dist/core/feedback/submitter.js +145 -0
  76. package/dist/core/file-cache.js +113 -1
  77. package/dist/core/hooks/events.js +44 -0
  78. package/dist/core/hooks/index.js +15 -0
  79. package/dist/core/hooks/registry.js +213 -0
  80. package/dist/core/hooks/runner.js +236 -0
  81. package/dist/core/hooks/v2/event-emitter.js +115 -0
  82. package/dist/core/hooks/v2/executor.js +282 -0
  83. package/dist/core/hooks/v2/index.js +25 -0
  84. package/dist/core/hooks/v2/lifecycle.js +104 -0
  85. package/dist/core/hooks/v2/loader.js +216 -0
  86. package/dist/core/hooks/v2/matcher.js +125 -0
  87. package/dist/core/hooks/v2/trust.js +143 -0
  88. package/dist/core/hooks/v2/types.js +86 -0
  89. package/dist/core/lsp/cache.js +105 -0
  90. package/dist/core/lsp/client.js +776 -0
  91. package/dist/core/lsp/language-detect.js +66 -0
  92. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  93. package/dist/core/mcp/client.js +75 -6
  94. package/dist/core/mcp/http-server.js +553 -0
  95. package/dist/core/mcp/orchestrator-tools.js +662 -0
  96. package/dist/core/mcp/permission.js +190 -0
  97. package/dist/core/mcp/registry.js +24 -2
  98. package/dist/core/mcp/server-tools.js +219 -0
  99. package/dist/core/mcp/server.js +397 -0
  100. package/dist/core/memory/dual-write.js +416 -0
  101. package/dist/core/memory/phase1-kinds.js +20 -0
  102. package/dist/core/memory-sync/queue.js +158 -0
  103. package/dist/core/onboarding/ensure-initialized.js +133 -0
  104. package/dist/core/onboarding/marker.js +111 -0
  105. package/dist/core/onboarding/telemetry-state.js +108 -0
  106. package/dist/core/output-style/presets.js +176 -0
  107. package/dist/core/output-style/state.js +185 -0
  108. package/dist/core/path-security.js +284 -2
  109. package/dist/core/permissions/auto-classifier.js +124 -0
  110. package/dist/core/permissions/circuit-breaker.js +83 -0
  111. package/dist/core/permissions/gate.js +278 -0
  112. package/dist/core/permissions/index.js +20 -0
  113. package/dist/core/permissions/mode.js +174 -0
  114. package/dist/core/permissions/state.js +241 -0
  115. package/dist/core/permissions/tool-class.js +93 -0
  116. package/dist/core/prd-check/parser.js +215 -0
  117. package/dist/core/prd-check/reporter.js +127 -0
  118. package/dist/core/prd-check/session-review.js +557 -0
  119. package/dist/core/prd-check/verifiers.js +223 -0
  120. package/dist/core/pugi-md/context-injector.js +76 -0
  121. package/dist/core/pugi-md/walk-up.js +207 -0
  122. package/dist/core/release-notes/parser.js +241 -0
  123. package/dist/core/release-notes/state.js +116 -0
  124. package/dist/core/repl/history.js +11 -1
  125. package/dist/core/repl/model-pricing.js +135 -0
  126. package/dist/core/repl/session.js +1897 -37
  127. package/dist/core/repl/slash-commands.js +430 -15
  128. package/dist/core/repl/store/session-store.js +31 -2
  129. package/dist/core/repl/workspace-context.js +22 -0
  130. package/dist/core/repo-map/build.js +125 -0
  131. package/dist/core/repo-map/cache.js +185 -0
  132. package/dist/core/repo-map/extractor.js +254 -0
  133. package/dist/core/repo-map/formatter.js +145 -0
  134. package/dist/core/repo-map/scanner.js +211 -0
  135. package/dist/core/retry-budget/budget.js +284 -0
  136. package/dist/core/retry-budget/index.js +5 -0
  137. package/dist/core/session.js +92 -0
  138. package/dist/core/settings.js +80 -0
  139. package/dist/core/share/formatter.js +271 -0
  140. package/dist/core/share/redactor.js +221 -0
  141. package/dist/core/share/uploader.js +267 -0
  142. package/dist/core/skills/defaults.js +457 -0
  143. package/dist/core/smoke/headless-driver.js +174 -0
  144. package/dist/core/smoke/orchestrator.js +194 -0
  145. package/dist/core/smoke/runner.js +238 -0
  146. package/dist/core/smoke/scenario-parser.js +316 -0
  147. package/dist/core/subagents/dispatcher-real.js +600 -0
  148. package/dist/core/subagents/dispatcher.js +113 -24
  149. package/dist/core/subagents/index.js +18 -5
  150. package/dist/core/subagents/isolation-matrix.js +213 -0
  151. package/dist/core/subagents/spawn.js +19 -4
  152. package/dist/core/telemetry/emitter.js +229 -0
  153. package/dist/core/telemetry/queue.js +251 -0
  154. package/dist/core/theme/context.js +91 -0
  155. package/dist/core/theme/presets.js +228 -0
  156. package/dist/core/theme/state.js +181 -0
  157. package/dist/core/todos/invariant.js +10 -0
  158. package/dist/core/todos/state.js +177 -0
  159. package/dist/core/transport/version-interceptor.js +166 -0
  160. package/dist/core/vim/keymap.js +288 -0
  161. package/dist/core/vim/state.js +92 -0
  162. package/dist/core/worktree-manager/cleanup.js +123 -0
  163. package/dist/core/worktree-manager/manager.js +303 -0
  164. package/dist/index.js +28 -0
  165. package/dist/runtime/bootstrap.js +190 -0
  166. package/dist/runtime/cli.js +3241 -343
  167. package/dist/runtime/commands/cancel.js +231 -0
  168. package/dist/runtime/commands/chain.js +489 -0
  169. package/dist/runtime/commands/codegraph-status.js +227 -0
  170. package/dist/runtime/commands/compact.js +297 -0
  171. package/dist/runtime/commands/cost.js +199 -0
  172. package/dist/runtime/commands/delegate.js +242 -11
  173. package/dist/runtime/commands/dispatch.js +126 -0
  174. package/dist/runtime/commands/doctor.js +412 -0
  175. package/dist/runtime/commands/feedback.js +184 -0
  176. package/dist/runtime/commands/hooks.js +184 -0
  177. package/dist/runtime/commands/lsp.js +368 -0
  178. package/dist/runtime/commands/mcp.js +879 -0
  179. package/dist/runtime/commands/memory.js +508 -0
  180. package/dist/runtime/commands/model.js +237 -0
  181. package/dist/runtime/commands/onboarding.js +275 -0
  182. package/dist/runtime/commands/patch.js +128 -0
  183. package/dist/runtime/commands/permissions.js +112 -0
  184. package/dist/runtime/commands/plan.js +143 -0
  185. package/dist/runtime/commands/prd-check.js +285 -0
  186. package/dist/runtime/commands/redo-blob-store.js +92 -0
  187. package/dist/runtime/commands/redo.js +361 -0
  188. package/dist/runtime/commands/release-notes.js +229 -0
  189. package/dist/runtime/commands/repo-map.js +95 -0
  190. package/dist/runtime/commands/report.js +299 -0
  191. package/dist/runtime/commands/resume.js +118 -0
  192. package/dist/runtime/commands/review-consensus.js +17 -2
  193. package/dist/runtime/commands/rewind.js +333 -0
  194. package/dist/runtime/commands/sessions.js +163 -0
  195. package/dist/runtime/commands/share.js +316 -0
  196. package/dist/runtime/commands/status.js +186 -0
  197. package/dist/runtime/commands/stickers.js +82 -0
  198. package/dist/runtime/commands/style.js +194 -0
  199. package/dist/runtime/commands/theme.js +196 -0
  200. package/dist/runtime/commands/undo.js +32 -0
  201. package/dist/runtime/commands/update.js +289 -0
  202. package/dist/runtime/commands/vim.js +140 -0
  203. package/dist/runtime/commands/worktree.js +177 -0
  204. package/dist/runtime/commands/worktrees.js +155 -0
  205. package/dist/runtime/headless-repl.js +195 -0
  206. package/dist/runtime/headless.js +543 -0
  207. package/dist/runtime/load-hooks-or-exit.js +71 -0
  208. package/dist/runtime/plan-decompose.js +531 -0
  209. package/dist/runtime/version.js +65 -0
  210. package/dist/tools/agent-tool.js +229 -0
  211. package/dist/tools/apply-patch.js +556 -0
  212. package/dist/tools/ask-user-question.js +213 -0
  213. package/dist/tools/ask-user.js +115 -0
  214. package/dist/tools/bash.js +203 -4
  215. package/dist/tools/file-tools.js +85 -14
  216. package/dist/tools/lsp-tools.js +189 -0
  217. package/dist/tools/mcp-tool.js +260 -0
  218. package/dist/tools/multi-edit.js +361 -0
  219. package/dist/tools/powershell.js +268 -0
  220. package/dist/tools/registry.js +51 -0
  221. package/dist/tools/skill-tool.js +96 -0
  222. package/dist/tools/tasks.js +208 -0
  223. package/dist/tools/todo-write.js +184 -0
  224. package/dist/tools/web-fetch.js +147 -2
  225. package/dist/tools/web-search.js +458 -0
  226. package/dist/tui/agent-progress-card.js +111 -0
  227. package/dist/tui/agent-tree.js +10 -0
  228. package/dist/tui/ask-modal.js +2 -2
  229. package/dist/tui/ask-user-question-prompt.js +192 -0
  230. package/dist/tui/compact-banner.js +81 -0
  231. package/dist/tui/conversation-pane.js +82 -8
  232. package/dist/tui/cost-table.js +111 -0
  233. package/dist/tui/doctor-table.js +46 -0
  234. package/dist/tui/feedback-prompt.js +156 -0
  235. package/dist/tui/input-box.js +218 -3
  236. package/dist/tui/markdown-render.js +4 -4
  237. package/dist/tui/onboarding-wizard.js +240 -0
  238. package/dist/tui/permissions-picker.js +86 -0
  239. package/dist/tui/render.js +35 -0
  240. package/dist/tui/repl-render.js +313 -35
  241. package/dist/tui/repl-splash-art.js +1 -1
  242. package/dist/tui/repl-splash-mascot.js +32 -8
  243. package/dist/tui/repl-splash.js +2 -2
  244. package/dist/tui/repl.js +85 -5
  245. package/dist/tui/splash.js +1 -1
  246. package/dist/tui/status-bar.js +94 -16
  247. package/dist/tui/status-table.js +7 -0
  248. package/dist/tui/stickers-art.js +136 -0
  249. package/dist/tui/style-table.js +28 -0
  250. package/dist/tui/theme-table.js +29 -0
  251. package/dist/tui/thinking-spinner.js +123 -0
  252. package/dist/tui/tool-stream-pane.js +52 -3
  253. package/dist/tui/update-banner.js +27 -2
  254. package/dist/tui/vim-input.js +267 -0
  255. package/dist/tui/welcome-banner.js +107 -0
  256. package/dist/tui/welcome-data.js +293 -0
  257. package/docs/examples/codegraph.mcp.json +10 -0
  258. package/package.json +13 -7
  259. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  260. package/test/scenarios/compact-force.scenario.txt +11 -0
  261. package/test/scenarios/identity.scenario.txt +11 -0
  262. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  263. package/test/scenarios/walkback.scenario.txt +12 -0
  264. package/dist/core/engine/compaction-hook.js +0 -154
package/dist/tui/repl.js CHANGED
@@ -26,9 +26,12 @@ import { ConversationPane } from './conversation-pane.js';
26
26
  import { InputBox } from './input-box.js';
27
27
  import { ReplSplash } from './repl-splash.js';
28
28
  import { StatusBar } from './status-bar.js';
29
+ import { ThinkingSpinner } from './thinking-spinner.js';
29
30
  import { ToolStreamPane } from './tool-stream-pane.js';
30
31
  import { UpdateBanner } from './update-banner.js';
32
+ import { WelcomeBanner } from './welcome-banner.js';
31
33
  import { collectWorkspaceContext } from './workspace-context.js';
34
+ import { useTheme } from '../core/theme/context.js';
32
35
  import { slugForCwd } from '../core/repl/history.js';
33
36
  import { SLASH_COMMAND_HELP, SLASH_COMMAND_GROUPS } from '../core/repl/slash-commands.js';
34
37
  const TICK_INTERVAL_MS = 200;
@@ -56,6 +59,12 @@ export function Repl(props) {
56
59
  // Tenant block crowding the top.
57
60
  const [splashVisible, setSplashVisible] = useState(false);
58
61
  const dismissSplash = useCallback(() => setSplashVisible(false), []);
62
+ // CEO P0 #2 (2026-05-29): CC-style welcome banner. Visible from boot
63
+ // until the operator submits the first brief OR the session emits
64
+ // its first agent event. The host owns dismissal lifecycle (kept
65
+ // symmetric with the splash) so the welcome card never lingers
66
+ // behind а live transcript.
67
+ const [welcomeVisible, setWelcomeVisible] = useState(Boolean(props.welcomeData));
59
68
  // α6.14 wave 3: workspace context snapshot for the status bar. We
60
69
  // read once at mount and freeze; a brand-new PUGI.md or skill is
61
70
  // surfaced on the next REPL boot rather than via a watcher.
@@ -99,6 +108,20 @@ export function Repl(props) {
99
108
  setSplashVisible(false);
100
109
  }
101
110
  }, [splashVisible, state.agents.length, state.transcript.length]);
111
+ // CEO P0 #2 (2026-05-29) v2: welcome banner stays until the operator
112
+ // actively engages the loop — first agent spawn. Boot-time auto-init
113
+ // emits system rows into `state.transcript` (skip-trust hints, dirty
114
+ // tree warnings) which used к dismiss the banner within ~2s, hiding
115
+ // the brand mascot before the operator could read it. Drop the
116
+ // `transcript.length` trigger; agent spawn (= real dispatch) remains
117
+ // the sole signal that the operator stopped reading the banner.
118
+ useEffect(() => {
119
+ if (!welcomeVisible)
120
+ return;
121
+ if (state.agents.length > 0) {
122
+ setWelcomeVisible(false);
123
+ }
124
+ }, [welcomeVisible, state.agents.length]);
102
125
  const personaNames = useMemo(() => buildPersonaNameMap(), []);
103
126
  const { exit } = useApp();
104
127
  const handleSubmit = useCallback((line) => {
@@ -106,6 +129,10 @@ export function Repl(props) {
106
129
  // `setSplashVisible(false)` is a no-op once the state already
107
130
  // settled to false (timer fired or `agent.spawned` arrived).
108
131
  setSplashVisible(false);
132
+ // CEO P0 #2 (2026-05-29): same dismissal for the welcome banner
133
+ // — the operator engaging the input box is the cleanest signal
134
+ // they have finished reading the boot card.
135
+ setWelcomeVisible(false);
109
136
  // Run async without awaiting - the session module owns the
110
137
  // network call, errors land in the transcript automatically.
111
138
  void props.session.handleInput(line).then((verdict) => {
@@ -183,6 +210,46 @@ export function Repl(props) {
183
210
  return undefined;
184
211
  return props.session.cancel();
185
212
  }, [props.session, modalActive]);
213
+ // Wave 6 BT 8 (Claude Code parity): Esc-Esc walkback. Forwards to
214
+ // ReplSession.walkbackLastTurn which trims the trailing operator
215
+ // turn + its persona response from the in-memory transcript. Returns
216
+ // `'walked-back'` so the input box knows the host did the work;
217
+ // `'nothing'` covers both the empty-transcript and dispatch-in-flight
218
+ // refusals (the session module owns the refusal copy in both cases).
219
+ const handleWalkback = useCallback(() => {
220
+ if (modalActive)
221
+ return 'nothing';
222
+ const verdict = props.session.walkbackLastTurn();
223
+ return verdict === 'walked-back' ? 'walked-back' : 'nothing';
224
+ }, [props.session, modalActive]);
225
+ // Wave 7 — Shift+Tab cycles the 6 canonical permission modes (CC
226
+ // parity). Refuses while a modal is active so the operator does not
227
+ // accidentally flip mode mid-prompt; otherwise resolves the current
228
+ // mode through the workspace > global > default merge, advances via
229
+ // `nextPermissionMode`, и persists к .pugi/session.json. Returns the
230
+ // new mode string so the InputBox can flash a one-line toast.
231
+ const handleCyclePermissionMode = useCallback(() => {
232
+ if (modalActive)
233
+ return null;
234
+ try {
235
+ // Lazy-require так this code path doesn't drag the permissions
236
+ // module into the splash + boot stages where it isn't needed.
237
+ // The require is sync but the inner work is pure JSON IO.
238
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
239
+ const perm = require('../core/permissions/index.js');
240
+ const workspaceRoot = process.cwd();
241
+ const current = perm.resolveMode({ workspaceRoot });
242
+ const next = perm.nextPermissionMode(current);
243
+ perm.setCurrentMode(workspaceRoot, next);
244
+ return next;
245
+ }
246
+ catch {
247
+ // Persistence is best-effort — if .pugi/session.json is read-only
248
+ // или ENOENT-on-parent the toast is suppressed so we don't lie
249
+ // about the flip к the operator.
250
+ return null;
251
+ }
252
+ }, [modalActive]);
186
253
  // α6.14.5 CEO dogfood 2026-05-25 (parity with Claude Code): input
187
254
  // box pinned to alt-screen BOTTOM, conversation grows above it.
188
255
  // Beta.3's height={rows} fix broke keystroke focus - raw echo at
@@ -191,14 +258,25 @@ export function Repl(props) {
191
258
  // input, and the input stays the sole focusable surface adjacent
192
259
  // to the cursor row, so all keystrokes route through it.
193
260
  const altScreenRows = process.stdout.rows ?? 24;
194
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, minHeight: altScreenRows, children: [props.updateBanner ? _jsx(UpdateBanner, { result: props.updateBanner }) : null, splashVisible ? (_jsx(ReplSplash, { cliVersion: state.cliVersion, workspaceLabel: state.workspaceLabel, plan: props.splashPlan, model: props.splashModel, tenant: props.splashTenant, onDismiss: dismissSplash, mascotPrePrinted: props.mascotPrePrinted === true })) : null, _jsx(Header, { state: state }), _jsx(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, justifyContent: "flex-end", children: overlay === 'help' ? (_jsx(HelpOverlay, {})) : overlay === 'roster' ? (_jsx(RosterOverlay, {})) : overlay === 'farewell' ? (_jsx(FarewellOverlay, {})) : (_jsx(MainArea, { state: state, personaNames: personaNames, nowEpochMs: tickNow, hideToolStream: props.hideToolStream === true, toolStreamCollapsed: toolStreamCollapsed })) }), state.pendingAsk ? (_jsx(Box, { marginTop: 1, children: _jsx(AskModal, { tag: state.pendingAsk, onResolve: handleAskResolve }) })) : null, state.pendingPlanReview ? (_jsx(Box, { marginTop: 1, children: _jsx(PlanReviewModal, { tag: state.pendingPlanReview, onResolve: handlePlanReviewResolve }) })) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [overlay === 'farewell' || modalActive ? null : (_jsx(InputBox, { onSubmit: handleSubmit, onExit: handleExit, onCancel: handleCancel, now: props.now,
261
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, minHeight: altScreenRows, children: [welcomeVisible && props.welcomeData ? (_jsx(WelcomeBanner, { data: props.welcomeData, mascotPrePrinted: props.mascotPrePrinted === true, autoInitStatus: props.autoInitStatus ?? null })) : null, splashVisible ? (_jsx(ReplSplash, { cliVersion: state.cliVersion, workspaceLabel: state.workspaceLabel, plan: props.splashPlan, model: props.splashModel, tenant: props.splashTenant, onDismiss: dismissSplash, mascotPrePrinted: props.mascotPrePrinted === true })) : null, _jsx(Header, { state: state }), _jsx(Box, { flexDirection: "column", marginTop: 1, flexGrow: 1, justifyContent: "flex-end", children: overlay === 'help' ? (_jsx(HelpOverlay, {})) : overlay === 'roster' ? (_jsx(RosterOverlay, {})) : overlay === 'farewell' ? (_jsx(FarewellOverlay, {})) : (_jsx(MainArea, { state: state, personaNames: personaNames, nowEpochMs: tickNow, hideToolStream: props.hideToolStream === true, toolStreamCollapsed: toolStreamCollapsed })) }), state.pendingAsk ? (_jsx(Box, { marginTop: 1, children: _jsx(AskModal, { tag: state.pendingAsk, onResolve: handleAskResolve }) })) : null, state.pendingPlanReview ? (_jsx(Box, { marginTop: 1, children: _jsx(PlanReviewModal, { tag: state.pendingPlanReview, onResolve: handlePlanReviewResolve }) })) : null, _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [overlay === 'farewell' || modalActive ? null : (_jsx(InputBox, { onSubmit: handleSubmit, onExit: handleExit, onCancel: handleCancel, onWalkback: handleWalkback, onCyclePermissionMode: handleCyclePermissionMode, now: props.now,
195
262
  // Slug from process.cwd() (full path) so two workspaces with
196
263
  // the same basename do not share history. state.workspaceLabel
197
264
  // is the basename only. Codex review P2.
198
- workspaceSlug: slugForCwd(process.cwd()) })), _jsx(StatusBar, { connection: state.connection, activeAgentCount: countActive(state), tokensDownstreamTotal: state.tokensDownstreamTotal, briefStartedAtEpochMs: state.briefStartedAtEpochMs, nowEpochMs: tickNow, pulsePhase: pulsePhase, pugiMdCount: workspaceContext.pugiMdCount, mcpServerCount: workspaceContext.mcpServerCount, skillCount: workspaceContext.skillCount, quotaPct: props.quotaPct, dispatchState: state.dispatchState, dispatchToolLabel: state.dispatchToolLabel })] })] }));
265
+ workspaceSlug: slugForCwd(process.cwd()) })), _jsx(ThinkingSpinner, { dispatchState: state.dispatchState, dispatchToolLabel: state.dispatchToolLabel }), _jsx(StatusBar, { connection: state.connection, activeAgentCount: countActive(state), tokensDownstreamTotal: state.tokensDownstreamTotal, briefStartedAtEpochMs: state.briefStartedAtEpochMs, nowEpochMs: tickNow, pulsePhase: pulsePhase, pugiMdCount: workspaceContext.pugiMdCount, mcpServerCount: workspaceContext.mcpServerCount, skillCount: workspaceContext.skillCount, quotaPct: props.quotaPct, dispatchState: state.dispatchState, dispatchToolLabel: state.dispatchToolLabel, lastCompletedOutcome: state.lastCompletedOutcome,
266
+ // α7 cost-meter sprint — surface accumulated session totals
267
+ // + per-turn delta flash on the status bar's top row. The
268
+ // session module owns accumulation; the bar is a pure render.
269
+ sessionTokensIn: state.sessionTokensIn, sessionTokensOut: state.sessionTokensOut, sessionCostUsd: state.sessionCostUsd, sessionStartedAtEpochMs: state.sessionStartedAtEpochMs, lastTurnDelta: state.lastTurnDelta }), props.updateBanner ? _jsx(UpdateBanner, { result: props.updateBanner }) : null] })] }));
199
270
  }
200
271
  function Header({ state }) {
201
- return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "cyan", children: ".io" }), _jsx(Text, { dimColor: true, children: ` · workspace: ${state.workspaceLabel} · v${state.cliVersion} · ` }), _jsx(Text, { color: "cyan", children: state.connection === 'on_watch' ? 'on watch' : state.connection.replace('_', ' ') })] }));
272
+ // Leak L30 (2026-05-27): the header `.io` brand accent + connection
273
+ // pill route through `useTheme()` so the operator's `/theme` flip
274
+ // (default / dark / light / colorblind) re-tints the chrome on
275
+ // re-mount. The `useTheme` hook returns the `default` preset's
276
+ // colors when no provider is mounted, preserving the previous
277
+ // `#3da9fc` constants for tests that import `<Repl />` standalone.
278
+ const theme = useTheme();
279
+ return (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: theme.accent, children: ".io" }), _jsx(Text, { dimColor: true, children: ` · workspace: ${state.workspaceLabel} · v${state.cliVersion} · ` }), _jsx(Text, { color: theme.accent, children: state.connection === 'on_watch' ? 'on watch' : state.connection.replace('_', ' ') })] }));
202
280
  }
203
281
  function MainArea({ state, personaNames, nowEpochMs, hideToolStream, toolStreamCollapsed, }) {
204
282
  // α6.12: three vertical panes stacked above the input box.
@@ -238,14 +316,14 @@ function HelpOverlay() {
238
316
  const rows = grouped.get(group);
239
317
  if (!rows || rows.length === 0)
240
318
  return null;
241
- return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ` -- ${group} --` }), rows.map((row) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: ` /${row.name} ${row.args}`.padEnd(22, ' ') }), _jsx(Text, { dimColor: true, children: row.gloss })] }, row.name)))] }, group));
319
+ return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { dimColor: true, children: ` -- ${group} --` }), rows.map((row) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "#3da9fc", children: ` /${row.name} ${row.args}`.padEnd(22, ' ') }), _jsx(Text, { dimColor: true, children: row.gloss })] }, row.name)))] }, group));
242
320
  }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: `${PUGI_TAGLINE}` }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Press any key to dismiss.' }) })] }));
243
321
  }
244
322
  function RosterOverlay() {
245
323
  return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "On-watch roster" }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: THE_TEN.map((persona) => (_jsxs(Box, { children: [_jsx(Text, { bold: true, children: ` ${persona.name.padEnd(10, ' ')}` }), _jsx(Text, { dimColor: true, children: `${persona.role.padEnd(20, ' ')}` }), _jsx(Text, { children: persona.oneLiner })] }, persona.slug))) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: 'Press any key to dismiss.' }) })] }));
246
324
  }
247
325
  function FarewellOverlay() {
248
- return (_jsx(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: _jsx(Text, { bold: true, color: "cyan", children: PUGI_TAGLINE }) }));
326
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: _jsx(Text, { bold: true, color: "#3da9fc", children: PUGI_TAGLINE }) }));
249
327
  }
250
328
  function applyVerdictSideEffects(verdict, handlers) {
251
329
  switch (verdict.kind) {
@@ -270,8 +348,10 @@ function applyVerdictSideEffects(verdict, handlers) {
270
348
  case 'consensus':
271
349
  case 'diff':
272
350
  case 'cost':
351
+ case 'quota':
273
352
  case 'status':
274
353
  case 'resume':
354
+ case 'mcp':
275
355
  case 'stub':
276
356
  // All non-overlay verdicts: the session module already appended
277
357
  // any operator-visible system lines (and, for `ask`, set
@@ -21,7 +21,7 @@ export function Splash({ data }) {
21
21
  cmd: 'pugi login',
22
22
  gloss: 'Connect this terminal to your Pugi account',
23
23
  };
24
- return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "cyan", children: "pugi.io" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `v${data.cliVersion} · ${data.apiUrl}` }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Account: " }), _jsx(Text, { children: accountLine })] }), data.isAuthenticated && data.plan ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Plan: " }), _jsx(Text, { children: data.plan })] })) : null] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Quick start:" }), _jsx(HintRow, { command: primaryHint.cmd, gloss: primaryHint.gloss }), data.isAuthenticated ? (_jsx(HintRow, { command: 'pugi login', gloss: 'Re-authenticate or switch accounts' })) : (_jsx(HintRow, { command: 'pugi code "fix the bug"', gloss: 'Run a one-shot coding task' })), _jsx(HintRow, { command: 'pugi review --triple', gloss: 'Run the Anvil triple-review gate' }), _jsx(HintRow, { command: 'pugi help', gloss: 'Full command reference' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Docs: https://pugi.dev \u00B7 Status: https://pugi.io/status" }) })] }));
24
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsx(Box, { children: _jsx(Text, { bold: true, color: "#3da9fc", children: "pugi.io" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `v${data.cliVersion} · ${data.apiUrl}` }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Account: " }), _jsx(Text, { children: accountLine })] }), data.isAuthenticated && data.plan ? (_jsxs(Text, { children: [_jsx(Text, { dimColor: true, children: "Plan: " }), _jsx(Text, { children: data.plan })] })) : null] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Quick start:" }), _jsx(HintRow, { command: primaryHint.cmd, gloss: primaryHint.gloss }), data.isAuthenticated ? (_jsx(HintRow, { command: 'pugi login', gloss: 'Re-authenticate or switch accounts' })) : (_jsx(HintRow, { command: 'pugi code "fix the bug"', gloss: 'Run a one-shot coding task' })), _jsx(HintRow, { command: 'pugi review --triple', gloss: 'Run the Anvil triple-review gate' }), _jsx(HintRow, { command: 'pugi help', gloss: 'Full command reference' })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Docs: https://pugi.dev \u00B7 Status: https://pugi.io/status" }) })] }));
25
25
  }
26
26
  function HintRow({ command, gloss }) {
27
27
  // Pad command names so the gloss column lines up across rows.
@@ -1,5 +1,12 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
+ import { formatCostUsd, formatTokens } from '../core/repl/model-pricing.js';
4
+ /**
5
+ * Window during which the per-turn delta flash stays visible on the
6
+ * cost-meter row. CEO spec: ~2 seconds after completion. Past that, the
7
+ * flash dimms out and the row shows session totals only.
8
+ */
9
+ const TURN_DELTA_FLASH_MS = 2_000;
3
10
  /**
4
11
  * Cyan dot glyphs across the pulse cycle. Three steps keep the motion
5
12
  * subtle - a true gradient would force an Ink rerender on every
@@ -17,8 +24,73 @@ export function StatusBar(props) {
17
24
  // first. When the connection is healthy (`on_watch` / `connecting`),
18
25
  // the FSM dispatch state takes over to show the dispatch lifecycle
19
26
  // (`dispatching` / `tool: read` / `aborting` / etc.).
20
- const status = composeStatusLabel(props.connection, props.dispatchState, props.dispatchToolLabel);
21
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: status.color, children: `${glyph ?? '●'} ${status.label}` }), _jsx(Text, { dimColor: true, children: ` · ${props.activeAgentCount} agents · ` }), _jsx(Text, { children: `↓ ${tokenLabel} tokens` }), _jsx(Text, { dimColor: true, children: ` · ${elapsedLabel}` })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `${formatCount(props.pugiMdCount)} PUGI.md · ${formatCount(props.mcpServerCount)} MCP · ${formatCount(props.skillCount)} skills · ${formatQuota(props.quotaPct)} quota` }) })] }));
27
+ const status = composeStatusLabel(props.connection, props.dispatchState, props.dispatchToolLabel, props.lastCompletedOutcome);
28
+ // α7 cost-meter sprint the cost row anchors above the legacy
29
+ // dispatch-state line so the operator's eye lands on the meter first
30
+ // (matches Claude Code TUI footer rhythm). The session-elapsed slot
31
+ // uses sessionStartedAtEpochMs (REPL boot), distinct from the
32
+ // per-brief `elapsedLabel` on the row below.
33
+ const costRow = renderCostMeterRow(props.sessionTokensIn ?? 0, props.sessionTokensOut ?? 0, props.sessionCostUsd ?? 0, props.sessionStartedAtEpochMs, now);
34
+ const deltaFlash = renderTurnDeltaFlash(props.lastTurnDelta, now);
35
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "cyan", children: `↑ ${costRow.tokensInLabel}` }), _jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "cyan", children: `↓ ${costRow.tokensOutLabel}` }), _jsx(Text, { dimColor: true, children: ` · ` }), _jsx(Text, { bold: true, children: costRow.costLabel }), _jsx(Text, { dimColor: true, children: ` · ${costRow.elapsedLabel}` }), deltaFlash ? (_jsxs(_Fragment, { children: [_jsx(Text, { dimColor: true, children: ' ' }), _jsx(Text, { color: "green", children: deltaFlash })] })) : null] }), _jsxs(Box, { children: [_jsx(Text, { color: status.color, children: `${glyph ?? '●'} ${status.label}` }), _jsx(Text, { dimColor: true, children: ` · ${props.activeAgentCount} agents · ` }), _jsx(Text, { children: `↓ ${tokenLabel} tokens` }), _jsx(Text, { dimColor: true, children: ` · ${elapsedLabel}` }), typeof props.externalDispatchCount === 'number' && props.externalDispatchCount > 0 ? (_jsx(Text, { color: "yellow", children: ` · ${props.externalDispatchCount} dispatch${props.externalDispatchCount === 1 ? '' : 'es'} active. /cancel к manage.` })) : null] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `${formatCount(props.pugiMdCount)} PUGI.md · ${formatCount(props.mcpServerCount)} MCP · ${formatCount(props.skillCount)} skills · ${formatQuota(props.quotaPct)} quota` }) })] }));
36
+ }
37
+ /**
38
+ * α7 cost-meter sprint — assemble the cost-meter row labels. Pure helper
39
+ * so the snapshot tests assert the formatted shape without standing up
40
+ * an Ink renderer.
41
+ */
42
+ export function renderCostMeterRow(tokensIn, tokensOut, costUsd, sessionStartedAtEpochMs, nowEpochMs) {
43
+ const elapsedMs = typeof sessionStartedAtEpochMs === 'number'
44
+ ? Math.max(0, nowEpochMs - sessionStartedAtEpochMs)
45
+ : 0;
46
+ return {
47
+ tokensInLabel: formatTokens(tokensIn),
48
+ tokensOutLabel: formatTokens(tokensOut),
49
+ costLabel: formatCostUsd(costUsd),
50
+ elapsedLabel: formatElapsedShort(elapsedMs),
51
+ };
52
+ }
53
+ /**
54
+ * α7 cost-meter sprint — render the per-turn delta flash when the most
55
+ * recent turn completed within the flash window. Returns null when no
56
+ * turn has completed yet OR the flash has expired (the elapsed slot
57
+ * gets the slot back). The flash format mirrors the spec:
58
+ *
59
+ * `+200/+1.1k +$0.01`
60
+ *
61
+ * Exported for snapshot tests.
62
+ */
63
+ export function renderTurnDeltaFlash(delta, nowEpochMs) {
64
+ if (!delta)
65
+ return null;
66
+ const elapsedSinceMs = nowEpochMs - delta.completedAtEpochMs;
67
+ if (elapsedSinceMs < 0 || elapsedSinceMs > TURN_DELTA_FLASH_MS)
68
+ return null;
69
+ const inLabel = formatTokens(delta.tokensIn);
70
+ const outLabel = formatTokens(delta.tokensOut);
71
+ const costLabel = delta.costUsd > 0 ? ` +${formatCostUsd(delta.costUsd)}` : '';
72
+ return `+${inLabel}/+${outLabel}${costLabel}`;
73
+ }
74
+ /**
75
+ * α7 cost-meter sprint — local copy of the session elapsed formatter.
76
+ * Mirrors the helper in `core/repl/session.ts` so the status bar stays
77
+ * a pure leaf component without a circular import on session.ts (the
78
+ * model-pricing module's `formatDuration` is similar but ships an
79
+ * `XhYm` ceiling that does not match the CEO spec's `2m44s` shape).
80
+ */
81
+ function formatElapsedShort(elapsedMs) {
82
+ if (!Number.isFinite(elapsedMs) || elapsedMs <= 0)
83
+ return '0s';
84
+ const totalSec = Math.floor(elapsedMs / 1000);
85
+ if (totalSec < 60)
86
+ return `${totalSec}s`;
87
+ const min = Math.floor(totalSec / 60);
88
+ const sec = totalSec % 60;
89
+ if (min < 60)
90
+ return `${min}m${sec.toString().padStart(2, '0')}s`;
91
+ const hr = Math.floor(min / 60);
92
+ const restMin = min % 60;
93
+ return `${hr}h${restMin.toString().padStart(2, '0')}m`;
22
94
  }
23
95
  /**
24
96
  * Render a count badge — number if defined, `—` placeholder otherwise.
@@ -75,7 +147,7 @@ export function connectionLabel(connection) {
75
147
  * `tool: <kind>` upstream so we just concatenate; null falls through
76
148
  * to the bare `tool` placeholder.
77
149
  */
78
- function composeStatusLabel(connection, dispatchState, toolLabel) {
150
+ function composeStatusLabel(connection, dispatchState, toolLabel, lastCompletedOutcome) {
79
151
  // Transport health wins.
80
152
  if (connection === 'offline' || connection === 'reconnecting') {
81
153
  return connectionLabel(connection);
@@ -93,6 +165,16 @@ function composeStatusLabel(connection, dispatchState, toolLabel) {
93
165
  case 'awaiting_response':
94
166
  return { label: 'dispatching', color: 'cyan' };
95
167
  case 'completed':
168
+ // Branch on the work-done outcome so the bottom-bar tells the
169
+ // same truth as the agent-tree (2026-05-26 — memory
170
+ // feedback_no_fake_dispatch_promises). `'replied'` = text-only
171
+ // turn, render with the same neutral gray + arrow used in the
172
+ // agent-tree. `'shipped'` = real side-effect (or older server
173
+ // that omits the outcome field). Defaults to `'shipped'` so
174
+ // older callers without the prop wired stay back-compat.
175
+ if (lastCompletedOutcome === 'replied') {
176
+ return { label: 'replied', color: 'gray' };
177
+ }
96
178
  return { label: 'shipped', color: 'green' };
97
179
  case 'idle':
98
180
  case undefined:
@@ -111,18 +193,14 @@ function formatElapsed(startedAt, now) {
111
193
  const seconds = Math.floor((ms % 60_000) / 1000);
112
194
  return `${minutes}m ${seconds.toString().padStart(2, '0')}s`;
113
195
  }
114
- /**
115
- * Format the downstream token counter as 1.2k / 12.4k / 1.0m. Anvil
116
- * F1 emits totals in the tens-of-thousands range during a single
117
- * brief, so anything more than three significant figures is noise.
118
- */
119
- function formatTokens(total) {
120
- if (total < 1_000)
121
- return total.toString();
122
- if (total < 1_000_000)
123
- return `${(total / 1_000).toFixed(1)}k`;
124
- return `${(total / 1_000_000).toFixed(1)}m`;
125
- }
196
+ // `formatTokens` for the downstream-throughput slot is imported from
197
+ // `core/repl/model-pricing.ts` single source of truth for token
198
+ // formatting across the cost-meter row, `/cost` slash, and the legacy
199
+ // downstream-tokens slot. The shape is identical to the prior local
200
+ // helper (`<1000` raw, `<1m` one-decimal k, `≥1m` one-decimal m); the
201
+ // only semantic difference is non-finite / negative inputs render as
202
+ // `0` instead of throwing, matching the cost-meter row's defensive
203
+ // posture.
126
204
  function clampPhase(phase) {
127
205
  if (typeof phase !== 'number' || Number.isNaN(phase))
128
206
  return 0;
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ export function StatusTable({ snapshot }) {
4
+ const labelWidth = Math.max('Label'.length, ...snapshot.fields.map((f) => f.label.length));
5
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi status" }) }), snapshot.fields.map((field) => (_jsxs(Box, { children: [_jsxs(Text, { children: [field.label.padEnd(labelWidth, ' '), " "] }), field.available ? (_jsx(Text, { children: field.value })) : (_jsx(Text, { dimColor: true, children: field.value }))] }, field.key))), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["CLI ", snapshot.meta.cliVersion, " Node ", snapshot.meta.nodeVersion, " cwd ", snapshot.meta.cwd] }) })] }));
6
+ }
7
+ //# sourceMappingURL=status-table.js.map
@@ -0,0 +1,136 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ /**
4
+ * Curated ASCII pug corpus. Five variants — wide enough that repeat
5
+ * invocations look fresh, narrow enough that every entry stays
6
+ * hand-vetted (no procedural slop). Each art block intentionally fits
7
+ * inside an 80-column terminal so the surrounding box border does not
8
+ * wrap on narrow shells.
9
+ *
10
+ * The trailing newline at the end of each `art` string is intentional —
11
+ * keeps the renderer's join logic uniform between the boxed and the
12
+ * `--ascii-only` paths.
13
+ */
14
+ export const PUG_STICKERS = Object.freeze([
15
+ {
16
+ id: 'classic-face',
17
+ caption: 'classic pug face',
18
+ art: [
19
+ ' _._ _,-\'""`-._',
20
+ ' (,-.`._,\'( |\\`-/|',
21
+ ' `-.-\' \\ )-`( , o o)',
22
+ ' `- \\`_`"\'-',
23
+ ].join('\n'),
24
+ },
25
+ {
26
+ id: 'sit-pose',
27
+ caption: 'sit, stay, ship',
28
+ art: [
29
+ ' /\\___/\\',
30
+ ' ( o o )',
31
+ ' ( =^= )',
32
+ ' (______)',
33
+ ].join('\n'),
34
+ },
35
+ {
36
+ id: 'peek',
37
+ caption: 'peek-a-pug',
38
+ art: [
39
+ ' __',
40
+ ' ___/ \\___',
41
+ ' / o o \\',
42
+ ' | > ^ < |',
43
+ ' \\__________/',
44
+ ].join('\n'),
45
+ },
46
+ {
47
+ id: 'sleepy',
48
+ caption: 'sleepy pug, no Zzz today',
49
+ art: [
50
+ ' .--.',
51
+ ' / - -\\',
52
+ ' ( ^ ^ )',
53
+ ' \\ ^^ /',
54
+ ' `----\'',
55
+ ].join('\n'),
56
+ },
57
+ {
58
+ id: 'shipping',
59
+ caption: 'shipping pug',
60
+ art: [
61
+ ' .---. .---.',
62
+ ' |o_o| |o_o|',
63
+ ' \\_^_/ \\_^_/',
64
+ ' /| |\\ /| |\\',
65
+ ' shipped • shipped',
66
+ ].join('\n'),
67
+ },
68
+ ]);
69
+ /**
70
+ * Curated rotating-quote pool. Brand voice gate (brandbook §08):
71
+ * `brief / dispatch / stop / agents / quit / shipped` are the power
72
+ * words; quotes lean on those and на the operator-mode register.
73
+ * Adding lines: keep each ≤ 64 chars so the boxed renderer never wraps,
74
+ * stay в the operator's voice, no AI attribution, no hype.
75
+ */
76
+ export const PUG_QUOTES = Object.freeze([
77
+ 'Pugi: your engineering co-pilot.',
78
+ 'Brief it. It ships.',
79
+ 'Built for operators, not for benchmarks.',
80
+ 'Pugi: твой инженерный напарник.',
81
+ 'Dispatch agents, not promises.',
82
+ 'Small CLI. Loud workforce.',
83
+ 'Engineering at the speed of brief.',
84
+ 'Pugi: shipping is the default mode.',
85
+ ]);
86
+ /**
87
+ * Clamp a raw rng draw to a safe array index. Handles every hostile
88
+ * shape the spec exercises:
89
+ * - rng returns NaN → fall back to 0
90
+ * - rng returns 1.0 → clamp to length-1 (Math.floor would land at n)
91
+ * - rng returns -ε → clamp to 0
92
+ * The caller hands в the corpus length; the helper never touches the
93
+ * corpus itself so it stays trivially testable.
94
+ */
95
+ function safeIndex(raw, length) {
96
+ if (!Number.isFinite(raw))
97
+ return 0;
98
+ const floored = Math.floor(raw);
99
+ if (floored < 0)
100
+ return 0;
101
+ if (floored >= length)
102
+ return length - 1;
103
+ return floored;
104
+ }
105
+ /**
106
+ * Pick one art variant. Defaults to `Math.random` but the caller can
107
+ * inject a deterministic source — the spec uses a sequence-driven
108
+ * stub to assert the picker hits each entry в the corpus.
109
+ */
110
+ export function pickArtVariant(rng = Math.random) {
111
+ const raw = rng() * PUG_STICKERS.length;
112
+ return PUG_STICKERS[safeIndex(raw, PUG_STICKERS.length)];
113
+ }
114
+ /**
115
+ * Pick one rotating brand quote. Same contract as `pickArtVariant` —
116
+ * test-injectable rng so the spec can pin the chosen index.
117
+ */
118
+ export function pickQuote(rng = Math.random) {
119
+ const raw = rng() * PUG_QUOTES.length;
120
+ return PUG_QUOTES[safeIndex(raw, PUG_QUOTES.length)];
121
+ }
122
+ /**
123
+ * Plain-text renderer for the `--ascii-only` flag and the non-TTY shell
124
+ * path. Emits the art verbatim, then a blank line, then the quote.
125
+ * No box border — scripting use-case (`pugi stickers --ascii-only`
126
+ * piped to `figlet`, `lolcat`, or a regression-fixture file) gets a
127
+ * stable contract free of decorative ANSI noise.
128
+ */
129
+ export function renderPugStickersText(art, quote) {
130
+ return `${art.art}\n\n${quote}`;
131
+ }
132
+ export function PugStickersArt({ art, quote }) {
133
+ const lines = art.art.split('\n');
134
+ return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", paddingX: 1, children: [_jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { bold: true, children: "Pugi stickers" }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", art.caption] })] }), _jsx(Box, { flexDirection: "column", children: lines.map((line, i) => (_jsx(Text, { children: line }, `art-${i}`))) }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["\"", quote, "\""] }) })] }));
135
+ }
136
+ //# sourceMappingURL=stickers-art.js.map
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { OUTPUT_STYLES, OUTPUT_STYLE_SLUGS, } from '../core/output-style/presets.js';
4
+ import { useTheme } from '../core/theme/context.js';
5
+ /**
6
+ * Banner above the table. Plain text (not bold) so the prefix `*`
7
+ * remains the dominant active-row cue.
8
+ */
9
+ function buildBanner(active, source) {
10
+ return `Active style: ${active} (${source})`;
11
+ }
12
+ export function StyleTable({ active, source }) {
13
+ // Leak L30 (2026-05-27): the active-row marker color flows through
14
+ // the theme so `colorblind` operators see cyan instead of green
15
+ // (which their palette re-maps to `success`). Falls back to the
16
+ // default theme's `success` token when no provider is mounted.
17
+ const theme = useTheme();
18
+ const slugWidth = Math.max('NAME'.length, ...OUTPUT_STYLE_SLUGS.map((slug) => slug.length));
19
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi output styles" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: ` ${'NAME'.padEnd(slugWidth)} GLOSS` }) }), OUTPUT_STYLE_SLUGS.map((slug) => (_jsx(StyleRow, { slug: slug, active: active, slugWidth: slugWidth, activeColor: theme.success }, slug))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: buildBanner(active, source) }) })] }));
20
+ }
21
+ function StyleRow({ slug, active, slugWidth, activeColor }) {
22
+ const isActive = slug === active;
23
+ const marker = isActive ? '*' : ' ';
24
+ const slugPart = slug.padEnd(slugWidth, ' ');
25
+ const gloss = OUTPUT_STYLES[slug].gloss;
26
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? activeColor : undefined, bold: isActive, children: `${marker} ${slugPart}` }), _jsx(Text, { children: ` ${gloss}` })] }));
27
+ }
28
+ //# sourceMappingURL=style-table.js.map
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Text } from 'ink';
3
+ import { compileSampleRow, THEMES, THEME_SLUGS, } from '../core/theme/presets.js';
4
+ /**
5
+ * Banner above the table. Plain text (not bold) so the prefix `*`
6
+ * remains the dominant active-row cue. Mirrors `<StyleTable>` so the
7
+ * Settings-group surfaces read identically.
8
+ */
9
+ function buildBanner(active, source) {
10
+ return `Active theme: ${active} (${source})`;
11
+ }
12
+ export function ThemeTable({ active, source }) {
13
+ const slugWidth = Math.max('NAME'.length, ...THEME_SLUGS.map((slug) => slug.length));
14
+ // The gloss column gets sized to the widest gloss + 2 padding so
15
+ // the sample column lines up. Computed once per render so the
16
+ // layout stays stable when the catalogue grows.
17
+ const glossWidth = Math.max('GLOSS'.length, ...THEME_SLUGS.map((slug) => THEMES[slug].gloss.length));
18
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Pugi themes" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: ` ${'NAME'.padEnd(slugWidth)} ${'GLOSS'.padEnd(glossWidth)} SAMPLE` }) }), THEME_SLUGS.map((slug) => (_jsx(ThemeRow, { slug: slug, active: active, slugWidth: slugWidth, glossWidth: glossWidth }, slug))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: buildBanner(active, source) }) })] }));
19
+ }
20
+ function ThemeRow({ slug, active, slugWidth, glossWidth }) {
21
+ const isActive = slug === active;
22
+ const marker = isActive ? '*' : ' ';
23
+ const slugPart = slug.padEnd(slugWidth, ' ');
24
+ const preset = THEMES[slug];
25
+ const gloss = preset.gloss.padEnd(glossWidth, ' ');
26
+ const sample = compileSampleRow(slug);
27
+ return (_jsxs(Box, { children: [_jsx(Text, { color: isActive ? preset.colors.accent : undefined, bold: isActive, children: `${marker} ${slugPart}` }), _jsx(Text, { children: ` ${gloss} ` }), _jsx(Text, { color: preset.colors.foreground, children: sample.foreground }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.accent, children: sample.accent }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.success, children: sample.success }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.warning, children: sample.warning }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: preset.colors.error, children: sample.error })] }));
28
+ }
29
+ //# sourceMappingURL=theme-table.js.map