@agfpd/iapeer 0.4.3 → 0.4.4

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.
package/README.md CHANGED
@@ -76,6 +76,18 @@ Two ways to work with an agent locally in your terminal:
76
76
 
77
77
  Agents reach each other with `send_to_peer`; you can also message any peer over Telegram.
78
78
 
79
+ ## Bypass Permissions mode
80
+
81
+ ⚠️ **iapeer runs your Claude and Codex peers in bypass-permissions / no-sandbox mode — they execute tools (shell, file edits, network) without asking you to approve each action.** This is by design: a peer is a background, headless session with no human watching its terminal, so there is no one to answer a per-tool permission prompt. The launcher therefore starts Claude with `--dangerously-skip-permissions` and Codex with `--dangerously-bypass-approvals-and-sandbox`, and **auto-accepts the one-time "accept bypass mode" screen on your behalf** the first time a peer boots on a fresh machine.
82
+
83
+ What this means for you, stated plainly:
84
+
85
+ - A peer can run any command its runtime can run — read and write files across your home dir, install software, make network calls — with **no confirmation step**. There is no sandbox between an agent and your machine.
86
+ - You are trusting the model's judgement (and the doctrine/instructions you give each peer) the same way you would trust a developer with a shell on your account.
87
+ - iapeer does **not** silently weaken your own interactive `claude`/`codex` setup: the bypass acceptance is recorded per peer in the agent's own config, and the project-MCP pre-approval is written **project-locally** (under the peer's folder), never to your global user settings.
88
+
89
+ Run iapeer on a host you control, give peers only the access they need, and treat a peer's instructions as security-relevant. `iapeer onboard` states this at setup; it is repeated here so the trade-off is explicit before you create your first agent. Details on every first-run dialog: [docs/07 — Install & update](docs/07-install-and-update.md#first-run-dialogs-on-a-clean-machine).
90
+
79
91
  ## What makes it different
80
92
 
81
93
  - **Real interactive CLI sessions, not headless one-shots.** Each AI peer is a full Claude Code or Codex CLI session, not a disposable `claude -p` / `codex exec` call (as of 2026-06-15, `claude -p` no longer counts against the subscription's main usage limits).
@@ -47,6 +47,24 @@ You'll see the core version, the daemon's health, and the memory-slot state. The
47
47
 
48
48
  A breakdown of each onboard step is in [02 — Quick start](02-quickstart.md); runtimes and memory in detail — in [09](09-runtimes.md) and [11](11-extensions-and-memory.md).
49
49
 
50
+ ## First-run dialogs on a clean machine
51
+
52
+ On a fresh macOS host the first install → onboard → peer launch surfaces a handful of dialogs. iapeer auto-clears the agent runtimes' own first-run modals when it brings a peer up; **three** items are yours to do.
53
+
54
+ **Auto-cleared (iapeer answers them on peer boot — you do nothing):**
55
+
56
+ - **Claude** — the theme picker ("Choose the text style"), folder-trust, external-CLAUDE.md-import, and dev-channels modals (the launcher accepts the default with Enter); the **bypass-permissions accept** screen ("WARNING: Claude Code running in Bypass Permissions mode") that a *virgin* config shows the first time `--dangerously-skip-permissions` runs — its default cursor is "No, exit", so the launcher **steps the cursor to "Yes, I accept"** and confirms (never a bare Enter, which would exit); and the **project MCP-server approval** ("N new MCP servers found in this project") when the peer's cwd carries a `.mcp.json` — pre-approved at `iapeer init` via a project-local `enableAllProjectMcpServers` flag and Enter-confirmed on boot as a backstop. Claude records each accept natively, so a re-launch on the same config is dialog-free.
57
+ - **Codex** — folder-trust ("Do you trust the contents of this directory") is pre-trusted at peer creation and also auto-accepted on boot; the self-update offer is disabled (`check_for_update_on_startup = false`) and auto-declined; hooks-review is auto-trusted.
58
+ - **Bypass / no-sandbox is the operating mode, not a one-time prompt.** The launcher runs Claude with `--dangerously-skip-permissions` and Codex with `--dangerously-bypass-approvals-and-sandbox`, so peers execute tools without per-action approval (a headless peer has no human to approve each call). The one-time *acceptance* screen above is the only interactive gate; the mode itself then stays on for every turn. See the security note in the [README](../README.md#bypass-permissions-mode) — this is a deliberate, disclosed trade-off.
59
+
60
+ **Three things you do (these cannot be automated):**
61
+
62
+ 1. **Log in to the runtimes — before onboard.** Sign in to Claude Code and Codex first. Their login screen is a browser OAuth flow that no headless peer can drive, so iapeer deliberately does **not** auto-answer it; a runtime that is installed but not signed in makes a peer fail its first wake loudly. `iapeer onboard` warns when it finds an unauthenticated runtime.
63
+ 2. **Login keychain password — once, during install.** The install code-signs the binary with a stable local identity so Full Disk Access survives updates; macOS asks for your login keychain password the one time that identity is created. It is expected (the installer announces it). If declined, the binary still works — TCC prompts would just recur on updates.
64
+ 3. **Full Disk Access — a manual grant, no prompt.** macOS TCC cannot be set by any flag, env var, or script. Without it, a peer reading or writing a TCC-protected path (an iCloud Obsidian vault, Desktop, Documents, Downloads) silently fails with EPERM — no prompt, no hang. Grant it in **System Settings → Privacy & Security → Full Disk Access** (`~/.iapeer` itself is under `$HOME` and needs nothing); onboard prints the reminder.
65
+
66
+ Gatekeeper does not gate the locally-built CLI run from a terminal (no quarantine attribute). Verified live on a truly-virgin Claude config (claude 2.1.183): the theme picker, folder-trust, the bypass-permissions accept (cursor stepped to "Yes"), and the project MCP-server approval all auto-clear in sequence, the peer reaches its input prompt, and a second launch on the same config is dialog-free; the Codex folder-trust / update auto-clearing likewise (an owner's clean-machine install + an isolated first-boot test).
67
+
50
68
  ## Updating
51
69
 
52
70
  ```bash
@@ -55,7 +55,7 @@ iapeer compact assistant # compact the conversation
55
55
 
56
56
  `interrupt` stops a turn that's gone off-track or hung, without losing context. `compact` compacts the conversation; if the peer is asleep after a clean sleep, the daemon resumes it first, then compacts, and for an empty session it honestly answers that there's nothing to compact.
57
57
 
58
- Separate are **aliases** — shortcuts like `/alias-new`, `/alias-compact`, defined in the peer profile. These aren't control but text templates: the runtime expands an alias into its full text and delivers it to the peer as an ordinary message. So with one short command the operator runs a pre-written scenario (e.g. "save state to memory and restart fresh").
58
+ Separate are **aliases** — shortcuts like `/alias_new`, `/alias_compact`, defined in the peer profile. These aren't control but text templates: the runtime expands an alias into its full text and delivers it to the peer as an ordinary message. So with one short command the operator runs a pre-written scenario (e.g. "save state to memory and restart fresh").
59
59
 
60
60
  ## Ephemeral workers
61
61
 
@@ -47,6 +47,24 @@ iapeer status
47
47
 
48
48
  Разбор каждого шага onboard — в [02 — Быстрый старт](02-быстрый-старт.md); рантаймы и память подробно — в [09](09-рантаймы.md) и [11](11-расширения-и-память.md).
49
49
 
50
+ ## Диалоги первого запуска на чистой машине
51
+
52
+ На свежем macOS-хосте первый цикл install → onboard → запуск пира выводит несколько диалогов. Собственные first-run-модалки агент-рантаймов iapeer авто-снимает при поднятии пира; **три** пункта — на тебе.
53
+
54
+ **Авто-снимается (iapeer отвечает на boot пира — от тебя ничего):**
55
+
56
+ - **Claude** — theme-picker («Choose the text style»), folder-trust, импорт внешнего CLAUDE.md, dev-channels (лончер принимает дефолт через Enter); экран **принятия bypass-permissions** («WARNING: Claude Code running in Bypass Permissions mode»), который *виргин*-конфиг показывает при первом запуске `--dangerously-skip-permissions` — его курсор по умолчанию стоит на «No, exit», поэтому лончер **шагает курсором на «Yes, I accept»** и подтверждает (никогда не голый Enter — он бы вышел из сессии); и **аппрув проектных MCP-серверов** («N new MCP servers found in this project»), когда cwd пира несёт `.mcp.json` — пред-одобряется на `iapeer init` через проектно-локальный флаг `enableAllProjectMcpServers` и на boot подтверждается Enter'ом как страховка. Каждый accept claude записывает нативно, поэтому повторный запуск на том же конфиге уже без диалогов.
57
+ - **Codex** — folder-trust («Do you trust the contents of this directory») пред-доверяется при создании пира и авто-принимается на boot; self-update-оффер отключён (`check_for_update_on_startup = false`) и авто-отклоняется; hooks-review авто-доверяется.
58
+ - **Bypass / no-sandbox — это режим работы, а не разовый промпт.** Лончер запускает Claude с `--dangerously-skip-permissions`, а Codex с `--dangerously-bypass-approvals-and-sandbox`, поэтому пиры выполняют инструменты без поштучного аппрува (у headless-пира нет человека, который подтверждал бы каждый вызов). Разовый экран *принятия* выше — единственный интерактивный гейт; сам режим затем остаётся включённым на каждом ходу. См. security-заметку в [README](../../README.md#bypass-permissions-mode) — это намеренный, раскрытый компромисс.
59
+
60
+ **Три пункта на тебе (автоматизировать нельзя):**
61
+
62
+ 1. **Залогинься в рантаймы — ДО onboard.** Войди в Claude Code и Codex заранее. Их экран логина — браузерный OAuth, который headless-пир не пройдёт, поэтому iapeer НАМЕРЕННО его не авто-отвечает; установленный, но не залогиненный рантайм роняет первый wake пира громко. `iapeer onboard` предупреждает, если найдёт неаутентифицированный рантайм.
63
+ 2. **Пароль логин-связки — один раз, при install.** Install подписывает бинарь стабильной локальной идентичностью, чтобы Full Disk Access пережил обновления; macOS один раз спросит пароль логин-связки при создании этой идентичности. Это ожидаемо (инсталлер анонсирует). При отказе бинарь всё равно работает — просто TCC-промпты будут повторяться на апдейтах.
64
+ 3. **Full Disk Access — ручной грант, без промпта.** macOS TCC нельзя выставить ни флагом, ни env, ни скриптом. Без него пир, читающий/пишущий TCC-защищённый путь (iCloud Obsidian vault, Desktop, Documents, Downloads), тихо падает с EPERM — без промпта, без зависания. Выдай в **System Settings → Privacy & Security → Full Disk Access** (`~/.iapeer` сам под `$HOME` и ничего не требует); onboard напомнит.
65
+
66
+ Gatekeeper не гейтит локально собранный CLI, запускаемый из терминала (нет quarantine-атрибута). Проверено живьём на по-настоящему виргин-конфиге Claude (claude 2.1.183): theme-picker, folder-trust, принятие bypass-permissions (курсор шагнут на «Yes») и аппрув проектных MCP-серверов авто-снимаются по очереди, пир доходит до строки ввода, а повторный запуск на том же конфиге — уже без диалогов; codex folder-trust / update — аналогично (clean-install владельца + изолированный first-boot тест).
67
+
50
68
  ## Обновление
51
69
 
52
70
  ```bash
@@ -55,7 +55,7 @@ iapeer compact assistant # сжать диалог
55
55
 
56
56
  `interrupt` останавливает зашедший не туда или зависший ход, не теряя контекст. `compact` сжимает диалог; если пир спит после чистого усыпления, демон сначала продолжит диалог, потом сожмёт, а для пустой сессии честно ответит, что сжимать нечего.
57
57
 
58
- Отдельно стоят **алиасы** — шорткаты вида `/alias-new`, `/alias-compact`, заданные в профиле пира. Это не контроль, а заготовки текста: рантайм разворачивает алиас в полный текст и доставляет его пиру как обычное сообщение. Так оператор одной короткой командой запускает заранее прописанный сценарий (например, «сохрани состояние в память и перезапустись свежим»).
58
+ Отдельно стоят **алиасы** — шорткаты вида `/alias_new`, `/alias_compact`, заданные в профиле пира. Это не контроль, а заготовки текста: рантайм разворачивает алиас в полный текст и доставляет его пиру как обычное сообщение. Так оператор одной короткой командой запускает заранее прописанный сценарий (например, «сохрани состояние в память и перезапустись свежим»).
59
59
 
60
60
  ## Эфемерные воркеры
61
61
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agfpd/iapeer",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "description": "Foundation core for the iapeer multi-agent ecosystem: identity, registry, storage, codec.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "agfpd",
@@ -44,9 +44,9 @@
44
44
  "test": "IAPEER_TEST_SANDBOX=1 bun test",
45
45
  "typecheck": "tsc --noEmit",
46
46
  "preversion": "npm run test && npm run typecheck",
47
- "release": "npm version patch && npm publish && git push --follow-tags",
48
- "release:minor": "npm version minor && npm publish && git push --follow-tags",
49
- "release:major": "npm version major && npm publish && git push --follow-tags",
47
+ "release": "npm version patch && npm publish && git push origin main --follow-tags",
48
+ "release:minor": "npm version minor && npm publish && git push origin main --follow-tags",
49
+ "release:major": "npm version major && npm publish && git push origin main --follow-tags",
50
50
  "prepublishOnly": "test -z \"$(git status --porcelain)\" || (echo 'release: working tree is dirty — commit or stash before release' >&2 && exit 1)"
51
51
  },
52
52
  "publishConfig": {
package/src/cli/index.ts CHANGED
@@ -480,7 +480,7 @@ export interface NewPeerOutcome {
480
480
  * new <peer> [runtime] — the UNCONDITIONAL fresh-restart control command
481
481
  * (docs/Control-команды §new).
482
482
  *
483
- * The emergency lever for a HUNG or dead agent session: /alias-new covers only
483
+ * The emergency lever for a HUNG or dead agent session: /alias_new covers only
484
484
  * the COOPERATIVE path (a live peer reads the expanded prompt and runs
485
485
  * `iapeer self-fresh`); a stuck/raving/dead peer never reads a prompt — this
486
486
  * command restarts it MECHANICALLY, bypassing the peer entirely:
@@ -121,13 +121,13 @@ export function validateProfileStandard(raw: unknown, cwd: string): ProfileIssue
121
121
  issues.push({ severity: 'error', field: 'interfaces', message: 'must be a JSON object' })
122
122
  }
123
123
  // Legacy top-level aliases — contract relocates to the `expansion.aliases` plugin-
124
- // config section with the /alias-* namespace (clean slash-keys are reserved for the
124
+ // config section with the /alias_* namespace (clean slash-keys are reserved for the
125
125
  // control layer). telegram-runtime owns that migration; flagged, not an error.
126
126
  if (obj.aliases !== undefined) {
127
127
  issues.push({
128
128
  severity: 'warn',
129
129
  field: 'aliases',
130
- message: 'legacy top-level `aliases` — contract relocates to expansion.aliases (/alias-* namespace); owner migration pending',
130
+ message: 'legacy top-level `aliases` — contract relocates to expansion.aliases (/alias_* namespace); owner migration pending',
131
131
  })
132
132
  }
133
133
  // aliases misfiled in the PASSPORT (interfaces.telegram) — an interim home.
package/src/init/index.ts CHANGED
@@ -6,16 +6,19 @@
6
6
  // THE BIG SIMPLIFICATION (install-gate, proven live): a peer gets send_to_peer
7
7
  // purely from a project-scope `.mcp.json` pointing at the host-wide HTTP-MCP daemon —
8
8
  // NO plugin install, NO /reload-plugins, NO GC version-snapshots (the whole legacy
9
- // persistent-peer install-gate class is gone). Verified: a cold-start claude session
10
- // (--dangerously-skip-permissions, as the launch primitive runs it) auto-enables the
11
- // `.mcp.json` http server and send_to_peer is callable on its FIRST turn, no approve.
9
+ // persistent-peer install-gate class is gone). A cold-start claude session
10
+ // (--dangerously-skip-permissions, as the launch primitive runs it) finds the
11
+ // `.mcp.json` http server and send_to_peer is callable on its FIRST turn. The server's
12
+ // project-approval is pre-granted by ensureClaudeProjectMcpEnabled (the
13
+ // enableAllProjectMcpServers flag below); on a VIRGIN config without it the launch's
14
+ // bootDialogKeys clears the one-time "new MCP servers found" dialog as a backstop.
12
15
  //
13
16
  // claude side here. codex side (`[mcp_servers.<name>]` in ~/.codex/config.toml +
14
17
  // default_tools_approval_mode="approve") lands with its own live codex check.
15
18
 
16
19
  import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
17
20
  import { homedir } from 'os'
18
- import { join } from 'path'
21
+ import { dirname, join } from 'path'
19
22
  import { CODEX_BEARER_ENV_VAR, CODEX_DUMMY_BEARER, IAPEER_DIR, isInfraRuntime, type Runtime } from '../core/constants.ts'
20
23
  import { IapError } from '../core/errors.ts'
21
24
  import {
@@ -26,6 +29,7 @@ import {
26
29
  type StorageOptions,
27
30
  } from '../storage/index.ts'
28
31
  import { provisionPeer, type ProvisionResult } from '../provision/index.ts'
32
+ import { claudeSettingsPath } from '../launch/nativeMemory.ts'
29
33
  import { launchctlBootstrap, launchdPlistPath, type BootstrapResult } from '../launch/launchd.ts'
30
34
  import { runtimeSelfConfig, type SelfConfigResult } from '../runtime/index.ts'
31
35
  import type { Intelligence } from '../core/constants.ts'
@@ -116,6 +120,46 @@ export function writeClaudeMcpConfig(cwd: string, personality: string, daemonUrl
116
120
  return path
117
121
  }
118
122
 
123
+ /**
124
+ * Idempotently enable project-scope MCP servers for a claude peer by merging
125
+ * `"enableAllProjectMcpServers": true` into `<cwd>/.claude/settings.json`.
126
+ *
127
+ * Why: on a VIRGIN claude config, the FIRST launch in a cwd carrying a `.mcp.json`
128
+ * (writeClaudeMcpConfig above) shows a BLOCKING "N new MCP servers found in this
129
+ * project" approval dialog BEFORE the input prompt — a headless peer has no one to
130
+ * approve it, so the wake stalls. This project-LOCAL flag pre-approves the cwd's MCP
131
+ * servers so the dialog never appears (verified live, claude 2.1.183). It is
132
+ * PROJECT-scoped (this peer's cwd only) — it does NOT touch the user's GLOBAL
133
+ * ~/.claude/settings.json, so it never alters the user's own claude environment (the
134
+ * same constraint that forbids a global skipDangerousModePermissionPrompt). The launch
135
+ * adapter's bootDialogKeys also Enter-confirms this dialog as a belt-and-suspenders
136
+ * backstop, so a peer whose cwd predates this flag still boots.
137
+ *
138
+ * NO-CLOBBER merge (the file may carry foreign blocks — plugin enables, statusline
139
+ * wrappers, the native-memory lever): parse/patch + atomic write; a non-object
140
+ * settings.json is REFUSED rather than overwritten. Returns the written path, or null
141
+ * when already set / on a refusal / on any error (best-effort, never throws — the
142
+ * bootDialogKeys backstop covers the dialog regardless).
143
+ */
144
+ export function ensureClaudeProjectMcpEnabled(cwd: string): string | null {
145
+ const path = claudeSettingsPath(cwd)
146
+ try {
147
+ let obj: Record<string, unknown> = {}
148
+ if (existsSync(path)) {
149
+ const raw = JSON.parse(readFileSync(path, 'utf8')) as unknown
150
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null // refuse to clobber a non-object
151
+ obj = raw as Record<string, unknown>
152
+ }
153
+ if (obj.enableAllProjectMcpServers === true) return path // already enabled
154
+ obj.enableAllProjectMcpServers = true
155
+ mkdirSync(dirname(path), { recursive: true })
156
+ writeFileAtomic(path, `${JSON.stringify(obj, null, 2)}\n`, 0o644)
157
+ return path
158
+ } catch {
159
+ return null // best-effort; the bootDialogKeys backstop still clears the dialog
160
+ }
161
+ }
162
+
119
163
  // ─────────────────────────────────────────────────────────────────────────────
120
164
  // codex MCP config (~/.codex/config.toml — HOST-WIDE; the token-free recipe)
121
165
  // ─────────────────────────────────────────────────────────────────────────────
@@ -429,6 +473,10 @@ export async function initPeer(opts: InitPeerOptions): Promise<InitPeerResult> {
429
473
  // MCP client), so it gets no `.mcp.json`.
430
474
  if (provisioned.runtime === 'claude' || hasClaudeMarker(provisioned.cwd)) {
431
475
  mcpConfigPaths.push(writeClaudeMcpConfig(provisioned.cwd, provisioned.personality, daemonUrl))
476
+ // Pre-approve the cwd's project MCP servers so the FIRST virgin-config launch does
477
+ // not stall on the blocking "N new MCP servers found" dialog (project-local, never
478
+ // touches the user's global settings; bootDialogKeys is the backstop).
479
+ ensureClaudeProjectMcpEnabled(provisioned.cwd)
432
480
  }
433
481
  // codex: write the token-free host-wide config.toml block (dummy bearer flips codex's
434
482
  // auth gate; the OPEN daemon authenticates by the identity header). send_to_peer works
@@ -50,11 +50,27 @@ const CLAUDE_BOOT_DIALOG_MARKERS = [
50
50
  'Resume from summary',
51
51
  'Resuming the full session',
52
52
  // First-run theme picker ("Let's get started. Choose the text style…") — appears
53
- // on a CLEAN host (fresh config dir) BEFORE the input prompt, verified live on
54
- // claude 2.1.181 in a pty. NOT removed by --dangerously-skip-permissions; if not
55
- // cleared the peer never reaches ready. (There is NO separate "accept dangerous
56
- // permissions" screen in 2.1.181 — verified absent.)
53
+ // on a CLEAN host (fresh config dir) BEFORE the input prompt. NOT removed by
54
+ // --dangerously-skip-permissions; if not cleared the peer never reaches ready.
57
55
  'Choose the text style',
56
+ // Bypass-permissions ACCEPT gate ("WARNING: Claude Code running in Bypass
57
+ // Permissions mode"; options "1. No, exit" / "2. Yes, I accept") — shown on a
58
+ // VIRGIN config the FIRST time --dangerously-skip-permissions runs, BEFORE the
59
+ // input prompt. (An earlier note here claimed this screen was "absent in 2.1.181";
60
+ // that was a NON-VIRGIN observation — the host had already accepted it once, so it
61
+ // was masked. On a truly virgin config it IS shown and BLOCKS the boot — verified
62
+ // live, claude 2.1.183 via tmux. bootDialogKeys steps the cursor to "2. Yes, I
63
+ // accept" and confirms; a bare Enter would hit the "1. No, exit" DEFAULT and kill
64
+ // the peer.) Once accepted, claude records it natively and the ready banner is the
65
+ // lowercase "bypass permissions on" (a DISTINCT string), so this marker vanishes —
66
+ // it never falsely traps isInputReady, and a re-launch is dialog-free (verified).
67
+ 'Bypass Permissions mode',
68
+ // Project MCP-server approval ("N new MCP servers found in this project", servers
69
+ // pre-checked [✔]) — shown when cwd carries a .mcp.json the config has not yet
70
+ // approved. Suppressed at the source by enableAllProjectMcpServers in
71
+ // <cwd>/.claude/settings.json (init writes it); bootDialogKeys Enter-confirms it as
72
+ // a backstop. Substring matches both "1 new MCP server" and "N new MCP servers".
73
+ 'new MCP server',
58
74
  ] as const
59
75
 
60
76
  function anyBootDialog(pane: string): boolean {
@@ -164,34 +180,43 @@ export const claudeAdapter: RuntimeAdapter = {
164
180
  /**
165
181
  * Map a visible startup dialog to the keys that clear it correctly:
166
182
  *
167
- * - RESUME COMPACT-PICKER ('Resume from summary …') → CURSOR-VERIFIED two-step.
168
- * This menu's cursor DEFAULTS to "1. Resume from summary (recommended)",
169
- * and selecting it COMPACTS the session (claude implements that choice as an
170
- * internal /compact) — but a resume must NOT trigger a compact. The original
171
- * blind ['Down','Enter'] burst (0e67e1f) put BOTH keys into one pty chunk;
172
- * under a heavy resume (a 313k-token session) the TUI dropped the Down and
173
- * the Enter confirmed the DEFAULT →
174
- * silent /compact (proven: pane log shows the picker frame with ❯ on
175
- * option 1 followed by "❯ /compact"; transcript compactMetadata
176
- * trigger=manual). Fix: ONE key per boot-iteration — Down while the cursor
177
- * is NOT yet seen on "2. Resume full session", Enter ONLY after the
178
- * captured pane PROVES it ("❯ 2."). A swallowed Down self-heals on the
179
- * next 2 s iteration; Enter can never hit the compacting default. If the
180
- * picker layout ever changes, the loop Downs until the boot deadline and
181
- * the wake fails LOUD — never a silent compact.
182
- * - OTHER MODALS (folder-trust / external-import / dev-channels) → ['Enter']:
183
- * their default-highlighted option IS the proceed path, so a bare Enter clears
184
- * each (claude-start.sh:341).
183
+ * - CURSOR-VERIFIED two-step menus ('Resume from summary …' AND 'Bypass
184
+ * Permissions mode') → step Down, confirm ONLY once the captured pane proves
185
+ * the cursor sits on "2.". Both menus share one hazard: their DEFAULT cursor is
186
+ * the WRONG option, so a blind Enter picks it.
187
+ * · Resume: default "1. Resume from summary (recommended)" COMPACTS the
188
+ * session (claude runs that choice as an internal /compact) — a resume must
189
+ * not compact.
190
+ * · Bypass: default "1. No, exit" makes the peer EXIT (die). "2. Yes, I
191
+ * accept" is the proceed path. (Virgin-config gate the first time
192
+ * --dangerously-skip-permissions runs; once accepted claude persists it.)
193
+ * The original blind ['Down','Enter'] burst (0e67e1f) put BOTH keys in one pty
194
+ * chunk; under load the TUI dropped the Down and the Enter confirmed the DEFAULT
195
+ * (proven for resume: pane frame ❯ on option 1 → "❯ /compact"). Fix: ONE key
196
+ * per boot-iteration — Down while the cursor is NOT yet on "2.", Enter ONLY
197
+ * after the captured pane PROVES it ("❯ 2."). A swallowed Down self-heals next
198
+ * iteration; Enter can never hit the bad default. If the layout ever changes the
199
+ * loop Downs until the boot deadline and the wake fails LOUD.
200
+ * - SINGLE-CONFIRM modals (folder-trust / external-import / dev-channels / theme /
201
+ * project MCP-server approval) → ['Enter']: the default-highlighted option IS
202
+ * the proceed path, so a bare Enter clears each. The MCP approval pre-checks the
203
+ * servers [✔] (Enter enables them, Esc rejects all) — it is also suppressed at
204
+ * the source by enableAllProjectMcpServers (init), so the Enter here is a
205
+ * backstop. (claude-start.sh:341.)
185
206
  * - anything else (incl. the post-select "Resuming…" load state) → null (wait).
186
207
  */
187
208
  bootDialogKeys(pane: string): string[] | null {
188
- if (pane.includes('Resume from summary')) {
209
+ if (pane.includes('Resume from summary') || pane.includes('Bypass Permissions mode')) {
189
210
  return /❯\s*2\./.test(pane) ? ['Enter'] : ['Down']
190
211
  }
191
212
  if (
192
213
  pane.includes('trust this folder') ||
193
214
  pane.includes('Allow external CLAUDE.md file imports?') ||
194
215
  pane.includes('I am using this for local development') ||
216
+ // Project MCP-server approval (servers pre-checked [✔]; Enter confirms, Esc
217
+ // rejects all). enableAllProjectMcpServers normally suppresses it; this is the
218
+ // backstop. Matches both "1 new MCP server" and "N new MCP servers".
219
+ pane.includes('new MCP server') ||
195
220
  // First-run theme picker (clean host): the default-highlighted row is always a
196
221
  // valid theme (any choice is harmless for a headless peer), so a bare Enter
197
222
  // accepts it. Without this the picker blocks the boot forever on a fresh host.
@@ -936,7 +936,7 @@ export interface ControlResult {
936
936
  * acts on a running session — a non-live peer has nothing to interrupt) then
937
937
  * executeControlOnTarget. Without a runtime, resolves the single live runtime (2+ →
938
938
  * "specify runtime"). The clean-slash control namespace (interrupt/compact/…) is the
939
- * caller's; /alias-* expansions are message, not control (docs/Control §namespace).
939
+ * caller's; /alias_* expansions are message, not control (docs/Control §namespace).
940
940
  */
941
941
  export async function routeControl(personality: string, runtime: string | undefined, command: ControlCommand): Promise<Result<ControlResult>> {
942
942
  if (!isValidName(personality)) {