@pugi/cli 0.1.0-beta.36 → 0.1.0-beta.38

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.
@@ -29,6 +29,7 @@
29
29
  * `/quit` confirmation and `/help` footer - never inline.
30
30
  */
31
31
  import { listRoles } from '../agents/registry.js';
32
+ import { PERMISSION_MODES, parsePermissionMode, } from '../permissions/index.js';
32
33
  /**
33
34
  * Deterministic stub copy returned by the Tier 3 commands. Spec'd
34
35
  * inline so the unit test can pin the exact text without poking at
@@ -86,7 +87,7 @@ export const SLASH_COMMAND_HELP = Object.freeze([
86
87
  // Settings
87
88
  { name: 'config', args: '', gloss: 'Show config', group: 'Settings', stub: true },
88
89
  { name: 'privacy', args: '', gloss: 'Show privacy mode + contract', group: 'Settings' },
89
- { name: 'permissions', args: '[mode] [--persist]', gloss: 'Show or flip permission mode (plan / ask / allow / bypass) (also: /plan)', group: 'Settings' },
90
+ { name: 'permissions', args: '[mode] [--persist]', gloss: 'Show or flip permission mode (default / acceptEdits / plan / auto / dontAsk / bypassPermissions) (also: /plan, Shift+Tab cycle)', group: 'Settings' },
90
91
  { name: 'plan', args: '[--back | --persist] [<prompt>]', gloss: 'Switch to plan mode (read-only). Same as /permissions plan, slicker UX.', group: 'Settings' },
91
92
  { name: 'model', args: '[<slug>]', gloss: 'Show or select the active model. Bare /model lists tier-gated options', group: 'Settings' },
92
93
  { name: 'budget', args: '', gloss: 'Show usage budget', group: 'Settings', stub: true },
@@ -332,26 +333,29 @@ export function parseSlashCommand(input) {
332
333
  }
333
334
  case 'permissions':
334
335
  case 'perms': {
335
- // Leak L6: `/permissions [mode] [--persist] [--confirm]`.
336
+ // Wave 7: `/permissions [mode] [--persist] [--confirm]`.
336
337
  //
337
338
  // Argument grammar (single line, no quoting):
338
- // /permissions -> show current mode + table
339
- // /permissions plan|ask|allow -> flip mode
340
- // /permissions bypass --confirm -> flip to bypass (refused
341
- // without --confirm — safety)
339
+ // /permissions -> show + table
340
+ // /permissions default|acceptEdits|plan|auto|dontAsk -> flip mode
341
+ // /permissions bypassPermissions --confirm -> flip to
342
+ // bypassPermissions (refused
343
+ // без --confirm — safety)
342
344
  // /permissions <mode> --persist -> also write to ~/.pugi/config.json
343
345
  //
344
- // Anything else returns an `error` result so the runtime can
345
- // render the usage hint inline.
346
+ // α6 aliases (`ask`, `allow`, `bypass`) are accepted и mapped to
347
+ // their Wave 7 canonical names via `parsePermissionMode`.
346
348
  const tokens = tail.length === 0 ? [] : tail.split(/\s+/).filter((s) => s.length > 0);
347
349
  if (tokens.length === 0) {
348
350
  return { kind: 'permissions', persist: false, confirmBypass: false };
349
351
  }
350
- const head0 = tokens[0]?.toLowerCase();
351
- if (head0 !== 'plan' && head0 !== 'ask' && head0 !== 'allow' && head0 !== 'bypass') {
352
+ const headRaw = tokens[0] ?? '';
353
+ const mode = parsePermissionMode(headRaw);
354
+ if (!mode) {
355
+ const modeList = [...PERMISSION_MODES].join('|');
352
356
  return {
353
357
  kind: 'error',
354
- message: `Usage: /permissions [plan|ask|allow|bypass] [--persist] [--confirm]; unknown mode '${tokens[0] ?? ''}'`,
358
+ message: `Usage: /permissions [${modeList}] [--persist] [--confirm]; unknown mode '${headRaw}'`,
355
359
  };
356
360
  }
357
361
  const flags = tokens.slice(1);
@@ -371,7 +375,7 @@ export function parseSlashCommand(input) {
371
375
  };
372
376
  }
373
377
  }
374
- return { kind: 'permissions', mode: head0, persist, confirmBypass };
378
+ return { kind: 'permissions', mode, persist, confirmBypass };
375
379
  }
376
380
  case 'init': {
377
381
  // β1 Sl11: surface the init flow inside the REPL. Tail args
@@ -62,6 +62,54 @@ export async function fireSessionStartMvp(session) {
62
62
  return 0;
63
63
  }
64
64
  }
65
+ /**
66
+ * Wave 7 P1 — fire the v2 `SessionStart` event from `~/.pugi/hooks.json`
67
+ * (global) + `<workspaceRoot>/.pugi/hooks.json` (project). Companion to
68
+ * `fireSessionStartMvp`; both surfaces run because they read different
69
+ * config files.
70
+ *
71
+ * Headless by default (no trust prompt) — the v2 trust ledger gates
72
+ * first-run executions. Operators with no prior trust decision will see
73
+ * the SessionStart hook skipped with a `denied by trust ledger` stderr
74
+ * note; running `pugi hooks trust allow <command>` enrolls it.
75
+ *
76
+ * Returns the number of hooks that ran (excluding trust-denied skips).
77
+ * Never throws.
78
+ */
79
+ export async function fireSessionStartV2(session) {
80
+ try {
81
+ const { fireSessionStart } = await import('./hooks/v2/index.js');
82
+ const outcome = await fireSessionStart({
83
+ sessionId: session.id,
84
+ workspaceRoot: session.root,
85
+ transcriptPath: session.eventsPath,
86
+ permissionMode: 'ask',
87
+ });
88
+ return outcome.results.filter((r) => r.exitCode !== -1).length;
89
+ }
90
+ catch {
91
+ return 0;
92
+ }
93
+ }
94
+ /**
95
+ * Wave 7 P1 — fire the v2 `SessionEnd` event. Called by the REPL
96
+ * teardown path. Companion to `fireSessionStartV2`.
97
+ */
98
+ export async function fireSessionEndV2(session) {
99
+ try {
100
+ const { fireSessionEnd } = await import('./hooks/v2/index.js');
101
+ const outcome = await fireSessionEnd({
102
+ sessionId: session.id,
103
+ workspaceRoot: session.root,
104
+ transcriptPath: session.eventsPath,
105
+ permissionMode: 'ask',
106
+ });
107
+ return outcome.results.filter((r) => r.exitCode !== -1).length;
108
+ }
109
+ catch {
110
+ return 0;
111
+ }
112
+ }
65
113
  export function recordCommandStarted(session, command) {
66
114
  if (!session.enabled)
67
115
  return;
@@ -755,7 +755,7 @@ async function dispatchRewind(args, flags, _session) {
755
755
  async function dispatchPermissions(args, flags, _session) {
756
756
  const head = args[0];
757
757
  if (head && parsePermissionMode(head) === null) {
758
- writeOutput(flags, { error: 'unknown_mode', mode: head }, `Unknown mode '${head}'. Allowed: plan, ask, allow, bypass.`);
758
+ writeOutput(flags, { error: 'unknown_mode', mode: head }, `Unknown mode '${head}'. Allowed: default, acceptEdits, plan, auto, dontAsk, bypassPermissions (α6 aliases ask/allow/bypass accepted).`);
759
759
  process.exitCode = 1;
760
760
  return;
761
761
  }
@@ -780,8 +780,8 @@ async function dispatchPermissions(args, flags, _session) {
780
780
  await runPermissionsCommand({
781
781
  mode: chosen,
782
782
  persist: Boolean(flags.persist),
783
- // The picker selection IS the confirm gesture for `bypass`.
784
- confirmBypass: chosen === 'bypass' ? true : Boolean(flags.confirm),
783
+ // The picker selection IS the confirm gesture for `bypassPermissions`.
784
+ confirmBypass: chosen === 'bypassPermissions' ? true : Boolean(flags.confirm),
785
785
  }, {
786
786
  workspaceRoot: process.cwd(),
787
787
  writeOutput: (text) => writeOutput(flags, { text }, text),
@@ -1524,7 +1524,7 @@ function parseArgs(argv) {
1524
1524
  else if (arg === '--mode') {
1525
1525
  const next = argv[index + 1];
1526
1526
  if (!next || next.startsWith('--')) {
1527
- throw new Error('--mode requires plan|ask|allow|bypass');
1527
+ throw new Error('--mode requires default|acceptEdits|plan|auto|dontAsk|bypassPermissions (α6 aliases ask|allow|bypass accepted)');
1528
1528
  }
1529
1529
  flags.mode = next;
1530
1530
  index += 1;
@@ -1669,12 +1669,12 @@ const COMMAND_HELP_BODIES = {
1669
1669
  ' pugi explain "trace the auth flow in src/auth/"',
1670
1670
  ],
1671
1671
  code: [
1672
- 'pugi code "<brief>" — engineering-mode write loop (30k token budget).',
1672
+ 'pugi code "<brief>" — engineering-mode write loop (80k token budget).',
1673
1673
  '',
1674
1674
  'Writes files in the current workspace. Use --no-tty in CI / pipes.',
1675
1675
  ],
1676
1676
  fix: [
1677
- 'pugi fix "<brief>" — minimal-diff bugfix loop (30k token budget).',
1677
+ 'pugi fix "<brief>" — minimal-diff bugfix loop (50k token budget).',
1678
1678
  '',
1679
1679
  'Same as `pugi code` but the prompt biases toward the smallest patch',
1680
1680
  'that closes the brief — refuses scope creep / refactor invitations.',
@@ -29,9 +29,10 @@ export async function runPermissionsCommand(command, ctx) {
29
29
  renderModeTable(ctx);
30
30
  return;
31
31
  }
32
- if (command.mode === 'bypass' && !command.confirmBypass) {
33
- ctx.writeOutput('Bypass mode disables policy hooks (skill steering, denial tracking).');
34
- ctx.writeOutput('Run `/permissions bypass --confirm` to acknowledge before flipping.');
32
+ if (command.mode === 'bypassPermissions' && !command.confirmBypass) {
33
+ ctx.writeOutput('bypassPermissions disables policy hooks (skill steering, denial tracking) AND skips the deny-list.');
34
+ ctx.writeOutput('Catastrophic patterns (rm -rf /, fork bomb, dd if=/) still trip the circuit-breaker, но that is the only guardrail left.');
35
+ ctx.writeOutput('Run `/permissions bypassPermissions --confirm` to acknowledge before flipping.');
35
36
  return;
36
37
  }
37
38
  setCurrentMode(ctx.workspaceRoot, command.mode);
@@ -42,8 +43,8 @@ export async function runPermissionsCommand(command, ctx) {
42
43
  ? ' Persisted to ~/.pugi/config.json for future sessions.'
43
44
  : '';
44
45
  ctx.writeOutput(`Permission mode set to '${command.mode}'.${persistedHint} ${PERMISSION_MODE_GLOSS[command.mode]}`);
45
- if (command.mode === 'bypass') {
46
- ctx.writeOutput('BYPASS MODE — all tools execute without prompts AND policy hooks disabled. Switch back with /permissions allow.');
46
+ if (command.mode === 'bypassPermissions') {
47
+ ctx.writeOutput('bypassPermissions — all tools execute without prompts AND policy hooks disabled (circuit-breaker still trips on rm -rf /). Switch back with /permissions dontAsk.');
47
48
  }
48
49
  }
49
50
  /**
@@ -68,12 +69,13 @@ function renderCurrentMode(ctx) {
68
69
  */
69
70
  function renderModeTable(ctx) {
70
71
  ctx.writeOutput('');
71
- ctx.writeOutput('Permission modes:');
72
+ ctx.writeOutput('Permission modes (Shift+Tab cycles in REPL):');
72
73
  for (const mode of PERMISSION_MODES) {
73
- ctx.writeOutput(` ${mode.padEnd(7)} ${PERMISSION_MODE_GLOSS[mode]}`);
74
+ // Wave 7: longest canonical name is `bypassPermissions` (17 chars).
75
+ ctx.writeOutput(` ${mode.padEnd(18)} ${PERMISSION_MODE_GLOSS[mode]}`);
74
76
  }
75
77
  ctx.writeOutput('');
76
- ctx.writeOutput('Switch with `/permissions <mode> [--persist]`. Bypass requires `--confirm`.');
78
+ ctx.writeOutput('Switch with `/permissions <mode> [--persist]`. bypassPermissions requires `--confirm`.');
77
79
  }
78
80
  /**
79
81
  * Render the one-shot banner shown on session boot when the effective
@@ -82,7 +84,7 @@ function renderModeTable(ctx) {
82
84
  * but the caller is responsible for the once-only semantics.
83
85
  */
84
86
  export function renderBypassBanner(writeOutput) {
85
- writeOutput('BYPASS MODE — all tools execute without prompts AND policy hooks disabled. Switch back with /permissions allow.');
87
+ writeOutput('bypassPermissions — all tools execute without prompts AND policy hooks disabled (circuit-breaker still trips on rm -rf /). Switch back with /permissions dontAsk.');
86
88
  }
87
89
  /**
88
90
  * Resolve the effective mode + the layered source label used by the
@@ -135,9 +135,9 @@ function effectiveMode(ctx) {
135
135
  const global = getGlobalDefaultMode(ctx.homeDir);
136
136
  if (global)
137
137
  return global;
138
- // Defensive: PERMISSION_MODES[1] is 'ask' (the canonical default). We
139
- // index off the canonical list rather than re-import DEFAULT_PERMISSION_MODE
140
- // here to keep the symbol surface narrow.
141
- return PERMISSION_MODES[1] ?? 'ask';
138
+ // Wave 7 canonical default — `PERMISSION_MODES[0]` is `default` (the
139
+ // CC-parity ask-every-call ground state). Fall back literally if the
140
+ // array is empty (defensive should never happen).
141
+ return PERMISSION_MODES[0] ?? 'default';
142
142
  }
143
143
  //# sourceMappingURL=plan.js.map
@@ -44,7 +44,7 @@ export function sanitizeSemver(raw) {
44
44
  * during import). When bumping the CLI version BOTH literals must be
45
45
  * updated; the release smoke-test (`pack:smoke`) verifies they agree.
46
46
  */
47
- export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.36');
47
+ export const PUGI_CLI_VERSION = sanitizeSemver('0.1.0-beta.38');
48
48
  /**
49
49
  * Outbound: the CLI's installed semver. Read at request time by
50
50
  * `version-interceptor.ts` and injected on every `fetch` call.
@@ -107,6 +107,11 @@ export function InputBox(props) {
107
107
  // panes when Ctrl+L wipes the terminal (the parent React tree is
108
108
  // otherwise stable and would not redraw on a stdout.write alone).
109
109
  const [, setRedrawTick] = useState(0);
110
+ // Wave 7 Shift+Tab toast — flashed for 2s after a mode cycle so the
111
+ // operator sees `Mode → acceptEdits` под the input divider. Cleared
112
+ // by a setTimeout so a quick second Shift+Tab refreshes the toast.
113
+ const [modeCycleToast, setModeCycleToast] = useState(null);
114
+ const modeCycleTimerRef = useRef(null);
110
115
  const now = props.now ?? Date.now;
111
116
  const { stdout } = useStdout();
112
117
  const columns = stdout?.columns ?? FALLBACK_COLUMNS;
@@ -197,6 +202,24 @@ export function InputBox(props) {
197
202
  }
198
203
  return;
199
204
  }
205
+ // Wave 7 — Claude Code parity: Shift+Tab cycles permission mode.
206
+ // The host owns the cycle logic + persistence; we just intercept
207
+ // the chord and surface a one-line toast on success. Place this
208
+ // BEFORE the search-mode and palette branches so a Shift+Tab fires
209
+ // even while reverse-search is active (operator habit-driven).
210
+ if (key.shift && key.tab && props.onCyclePermissionMode) {
211
+ const nextMode = props.onCyclePermissionMode();
212
+ if (nextMode) {
213
+ setModeCycleToast(`Mode → ${nextMode}`);
214
+ if (modeCycleTimerRef.current)
215
+ clearTimeout(modeCycleTimerRef.current);
216
+ modeCycleTimerRef.current = setTimeout(() => {
217
+ setModeCycleToast(null);
218
+ modeCycleTimerRef.current = null;
219
+ }, 2_000);
220
+ }
221
+ return;
222
+ }
200
223
  // Search-mode key handling. Ctrl+R / Ctrl+S cycle, Enter accepts,
201
224
  // Esc cancels (restoring the pre-search draft), backspace shortens
202
225
  // the query, typed characters extend it.
@@ -543,7 +566,7 @@ export function InputBox(props) {
543
566
  : Math.min(paletteIndex, paletteView.rows.length - 1);
544
567
  const divider = '─'.repeat(innerWidth);
545
568
  const focusedMatch = search ? search.matches[search.focusedIndex] : undefined;
546
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Enter brief · Esc cancel · Ctrl+C abort / ×2 exit' }) })] }));
569
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), _jsx(Box, { paddingX: 1, flexDirection: "column", children: search ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '(reverse-i-search) ' }), _jsx(Text, { children: `\`${search.query}\`: ` }), _jsx(Text, { color: "yellow", children: focusedMatch ? focusedMatch.brief : '(no match)' })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: `Ctrl+R next · Ctrl+S prev · Enter accept · Esc cancel · ${search.matches.length} match${search.matches.length === 1 ? '' : 'es'}` }) })] })) : (_jsxs(Box, { children: [_jsx(Text, { color: "#3da9fc", children: '› ' }), _jsx(Text, { children: renderLineWithCursor(line, cursor, cursorVisible) })] })) }), _jsx(Text, { color: "#3da9fc", dimColor: true, children: divider }), modeCycleToast ? (_jsx(Box, { children: _jsx(Text, { color: "#3da9fc", bold: true, children: ` ${modeCycleToast}` }) })) : null, line.length > innerWidth - 4 ? (_jsxs(Box, { children: [_jsx(Text, { color: "gray", children: '┊ ' }), _jsx(Text, { dimColor: true, children: 'line wraps - Enter still submits' })] })) : null, _jsx(SlashPalette, { rows: paletteView.rows, focusedIndex: clampedPaletteIndex, totalBeforeLimit: paletteView.totalBeforeLimit }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: '↑/↓ history · Ctrl+R search · / commands · Shift+Tab mode · Enter brief · Esc cancel · Ctrl+C abort / ×2 exit' }) })] }));
547
570
  }
548
571
  /**
549
572
  * Render the line with the cursor glyph inserted at `cursor`. The cursor
@@ -49,15 +49,21 @@ export function PermissionsPicker(props) {
49
49
  props.onCancel();
50
50
  return;
51
51
  }
52
- // Number shortcuts mirror the legacy text table order.
52
+ // Wave 7: number shortcuts mirror the canonical 6-mode order.
53
+ // Operators coming from Claude Code reach the same mode with the
54
+ // same digit (1=default, 2=acceptEdits, …, 6=bypassPermissions).
53
55
  if (input === '1')
54
- props.onSelect('plan');
56
+ props.onSelect('default');
55
57
  if (input === '2')
56
- props.onSelect('ask');
58
+ props.onSelect('acceptEdits');
57
59
  if (input === '3')
58
- props.onSelect('allow');
60
+ props.onSelect('plan');
59
61
  if (input === '4')
60
- props.onSelect('bypass');
62
+ props.onSelect('auto');
63
+ if (input === '5')
64
+ props.onSelect('dontAsk');
65
+ if (input === '6')
66
+ props.onSelect('bypassPermissions');
61
67
  });
62
68
  return (_jsxs(Box, { flexDirection: "column", paddingX: 2, paddingY: 1, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, children: "Permission mode" }), _jsx(Text, { dimColor: true, children: ` (current: ${props.currentMode} — ${props.sourceLabel})` })] }), props.firstRun ? (_jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "First time? Mode = Ask \u043F\u043E \u0443\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u044E. Use /permissions \u043A change later." }) })) : null, _jsx(Box, { marginTop: 1, flexDirection: "column", children: ITEMS.map((item, itemIndex) => {
63
69
  const isSelected = itemIndex === index;
@@ -71,7 +77,9 @@ function PickerRow({ isSelected, isCurrent, title, hint, }) {
71
77
  // mode (separate from cursor focus) so an operator instantly sees
72
78
  // which row is "what I have now" vs "what I'm hovering".
73
79
  const indicator = isSelected ? '▸ ' : ' ';
74
- const padded = title.padEnd(10, ' ');
80
+ // Wave 7: longest title is `BypassPermissions` (17 chars). Bump the
81
+ // pad column так the gloss column stays aligned across all 6 rows.
82
+ const padded = title.padEnd(18, ' ');
75
83
  const currentMarker = isCurrent ? ' ●' : ' ';
76
84
  return (_jsxs(Text, { children: [_jsxs(Text, { color: isSelected ? 'cyan' : undefined, bold: isSelected, children: [indicator, padded] }), _jsx(Text, { color: isCurrent ? 'green' : undefined, children: currentMarker }), _jsx(Text, { dimColor: true, children: ` ${hint}` })] }));
77
85
  }
package/dist/tui/repl.js CHANGED
@@ -196,6 +196,34 @@ export function Repl(props) {
196
196
  const verdict = props.session.walkbackLastTurn();
197
197
  return verdict === 'walked-back' ? 'walked-back' : 'nothing';
198
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]);
199
227
  // α6.14.5 CEO dogfood 2026-05-25 (parity with Claude Code): input
200
228
  // box pinned to alt-screen BOTTOM, conversation grows above it.
201
229
  // Beta.3's height={rows} fix broke keystroke focus - raw echo at
@@ -204,7 +232,7 @@ export function Repl(props) {
204
232
  // input, and the input stays the sole focusable surface adjacent
205
233
  // to the cursor row, so all keystrokes route through it.
206
234
  const altScreenRows = process.stdout.rows ?? 24;
207
- 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, 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,
208
236
  // Slug from process.cwd() (full path) so two workspaces with
209
237
  // the same basename do not share history. state.workspaceLabel
210
238
  // is the basename only. Codex review P2.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pugi/cli",
3
- "version": "0.1.0-beta.36",
3
+ "version": "0.1.0-beta.38",
4
4
  "description": "Pugi CLI - terminal-native software execution system",
5
5
  "homepage": "https://pugi.io",
6
6
  "repository": {
@@ -55,7 +55,7 @@
55
55
  "undici": "^8.3.0",
56
56
  "zod": "^3.23.0",
57
57
  "@pugi/personas": "0.1.2",
58
- "@pugi/sdk": "0.1.0-beta.36"
58
+ "@pugi/sdk": "0.1.0-beta.38"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@types/node": "^22.0.0",