@pugi/cli 0.1.0-beta.4 → 0.1.0-beta.40

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 (249) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/assets/pugi-mascot.ansi +15 -25
  3. package/bin/run.js +33 -1
  4. package/dist/commands/jobs-watch.js +201 -0
  5. package/dist/commands/jobs.js +15 -0
  6. package/dist/commands/smoke.js +133 -0
  7. package/dist/core/agent-progress/cleanup.js +134 -0
  8. package/dist/core/agent-progress/schema.js +144 -0
  9. package/dist/core/agent-progress/writer.js +101 -0
  10. package/dist/core/artifact-chain/dispatcher.js +148 -0
  11. package/dist/core/artifact-chain/exporter.js +164 -0
  12. package/dist/core/artifact-chain/state.js +243 -0
  13. package/dist/core/artifact-chain/steps.js +169 -0
  14. package/dist/core/auth/ensure-authenticated.js +129 -0
  15. package/dist/core/auth/env-provider.js +238 -0
  16. package/dist/core/auto-update/channels.js +122 -0
  17. package/dist/core/auto-update/checker.js +241 -0
  18. package/dist/core/auto-update/state.js +235 -0
  19. package/dist/core/bare-mode/index.js +107 -0
  20. package/dist/core/bash-classifier.js +108 -1
  21. package/dist/core/checkpoint/resumer.js +149 -0
  22. package/dist/core/checkpoint/rewinder.js +291 -0
  23. package/dist/core/codegraph/decision-store.js +248 -0
  24. package/dist/core/codegraph/detect-repo.js +459 -0
  25. package/dist/core/codegraph/install.js +134 -0
  26. package/dist/core/codegraph/offer-hook.js +220 -0
  27. package/dist/core/compact/auto-trigger.js +96 -0
  28. package/dist/core/compact/buffer-rewriter.js +115 -0
  29. package/dist/core/compact/summarizer.js +208 -0
  30. package/dist/core/compact/token-counter.js +108 -0
  31. package/dist/core/consensus/diff-capture.js +73 -0
  32. package/dist/core/context/index.js +7 -0
  33. package/dist/core/context/markdown-traverse.js +255 -0
  34. package/dist/core/cost/rate-card.js +129 -0
  35. package/dist/core/cost/tracker.js +221 -0
  36. package/dist/core/denial-tracking/index.js +8 -0
  37. package/dist/core/denial-tracking/state.js +264 -0
  38. package/dist/core/diagnostics/probe-runner.js +93 -0
  39. package/dist/core/diagnostics/probes/api.js +46 -0
  40. package/dist/core/diagnostics/probes/auth.js +86 -0
  41. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  42. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  43. package/dist/core/diagnostics/probes/config.js +72 -0
  44. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  45. package/dist/core/diagnostics/probes/disk.js +81 -0
  46. package/dist/core/diagnostics/probes/git.js +65 -0
  47. package/dist/core/diagnostics/probes/mcp.js +75 -0
  48. package/dist/core/diagnostics/probes/node.js +59 -0
  49. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  50. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  51. package/dist/core/diagnostics/probes/session.js +74 -0
  52. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  53. package/dist/core/diagnostics/probes/workspace.js +63 -0
  54. package/dist/core/diagnostics/types.js +70 -0
  55. package/dist/core/dispatch/cache-cleanup.js +197 -0
  56. package/dist/core/dispatch/cache-handoff.js +295 -0
  57. package/dist/core/edits/dispatch.js +218 -2
  58. package/dist/core/edits/journal.js +199 -0
  59. package/dist/core/edits/layer-d-ast.js +557 -14
  60. package/dist/core/edits/verify-hook.js +273 -0
  61. package/dist/core/edits/worktree.js +322 -0
  62. package/dist/core/engine/anvil-client.js +115 -5
  63. package/dist/core/engine/budgets.js +98 -0
  64. package/dist/core/engine/context-prefix.js +155 -0
  65. package/dist/core/engine/intent.js +260 -0
  66. package/dist/core/engine/native-pugi.js +860 -211
  67. package/dist/core/engine/prompts.js +88 -2
  68. package/dist/core/engine/strip-internal-fields.js +124 -0
  69. package/dist/core/engine/tool-bridge.js +992 -36
  70. package/dist/core/feedback/queue.js +177 -0
  71. package/dist/core/feedback/submitter.js +145 -0
  72. package/dist/core/file-cache.js +113 -1
  73. package/dist/core/hooks/events.js +44 -0
  74. package/dist/core/hooks/index.js +15 -0
  75. package/dist/core/hooks/registry.js +213 -0
  76. package/dist/core/hooks/runner.js +236 -0
  77. package/dist/core/hooks/v2/event-emitter.js +115 -0
  78. package/dist/core/hooks/v2/executor.js +282 -0
  79. package/dist/core/hooks/v2/index.js +25 -0
  80. package/dist/core/hooks/v2/lifecycle.js +104 -0
  81. package/dist/core/hooks/v2/loader.js +216 -0
  82. package/dist/core/hooks/v2/matcher.js +125 -0
  83. package/dist/core/hooks/v2/trust.js +143 -0
  84. package/dist/core/hooks/v2/types.js +86 -0
  85. package/dist/core/lsp/cache.js +105 -0
  86. package/dist/core/lsp/client.js +776 -0
  87. package/dist/core/lsp/language-detect.js +66 -0
  88. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  89. package/dist/core/mcp/client.js +75 -6
  90. package/dist/core/mcp/http-server.js +553 -0
  91. package/dist/core/mcp/orchestrator-tools.js +662 -0
  92. package/dist/core/mcp/permission.js +190 -0
  93. package/dist/core/mcp/registry.js +24 -2
  94. package/dist/core/mcp/server-tools.js +219 -0
  95. package/dist/core/mcp/server.js +397 -0
  96. package/dist/core/memory/dual-write.js +416 -0
  97. package/dist/core/memory/phase1-kinds.js +20 -0
  98. package/dist/core/memory-sync/queue.js +158 -0
  99. package/dist/core/onboarding/ensure-initialized.js +133 -0
  100. package/dist/core/onboarding/marker.js +111 -0
  101. package/dist/core/onboarding/telemetry-state.js +108 -0
  102. package/dist/core/output-style/presets.js +176 -0
  103. package/dist/core/output-style/state.js +185 -0
  104. package/dist/core/permissions/auto-classifier.js +124 -0
  105. package/dist/core/permissions/circuit-breaker.js +83 -0
  106. package/dist/core/permissions/gate.js +278 -0
  107. package/dist/core/permissions/index.js +20 -0
  108. package/dist/core/permissions/mode.js +174 -0
  109. package/dist/core/permissions/state.js +241 -0
  110. package/dist/core/permissions/tool-class.js +93 -0
  111. package/dist/core/prd-check/parser.js +215 -0
  112. package/dist/core/prd-check/reporter.js +127 -0
  113. package/dist/core/prd-check/session-review.js +557 -0
  114. package/dist/core/prd-check/verifiers.js +223 -0
  115. package/dist/core/pugi-md/context-injector.js +76 -0
  116. package/dist/core/pugi-md/walk-up.js +207 -0
  117. package/dist/core/release-notes/parser.js +241 -0
  118. package/dist/core/release-notes/state.js +116 -0
  119. package/dist/core/repl/history.js +11 -1
  120. package/dist/core/repl/model-pricing.js +135 -0
  121. package/dist/core/repl/session.js +1899 -38
  122. package/dist/core/repl/slash-commands.js +406 -21
  123. package/dist/core/repl/store/session-store.js +31 -2
  124. package/dist/core/repl/workspace-context.js +22 -0
  125. package/dist/core/repo-map/build.js +125 -0
  126. package/dist/core/repo-map/cache.js +185 -0
  127. package/dist/core/repo-map/extractor.js +254 -0
  128. package/dist/core/repo-map/formatter.js +145 -0
  129. package/dist/core/repo-map/scanner.js +211 -0
  130. package/dist/core/retry-budget/budget.js +284 -0
  131. package/dist/core/retry-budget/index.js +5 -0
  132. package/dist/core/session.js +92 -0
  133. package/dist/core/settings.js +80 -0
  134. package/dist/core/share/formatter.js +271 -0
  135. package/dist/core/share/redactor.js +221 -0
  136. package/dist/core/share/uploader.js +267 -0
  137. package/dist/core/skills/defaults.js +457 -0
  138. package/dist/core/smoke/headless-driver.js +174 -0
  139. package/dist/core/smoke/orchestrator.js +194 -0
  140. package/dist/core/smoke/runner.js +238 -0
  141. package/dist/core/smoke/scenario-parser.js +316 -0
  142. package/dist/core/subagents/dispatcher-real.js +600 -0
  143. package/dist/core/subagents/dispatcher.js +113 -24
  144. package/dist/core/subagents/index.js +18 -5
  145. package/dist/core/subagents/isolation-matrix.js +213 -0
  146. package/dist/core/subagents/spawn.js +19 -4
  147. package/dist/core/telemetry/emitter.js +229 -0
  148. package/dist/core/telemetry/queue.js +251 -0
  149. package/dist/core/theme/context.js +91 -0
  150. package/dist/core/theme/presets.js +228 -0
  151. package/dist/core/theme/state.js +181 -0
  152. package/dist/core/todos/invariant.js +10 -0
  153. package/dist/core/todos/state.js +177 -0
  154. package/dist/core/transport/version-interceptor.js +166 -0
  155. package/dist/core/vim/keymap.js +288 -0
  156. package/dist/core/vim/state.js +92 -0
  157. package/dist/index.js +28 -0
  158. package/dist/runtime/bootstrap.js +190 -0
  159. package/dist/runtime/cli.js +3073 -321
  160. package/dist/runtime/commands/cancel.js +231 -0
  161. package/dist/runtime/commands/chain.js +489 -0
  162. package/dist/runtime/commands/codegraph-status.js +227 -0
  163. package/dist/runtime/commands/compact.js +297 -0
  164. package/dist/runtime/commands/cost.js +199 -0
  165. package/dist/runtime/commands/delegate.js +242 -11
  166. package/dist/runtime/commands/dispatch.js +126 -0
  167. package/dist/runtime/commands/doctor.js +390 -0
  168. package/dist/runtime/commands/feedback.js +184 -0
  169. package/dist/runtime/commands/hooks.js +184 -0
  170. package/dist/runtime/commands/lsp.js +368 -0
  171. package/dist/runtime/commands/mcp.js +879 -0
  172. package/dist/runtime/commands/memory.js +508 -0
  173. package/dist/runtime/commands/model.js +237 -0
  174. package/dist/runtime/commands/onboarding.js +275 -0
  175. package/dist/runtime/commands/patch.js +128 -0
  176. package/dist/runtime/commands/permissions.js +112 -0
  177. package/dist/runtime/commands/plan.js +143 -0
  178. package/dist/runtime/commands/prd-check.js +285 -0
  179. package/dist/runtime/commands/redo-blob-store.js +92 -0
  180. package/dist/runtime/commands/redo.js +361 -0
  181. package/dist/runtime/commands/release-notes.js +229 -0
  182. package/dist/runtime/commands/repo-map.js +95 -0
  183. package/dist/runtime/commands/report.js +299 -0
  184. package/dist/runtime/commands/resume.js +118 -0
  185. package/dist/runtime/commands/review-consensus.js +17 -2
  186. package/dist/runtime/commands/rewind.js +333 -0
  187. package/dist/runtime/commands/sessions.js +163 -0
  188. package/dist/runtime/commands/share.js +316 -0
  189. package/dist/runtime/commands/status.js +186 -0
  190. package/dist/runtime/commands/stickers.js +82 -0
  191. package/dist/runtime/commands/style.js +194 -0
  192. package/dist/runtime/commands/theme.js +196 -0
  193. package/dist/runtime/commands/undo.js +32 -0
  194. package/dist/runtime/commands/update.js +289 -0
  195. package/dist/runtime/commands/vim.js +140 -0
  196. package/dist/runtime/commands/worktree.js +177 -0
  197. package/dist/runtime/headless-repl.js +195 -0
  198. package/dist/runtime/headless.js +543 -0
  199. package/dist/runtime/load-hooks-or-exit.js +71 -0
  200. package/dist/runtime/plan-decompose.js +531 -0
  201. package/dist/runtime/version.js +65 -0
  202. package/dist/tools/agent-tool.js +229 -0
  203. package/dist/tools/apply-patch.js +556 -0
  204. package/dist/tools/ask-user-question.js +213 -0
  205. package/dist/tools/ask-user.js +115 -0
  206. package/dist/tools/file-tools.js +85 -14
  207. package/dist/tools/lsp-tools.js +189 -0
  208. package/dist/tools/mcp-tool.js +260 -0
  209. package/dist/tools/multi-edit.js +361 -0
  210. package/dist/tools/registry.js +46 -0
  211. package/dist/tools/skill-tool.js +96 -0
  212. package/dist/tools/tasks.js +208 -0
  213. package/dist/tools/todo-write.js +184 -0
  214. package/dist/tools/web-fetch.js +147 -2
  215. package/dist/tools/web-search.js +458 -0
  216. package/dist/tui/agent-progress-card.js +111 -0
  217. package/dist/tui/agent-tree.js +10 -0
  218. package/dist/tui/ask-modal.js +2 -2
  219. package/dist/tui/ask-user-question-prompt.js +192 -0
  220. package/dist/tui/compact-banner.js +81 -0
  221. package/dist/tui/conversation-pane.js +82 -8
  222. package/dist/tui/cost-table.js +111 -0
  223. package/dist/tui/doctor-table.js +46 -0
  224. package/dist/tui/feedback-prompt.js +156 -0
  225. package/dist/tui/input-box.js +69 -2
  226. package/dist/tui/markdown-render.js +4 -4
  227. package/dist/tui/onboarding-wizard.js +240 -0
  228. package/dist/tui/permissions-picker.js +86 -0
  229. package/dist/tui/render.js +35 -0
  230. package/dist/tui/repl-render.js +303 -13
  231. package/dist/tui/repl-splash.js +2 -2
  232. package/dist/tui/repl.js +72 -14
  233. package/dist/tui/splash.js +1 -1
  234. package/dist/tui/status-bar.js +94 -16
  235. package/dist/tui/status-table.js +7 -0
  236. package/dist/tui/stickers-art.js +136 -0
  237. package/dist/tui/style-table.js +28 -0
  238. package/dist/tui/theme-table.js +29 -0
  239. package/dist/tui/tool-stream-pane.js +52 -3
  240. package/dist/tui/update-banner.js +20 -2
  241. package/dist/tui/vim-input.js +267 -0
  242. package/docs/examples/codegraph.mcp.json +10 -0
  243. package/package.json +12 -6
  244. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  245. package/test/scenarios/compact-force.scenario.txt +11 -0
  246. package/test/scenarios/identity.scenario.txt +11 -0
  247. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  248. package/test/scenarios/walkback.scenario.txt +12 -0
  249. package/dist/core/engine/compaction-hook.js +0 -154
@@ -2,6 +2,7 @@ import React from 'react';
2
2
  import { render } from 'ink';
3
3
  import { DeviceFlow } from './device-flow.js';
4
4
  import { LoginPicker } from './login-picker.js';
5
+ import { PermissionsPicker } from './permissions-picker.js';
5
6
  import { Splash } from './splash.js';
6
7
  import { collectSplashData } from './splash-data.js';
7
8
  /**
@@ -66,6 +67,40 @@ export function renderLoginPicker(apiUrl) {
66
67
  }));
67
68
  });
68
69
  }
70
+ /**
71
+ * Sentinel thrown when the operator dismisses the permissions picker
72
+ * via Esc / q. Mirrors `LoginCancelledError` — the host catches and
73
+ * prints a one-line abort message; no mode flip lands.
74
+ */
75
+ export class PermissionsPickerCancelledError extends Error {
76
+ constructor() {
77
+ super('Permissions picker cancelled');
78
+ this.name = 'PermissionsPickerCancelledError';
79
+ }
80
+ }
81
+ export function renderPermissionsPicker(options) {
82
+ return new Promise((resolveMode, rejectMode) => {
83
+ let settled = false;
84
+ const finish = (cb) => {
85
+ if (settled)
86
+ return;
87
+ settled = true;
88
+ instance.unmount();
89
+ setImmediate(cb);
90
+ };
91
+ const instance = render(React.createElement(PermissionsPicker, {
92
+ currentMode: options.currentMode,
93
+ sourceLabel: options.sourceLabel,
94
+ firstRun: options.firstRun ?? false,
95
+ onSelect: (mode) => {
96
+ finish(() => resolveMode(mode));
97
+ },
98
+ onCancel: () => {
99
+ finish(() => rejectMode(new PermissionsPickerCancelledError()));
100
+ },
101
+ }));
102
+ });
103
+ }
69
104
  /**
70
105
  * Mount `<DeviceFlow />` on a TTY and return a handle the host uses to
71
106
  * drive the frame. Mirrors `renderLoginPicker`'s lifecycle: we
@@ -16,14 +16,19 @@
16
16
  * a tiny `event:`/`data:`/`id:` parser. This keeps the dependency
17
17
  * graph at zero new packages.
18
18
  */
19
+ import { existsSync } from 'node:fs';
20
+ import { resolve } from 'node:path';
19
21
  import React from 'react';
20
22
  import { render } from 'ink';
21
23
  import { Repl } from './repl.js';
22
24
  import { printPugMascotPreInk } from './repl-splash-mascot.js';
25
+ import { ThemeProvider } from '../core/theme/context.js';
26
+ import { resolveTheme } from '../core/theme/state.js';
23
27
  import { ReplSession, } from '../core/repl/session.js';
24
28
  import { resolveWorkspaceContext } from '../core/repl/workspace-context.js';
25
29
  import { SqliteSessionStore } from '../core/repl/store/index.js';
26
30
  import { slugForCwd } from '../core/repl/history.js';
31
+ import { loadSettings } from '../core/settings.js';
27
32
  import { WorkingSet, buildRepoSkeleton, loadPugiIgnore, PugiWatcher, } from '../core/context/index.js';
28
33
  /**
29
34
  * Mount the REPL and resolve when the user exits via Ctrl+C × 2 or
@@ -31,12 +36,85 @@ import { WorkingSet, buildRepoSkeleton, loadPugiIgnore, PugiWatcher, } from '../
31
36
  * `pugi resume <sessionId>` once that command exists).
32
37
  */
33
38
  export async function renderRepl(options) {
39
+ // beta.9 CEO dogfood 2026-05-26: claim stdin raw mode + alt-screen
40
+ // BEFORE any async bootstrap step so keystrokes typed during the
41
+ // [launch -> Ink mount] window cannot echo into the terminal in
42
+ // cooked mode. Previously openLocalStore (SQLite open) +
43
+ // bootstrapContext (chokidar start) could take hundreds of ms to
44
+ // multiple seconds on a fresh install / large repo; during that
45
+ // window stdin stayed in cooked mode and the terminal echoed
46
+ // every typed character literally onto the screen below the
47
+ // pre-printed mascot/header. The visible result was the operator's
48
+ // "ssssss" landing on the rendered status-bar bottom row (CEO
49
+ // screenshot 2026-05-26: beta.8 REPL bug 2).
50
+ //
51
+ // The claim is idempotent with Ink's own raw-mode enable: Ink
52
+ // ref-counts setRawMode calls, and Node's stdin.setRawMode is
53
+ // safe to call twice with the same value. The pre-Ink claim acts
54
+ // as a "raw-mode floor" - whatever Ink does after mount layers on
55
+ // top, and our finally{} restore drops the floor only after Ink
56
+ // has cleanly torn down (or never mounted on a bootstrap crash).
57
+ const bootstrap = claimTerminalForRepl();
58
+ // beta.13 auto-init wire (CEO dogfood 2026-05-26): scaffold the
59
+ // `.pugi/` workspace silently on REPL boot so launching `pugi` in a
60
+ // fresh cwd no longer demands an explicit `pugi init` round-trip.
61
+ // Idempotent — every helper inside scaffoldPugiWorkspace is a
62
+ // `*_IfMissing` write, so re-running over an existing workspace is
63
+ // a no-op. Fail-safe: any FS / perms error never blocks REPL launch.
64
+ // Operator escape hatch: PUGI_NO_AUTO_INIT=1.
65
+ //
66
+ // Beta.13 P2 fix 2026-05-26: gate the scaffold on project-root markers
67
+ // so launching `pugi` from `$HOME` / `/tmp` / arbitrary dirs does NOT
68
+ // sprinkle `.pugi/` directories all over the filesystem. The gate
69
+ // mirrors `isBoundWorkspace` from workspace-context.ts but also
70
+ // accepts non-JS roots (Cargo / pyproject / go.mod) because the CLI
71
+ // is language-agnostic and an operator working in a Rust repo deserves
72
+ // the same auto-init UX as a Node operator. Already-bound `.pugi/`
73
+ // dirs also opt back in so the scaffold can fill any missing
74
+ // sub-artifacts the operator deleted.
75
+ // Leak L22 (2026-05-27): `--bare` (PUGI_BARE=1) ALSO suppresses the
76
+ // auto-init scaffold. Bare mode is the deterministic "fresh install
77
+ // anywhere" path — no `.pugi/` writes, no PUGI.md scaffold, no
78
+ // settings.json seed. The pre-existing PUGI_NO_AUTO_INIT escape
79
+ // hatch stays — bare mode just unions with it.
80
+ const { isBareMode } = await import('../core/bare-mode/index.js');
81
+ if (process.env.PUGI_NO_AUTO_INIT !== '1' &&
82
+ !isBareMode() &&
83
+ isProjectRoot(process.cwd())) {
84
+ try {
85
+ const { scaffoldPugiWorkspace } = await import('../runtime/cli.js');
86
+ await scaffoldPugiWorkspace({
87
+ cwd: process.cwd(),
88
+ noDefaults: true,
89
+ log: () => {
90
+ /* silent — never leak scaffold progress into the REPL alt-screen */
91
+ },
92
+ });
93
+ }
94
+ catch (err) {
95
+ // Fail-safe: read-only FS or perms error never blocks REPL launch.
96
+ // Beta.13 P2 fix 2026-05-26: bare-catch swallowed the diagnostic;
97
+ // surface it on stderr under PUGI_DEBUG=1 so operator-triage on
98
+ // "why isn't .pugi/ being created?" has a starting point without
99
+ // having to re-instrument the bootstrap.
100
+ if (process.env.PUGI_DEBUG === '1') {
101
+ const msg = err instanceof Error ? err.message : String(err);
102
+ process.stderr.write(`[pugi-debug] auto-init failed: ${msg}\n`);
103
+ }
104
+ }
105
+ }
34
106
  const transport = createProductionTransport();
35
107
  // Auto-bind the workspace context from process.cwd() so Mira knows
36
108
  // which repo the operator launched the CLI in. The resolver is
37
109
  // best-effort — any FS error falls back to a basename-only summary,
38
110
  // never blocks REPL launch. Wave 4 fix 2026-05-25.
39
111
  const workspace = options.workspace ?? resolveWorkspaceContext(process.cwd());
112
+ // Beta.13 P1 fix 2026-05-26: read `ui.cyberZoo` from
113
+ // `.pugi/settings.json` so the operator's splash posture flows to
114
+ // admin-api on session open. Without this, the renderer's `cyberZoo`
115
+ // parameter (added beta.13) was always defaulted to 'on' regardless
116
+ // of the operator's actual setting.
117
+ const cyberZoo = readCyberZooSetting(process.cwd());
40
118
  // α6.4: open the local SessionStore for `/resume` persistence. The
41
119
  // store lives under `~/.pugi/projects/<slug>/`; failure is fail-safe
42
120
  // — we log a one-line warning to stderr and continue with the REPL
@@ -64,6 +142,7 @@ export async function renderRepl(options) {
64
142
  cliVersion: options.cliVersion,
65
143
  transport,
66
144
  workspace,
145
+ cyberZoo,
67
146
  store,
68
147
  localSessionId: openedSessionId,
69
148
  repoSkeleton: skeleton,
@@ -86,26 +165,54 @@ export async function renderRepl(options) {
86
165
  // Kick off the connect; the Repl renders the connecting state until
87
166
  // the session pushes `connection: 'on_watch'` from the SSE onOpen.
88
167
  void session.start();
168
+ // beta.9: drain any keystrokes that landed in stdin between the
169
+ // pre-Ink raw-mode claim and now. Without this, the queued bytes
170
+ // would feed Ink's first useInput tick as a flood of "stale"
171
+ // characters once the InputBox mounts - the operator would see
172
+ // their pre-typed input materialise in the prompt as if they had
173
+ // typed it after the REPL became interactive. Idempotent: no-op
174
+ // when stdin is not a TTY or no bytes were buffered.
175
+ drainBufferedStdin(process.stdin);
89
176
  // α6.14.2 wave 5: paint the chafa-baked brand-pug ANSI render to
90
- // stdout BEFORE Ink mounts. Ink's layout engine would mis-measure
91
- // the truecolor escape sequences, so the pug must land verbatim.
92
- // The flag is passed into <Repl /> so the splash component knows to
93
- // skip its own hand-crafted PUG_MASCOT column — otherwise the
94
- // operator sees both the chafa pug AND the ASCII fallback stacked.
95
- // When skipSplash is true (operator opted out via --no-splash), we
96
- // suppress the pre-print too so the boot stays silent.
177
+ // stdout BEFORE Ink mounts (but AFTER alt-screen enter). Ink's
178
+ // layout engine would mis-measure the truecolor escape sequences,
179
+ // so the pug must land verbatim. The flag is passed into <Repl />
180
+ // so the splash component knows to skip its own hand-crafted
181
+ // PUG_MASCOT column - otherwise the operator sees both the chafa
182
+ // pug AND the ASCII fallback stacked. When skipSplash is true
183
+ // (operator opted out via --no-splash), we suppress the pre-print
184
+ // too so the boot stays silent.
97
185
  const mascotPrePrinted = options.skipSplash === true ? false : printPugMascotPreInk(process.stdout);
98
- const instance = render(React.createElement(Repl, {
186
+ // Leak L30 (2026-05-27): resolve the active theme ONCE at mount
187
+ // and wrap `<Repl />` in `<ThemeProvider>` so every Ink consumer
188
+ // (`<Header>`, `<DoctorTable>`, `<StyleTable>`, `<ThemeTable>`,
189
+ // …) picks up the same color tokens. The provider is stable for
190
+ // the lifetime of the REPL — operator `/theme <name>` writes to
191
+ // disk + appends a system line, and the next `pugi` launch re-
192
+ // mounts with the new slug. Re-mounting mid-session would race
193
+ // against Ink's raw-mode handler so we deliberately keep the
194
+ // session-lifetime contract instead of polling the config file.
195
+ const resolvedTheme = resolveTheme({
196
+ workspaceRoot: process.cwd(),
197
+ env: process.env,
198
+ });
199
+ const instance = render(React.createElement(ThemeProvider, { slug: resolvedTheme.slug }, React.createElement(Repl, {
99
200
  session,
100
201
  updateBanner: options.updateBanner ?? null,
101
202
  skipSplash: options.skipSplash === true,
102
203
  hideToolStream: options.hideToolStream === true,
103
204
  mascotPrePrinted,
104
- }));
205
+ })));
206
+ // Make sure we leave the alt screen on abrupt exits too. Without
207
+ // this the operator's shell stays "frozen" on the Pugi splash.
208
+ process.once('exit', bootstrap.restore);
209
+ process.once('SIGINT', bootstrap.restore);
210
+ process.once('SIGTERM', bootstrap.restore);
105
211
  try {
106
212
  await instance.waitUntilExit();
107
213
  }
108
214
  finally {
215
+ bootstrap.restore();
109
216
  session.close();
110
217
  if (store) {
111
218
  try {
@@ -125,6 +232,138 @@ export async function renderRepl(options) {
125
232
  }
126
233
  }
127
234
  }
235
+ export function claimTerminalForRepl(stdin = process.stdin, stdout = process.stdout) {
236
+ const isStdoutTty = stdout.isTTY === true;
237
+ const isStdinTty = stdin.isTTY === true && typeof stdin.setRawMode === 'function';
238
+ let altScreenEntered = false;
239
+ if (isStdoutTty) {
240
+ try {
241
+ stdout.write('\x1b[?1049h');
242
+ stdout.write('\x1b[H');
243
+ altScreenEntered = true;
244
+ }
245
+ catch {
246
+ /* terminal already detached */
247
+ }
248
+ }
249
+ let rawModeClaimed = false;
250
+ if (isStdinTty) {
251
+ try {
252
+ stdin.setEncoding('utf8');
253
+ stdin.setRawMode(true);
254
+ // Resume so the kernel actually delivers bytes to Node's event
255
+ // loop. Without resume, raw mode is set but data does not flow
256
+ // until something else (e.g. Ink) attaches a 'data' listener.
257
+ stdin.resume();
258
+ rawModeClaimed = true;
259
+ }
260
+ catch {
261
+ /* raw mode unsupported - the operator's shell still works */
262
+ }
263
+ }
264
+ let restored = false;
265
+ const restore = () => {
266
+ if (restored)
267
+ return;
268
+ restored = true;
269
+ if (rawModeClaimed && isStdinTty) {
270
+ try {
271
+ stdin.setRawMode(false);
272
+ }
273
+ catch {
274
+ /* terminal already detached */
275
+ }
276
+ }
277
+ if (altScreenEntered) {
278
+ try {
279
+ stdout.write('\x1b[?1049l');
280
+ }
281
+ catch {
282
+ /* shutdown race - terminal already detached */
283
+ }
284
+ }
285
+ };
286
+ return { altScreenEntered, rawModeClaimed, restore };
287
+ }
288
+ /**
289
+ * Read and discard any bytes buffered in stdin between
290
+ * `claimTerminalForRepl()` and the Ink mount. Returns the number of
291
+ * bytes drained so tests can assert the behaviour without intercepting
292
+ * the side effect.
293
+ *
294
+ * `stdin.read()` is a no-op when no data is buffered, so this is safe
295
+ * to call whether or not the operator actually typed during bootstrap.
296
+ * Wrapped in try/catch because a closed / piped stdin will throw on
297
+ * read in some Node versions.
298
+ */
299
+ export function drainBufferedStdin(stdin = process.stdin) {
300
+ if (stdin.isTTY !== true)
301
+ return 0;
302
+ try {
303
+ let bytesDrained = 0;
304
+ // Loop until read() returns null - readable streams may chunk
305
+ // buffered bytes across multiple read() calls when the operator
306
+ // typed faster than the kernel could deliver to Node's loop.
307
+ for (;;) {
308
+ const chunk = stdin.read();
309
+ if (chunk === null)
310
+ return bytesDrained;
311
+ bytesDrained += typeof chunk === 'string' ? chunk.length : chunk.byteLength;
312
+ }
313
+ }
314
+ catch {
315
+ return 0;
316
+ }
317
+ }
318
+ /**
319
+ * Project-root probe — beta.13 P2 fix 2026-05-26.
320
+ *
321
+ * Beta.13 auto-init was unconditional and silently created `.pugi/` in
322
+ * every cwd the REPL was launched from, including `$HOME` and `/tmp`.
323
+ * Operators who ran `pugi` to ask a quick question outside of any
324
+ * project ended up with stray `.pugi/` directories polluting their
325
+ * filesystem. The gate looks for any of six project-root markers
326
+ * before scaffolding:
327
+ *
328
+ * - `package.json` — JS / TS workspaces
329
+ * - `.git` — any cloned repo regardless of language
330
+ * - `.pugi` — already-bound Pugi workspace (re-scaffold
331
+ * fills any missing artifacts the operator
332
+ * deleted, idempotent over existing files)
333
+ * - `Cargo.toml` — Rust crates
334
+ * - `pyproject.toml` — Python projects (PEP 518)
335
+ * - `go.mod` — Go modules
336
+ *
337
+ * The probe is six cheap `existsSync` calls; the cost is negligible
338
+ * compared with the alt-screen + Ink mount that follows. Exported so a
339
+ * future unit spec can lock the contract.
340
+ */
341
+ export function isProjectRoot(cwd) {
342
+ // ESM static imports — `require()` is not defined in a `"type": "module"`
343
+ // bundle and would throw `ReferenceError: require is not defined` the
344
+ // moment the REPL bootstrap calls this gate. Beta.16 P0 fix 2026-05-27.
345
+ return (existsSync(resolve(cwd, 'package.json')) ||
346
+ existsSync(resolve(cwd, '.git')) ||
347
+ existsSync(resolve(cwd, '.pugi')) ||
348
+ existsSync(resolve(cwd, 'Cargo.toml')) ||
349
+ existsSync(resolve(cwd, 'pyproject.toml')) ||
350
+ existsSync(resolve(cwd, 'go.mod')));
351
+ }
352
+ /**
353
+ * Read the operator's cyber-zoo posture from `.pugi/settings.json`.
354
+ * Best-effort: when the file is missing / malformed, fall through to
355
+ * the historical 'on' default so the REPL never refuses to launch on
356
+ * a settings error. Beta.13 P1 fix 2026-05-26.
357
+ */
358
+ function readCyberZooSetting(cwd) {
359
+ try {
360
+ const settings = loadSettings(cwd);
361
+ return settings.ui?.cyberZoo ?? 'on';
362
+ }
363
+ catch {
364
+ return 'on';
365
+ }
366
+ }
128
367
  /**
129
368
  * Open the local SessionStore for the REPL bootstrap. Returns
130
369
  * `{ store: null, openedSessionId: undefined }` on any error so the
@@ -216,13 +455,20 @@ async function bootstrapContext(input) {
216
455
  /* ------------------------------------------------------------------ */
217
456
  /* Production transport */
218
457
  /* ------------------------------------------------------------------ */
219
- function createProductionTransport() {
458
+ export function createProductionTransport() {
220
459
  return {
221
- async createSession({ apiUrl, apiKey, workspace }) {
460
+ async createSession({ apiUrl, apiKey, workspace, cyberZoo }) {
222
461
  // Forward the workspace bundle in the POST body so admin-api can
223
462
  // surface `<workspace-context>` in Mira's prompt. Older admin-api
224
463
  // builds ignore unknown fields, so this stays forward-compatible.
225
464
  // Wave 4 fix 2026-05-25.
465
+ //
466
+ // Beta.13 P1 fix 2026-05-26: also forward `cyberZoo` so admin-api
467
+ // can render Mira's `<cyber-zoo>` marker matching the operator's
468
+ // `.pugi/settings.json::ui.cyberZoo` toggle instead of the
469
+ // historical 'on' default. Only included on the wire when set
470
+ // explicitly so a missing setting still survives older admin-api
471
+ // builds that do not declare the DTO field.
226
472
  const body = {};
227
473
  if (workspace?.workspaceCwd)
228
474
  body.workspaceCwd = workspace.workspaceCwd;
@@ -230,6 +476,8 @@ function createProductionTransport() {
230
476
  body.workspaceSlug = workspace.workspaceSlug;
231
477
  if (workspace?.workspaceSummary)
232
478
  body.workspaceSummary = workspace.workspaceSummary;
479
+ if (cyberZoo === 'on' || cyberZoo === 'off')
480
+ body.cyberZoo = cyberZoo;
233
481
  const response = await fetch(joinUrl(apiUrl, '/api/pugi/sessions'), {
234
482
  method: 'POST',
235
483
  headers: jsonHeaders(apiKey),
@@ -275,6 +523,31 @@ function createProductionTransport() {
275
523
  if (lastEventId) {
276
524
  headers['Last-Event-ID'] = lastEventId;
277
525
  }
526
+ // beta.9 CEO dogfood 2026-05-26: hard timeout on the SSE
527
+ // handshake so a CDN/proxy that buffers the response (or an
528
+ // admin-api that accepted the route but never flushed headers)
529
+ // cannot freeze the REPL in `connecting` forever. The 5s budget
530
+ // is generous - admin-api routinely responds in <500ms when
531
+ // healthy - but tight enough that an operator who launched
532
+ // `pugi` and is staring at the screen will see the status flip
533
+ // to `reconnecting` instead of an indefinite hang. The
534
+ // AbortController bound to the fetch aborts the in-flight
535
+ // request when the timer fires, which surfaces as an
536
+ // `AbortError` and routes through the existing onError handler
537
+ // (which calls scheduleReconnect via the session). The timer
538
+ // is cleared the moment onOpen fires so a slow-but-eventually-
539
+ // successful handshake still works.
540
+ const handshakeDeadlineMs = 5_000;
541
+ const handshakeTimer = setTimeout(() => {
542
+ controller.abort();
543
+ // onError is called from the catch block below (the abort
544
+ // synthesises an AbortError that consumeSseStream / fetch
545
+ // will throw). No explicit onError call here - we let the
546
+ // catch path normalise the error message so the operator
547
+ // sees the consistent "SSE handshake timed out (5s)" prose
548
+ // through the same plumbing that surfaces every other
549
+ // transport failure.
550
+ }, handshakeDeadlineMs);
278
551
  void (async () => {
279
552
  try {
280
553
  const response = await fetch(url, {
@@ -288,6 +561,9 @@ function createProductionTransport() {
288
561
  if (!response.body) {
289
562
  throw new Error('SSE response has no body');
290
563
  }
564
+ // Handshake survived; cancel the deadline so a slow
565
+ // first-event stream does not get aborted later.
566
+ clearTimeout(handshakeTimer);
291
567
  onOpen();
292
568
  await consumeSseStream(response.body, onEvent);
293
569
  // Server closed the stream cleanly. Treat as an error so
@@ -297,13 +573,27 @@ function createProductionTransport() {
297
573
  onError(new Error('SSE stream ended'));
298
574
  }
299
575
  catch (error) {
300
- if (controller.signal.aborted)
576
+ clearTimeout(handshakeTimer);
577
+ if (controller.signal.aborted) {
578
+ // Distinguish operator-driven close (session.close())
579
+ // from the handshake-deadline abort. The session sets a
580
+ // `closed` flag before calling controller.abort(); the
581
+ // handshake-deadline abort fires while the session is
582
+ // still expecting onOpen. We cannot read session state
583
+ // from here, so we surface a single error class with a
584
+ // clear message - the session-side onError handler
585
+ // already short-circuits when `closed=true`.
586
+ onError(new Error(`SSE handshake timed out after ${handshakeDeadlineMs}ms`));
301
587
  return;
588
+ }
302
589
  onError(error instanceof Error ? error : new Error(String(error)));
303
590
  }
304
591
  })();
305
592
  return {
306
- close: () => controller.abort(),
593
+ close: () => {
594
+ clearTimeout(handshakeTimer);
595
+ controller.abort();
596
+ },
307
597
  };
308
598
  },
309
599
  };
@@ -67,7 +67,7 @@ export function ReplSplash(props) {
67
67
  // pugs. The header card still renders inline so wordmark + status
68
68
  // rows stay attached to the splash flow.
69
69
  const showHandCraftedMascot = props.mascotPrePrinted !== true;
70
- return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { flexDirection: "row", children: [showHandCraftedMascot ? _jsx(MascotColumn, {}) : null, _jsxs(Box, { flexDirection: "column", marginLeft: showHandCraftedMascot ? 2 : 0, marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "cyan", children: ".io" }), _jsx(Text, { dimColor: true, children: ` v${props.cliVersion}` })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(HeaderRow, { label: "Plan", value: props.plan ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Model", value: props.model ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Tenant", value: props.tenant ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Workspace", value: props.workspaceLabel })] })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(40) }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Tips for getting started:" }), _jsx(TipRow, { index: 1, text: "Type a brief, the workforce dispatches" }), _jsx(TipRow, { index: 2, text: "/help for slash commands, /web <url> to pull a page" }), _jsx(TipRow, { index: 3, text: "/skills install <name> for Anthropic / OpenClaw skills" })] })] }));
70
+ return (_jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [_jsxs(Box, { flexDirection: "row", children: [showHandCraftedMascot ? _jsx(MascotColumn, {}) : null, _jsxs(Box, { flexDirection: "column", marginLeft: showHandCraftedMascot ? 2 : 0, marginTop: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Pugi" }), _jsx(Text, { bold: true, color: "#3da9fc", children: ".io" }), _jsx(Text, { dimColor: true, children: ` v${props.cliVersion}` })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(HeaderRow, { label: "Plan", value: props.plan ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Model", value: props.model ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Tenant", value: props.tenant ?? PLACEHOLDER }), _jsx(HeaderRow, { label: "Workspace", value: props.workspaceLabel })] })] })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: '─'.repeat(40) }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "Tips for getting started:" }), _jsx(TipRow, { index: 1, text: "Type a brief, the workforce dispatches" }), _jsx(TipRow, { index: 2, text: "/help for slash commands, /web <url> to pull a page" }), _jsx(TipRow, { index: 3, text: "/skills install <name> for Anthropic / OpenClaw skills" })] })] }));
71
71
  }
72
72
  /**
73
73
  * Renders the multi-line ASCII pug. Each row is split into colored
@@ -105,7 +105,7 @@ function MascotRow({ row, mask, }) {
105
105
  if (buffer.length > 0) {
106
106
  runs.push({ text: buffer, cyan: bufferCyan });
107
107
  }
108
- return (_jsx(Text, { children: runs.map((run, runIndex) => run.cyan ? (_jsx(Text, { color: "cyan", children: run.text }, runIndex)) : (_jsx(Text, { color: "gray", children: run.text }, runIndex))) }));
108
+ return (_jsx(Text, { children: runs.map((run, runIndex) => run.cyan ? (_jsx(Text, { color: "#3da9fc", children: run.text }, runIndex)) : (_jsx(Text, { color: "gray", children: run.text }, runIndex))) }));
109
109
  }
110
110
  function HeaderRow({ label, value }) {
111
111
  const padded = `${label}:`.padEnd(11, ' ');
package/dist/tui/repl.js CHANGED
@@ -29,6 +29,7 @@ import { StatusBar } from './status-bar.js';
29
29
  import { ToolStreamPane } from './tool-stream-pane.js';
30
30
  import { UpdateBanner } from './update-banner.js';
31
31
  import { collectWorkspaceContext } from './workspace-context.js';
32
+ import { useTheme } from '../core/theme/context.js';
32
33
  import { slugForCwd } from '../core/repl/history.js';
33
34
  import { SLASH_COMMAND_HELP, SLASH_COMMAND_GROUPS } from '../core/repl/slash-commands.js';
34
35
  const TICK_INTERVAL_MS = 200;
@@ -49,7 +50,12 @@ export function Repl(props) {
49
50
  // α6.14 wave 3: boot splash visible until first input, first
50
51
  // `agent.spawned` event, or 10s idle. The host gates the initial
51
52
  // visibility on `--no-splash` / PUGI_SKIP_SPLASH via `skipSplash`.
52
- const [splashVisible, setSplashVisible] = useState(props.skipSplash !== true);
53
+ // α6.14.6 CEO dogfood 2026-05-25: default splash to HIDDEN at boot
54
+ // (parity with Claude Code's minimal one-line banner). Operator can
55
+ // opt back in via `/splash` slash. The chafa pug pre-print + header
56
+ // line already give the brand cue without the multi-row Plan/Model/
57
+ // Tenant block crowding the top.
58
+ const [splashVisible, setSplashVisible] = useState(false);
53
59
  const dismissSplash = useCallback(() => setSplashVisible(false), []);
54
60
  // α6.14 wave 3: workspace context snapshot for the status bar. We
55
61
  // read once at mount and freeze; a brand-new PUGI.md or skill is
@@ -178,23 +184,73 @@ export function Repl(props) {
178
184
  return undefined;
179
185
  return props.session.cancel();
180
186
  }, [props.session, modalActive]);
181
- // α6.14.5 CEO dogfood 2026-05-25 (parity with Claude Code): the
182
- // input box must pin to the BOTTOM of the alt-screen viewport, not
183
- // float right under the conversation. Beta.3 attempt at full-height
184
- // broke keystroke focus (raw echo at row 79). The right pattern is
185
- // minHeight on the root + flexGrow on the conversation pane so the
186
- // empty space sits ABOVE the input, not below it. The input then
187
- // captures all keystrokes because it is the only Ink-focusable
188
- // surface adjacent to the cursor row.
187
+ // Wave 6 BT 8 (Claude Code parity): Esc-Esc walkback. Forwards to
188
+ // ReplSession.walkbackLastTurn which trims the trailing operator
189
+ // turn + its persona response from the in-memory transcript. Returns
190
+ // `'walked-back'` so the input box knows the host did the work;
191
+ // `'nothing'` covers both the empty-transcript and dispatch-in-flight
192
+ // refusals (the session module owns the refusal copy in both cases).
193
+ const handleWalkback = useCallback(() => {
194
+ if (modalActive)
195
+ return 'nothing';
196
+ const verdict = props.session.walkbackLastTurn();
197
+ return verdict === 'walked-back' ? 'walked-back' : 'nothing';
198
+ }, [props.session, modalActive]);
199
+ // Wave 7 — Shift+Tab cycles the 6 canonical permission modes (CC
200
+ // parity). Refuses while a modal is active so the operator does not
201
+ // accidentally flip mode mid-prompt; otherwise resolves the current
202
+ // mode through the workspace > global > default merge, advances via
203
+ // `nextPermissionMode`, и persists к .pugi/session.json. Returns the
204
+ // new mode string so the InputBox can flash a one-line toast.
205
+ const handleCyclePermissionMode = useCallback(() => {
206
+ if (modalActive)
207
+ return null;
208
+ try {
209
+ // Lazy-require так this code path doesn't drag the permissions
210
+ // module into the splash + boot stages where it isn't needed.
211
+ // The require is sync but the inner work is pure JSON IO.
212
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
213
+ const perm = require('../core/permissions/index.js');
214
+ const workspaceRoot = process.cwd();
215
+ const current = perm.resolveMode({ workspaceRoot });
216
+ const next = perm.nextPermissionMode(current);
217
+ perm.setCurrentMode(workspaceRoot, next);
218
+ return next;
219
+ }
220
+ catch {
221
+ // Persistence is best-effort — if .pugi/session.json is read-only
222
+ // или ENOENT-on-parent the toast is suppressed so we don't lie
223
+ // about the flip к the operator.
224
+ return null;
225
+ }
226
+ }, [modalActive]);
227
+ // α6.14.5 CEO dogfood 2026-05-25 (parity with Claude Code): input
228
+ // box pinned to alt-screen BOTTOM, conversation grows above it.
229
+ // Beta.3's height={rows} fix broke keystroke focus - raw echo at
230
+ // viewport bottom. The right pattern is minHeight on the root +
231
+ // flexGrow=1 on the MainArea Box: empty alt-screen lives ABOVE the
232
+ // input, and the input stays the sole focusable surface adjacent
233
+ // to the cursor row, so all keystrokes route through it.
189
234
  const altScreenRows = process.stdout.rows ?? 24;
190
- 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, 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,
235
+ 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, onWalkback: handleWalkback, onCyclePermissionMode: handleCyclePermissionMode, now: props.now,
191
236
  // Slug from process.cwd() (full path) so two workspaces with
192
237
  // the same basename do not share history. state.workspaceLabel
193
238
  // is the basename only. Codex review P2.
194
- 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 })] })] }));
239
+ 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, lastCompletedOutcome: state.lastCompletedOutcome,
240
+ // α7 cost-meter sprint — surface accumulated session totals
241
+ // + per-turn delta flash on the status bar's top row. The
242
+ // session module owns accumulation; the bar is a pure render.
243
+ sessionTokensIn: state.sessionTokensIn, sessionTokensOut: state.sessionTokensOut, sessionCostUsd: state.sessionCostUsd, sessionStartedAtEpochMs: state.sessionStartedAtEpochMs, lastTurnDelta: state.lastTurnDelta })] })] }));
195
244
  }
196
245
  function Header({ state }) {
197
- 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('_', ' ') })] }));
246
+ // Leak L30 (2026-05-27): the header `.io` brand accent + connection
247
+ // pill route through `useTheme()` so the operator's `/theme` flip
248
+ // (default / dark / light / colorblind) re-tints the chrome on
249
+ // re-mount. The `useTheme` hook returns the `default` preset's
250
+ // colors when no provider is mounted, preserving the previous
251
+ // `#3da9fc` constants for tests that import `<Repl />` standalone.
252
+ const theme = useTheme();
253
+ 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('_', ' ') })] }));
198
254
  }
199
255
  function MainArea({ state, personaNames, nowEpochMs, hideToolStream, toolStreamCollapsed, }) {
200
256
  // α6.12: three vertical panes stacked above the input box.
@@ -234,14 +290,14 @@ function HelpOverlay() {
234
290
  const rows = grouped.get(group);
235
291
  if (!rows || rows.length === 0)
236
292
  return null;
237
- 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));
293
+ 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));
238
294
  }), _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.' }) })] }));
239
295
  }
240
296
  function RosterOverlay() {
241
297
  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.' }) })] }));
242
298
  }
243
299
  function FarewellOverlay() {
244
- return (_jsx(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: _jsx(Text, { bold: true, color: "cyan", children: PUGI_TAGLINE }) }));
300
+ return (_jsx(Box, { flexDirection: "column", alignItems: "center", paddingY: 2, children: _jsx(Text, { bold: true, color: "#3da9fc", children: PUGI_TAGLINE }) }));
245
301
  }
246
302
  function applyVerdictSideEffects(verdict, handlers) {
247
303
  switch (verdict.kind) {
@@ -266,8 +322,10 @@ function applyVerdictSideEffects(verdict, handlers) {
266
322
  case 'consensus':
267
323
  case 'diff':
268
324
  case 'cost':
325
+ case 'quota':
269
326
  case 'status':
270
327
  case 'resume':
328
+ case 'mcp':
271
329
  case 'stub':
272
330
  // All non-overlay verdicts: the session module already appended
273
331
  // 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.