Skip to content

Multi-AI: vendor/route split, blind rounds, Antigravity, and a review panel that found its own bugs - #87

Closed
Unconfirmed2 wants to merge 67 commits into
Zen4-bit:mainfrom
Unconfirmed2:multiai-tab
Closed

Multi-AI: vendor/route split, blind rounds, Antigravity, and a review panel that found its own bugs#87
Unconfirmed2 wants to merge 67 commits into
Zen4-bit:mainfrom
Unconfirmed2:multiai-tab

Conversation

@Unconfirmed2

Copy link
Copy Markdown

Brings the Multi-AI tab's local branch up to date. 67 commits ahead of main; the last 19 are from one working session and are what this description covers. Full suite green: 799 tests, 0 failures.

The roster answers two questions instead of one

"Claude" and "Claude Code" sat side by side in the Provider menu as rival vendors while the Route cell beside them was greyed out — the provider id had already decided the route, so the row stated the same fact twice, and the choice that mattered (subscription or API key) hid inside a menu entry called auto whose label read auto → tab next to a separate browser tab entry.

Provider is the vendor now; Route is the surface — browser tab, that vendor's local CLI, or its API key. Internally the route is still half of the provider id, because that is what the main process, the REST port and every saved chat speak, so nothing downstream changed and no chat needed migrating.

The API route is offered only where Settings can honour it. It used to be permanent, and choosing it with API mode off changed nothing at all — resolveRoute() sends api and auto down the same branch — so the menu advertised a choice the app ignored.

The model picker was an <input list=…>, and Chromium filters a datalist by whatever is already in the box: a seat set to sonnet was only ever offered the four sonnet ids and the other fourteen looked as though they had failed to load. It is a grouped <select> now, labelled by where each entry came from, with custom… for the ids no list enumerates.

Every round runs blind

Turn-taking makes a round asymmetric — the first seat answers you, the last answers five peers — and later seats converge on whoever spoke first. independentRounds (off | first | all) defaults to all: each round is one parallel batch, blind to itself and shown every completed round.

The blind filter was also too narrow to carry that default. It hid role: 'assistant' for the whole cycle, which would have blinded round 2 to round 1, and it leaked a peer's PASS and its workspace diff into a supposedly blind round.

Verified live before merging: three CLI seats over two rounds, each asked to report only what it could see. Every seat in round 1 answered SEEN-NOW: NONE / SEEN-BEFORE: NONE; every seat in round 2 answered SEEN-NOW: NONE / SEEN-BEFORE: <all three>.

Antigravity, and Gemini CLI

Google retired Gemini CLI for individual plans on 2026-06-18. Verified rather than assumed — the installed CLI answers IneligibleTierError — so every Gemini CLI seat was failing outright. Antigravity (agy) is added as a vendor of its own rather than Gemini's CLI route, because agy models also lists Claude Sonnet 4.6, Claude Opus 4.6 and GPT-OSS 120B.

Its ids encode effort (gemini-3.8-flash-high) and it takes --effort, and passing both is a hard error — so discovery folds the ids into families and the Effort column drives --effort, as it does for every other CLI.

Two bugs surfaced underneath: whichAsync skipped preferExecutable, took where's first line (gemini.ps1) and wrote it into the cache which() reads — so every Gemini turn died at spawn … ENOENT while discovery still reported a version. And nothing passed --skip-trust, which a headless turn cannot answer interactively.

Read-only runs stopped hijacking your checkout

prepareWorkspace fired for any tools level, and read is one. Attaching a folder to a review panel checked your working copy out onto multiai/… as a side effect of reading files, and refused to start over uncommitted work — which is the normal state of a folder you want reviewed. A read-only run commits nothing, so it needs no branch and no git repository.

Documentation, a tour, and a review panel

Help moves beside Settings, opens at 90% with a section rail, and the text caught up with the app — four features had never been documented at all.

electron/multiai-tour.js is nine steps over the real screen, answering "a video for each part of the tab": a recorded video of a UI is wrong the first time a label moves and says nothing about it, and this week the Provider column changed meaning twice. tests/electron/multiai-tour.test.js reads every id, selector and column index out of the step definitions and asserts they exist, so the tour breaks loudly rather than quietly pointing at the wrong cell.

scripts/review-panel.mjs hands a diff to every installed CLI at once, blind, then lets round two read the others' findings. Run against this branch, it found seven real bugs in it — including a runner that recorded a failed process's stdout as a seat's reply, a CLI seat that could never reach its vendor's API key, and a temp directory leaked on every turn. All fixed here.

Testing

754 → 799 tests. The agent roster had none; the flakiest assertions were wall clocks ("all three start within 30ms"), replaced by an ordering check that serial dispatch cannot satisfy.

Unconfirmed2 and others added 30 commits September 3, 2026 04:47
Classic (discuss/debate/personas/judge) and Orchestrated (crew/workflow/loop/agent) modes inside the Proxima Electron app, with ledger, message labels, attachments and context references.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…e, directed turns, REST for local agents

Fixes from the gap analysis of the tab against the PCA/Proxima transcripts:

- Prompts are self-sufficient and bounded (recent window verbatim + condensed digest + pinned state summary) instead of re-sending the whole transcript; errors/passes never fed back (multiai-core.cjs).

- One provider-side thread per chat per provider, rotated every 12 turns; judge/summary/title use fresh aux threads. No more bleed between chats, the MCP tools and the auto-titler.

- Stop token (DONE) / PASS convergence ends runs early; optional parallel blind first round; assigned stances for 3+ debaters; @mention directed turns; state-summary compaction; Gemini engine picker.

- Correctness: judge scope by (cycle, round); per-chat running state (no double runs, switching chats no longer re-enables Send); orchestrated steps recorded by main so background runs are kept; failed crew role stops the pipeline; workflow parser only treats real provider names as providers; local titles by default.

- Store: atomic writes with rolling backup and corruption parking; renderer snapshots are upsert-only and merge with main-process appends; explicit deletes with tombstones.

- REST /v1/multiai/* so Codex / Claude Code can list, read (since), append turns, run rounds and stop; route-extension hook in rest-api.cjs; WebSocket init no longer runs twice on port fallback.

- Dev checkout uses its own userData (proxima-dev) and a per-profile single-instance lock; fork builds do not auto-update from upstream unless settings.autoUpdate=true.

- Tests: tests/electron/ipc (29) covering store, prompt builder, run loop, cancellation, judge scope, REST. Docs: docs/MULTIAI.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…mary / full transcript); version 5.1.0

Per-chat Context option: Bounded (default, summary + digest + recent window), Delta (only messages since this provider's last turn on its own thread, rolling auto-summary as the topic block, full catch-up on a fresh/rotated thread), Full (entire transcript every turn, the original PCA behaviour). Delta threads rotate every 40 turns instead of 12. Fork version bumped to 5.1.0 for the installer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…d, bench repeat failures

- Delta mode: an error starts a new provider generation so the next turn is a full catch-up (Gemini's engine wipes its thread on any failure).

- Judge/summary/title share one aux thread per chat and provider, rotated every 5 uses, instead of opening a new web conversation per call.

- A participant that fails twice in a row is benched for the rest of the run with a system note.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…ex; real Stop via engine abort

- src/mcp/tools-multiai.js: multiai_list_chats, multiai_read_chat, multiai_create_chat, multiai_post_message, multiai_run_round, multiai_wait_for_run, multiai_stop — via new multiai* IPC actions in main-v2.cjs, backed by a shared api object in ipc/multiai.cjs that the REST routes now use too (plus POST /wait).

- Engines expose abort(); send() is wrapped so a user cancel rejects with 'Cancelled by user' (terminal, not retried) and Gemini's self-heal retry is suppressed after a cancel. sender.abortActive(provider, conversationId) aborts only the matching in-flight call; options.shouldRun skips queued ones. Stop in the tab, REST and MCP all go through it; in Delta mode the cancelled provider gets a fresh thread.

- Tests: 77 across ipc/providers/mcp for the new paths; full suite 671/671.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…s; live-tested abort and per-chat threads

- Charter mode (per chat, also via REST/MCP create): claim tags VERIFIED/UNVERIFIED/ASSUMED, agreement-needs-a-reason, mandatory HANDOFF/OPEN/COMPLETE trailer, HANDOFF → routing (next in round or first next round, including from a local agent's posted message), rotating red-team seat marked in the feed.

- Gemini engine logs per-frame candidate lengths (window.__proximaGeminiFrames) to pin down the stitched-reply parser bug.

- REST/MCP version strings read package.json; createChat accepts the full option set.

- Live test on this machine (dev checkout, shared profile): ChatGPT turns on per-chat threads, Stop aborted the in-flight call inside the tab ('Cancelled by user', not retried) and the provider answered normally afterwards. Full suite 674/674.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…e & Codex CLIs), models and efforts

Participants become agents: name + provider + route + model + effort + persona.
The same provider can sit at the table more than once ("Opus max" vs "Sonnet
low", "GPT high" vs "GPT low"), which is what a single-provider panel needs.

- Routes: auto follows Settings (API mode on + key -> provider API with the
  model chosen there or the agent's own; else browser tab); browser; api
  (falls back to the tab with a warning); cli for claude-cli / codex-cli,
  run locally on the user's subscription (electron/providers/cli.cjs:
  one-shot, tool-less, empty scratch dir, <=2 parallel per CLI, abortable).
- BYOK: reasoningEffort -> OpenAI reasoning_effort / Anthropic extended
  thinking budget. Keys and API mode live only in Settings; the tab reads them.
- Delta context falls back to bounded for one-shot routes; browser threads
  are keyed per agent (multiai:<chat>:<agentId>:<gen>).
- Renderer: Agents block (cards with provider/route/model/effort/persona,
  quick-add, route status, Settings re-read); legacy participants migrate.
- REST/MCP: create accepts agents, reads show resolved routes; labels use
  agent codes.
- Tests: core agents helpers, handlers (api/cli routes, fallback, stop),
  MCP schema, BYOK effort payloads.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…t; status line follows agent edits

Found in the live CLI test: two Claude Code seats both labelled 0101CL.
Messages now carry the agent code (SL/SH, CX for Codex, CG2 for a second
ChatGPT seat) and refLabel prefers it; claude-cli/codex-cli get CC/CX.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…he API

When Settings flips API mode on, an auto agent whose model is a Gemini engine
name ("3.1-pro") would have named a model the API rejects. On auto, the
agent's model is used only if Settings lists it; otherwise the Settings
selection applies. An explicit api route still trusts what was typed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
… the Multi-AI tab

- Agents block is now a roster: one compact row per agent with a checkbox.
  Unticked agents are benched for that chat (kept configured; no turns,
  no mentions, not introduced to the others — normalizeAgents drops
  enabled:false). all / none shortcuts; run guard asks to tick a box.
- Agents are defined once: "+ Add agent" (quick presets, library, Custom…
  editor) and live in uiPrefs.agentLibrary keyed by uid; every new chat
  starts with the whole library on; editing a library agent applies to
  every chat carrying it; row ✕ = this chat only, library ✕ = for good.
  Legacy/REST-created agents stay chat-local.
- REST/MCP accept enabled on agents; readers see the active roster.
- Startup: opens on Multi-AI (Settings → General → Open on start to
  change); main's set-active-provider no longer yanks the user to a
  provider tab; provider views start off-screen so they never cover the
  Multi-AI panel while initializing.
- Help modal gains the Agents/Routes sections that were written but never
  saved in the previous commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…into the header

Option D from the layout review:
- Options panel is a tab strip (Agents · Discussion · Context · Charter ·
  Judge; Pipeline in Orchestrated) over one pane, collapsible to the strip
  plus a one-line summary. Active tab / collapsed state persist in uiPrefs.
- Agents tab is a table: one row per agent, every cell edited in place
  (name, provider, route with resolved target, model with hints, effort,
  persona), checkbox per row, ⧉ / ✕, "+ add agent…" menu (library + new
  row per provider), all / none, library chips, personas folder, ↻.
- Composer is a boxed message field with an action row (📎, Judge,
  Summarize, .md export, "to <agents> · mode · rounds" hint, ▶ Send / ■ Stop).
- Header: provider tabs and Settings sit inline with the logo, smaller;
  kept clear of the window-controls overlay; status dots inline.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…s; Gemini CLI route

cli.discover() probes what each installed CLI actually offers on this machine
and the agent table uses it instead of hard-coded lists:
- Claude Code: version, --effort levels and --model aliases parsed from
  claude --help, default model from ~/.claude/settings.json.
- Codex: version, the account's model list from ~/.codex/models_cache.json
  (slug, display name, supported reasoning levels, default level), defaults
  and [profiles.*] from ~/.codex/config.toml. A configured default that is
  not on the list is flagged; a typed model off the list gets an amber
  border; the effort is clamped to what the model supports at run time.
- Gemini CLI (new provider gemini-cli): version and whether a login method
  is configured; runs gemini -p … -o json --approval-mode plan with the
  prompt on stdin; no effort switch.
Discovery runs in the background after first paint and on ↻; the roster
footer shows versions / model counts / login state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…any MCP client

Three tools on the existing proxima MCP server (src/mcp/tools-cli.js):
- cli_variants: what the installed CLIs offer (versions, models, effort
  levels, defaults, login) plus the named CLI agents in the Multi-AI library.
- ask_variant: one question through one model x effort seat on Claude Code /
  Codex / Gemini CLI, by library name or shorthand (opus:max, gpt-5.5:high,
  gemini-2.5-flash); a library agent's persona rides along as system prompt.
- ask_variants: the same question to up to 8 seats in parallel, side by side;
  failures reported per seat.
Backed by multiaiVariants / multiaiAskVariant IPC actions (api.variants /
api.askVariant in electron/ipc/multiai.cjs), so the app's discovery cache,
library and concurrency caps apply.

Also from the live probe: CLI concurrency slots are created per kind
(Gemini calls crashed on a missing slot), and the Gemini CLI error envelope
is read from stderr with a plain "not logged in" message.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…ompletions, WebSocket fan-out

- electron/providers/variants.cjs: the shared variant layer (library seats,
  model[:effort] shorthand, explicit provider/model/effort, one-shot run,
  fan-out, OpenAI model entries + chat.completion envelope). The app's
  multiai handlers and the standalone server both use it.
- src/mcp/variants-server.js (npm run mcp:variants / proxima-variants-mcp):
  cli_variants / ask_variant / ask_variants with no running app — probes and
  runs the CLIs in-process, reads the agent library from Proxima's store
  file. Settings -> MCP shows its config separately. tools-cli.js takes an
  in-process backend or falls back to IPC.
- REST: GET /v1/multiai/variants, POST /v1/multiai/variants/ask and
  /ask-many; POST /v1/chat/completions with model "cli:opus:max",
  "cli:gpt-5.5:high" or a library seat's name (json or SSE); GET /v1/models
  lists cli:<seat> entries so OpenAI-compatible clients can pick them.
- WebSocket: variants, ask_variant, ask_variants (one response per seat as
  it finishes, then done).
- cli.cjs: MCP clients spawn servers without PATHEXT, which hid codex.exe
  from where; restored, and the usual install folders are checked when a
  CLI is not on PATH; the resolved path is used to spawn.
- Packaging: cli.cjs, variants.cjs and multiai-core.cjs are asar-unpacked
  so the standalone server runs from the installed app.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…side the asar)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…l seats

Workspace seats: the Workspace tab attaches a git folder to a chat; CLI
seats get a Tools level (none / read / write / full) that maps to Claude
Code allowedTools + permission modes, Codex sandbox modes and Gemini CLI
approval modes, all run with cwd = the folder. Before a run the folder is
put on the chat's own branch (multiai/<chat>) - refusing to touch
uncommitted work on another branch, committing leftovers on its own - and
every write/full turn is committed under the Proxima identity and recorded
as a role 'change' message (file list, stat, bounded diff) that the other
seats read and the feed shows as a folding diff card. Writers are
serialised per chat; readers overlap. Workspace refusals land in the
transcript.

Local-model seats: Settings > API > Local / custom endpoint registers any
OpenAI-compatible server (Ollama, LM Studio, vLLM...) as an api-only
provider; routeInfo lists it with the models its /models reports, the
roster pins it to the api route with no effort switch and a required
model id, and it runs with API mode off. BYOK transport now allows plain
http:// so localhost servers actually work.

Codex on Windows: `codex exec` has no -a flag; read/write need the
sandbox helpers, which only the Codex desktop app bundles - sandboxed
turns run that bundled codex.exe when the PATH one lacks them, discovery
reports sandbox: native|bundle|missing, and the Tools cell explains.

MCP/REST create-chat accept workspace {root} and per-agent tools; the
provider is a free string so custom endpoints work. Tests: workspace.cjs
(real git), handler runs (commit + change message, writer serialisation,
refusals), CLI arg levels, http transport, renderer smoke.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
resolveCodex() compares the Codex desktop app's bundled codex.exe
(%LOCALAPPDATA%\OpenAI\Codex\bin\<hash>\, ships the Windows sandbox
helpers) with the codex on PATH by --version and uses the newer one for
seats, variants and discovery; ties and unknown versions go to the bundle
because only it can sandbox read/write turns. Discovery reports
exe/source/alternatives/sandbox, the Agents footer shows "Codex 0.152.1
(desktop app) . also 0.142.5 on PATH - newest wins" and warns when the
chosen binary has no sandbox helpers. A machine with only the desktop app
now gets Codex seats too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
A chat's branch is now cut from the base branch - the branch the folder is
on if it is one of the user's (main, feature-x...), otherwise origin's
default, main, master, or the first local non-chat branch - never from
another chat's multiai/* branch, so chat B no longer inherits chat A's
edits. workspace.inspect() + the multiai-workspace-info IPC let the
Workspace tab preview the folder before any run: repo or not, current
branch, clean or dirty, and where this chat's branch would start.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ACdKJmXfzvwfCCLneCWd12
…the code does

Five strings asserted safety guarantees the branch contradicts, in the two
places a cautious user looks before pointing this at a real repository:

- the help modal said "none can read or write files on this computer" while a
  CLI seat with a Tools level reads and writes the workspace folder;
- the modal and the Workspace pane both promised uncommitted work is never
  swept in, while workspace.prepare() commits everything dirty on the chat's
  own branch as "WIP left by an interrupted turn";
- the modal called the Orchestrated Agent strategy "the only strategy that
  touches your actual filesystem" — true before this branch, and now steering
  attention away from the Classic seats that are confined by less;
- the Tools dropdown labelled `none` "no file access" when a Codex seat at
  `none` runs with approval:never and sandbox:danger-full-access, and `none`
  is the default.

Copy only, no behaviour change. A missing warning is a gap; a stated guarantee
that is wrong is what makes people stop checking.

Also adds docs/MULTIAI-REVIEW.md, the master findings document behind this and
the following commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…placing the no-tools clause

Two defects on the same externally-reachable path (REST /v1/multiai/variants/ask,
POST /v1/chat/completions, the WebSocket ask_variant action and the ask_variant
MCP tool — i.e. reachable from another model).

1. `model` and `effort` were assigned from the request body with only String()
   coercion, then pushed as argv to a CLI spawned with shell:true. On Windows
   that is cmd.exe, which interprets & | < > ^ ( ) %, and the quoting helper
   only quotes on whitespace-or-quote and escapes with \" — not a cmd escape.
   All four payloads in the new test were verified to execute `whoami` before
   this change, including the quoted one. Both fields are now validated against
   the character class the shorthand form already required, which still admits
   Claude Code's [1m] context suffix.

2. `body.system || ONE_SHOT_SYSTEM_PROMPT` let a caller *replace* the prompt
   rather than add to it. That prompt's no-tools clause is the only thing
   keeping a tool-less seat from emitting fabricated <invoke> blocks and
   inventing tool results — measured: 3/3 clean refusals with it, immediate
   fabrication without. Every OpenAI-compatible client sends a system message,
   so this was the default path, not an edge case. The clause is now always
   emitted and the caller's text is appended after it.

ONE_SHOT_SYSTEM_PROMPT also lacked the "Never run commands" sentence that
DISCUSSION_SYSTEM_PROMPT has; the shared clause now carries it, plus an explicit
instruction never to emit tool-call syntax or invent tool results.

Three assertions that pinned the old substitute-the-prompt behaviour are
rewritten to assert the new order, and the spawn/quoting layer gains its first
regression coverage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
spawnCli built a single command string and passed shell:true, so on Windows
every argument went through cmd.exe, which interprets & | < > ( ) ^ %. The
quoting helper only quoted on whitespace-or-quote and escaped with \", which
cmd does not honour, so metacharacters in any argument — a model id, or just a
repository at C:\work\a&b — broke out of the command.

which() already returns a resolved path, so a real .exe never needed a shell:
it is now spawned directly with an argv array, and nothing on that path can be
reinterpreted. Only npm's .cmd/.bat shims still go through cmd.exe, and those
get an explicitly quoted command line where every token is wrapped, with
windowsVerbatimArguments so Node does not re-quote on top.

A literal double quote is refused rather than escaped: cmd has no in-quote
escape for one, so every encoding silently mangles the argument. Windows
filenames cannot contain one and the validated model/effort fields cannot
either. % is deliberately left alone — ^% does not escape inside quotes and
corrupted legitimate values such as "50%done"; %VAR% can substitute an
environment value but cannot execute anything.

Verified against a shim that echoes its argv: every payload that previously
ran `whoami` now arrives as a literal argument, and "50%done" and "sonnet[1m]"
survive intact. This layer had no test coverage at all; it has some now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pace

prepareWorkspace is called from exactly one place, runClassic, so every other
route to a CLI seat reached the workspace with no repo check, no dirty-tree
refusal, no branch switch, no writer lock and no commit.

Two of those routes are easy to hit without meaning to:

- aux turns (judge, summary, auto-summary, title). maybeAutoSummary falls back
  to the roster's *first* seat when no judge is configured, and findAgent
  resolves a bare provider string by id and name before provider — and
  normalizeAgents gives a codex-cli seat the id "codex" — so a seat with tools
  set is reached with no collision or rename required at all. Its edits are
  never committed and sit in the tree for the next writer turn's `git add -A`
  to claim under the wrong author.
- Orchestrated steps. The Workspace tab is hidden in that mode, so the user
  cannot see the attached folder, the tools levels, or clear the workspace,
  while workspaceFor would still honour chat.workspace on every step.

Both now pin the seat to tools:'none'. This is the load-bearing half of the
fix; a stricter provider-only resolver is still worth adding, but findAgent
itself must keep matching on name — @mentions and HANDOFF routing depend on it.

Also in this commit:

- runClassic silently dropped browser seats whose provider tab is off in
  Settings, while the roster kept ticking them and the composer hint kept
  naming them. It now says so in the transcript, the way a benched agent
  already does.
- discover() cleared its in-flight promise only on success, so one throw left
  a permanently rejected promise that every later call re-awaited until
  restart — silently costing the agent editor its CLI version, login state and
  "no sandbox helpers" warning while still looking healthy, and failing
  GET /v1/multiai/variants and the cli_variants MCP tool outright. Cleared in
  a finally now, `force` beats the in-flight check, and resetAvailabilityCache
  drops it too, so the refresh button can actually recover.
- multiaiRouteLabel warned "model not in this CLI's list" for Claude Code,
  which legitimately accepts full claude-… ids that discovery never lists. The
  roster cell already excluded it; the tooltip did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ck abort before spawn

Three lifecycle defects, all of which end with a CLI process touching the
user's repository when nothing is watching it.

Children were tracked only inside a runProcess closure, so nothing could reap
them: closing Proxima mid-run left claude.exe / codex.exe / gemini.exe running
detached, and a seat with file tools kept editing the repo with no parent and
no timeout (its timer went with the app). The next workspace.prepare() then
committed whatever it produced as "WIP left by an interrupted turn". There is
now a module-level set of live children and a killAll() called from
cleanupSpawnedChildren, which already runs on before-quit and on SIGINT/TERM/HUP.

The scratch folder was shared by every run in the process. The system-prompt
filename was pid + milliseconds with no random part, so seats starting in the
same tick — normal in an independent first round — overwrote each other's
prompt. That is a privilege-label mix-up, not a cosmetic race: a `none` seat
handed the workspace prompt is exactly the configuration measured to fabricate
tool calls and invent file contents. Failed turns also leaked their prompt
forever (the unlink was in .then, not .finally), and mcp_empty.json — what
--strict-mcp-config points at — sat in a directory a Codex `none` seat runs
inside with its sandbox bypassed, so a seat writing real MCP servers there
changed what the next Claude seat loaded. Each run now gets its own mkdtemp
directory, removed in a finally on both branches.

runProcess re-checked `aborted` before taking the concurrency slot but not
after resolving the executable — a window that for Codex includes running
--version on two candidates at 8s each. A Stop in that window spawned a process
abort() could no longer reach, which ran to its full timeout writing to the
repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
handle.abort() set a flag and fired taskkill without waiting, while commitAll
ran on the next tick and the writer lock was released only after that commit.
Three things followed from it: a half-written file could be committed as the
seat's work; writes landing after the commit became untracked leftovers that
the next prepare() claimed as WIP under the wrong message; and because the lock
was released while the zombie was still writing, the next writer seat overlapped
it — defeating the per-chat serialisation the workspace design rests on.

abort() now returns a promise that settles when the child's close fires, with a
5s deadline so a wedged kill cannot hang a run, and sendCli awaits it before
returning to the caller that commits.

takeTurn also never re-checked the run signal after acquiring the writer lock,
which can take a full CLI turn. In a parallel round, Stop while writer 1 held
the lock still spawned writers 2..n as it drained — each aborted a moment
later, but each already started and each still reaching commitAll. They now
bail out with a "stopped" record and release the lock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The renderer learned that a chat was running from a single poll at load, and
nothing ever set that state afterwards — only multiai-done cleared it. So a run
a local agent started streamed its turns into the open chat with no running
marker, no Stop button, and Send still live. Three failures stacked on that:

- pressing Send merged a user message and a bumped cycle into the store before
  the IPC round-trip, and the IPC was then refused; the live run read the
  message back as its newest instruction, in a cycle its other turns did not
  share, and runClassic's return filter dropped the mismatched ones;
- deleting the chat only stopped the run if the renderer believed it was
  running, so an external run kept holding CLI slots and kept committing to the
  workspace for a chat that no longer existed;
- the failure that would have explained any of this was a 2.5s toast.

startSignal now emits multiai-run-started with who started it, the renderer
listens (idempotently — its own runs already set the state locally), and the
running-state channel carries startedBy so an agent-driven run shows an amber
marker instead of the green one. multiaiRunRound asks main for live state
before it mutates anything, delete stops unconditionally, and a run failure is
written into the transcript as well as toasted.

The running channel also carries an empty `agents` array, so filling in the
per-agent status strip later needs no second version of this payload. The REST
contract (GET /v1/multiai/running → {running:[chatId]}) is deliberately
unchanged; only the internal IPC channel is enriched, and a test now pins both.

Cross-client run visibility had no coverage at all; it has some now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…by chat

writerLocks was a Map<chatId>, and runSignals enforced one run per chat — but
the thing being protected is the working tree. Two chats pointing at the same
folder (one from the UI, one over REST — the concurrent-agents case this
feature exists to enable) could run at once, checking out different multiai/*
branches while the other's seats were mid-edit, each `git add -A` sweeping the
other's in-progress work into its own commit.

The lock is now keyed on the resolved workspace root, falling back to the chat
id when there is no workspace, and a run whose folder is already claimed by
another running chat is refused up front.

That refusal is deliberately synchronous in api.run, next to the existing
is-already-running check: without wait:true the run is fire-and-forget, so a
conflict discovered inside runClassic would have come back as {started:true}
and left the caller polling /wait forever.

Same reasoning applied to failures generally: api.run swallowed runClassic's
rejection to [], so a run that died before producing anything — the dirty-tree
refusal being the realistic case, and the feature's headline safety check —
returned 200 {messages:[]} with wait:true or 202 {started:true} without, with
the reason only in the Electron console. A caller could not tell "the workspace
refused" from "the panel had nothing to say". Both paths now report the real
error and status.

Tests cover the per-repo refusal over both REST and IPC, that a chat with no
workspace is unaffected, that the folder frees up when the first run ends, and
that an immediately-failing run no longer claims to have started.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
prepare() committed anything dirty on the chat's own branch as "WIP left by an
interrupted turn" — under the app's identity, with --no-verify, with no check
on who made the changes. Since a run deliberately leaves the folder checked out
on that branch (the documented review-and-merge workflow), the user's next
piece of work lands exactly there, and the next run of that chat swallowed it.
inspect() also said nothing in this state: it warned only when the tree was
dirty on a *different* branch, which is the less likely case.

A writer turn now marks itself in flight before anything can write, recording
the HEAD it started from, and clears the marker once it has committed. prepare()
commits leftovers as WIP only when that marker is present and still matches
HEAD — an interrupted turn is then the actual explanation. Anything else is the
user's work and the run refuses, saying so. A stale marker from before some
other commit does not license a sweep either.

inspect() now warns in both dirty cases, and says which one it is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
toolsWarn was `isCli && tools !== 'none' && (!hasWs || codexNoSandbox)` — so it
fired when a seat had tools configured but *inert* (no workspace) or
unenforceable (Codex without its sandbox helpers), and went quiet the moment a
workspace was attached. The result was backwards: a `full` seat pointed at a
real repository, with a shell and an auto-commit after every turn, was the one
configuration in the roster with no marker at all, and looked identical to
`none`.

Widening toolsWarn is not the fix, because toolsTitle branches on the same
condition: an armed `full` seat would then be described as "runs without tools
until a workspace is set". So there are now two independent signals with
different treatments — amber for "configured but inert or unenforceable", red
for "armed: this seat edits the workspace and every turn is committed there",
with the folder and branch named in the tooltip.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unconfirmed2 and others added 21 commits September 6, 2026 00:06
Two modes, and the Model cell now says which one a seat is in. A browser-route
seat answers on whatever model that provider's tab is currently set to — the
engines contain no model selection at all (claude-engine has zero mentions of
model, chatgpt-engine one, and nothing anywhere clicks a web UI's model menu),
so the cell reads "whatever the tab is set to" and explains where to change it.
Offering a list there would be a control that does nothing and a transcript that
misreports what replied.

Local CLI seats are the opposite case and keep the full three-tier list. Gemini
stays the one browser exception, because its engine really does map an engine
name onto the request rather than driving a menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A browser turn recorded model: agent.model — the seat's configured value, which
means nothing on that route since the engines cannot drive a web UI's model
menu. In practice that was null, so the transcript said nothing at all about
what half the seats were: a five-round debate with two tab seats kept no record
of whether they answered on Opus or Haiku.

sender.readTabModel scrapes the provider's model-menu button and the browser
path records that instead. Verified against a real logged-in tab: a turn now
carries model "Opus 5 High", matching what the Claude tab displays, effort
included.

Best-effort by construction. It reads a menu label, so it is subject to those
sites restyling; every failure path returns null rather than a guess, the call
is optional so an older sender or a test double cannot break a turn, and the
value is only ever used as a label — never to route a request. Guarding that
call was not defensive padding: the first version threw on a sender without the
method and took down every browser turn in the suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The roster asked one question where there are two. "Claude" and "Claude Code"
sat side by side in the Provider menu as if they were rival vendors, and the
Route cell beside them was greyed out — because the provider id had already
decided the route. The row stated the same fact twice, and the choice that
actually mattered, subscription or API key, hid inside a menu entry called
"auto" whose label read "auto → tab" next to a separate "browser tab" entry that
did exactly the same thing today and something different tomorrow.

Provider is now the vendor and Route is the surface: subscription · browser tab,
subscription · <that vendor's CLI>, or API key. MULTIAI_VENDORS is the only
place the two views meet — `provider` is still 'claude' vs 'claude-cli' on disk,
because that is what the main process, the REST port and every saved chat speak,
so nothing downstream changed and no chat needed migrating. The Route cell is
live on CLI seats now instead of greyed out.

The API entry is offered only where Settings can honour it. It used to be
permanent, and choosing it with API mode off changed nothing at all —
resolveRoute() sends 'api' and 'auto' down the same branch — so the menu
advertised a choice the app then ignored. With API mode on but no key for that
vendor it is listed disabled rather than hidden, so it stays discoverable.
'auto' is no longer offered; rows that hold it keep it, labelled "follow
Settings", until the route is changed. Rewriting it would re-route seats behind
the user.

Switching a seat's surface now re-derives what cannot survive the move. A
browser Gemini seat carries an engine string ("3.1-pro") that the API rejects,
and apiModelFor() trusts the box verbatim on an explicit api route, so it would
have been sent as typed. A vendor with no CLI (Perplexity) lands on the tab
rather than storing route 'cli' against a provider that has none.

The model box was an <input list=…>, and Chromium filters a datalist by whatever
is already in it: a seat set to "sonnet" was only ever offered the four sonnet
ids, and the other fourteen looked as though they had failed to load. It is a
grouped <select> now — answered here / aliases the CLI lists / full ids · not
tried here / dated pins — with the provenance said once in the group label
instead of "· not seen on this machine yet" repeated on all fourteen rows. Free
text stays reachable through "custom…", because Claude Code accepts ids no list
here enumerates, and an id on no list opens in the box by itself.

Two lists behind it were wrong rather than merely narrow. discover() fired
catalog.load() and did not wait, so on a machine with no cached copy it landed
after the merge and a fresh install offered three aliases for Claude Code and
nothing at all for Gemini CLI — permanently, since nothing re-probes. It now
re-merges in place when it arrives. And API seats never saw /v1/models unless
the user went to Settings and pressed fetch: ↻ now asks the provider itself,
through the same model-fetcher Settings uses, kept in memory and never written
to the curated list (OpenAI answers with its whole surface, embeddings and TTS
included).

"clear all" empties the roster, at the end of the footer and set apart from
all/none — emptying it and benching it should not be one slip apart. It clears
the legacy participants list too, without which multiaiEnsureAgents rebuilds the
roster from it on the next render.

tests/electron/multiai-roster.test.js is new: the screen had no tests at all and
every route rule lived inside one 300-character template literal. It evaluates
the renderer in a vm with a stub DOM. One of them caught a bug in this commit —
moving a CLI seat to a vendor with no CLI stored an impossible route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Turn-taking makes a round asymmetric. The first seat answers the human and the
last answers five peers, so later seats converge on whoever spoke first and a
round cannot be read as five independent opinions. Only round 1 could avoid
that, through independentFirstRound, and it was off by default.

independentRounds replaces the boolean with 'off' | 'first' | 'all', defaulting
to 'all': every round is one parallel batch, blind to itself and shown every
completed round. Round 2 answers round 1 in full, so it is still a discussion,
one round later — and a run now takes about as long as its slowest seat rather
than the sum of them. Chats saved before this keep exactly what their boolean
asked for (true → 'first'); nothing is rewritten on disk, independentMode()
resolves it on read.

The blind filter was also too narrow to carry the new default. It hid
role 'assistant' for the whole *cycle*: correct when only round 1 could be
blind, but it would have blinded round 2 to round 1 as well, and it leaked a
peer's PASS and its workspace diff into a supposedly blind round. It is scoped
to cycle *and* round now and covers all three roles a participant produces. A
caller that names no round still gets the whole-cycle behaviour — hiding more is
the safe direction when the caller is vague.

Verified live before this landed: three local CLI seats over two rounds, each
asked to report only what it could actually see. Every seat in round 1 answered
SEEN-NOW: NONE / SEEN-BEFORE: NONE, and every seat in round 2 answered
SEEN-NOW: NONE / SEEN-BEFORE: CC-sonnet, CC-haiku, CX-sol. Under turn-taking
seats 2 and 3 would have named peers in SEEN-NOW.

The cost is charter mode: a HANDOFF cannot pull a seat forward inside a blind
round because everyone is already in flight, so it applies to the next round and
the run posts a note saying so once. 'off' restores in-round handoffs.

Applying a setup saved before this existed used to lose its intent — it carries
only the old boolean, MULTIAI_SETUP_FIELDS copied it over, and independentMode()
reads the new field first, so the chat kept whatever it already had. It now
translates on apply.

The handler suite assumed turn-taking throughout, so newChat pins
independentRounds: 'off' and the blind-round tests ask for it by name. Seven new
tests cover all-rounds blindness (each round's prompt carries every earlier
round's replies and none of its own), the hidden PASS, the legacy boolean, and
the handoff sliding to the next round.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Grouping "+ add agent…" by surface — Browser tab, Local CLI, API endpoint —
asked the same question the Route cell owns. Pick "Browser tab · Claude" and the
row that appears offers browser / Claude Code / API key right beside it: the
duplication just removed from Provider and Route, put back one control along.
The menu now lists your library and then a vendor, and the route is decided in
exactly one place. Endpoints you registered keep their own group, because that
genuinely is one route rather than a second chance to choose.

Two things fell out of that. Library entries printed their name twice — seats
are auto-named from provider, model and effort, so "name — meta" rendered
"Claude Code · haiku · max — Claude Code · haiku · max" for every CLI seat.
And a new browser seat was still created on the legacy 'auto' route, so a row
made one second ago opened labelled "follow Settings", which is a note about
migrating old chats.

A first run also had nothing to open: an empty roster over a blank Discussion
tab says nothing about what the screen is for. It now seeds one sample chat —
two browser seats, one round, and a system note saying what to press. The
transcript is deliberately empty; a sample with replies already in it would be
fabricated AI turns presented as a record of a conversation that never happened.
A system note is a role readable() excludes, so it is not sent to anyone as
context either. Seeded once and remembered, so emptying the chat list does not
conjure it back on every launch.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"@gemini kick off, others react" reorders the turn order and posts "Directed
turn: Gemini first" — and then, with independent rounds on, the run starts all
three at once and the order it just announced means nothing. The instruction was
silently dead against its own system note.

An ordering mention is an explicit instruction about who speaks when, and a
blind round has no "when", so the instruction wins for the round it refers to
and the setting resumes from round 2 — the run says so. "only @claude and
@gemini" is untouched: it restricts who takes part, not the order, which a blind
round honours perfectly well.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ? sat in the Multi-AI chat header, which is the one place you already know
what you are looking at. It is now a Help item in the tab bar next to Settings,
where someone looking for help looks. It switches to the Multi-AI tab before
opening: the modal is a DOM element and a BrowserView covers it completely, so
clicked from a provider tab it would have opened behind the page.

The text had fallen behind. Routes still described auto / browser / api, which
no longer exist; the model picker paragraph described a datalist that is now a
grouped select; "Independent first round" is now three settings with a different
default; and Charter mode still claimed a HANDOFF pulls a participant forward
inside a round, which is exactly what a blind round cannot do.

Four things were never documented at all: setups (setups… / save setup /
delete…), clear all, the seat colours toggle, and that ↻ asks each key's
provider for its model list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nine steps over the real screen: dim everything, cut a hole around one control,
say what it is for. Roster, Provider, Route, Model, Tools, the roster footer,
Independent rounds, the composer, and what comes after a discussion.

This is the answer to "a video for each part of the tab". A recorded video of a
UI is wrong the first time a label moves and says nothing about it, and this
week alone the Provider column changed meaning, the Route menu lost an entry,
the model picker stopped being a datalist and the add menu was rebuilt twice.
Pointing at live elements means the tour is either correct or visibly broken —
and tests/electron/multiai-tour.test.js makes "visibly" load-bearing: it reads
every id, selector and options tab out of the step definitions and looks for
them in the markup, and checks each roster column index still lands on the
column its step names. Insert a column before Route and the test fails rather
than the spotlight quietly moving one cell left.

The tour restores the options tab and panel state it borrowed, and touches no
chat data.

For the moving picture there is still a way: the tour's "auto" button advances
it hands-off at a fixed six seconds a step, and scripts/record-tour.mjs
(npm run tour:gif) records the window around it — gdigrab on Windows, x11grab
and avfoundation elsewhere, two-pass palette so the dark UI does not band. The
fixed pacing is the point: a GIF that can be remade identically after a UI
change is one that gets remade, rather than left up out of date. ffmpeg is not
bundled and the script says how to get it; it has been run only as far as that
check on this machine, which has no ffmpeg installed.
640px was fine for six paragraphs and cramped for thirty. At 90vw x 90vh the
extra width goes to a sticky section rail — Agents / Classic / Orchestrated /
General — and the prose keeps an 82ch measure, because 90vw of running text is
worse than 640px of it: the eye loses the line on the way back.
gpt-6-astra is in the catalogue and was not offered, because the Codex merge
dropped the catalogue outright whenever the local list had anything in it. The
reasoning still holds — Codex's own list is entitlement-aware, and padding it
silently would offer gpt-4o, o1-pro and a realtime model as things a coding CLI
can run — but the cost was that a model the account had gained and the cache had
not was unreachable except by typing it. That cache is written by whichever
codex the user last ran interactively, so it lags by design.

The catalogue entries come through now under "not on your account list · may be
rejected", newest family first, with the account list above them untouched.
Silently padding was the problem; a labelled group is not silent, and ▷ settles
any individual case. The group label follows the CLI: full ids for Claude Code,
which knowingly lists only aliases, and "from the catalogue" for Gemini CLI,
which lists nothing at all.
Every agent added anywhere was saved to the library, and every new chat cloned
the whole library. Nine near-duplicate seats in a brand-new chat is the
arithmetic of those two rules, and it only ever goes one way — the library has
no shrinking step.

Neither rule was as harmless as it looked. A library entry is not just what new
chats start with: it is offered in every chat's add menu and served under its
own id on the REST port and to MCP clients, so "let me try haiku for a second"
became a published endpoint. And a new chat inheriting nine seats is expensive
before you have typed anything.

Now: a seat belongs to the chat you added it to, and the new 📚 button on its
row puts it in the library on purpose. New chats start empty, or from a setup
pinned under "setups… → Start new chats from…" — per-setup rather than
"remember the last chat", because a chat drifts as you work in it and
inheriting yesterday's drift is the thing being fixed. The closed menu shows 📌
and the pinned name, so the starting point is visible rather than remembered.

Also fixed on the way past: picking a seat colour called multiaiLibrarySave
unconditionally, so choosing a colour for a chat-local seat published it. And
multiaiToggleAgentColors and multiaiSetAgentColor were each defined twice, one
pair straight after the other, the first unreachable.
Splitting the last two commits, the blind-round tests failed once and passed on
retry. Every assertion behind them was a wall clock — the round-1 calls must all
start within 30ms of each other, three parallel rounds must finish inside 360ms
— which measures how busy the machine is. A loaded run failed a correct
implementation, and under the full suite, where the files run alongside each
other, it failed reliably.

The actual claim is about ordering: in a parallel round every seat is dispatched
before any of them answers, and serial dispatch cannot do that, because each
turn waits for the one before it. startedTogether() checks exactly that against
the fake sender's own timestamps, with no threshold to tune. The duration
assertions are gone rather than loosened; they only ever restated what the
ordering check now proves, and a threshold that has to be widened once will have
to be widened again.

Full suite run three times: 787 pass, 0 fail.
Verified on this machine rather than taken from the announcement: `gemini
--skip-trust -p` answers

  IneligibleTierError: This client is no longer supported for Gemini Code
  Assist for individuals. To continue using Gemini, please migrate to the
  Antigravity suite of products

so every Gemini CLI seat in Proxima fails outright. Google retired the CLI for
free, AI Pro and AI Ultra on 2026-06-18; an enterprise licence or a paid API key
still works, which is the only reason the provider stays.

Antigravity is its own vendor rather than Gemini's CLI route, because it does
not only serve Gemini: `agy models` also lists Claude Sonnet 4.6, Claude Opus
4.6 and GPT-OSS 120B, and filing those under a vendor called Gemini would be the
mislabelling the Provider column was just rebuilt to end. It has no browser tab,
so its Route offers one entry, and the vendor map grew a null browser to say so.

Three things it does better than the CLI it replaces. It enumerates its own
models, so no catalogue padding and no "not tried here" guesswork. It has a real
--effort switch, which Gemini CLI never had. And its ids encode effort as well
(gemini-3.8-flash-high), which the CLI refuses to combine with the flag —

  --model gemini-3.8-flash-low conflicts with --effort=high

— so discovery folds the fifteen ids back into seven families carrying the
efforts they were listed at, and the Effort column drives --effort exactly as it
does for Claude Code and Codex. gemini-3.1-pro correctly offers low and high but
no medium.

Verified end to end: discovery against the real binary, and a turn through
cli.run('antigravity-cli') that came back "PROXIMA-OK".

One limit, measured rather than assumed: agy takes its prompt as an argument and
has no stdin prompt mode that returns an answer — bare -p reads no stdin, and the
documented stream-json input wants an "event" shape that is not published and
that three guesses did not hit. So a turn is bounded by the Windows command line:
30,000 characters runs, 60,000 is ENAMETOOLONG. The seat refuses just under the
cliff with a message naming the Context tab, rather than failing at spawn.
Two failures stacked on the same seat, and the second was hiding under the first.

Gemini CLI refuses to run in a folder it has not been told to trust — "not
running in a trusted directory ... use `--skip-trust`" — and says so instead of
answering. Every seat here runs either in a scratch folder Proxima made or in
the workspace the user attached, and a headless turn cannot answer an
interactive trust prompt, so the flag is the only way through. It is not on
every build and an unknown flag is itself a failed turn, so discovery reads
--help once and records whether this one has it; the note says so when it does
not. Trust is about the folder, not about what the seat may do in it: the tools
level still decides that, and a `none` seat is in plan mode in an empty
directory either way.

Underneath that, every Gemini turn was dying at spawn with ENOENT on
C:\Users\...\npm\gemini — the extensionless npm shim, which Windows cannot
execute. which() runs its candidates through preferExecutable to pick the .cmd;
whichAsync did not, took `where`'s first line, and wrote it into the same _avail
cache which() then reads. warmAvailability() calls the async one for every CLI
at startup, so the good picker never got a look in. Discovery kept working and
reported a version, which is why this stayed invisible: only real turns spawned
through the poisoned path.

Verified against the installed CLI: discovery reports 0.58.0 with skipTrust
true, and a turn now reaches the account check and fails there —
IneligibleTierError, which is Google's retirement of individual plans and not
something this can fix. That error is the whole point of the Antigravity
provider; this makes the enterprise and API-key paths work again.
One model reviewing a diff gives you one model's blind spots. review-panel.mjs
puts the same brief in front of every CLI installed and signed in, blind to each
other, and lets round two read what the others found — so what comes back is the
union of what they catch, and the short list they disagree about. Independent
rounds are what make it worth doing; under turn-taking the later seats mostly
agree with whoever went first.

It discovers seats through Proxima's own provider layer rather than assuming
any, so the panel is whatever this machine actually has, and it picks a model
the account is entitled to: discovery pads a CLI's list with a public catalogue,
and taking models[0] blindly chose gpt-6-astra for a Codex seat when that was a
catalogue candidate rather than something the account had. Entitled entries
first, the CLI's own default after that, and blank — meaning "whatever the CLI
defaults to" — rather than a guess.

Seats get the repository with read-only tools, and the brief tells them to open
the files: the inlined diff is capped at 12k because every seat pays for the
brief on every round and an Antigravity seat refuses past ~28k. Gemini CLI is
left out of the default panel; it answers IneligibleTierError on individual
plans.

Nothing runs without --go. Without it the script prints the target, the panel,
the brief size and the number of turns it would spend, and exits — the mistake
this guards against is a four-seat three-round run that nobody meant to start.
A seat with any tools level makes prepareWorkspace cut and check out a
multiai/... branch in the folder it is given, and "read" is a tools level. So
pointing the panel at the repo you are working in moves your checkout onto a
review branch as a side effect of reading files — right for a writing seat,
wrong for a review.

The panel now reads a detached worktree at HEAD, created before the run and
removed in a finally afterwards, so a failed run does not leave a second
checkout for git to keep reporting. --workspace <path> overrides it.

Confirmed by running it: the working tree stayed on multiai-tab while the
worktree was moved to multiai/6957613-pnahix, which is what the real repo would
have got.

The underlying asymmetry is still there — a read-only seat has no need of a
branch at all, since it commits nothing — and is worth fixing in
prepareWorkspace rather than worked around by every caller.
prepareWorkspace fired for any tools level, and "read" is one. So attaching a
folder to a review panel checked your working copy out onto multiai/... as a
side effect of reading files, and refused to start at all if you had uncommitted
work — which is the normal state of a folder you want reviewed, and often the
very thing you wanted looked at.

A read-only run commits nothing, so it needs no branch and no git repository:
the seats read the folder as it stands. Only a run with a write or full seat
prepares one, and the note says which kind of run this is.

takeTurn had the same fallback in its prompt: chat.workspace.branch ||
branchFor(chat.id), whether or not a branch had been cut, so a read-only run
told every seat it was working on a branch that did not exist. It names one only
when there is one.
The brief asked for defects and explicitly told the panel to drop everything
else — "a list padded with style opinions buries the one real finding". That is
the right instinct aimed at the wrong target: it also discarded the findings
that matter most in a UI change, where nothing is broken and the design is still
wrong. A control whose label does not match what it does, a default that will
surprise, duplicated state that will drift apart, a name that teaches the wrong
model: none of those are defects, and all of them are what a reviewer is for.

Three sections now, each with the same "point at it" discipline and each allowed
to be empty: DEFECTS, DESIGN, and MISSING — the test, migration or error message
the change should have arrived with. --focus defects restores the narrow one.
Ran the panel on this session's work. It read the wrong revision, which is the
first finding and the reason for the other two being worth having: both Claude
seats refused to invent anything from a diff they could not open, and Codex
reasoned from the diff text and named the mismatch itself.

1. The review worktree was detached, and workspace.prepare() cuts its chat
   branch from the *default* branch whenever HEAD is detached or already on
   multiai/*. So the seats were checked out onto a branch cut from main and read
   Proxima v5.0.0 while the brief described this branch. The worktree is created
   on a named branch now. (A read-only run no longer prepares a workspace at all
   since the previous commit, so this only bites a panel with a writer in it —
   but the script should not depend on that.)

2. A CLI seat could never reach its vendor's API key. routes.providers is keyed
   by vendor and has no 'claude-cli' entry, so the roster looked up the seat's
   own provider id, found nothing, and offered "API key — no key in Settings",
   disabled, while providers.claude.hasKey was true. The API route belongs to
   the vendor, not to the surface the seat happens to be on. Mine, from the
   Provider/Route split.

3. The script held one HTTP request open for the whole run. Node's fetch gives
   up after five minutes, and the REST layer aborts a run whose caller has hung
   up — so the run died mid-round-two with the client. It starts the run and
   polls /wait instead.

Codex's second finding — that --paths cannot review uncommitted changes, because
prepare() rejects a dirty tree — was real at the commit it reviewed and is
already fixed by the read-only workspace change.
Both found by the review panel, both mine, both in the runner added this
morning.

runAntigravity fell back to raw stdout whenever the JSON envelope was missing.
A process that died non-zero — a usage error, a signal, a half-written line —
was therefore recorded as that seat's reply, which is worse than the turn
failing: it puts something the model never said into the transcript that every
other seat then reads and answers. Only a SUCCESS envelope is an answer now;
anything else raises with the exit code and whatever the process did say.

It was also the one runner that never removed its scratch directory — the other
three do it in a finally and this one did not, so every Antigravity turn left
one behind. Eleven had accumulated here.

Adding the finally was not enough on its own: Windows will not delete a
directory that is still a process's current directory, and the seat runs with
the scratch dir as its cwd, so the single attempt kept failing silently.
removeScratch now retries a few times, unref'd so nothing waits on it, and still
gives up quietly — a stale temp directory is not worth failing a turn over.
Verified by running a turn and counting: eleven directories before, eleven
after.
--paths reviews work you have not committed, and a worktree is at HEAD by
construction, so the brief described edits the seats could not find. A read-only
run no longer touches the folder it is given, so --paths now points at the real
repository, which is both safe and the only workspace that matches the brief.

Failed turns were dropped from the export and a run where every seat failed
still exited 0 — the last run's three failures are nowhere in its file. They are
listed at the end of the markdown now, printed as the run goes, and no replies
at all exits non-zero.

Chat creation sat outside the try that removes the worktree, so a failure
between creating the chat and starting the run leaked one.

An Antigravity seat on the panel now gets a two-message verbatim window instead
of ten. Round two carries round one's replies, which is what pushed the turn to
32,496 characters and got the seat benched — with a 4k diff budget, so the diff
was never the problem.

"custom…" lost its mode on the seat that most needs it. Choosing it clears the
model, an auto-named seat is then renamed from the new model, and renaming
re-slugs a.id — so the free-text flag, filed under the id the row had a moment
earlier, no longer matched. It hangs off the seat object now, which nothing
renames. The test that missed this passed a fixed name and stepped over the
rename entirely.

And the legacy independentFirstRound boolean read two opposite ways: false
became 'off' when it arrived in a setup and 'all' when it arrived on a chat.
False was the old default rather than a choice, so it takes the new default in
both places.
Copilot AI lite review requested due to automatic review settings September 6, 2026 07:43
@Unconfirmed2

Copy link
Copy Markdown
Author

Closing — keeping this work local for now.

@Unconfirmed2
Unconfirmed2 deleted the multiai-tab branch September 6, 2026 07:45
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add native Multi-AI chat, blind rounds, CLI variants, and workspaces

✨ Enhancement 🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Adds multi-provider discussions, blind rounds, pipelines, workspaces, and review tooling.
• Exposes chats and local CLI variants through REST, WebSocket, and MCP.
• Hardens routing, process cancellation, persistence, command execution, and workspace isolation.
Diagram

graph TD
  UI["Multi-AI UI"] --> IPC["Run Orchestrator"] --> ROUTES["Route Adapters"] --> BROWSER["Browser Tabs"]
  IPC --> STORE["Chat Store"]
  ROUTES --> API["BYOK APIs"]
  ROUTES --> CLI["Local CLIs"] --> WORKSPACE["Git Workspace"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Standalone Multi-AI service
  • ➕ Isolates orchestration and CLI processes from Electron
  • ➕ Allows independent deployment and non-Electron clients
  • ➖ Duplicates browser-session authentication and provider routing
  • ➖ Complicates local state, cancellation, and UI synchronization
2. Provider plugin architecture
  • ➕ Reduces provider conditionals as additional CLIs are added
  • ➕ Creates clearer capability and permission contracts
  • ➖ Requires a larger foundational refactor before delivering the feature
  • ➖ Adds abstraction around a small initial provider set
3. Ship surfaces incrementally
  • ➕ Creates smaller review units for UI, workspaces, and external APIs
  • ➕ Reduces regression scope per release
  • ➖ Creates temporarily incomplete contracts between dependent surfaces
  • ➖ Delays end-to-end testing of shared routing and persistence

Recommendation: Keep the integrated approach for this release: sharing existing BrowserViews, BYOK settings, IPC, and the REST gateway avoids duplicate authentication and state. The separation into pure core logic, route adapters, CLI variants, and workspace modules is appropriate; a capability-based provider plugin layer is the best follow-up if more vendors are added.

Files changed (56) +14995 / -196

Enhancement (23) +8841 / -86
multiai-icon.svgAdd the Multi-AI tab icon +13/-0

Add the Multi-AI tab icon

• Introduces a gradient network icon for the new application tab.

assets/multiai-icon.svg

_http.cjsSupport plain HTTP custom model endpoints +7/-3

Support plain HTTP custom model endpoints

• Selects HTTP or HTTPS transport from the endpoint protocol, enabling local model servers.

electron/api/byok/providers/_http.cjs

anthropic.cjsMap agent effort to Anthropic extended thinking +10/-1

Map agent effort to Anthropic extended thinking

• Supports caller-defined token limits and converts effort levels into safe thinking budgets.

electron/api/byok/providers/anthropic.cjs

openai.cjsForward reasoning effort to OpenAI models +11/-1

Forward reasoning effort to OpenAI models

• Maps Multi-AI effort levels to OpenAI reasoning effort and expands default completion budgets when needed.

electron/api/byok/providers/openai.cjs

rest-api.cjsAdd extensible routes and resilient responses +34/-5

Add extensible routes and resilient responses

• Registers feature-owned API routes, includes extra CLI models, reads the package version, and handles disconnected clients.

electron/api/rest-api.cjs

routes.cjsExpose local CLI seats through the models endpoint +4/-0

Expose local CLI seats through the models endpoint

• Appends Multi-AI variant models to the OpenAI-compatible model list.

electron/api/routes.cjs

ws-server.cjsAdd WebSocket actions for local CLI variants +35/-0

Add WebSocket actions for local CLI variants

• Supports variant discovery, single requests, and completion-ordered fan-out for up to eight seats.

electron/api/ws-server.cjs

index-v2.htmlAdd the Multi-AI application surface and settings +560/-58

Add the Multi-AI application surface and settings

• Introduces the tab, roster, transcript, ledger, options, workspace, help, custom endpoint, and MCP configuration UI.

electron/index-v2.html

multiai-core.cjsImplement pure Multi-AI state and prompt logic +781/-0

Implement pure Multi-AI state and prompt logic

• Adds atomic storage, agent normalization, bounded context, blind filtering, convergence, mentions, handoffs, and parsers.

electron/ipc/multiai-core.cjs

multiai.cjsImplement Multi-AI orchestration and external APIs +1623/-0

Implement Multi-AI orchestration and external APIs

• Coordinates browser, API, and CLI runs with cancellation, workspace locking, REST integration, and transcript persistence.

electron/ipc/multiai.cjs

settings.cjsPersist custom BYOK endpoint metadata +3/-2

Persist custom BYOK endpoint metadata

• Allows API key entries to include an endpoint URL and display name for custom providers.

electron/ipc/settings.cjs

main-v2.cjsWire Multi-AI into the Electron main process +92/-11

Wire Multi-AI into the Electron main process

• Registers Multi-AI IPC and MCP actions, separates development profiles, enforces single instances, and terminates CLI children.

electron/main-v2.cjs

multiai-renderer.jsImplement the Multi-AI renderer and roster +3399/-0

Implement the Multi-AI renderer and roster

• Adds chat management, vendor/route selection, model pickers, setups, transcripts, workspaces, exports, pipelines, and safety confirmations.

electron/multiai-renderer.js

preload.cjsExpose Multi-AI IPC methods to the renderer +31/-2

Expose Multi-AI IPC methods to the renderer

• Adds preload bridges for chats, runs, routes, workspaces, exports, model tests, and live events.

electron/preload.cjs

catalog.cjsAdd a cached public model catalogue +133/-0

Add a cached public model catalogue

• Loads and filters models.dev data as a non-authoritative fallback for CLI model and effort discovery.

electron/providers/catalog.cjs

cli.cjsAdd secure local CLI provider execution +1076/-0

Add secure local CLI provider execution

• Implements discovery, concurrency, cancellation, scratch isolation, secure spawning, and four local CLI runners.

electron/providers/cli.cjs

variants.cjsImplement shared local CLI variant resolution +213/-0

Implement shared local CLI variant resolution

• Resolves variants, validates CLI arguments, protects no-tools prompts, bounds queues, and formats OpenAI completions.

electron/providers/variants.cjs

workspace.cjsAdd guarded git workspaces for CLI seats +174/-0

Add guarded git workspaces for CLI seats

• Creates per-chat branches, protects uncommitted work, attributes interrupted turns, and produces bounded diff cards.

electron/providers/workspace.cjs

review-panel.mjsAdd a blind local-AI code review panel +262/-0

Add a blind local-AI code review panel

• Builds a bounded diff brief, discovers CLI seats, creates a read-only review chat, polls results, and exports findings.

scripts/review-panel.mjs

index.jsRegister Multi-AI and CLI variant MCP tools +10/-3

Register Multi-AI and CLI variant MCP tools

• Loads the package version dynamically and expands the primary MCP server with ten new tools.

src/mcp/index.js

tools-cli.jsAdd MCP tools for local CLI variants +107/-0

Add MCP tools for local CLI variants

• Provides discovery, single-seat requests, and bounded multi-seat fan-out through IPC or an in-process backend.

src/mcp/tools-cli.js

tools-multiai.jsAdd MCP tools for Multi-AI chat lifecycle +181/-0

Add MCP tools for Multi-AI chat lifecycle

• Allows MCP clients to create, read, append, run, wait for, and stop Multi-AI chats.

src/mcp/tools-multiai.js

variants-server.jsAdd a standalone CLI variants MCP server +82/-0

Add a standalone CLI variants MCP server

• Runs shared CLI variant tools without Electron and reads the agent library from user data.

src/mcp/variants-server.js

Bug fix (6) +246 / -11
api.cjsAllow active browser requests to be cancelled +23/-0

Allow active browser requests to be cancelled

• Adds a provider-engine abort bridge used by the Multi-AI Stop action.

electron/providers/api.cjs

chatgpt-engine.jsMake ChatGPT browser sends cancellable +31/-1

Make ChatGPT browser sends cancellable

• Tracks active request controllers and exposes user-initiated abort without triggering retries.

electron/providers/engines/chatgpt-engine.js

claude-engine.jsMake Claude browser sends cancellable +31/-1

Make Claude browser sends cancellable

• Tracks active request controllers and converts explicit aborts into terminal cancellation errors.

electron/providers/engines/claude-engine.js

gemini-engine.jsMake Gemini sends cancellable and improve diagnostics +53/-5

Make Gemini sends cancellable and improve diagnostics

• Adds cancellation, prevents retry after abort, and records frame diagnostics for stitched responses.

electron/providers/engines/gemini-engine.js

perplexity-engine.jsMake Perplexity browser sends cancellable +30/-1

Make Perplexity browser sends cancellable

• Exposes active-request cancellation and prevents deliberate stops from entering retry behavior.

electron/providers/engines/perplexity-engine.js

sender.cjsAdd queued-send cancellation and model reporting +78/-3

Add queued-send cancellation and model reporting

• Skips cancelled queued requests, aborts matching active conversations, and reads provider-tab model labels.

electron/providers/sender.cjs

Tests (16) +3313 / -0
http.test.jsTest local plain-HTTP BYOK endpoints +11/-0

Test local plain-HTTP BYOK endpoints

• Verifies POST and GET requests use HTTP successfully for local model servers.

tests/byok/http.test.js

providers.test.jsTest provider reasoning effort mappings +24/-0

Test provider reasoning effort mappings

• Covers OpenAI reasoning effort and Anthropic extended-thinking budgets and response extraction.

tests/byok/providers.test.js

routes.test.jsTest CLI models in the OpenAI model list +8/-0

Test CLI models in the OpenAI model list

• Verifies extra local CLI seats are included before the automatic model entry.

tests/electron/api/routes.test.js

ws-variants.test.jsTest WebSocket variant discovery and fan-out +77/-0

Test WebSocket variant discovery and fan-out

• Covers status, results, per-seat failures, ordering metadata, and completion messages.

tests/electron/api/ws-variants.test.js

multiai-core.test.jsTest Multi-AI storage, context, and round logic +352/-0

Test Multi-AI storage, context, and round logic

• Covers persistence, prompt compaction, blind filtering, convergence, agents, mentions, handoffs, and legacy defaults.

tests/electron/ipc/multiai-core.test.js

multiai-handlers.test.jsExercise Multi-AI orchestration end to end +1161/-0

Exercise Multi-AI orchestration end to end

• Tests routes, sessions, cancellation, external runs, blind rounds, variants, workspaces, locks, and pipelines.

tests/electron/ipc/multiai-handlers.test.js

multiai-roster.test.jsTest roster vendor, route, model, and setup behavior +605/-0

Test roster vendor, route, model, and setup behavior

• Covers route translation, API availability, model selection, custom models, libraries, setups, and Antigravity.

tests/electron/multiai-roster.test.js

multiai-tour.test.jsValidate guided-tour targets against the UI +108/-0

Validate guided-tour targets against the UI

• Ensures every step, selector, options tab, and roster column remains aligned with the markup.

tests/electron/multiai-tour.test.js

antigravity.test.jsTest Antigravity model discovery +58/-0

Test Antigravity model discovery

• Verifies effort-suffixed model IDs are folded into families and recognized as CLI seats.

tests/electron/providers/antigravity.test.js

cli.test.jsTest CLI discovery, permissions, spawning, and queues +318/-0

Test CLI discovery, permissions, spawning, and queues

• Covers parsers, permission flags, Codex selection, Windows quoting, effort clamping, and queue deadlines.

tests/electron/providers/cli.test.js

sender.test.jsTest browser send cancellation behavior +35/-0

Test browser send cancellation behavior

• Verifies queued calls are skipped, active conversations are matched, and cancellation is not retried.

tests/electron/providers/sender.test.js

variants.test.jsTest shared CLI variant contracts +136/-0

Test shared CLI variant contracts

• Covers resolution, injection rejection, fan-out limits, prompt protection, model publication, and OpenAI conversion.

tests/electron/providers/variants.test.js

workspace.test.jsTest git workspace safety and attribution +194/-0

Test git workspace safety and attribution

• Uses temporary repositories to validate branching, dirty-tree guards, commits, diffs, and fresh chat bases.

tests/electron/providers/workspace.test.js

tools-cli.test.jsTest local CLI variant MCP tools +79/-0

Test local CLI variant MCP tools

• Verifies IPC translation, formatted output, fan-out failures, and Zod input constraints.

tests/mcp/tools-cli.test.js

tools-multiai.test.jsTest Multi-AI chat MCP tools +98/-0

Test Multi-AI chat MCP tools

• Covers registration, IPC actions, transcript rendering, attribution, chat creation, runs, waits, and errors.

tests/mcp/tools-multiai.test.js

variants-server.test.jsTest standalone variant MCP operation +49/-0

Test standalone variant MCP operation

• Verifies library loading and in-process discovery and requests without Electron IPC.

tests/mcp/variants-server.test.js

Documentation (8) +2569 / -92
CHANGELOG.mdDocument the 5.1.0 Multi-AI release +25/-0

Document the 5.1.0 Multi-AI release

• Adds release notes for Multi-AI chats, CLI variants, workspaces, security hardening, and routing fixes.

CHANGELOG.md

README.mdIntroduce Multi-AI Chat in the project overview +22/-0

Introduce Multi-AI Chat in the project overview

• Adds a feature overview covering seats, workspaces, and external REST-compatible access.

README.md

MULTIAI-REVIEW.mdRecord the multi-review security and UX findings +750/-0

Record the multi-review security and UX findings

• Documents the adversarial review process, confirmed defects, resolutions, implementation status, and rejected alternatives.

docs/MULTIAI-REVIEW.md

MULTIAI.mdAdd the complete Multi-AI feature reference +440/-0

Add the complete Multi-AI feature reference

• Documents agents, routes, blind rounds, context modes, workspaces, Antigravity, review tooling, and external interfaces.

docs/MULTIAI.md

openapi.jsonPublish Multi-AI and CLI variant API contracts +848/-92

Publish Multi-AI and CLI variant API contracts

• Adds chat lifecycle and variant endpoints, CLI model support, pseudo-streaming behavior, schemas, and responses.

docs/openapi.json

session-review.mdPreserve the review panel's generated findings +129/-0

Preserve the review panel's generated findings

• Captures model-by-model findings from the review session, including Antigravity, migration, and review-script defects.

docs/review/session-review.md

multiai-tour.jsAdd an interactive Multi-AI guided tour +223/-0

Add an interactive Multi-AI guided tour

• Provides nine live UI walkthrough steps with keyboard navigation, spotlight positioning, and automatic advancement.

electron/multiai-tour.js

record-tour.mjsAdd automated guided-tour GIF recording +132/-0

Add automated guided-tour GIF recording

• Finds ffmpeg, captures the live application window, and creates a palette-optimized GIF.

scripts/record-tour.mjs

Other (3) +26 / -7
core.cjsExpose standalone variant MCP configuration +9/-1

Expose standalone variant MCP configuration

• Builds a separate MCP server command for local CLI variants that can run without Electron.

electron/ipc/core.cjs

package.jsonBump the Electron package to 5.1.0 +2/-2

Bump the Electron package to 5.1.0

• Updates the Electron package version for the Multi-AI release.

electron/package.json

package.jsonPackage and publish Multi-AI tooling as version 5.1.0 +15/-4

Package and publish Multi-AI tooling as version 5.1.0

• Adds variant MCP, review-panel, and tour scripts; unpacks shared modules; and marks the build as a fork.

package.json

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Symlinks escape workspace boundary 🐞 Bug ⛨ Security
Description
resolveInRoot() checks only the lexical path, while subsequent filesystem operations follow
symlinks under the selected root. An agent can therefore read, overwrite, create, or delete files
outside the user-approved folder through an in-root symlink.
Code

electron/ipc/multiai.cjs[R1240-1243]

+        const normalizedRoot = path.resolve(root);
+        const target = path.resolve(normalizedRoot, relPath || '.');
+        if (target !== normalizedRoot && !target.startsWith(normalizedRoot + path.sep)) {
+            throw new Error('That path is outside the working folder — refused.');
Evidence
The containment check never resolves filesystem links, then readFileSync, writeFileSync,
appendFileSync, and rmSync operate on the unchecked target. The UI explicitly promises that this
folder is the safety boundary.

electron/ipc/multiai.cjs[1239-1249]
electron/ipc/multiai.cjs[1251-1279]
electron/ipc/multiai.cjs[1321-1337]
electron/multiai-renderer.js[3147-3162]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The orchestrated file tools validate paths using only `path.resolve()` and a string prefix. Filesystem operations follow symlinks, allowing a path that appears inside the workspace to resolve outside it.

## Issue Context
Resolve the real workspace root and verify every existing path component remains beneath it. For create/write operations, validate the real parent directory and reject symlink components; avoid validation/use races where practical.

## Fix Focus Areas
- electron/ipc/multiai.cjs[1239-1280]
- electron/multiai-renderer.js[3147-3162]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Stop misses automatic summaries 🐞 Bug ☼ Reliability
Description
If Stop is pressed while maybeAutoSummary awaits sendAux, the summary request is neither raced
against the run signal nor tracked for abort, so the run remains blocked until it completes and then
records a summary after being stopped. Automatic summaries can therefore keep a cancelled browser
request active and append unexpected output to the transcript.
Code

electron/ipc/multiai.cjs[R825-826]

+            const result = await sendAux(chat, judge, core.buildSummaryPrompt({ topic: brief, messages: chat.messages }));
+            record(chatId, { who: `State summary · ${judge.name} (auto)`, text: result.response, provider: judge.provider, agentId: judge.id, role: 'summary', cycle: chat.cycle || 1, round: 0 });
Evidence
The automatic-summary path directly awaits sendAux and records its result. The browser auxiliary
implementation bypasses raceProvider and trackInFlight, whereas stopChat only resolves the
signal and aborts entries found in inFlight; the run loop awaits the summary before it can reach
its cancellation handling.

electron/ipc/multiai.cjs[109-123]
electron/ipc/multiai.cjs[485-515]
electron/ipc/multiai.cjs[814-829]
electron/ipc/multiai.cjs[966-979]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Automatic summaries await `sendAux` directly. Unlike normal turns, this path is not raced with the chat cancellation signal and is not added to `inFlight`, so `multiai-stop-run` cannot promptly end or abort it.

## Issue Context
`runClassic` calls `maybeAutoSummary` between rounds. `stopChat` only aborts tracked entries, while the browser implementation of `sendAux` directly awaits `sender.sendMessageToProvider` and then `maybeAutoSummary` records its result without checking cancellation.

## Fix Focus Areas
- electron/ipc/multiai.cjs[109-123]
- electron/ipc/multiai.cjs[485-515]
- electron/ipc/multiai.cjs[814-829]
- electron/ipc/multiai.cjs[966-979]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Failures permit false convergence 🐞 Bug ≡ Correctness
Description
roundOutcome() evaluates only assistant and PASS messages, excluding errors and participants that
never replied. If one seat fails while the remaining seats pass or emit the stop token, the run
incorrectly announces that every participant converged and ends early.
Code

electron/ipc/multiai-core.cjs[R731-734]

+function roundOutcome(roundMessages, token) {
+    const replies = roundMessages.filter(m => m.role === 'assistant' || m.role === 'pass');
+    if (!replies.length) return { converged: false, allPassed: false };
+    const tokenCount = replies.filter(m => m.role === 'assistant' && startsWithToken(m.text, token)).length;
Evidence
Error turns are recorded with a role excluded by roundOutcome(), and benched seats take no later
turns. Nevertheless, runClassic() interprets the reduced result as applying to every participant.

electron/ipc/multiai-core.cjs[729-738]
electron/ipc/multiai.cjs[704-707]
electron/ipc/multiai.cjs[889-901]
electron/ipc/multiai.cjs[966-974]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Round convergence is calculated over received assistant/PASS messages rather than the complete expected participant set. Failed, absent, or benched seats can therefore be ignored when deciding that everyone converged.

## Issue Context
Pass the expected active participant identities or count into the outcome calculation. Do not report convergence or all-passed unless every expected seat produced a qualifying response for that round.

## Fix Focus Areas
- electron/ipc/multiai-core.cjs[729-738]
- electron/ipc/multiai.cjs[889-901]
- electron/ipc/multiai.cjs[966-974]
- tests/electron/ipc/multiai-core.test.js[170-184]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Disabled browser agents restored 🐞 Bug ☼ Reliability
Description
When every configured seat resolves to a disabled browser provider, runClassic() undoes the
filtering by restoring the original un-runnable roster and dispatches requests to tabs the user
explicitly disabled. Those sends then wait up to 30 seconds for unavailable BrowserViews before
failing, rather than rejecting the run immediately.
Code

electron/ipc/multiai.cjs[R857-860]

+            if (!agents.length) {
+                if (all.length) note(chatId, 'No provider tabs are enabled in Settings, so every seat is running on its own route.');
+                agents = all.slice();
+            }
Evidence
The new logic establishes that browser agents require enabled provider tabs and filters out agents
absent from enabledProviders, but then explicitly replaces an empty filtered result with the
complete roster. Each restored agent is passed to takeTurn; the browser route calls sendBrowser,
whose sender waits up to 30 seconds for a missing BrowserView before throwing.

electron/ipc/multiai.cjs[845-860]
electron/ipc/multiai.cjs[214-219]
electron/providers/sender.cjs[54-67]
electron/ipc/multiai.cjs[616-625]
electron/ipc/multiai.cjs[940-951]
electron/ipc/multiai.cjs[473-477]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description

`runClassic` filters out browser-route agents whose provider tabs are disabled, but restores the full roster when the filtered result is empty. This defeats the Settings provider-disable switch, schedules sends for unavailable BrowserViews, and produces delayed failures instead of rejecting the run immediately.

## Issue Context

Keep the filtered roster authoritative. API and CLI seats are independent of the browser-provider toggle, but if filtering leaves no runnable agent, record or report a clear explanation and terminate before starting provider sends rather than treating disabled browser seats as runnable.

## Fix Focus Areas

- electron/ipc/multiai.cjs[845-861]
- electron/providers/sender.cjs[54-67]
- tests/electron/ipc/multiai-handlers.test.js[544-552]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Disconnected fan-out keeps running 🐞 Bug ☼ Reliability
Description
The ask-many REST route does not attach disconnect cancellation, unlike the single-variant route.
Up to eight abandoned CLI calls can continue for as long as 20 minutes and occupy the two shared
per-provider slots, blocking chat and variant work.
Code

electron/ipc/multiai.cjs[1574]

+                if (method === 'POST' && p === '/v1/multiai/variants/ask-many') { sendJSON(res, 200, await askVariants(body)); return true; }
Evidence
The single-call route wraps its body with abortOnDisconnect, but ask-many passes the body
directly. Its fan-out also omits onStart, so no caller obtains the handles needed to invoke the
implemented abort mechanism.

electron/ipc/multiai.cjs[1550-1574]
electron/providers/variants.cjs[142-173]
electron/providers/cli.cjs[618-649]
electron/providers/cli.cjs[801-809]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`POST /v1/multiai/variants/ask-many` starts multiple CLI operations without retaining their handles or aborting them when the HTTP client disconnects. Abandoned calls continue consuming shared CLI concurrency slots.

## Issue Context
Allow `askVariants()` to expose every started handle, register one disconnect listener for the request, and abort all active or queued handles. Preserve per-variant result behavior for connected clients.

## Fix Focus Areas
- electron/ipc/multiai.cjs[1550-1574]
- electron/providers/variants.cjs[142-173]
- electron/providers/cli.cjs[618-649]
- electron/providers/cli.cjs[801-809]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (1)
6. Oversized prompts leak scratch 🐞 Bug ☼ Reliability
Description
runAntigravity() creates its per-run scratch directory before validating prompt length, then
returns early without cleanup when the prompt is oversized. Every such rejected request permanently
leaves a multiai-cli/run-* directory and mcp_empty.json behind.
Code

electron/providers/cli.cjs[R979-982]

+    const dir = scratchDir(cwd);
+    const level = workspace && workspace.root && workspace.tools && workspace.tools !== 'none' ? workspace.tools : 'none';
+    const full = `${systemPrompt || (level !== 'none' ? workspaceSystemPrompt(workspace) : DISCUSSION_SYSTEM_PROMPT)}\n\n${prompt}`;
+    if (full.length > AGY_PROMPT_MAX) {
Evidence
scratchDir() creates a unique directory and configuration file, while cleanup is installed only
after the early size rejection. The oversized branch therefore has no cleanup path.

electron/providers/cli.cjs[664-681]
electron/providers/cli.cjs[978-987]
electron/providers/cli.cjs[1013-1016]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Antigravity creates a scratch directory before its prompt-size check. The oversized-prompt return bypasses the cleanup registered later on the normal execution path.

## Issue Context
Validate the assembled prompt before creating scratch state, or explicitly remove the directory before returning the rejected handle. Add a regression test confirming no run directory remains.

## Fix Focus Areas
- electron/providers/cli.cjs[978-1016]
- tests/electron/providers/antigravity.test.js[1-58]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This is a broad, high-density change spanning UI, IPC, provider/CLI integrations, REST/WebSocket APIs, MCP tooling, persistence-compatible routing, and blind-round logic, creating many independent paths where subtle defects could be missed in one pass.

Grey Divider

Tip of the day
💡 Did you know, you can show, collapse, or hide each part of a finding: code, evidence, and all

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread electron/ipc/multiai.cjs
Comment on lines +1240 to +1243
const normalizedRoot = path.resolve(root);
const target = path.resolve(normalizedRoot, relPath || '.');
if (target !== normalizedRoot && !target.startsWith(normalizedRoot + path.sep)) {
throw new Error('That path is outside the working folder — refused.');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Symlinks escape workspace boundary 🐞 Bug ⛨ Security

resolveInRoot() checks only the lexical path, while subsequent filesystem operations follow
symlinks under the selected root. An agent can therefore read, overwrite, create, or delete files
outside the user-approved folder through an in-root symlink.
Agent Prompt
## Issue description
The orchestrated file tools validate paths using only `path.resolve()` and a string prefix. Filesystem operations follow symlinks, allowing a path that appears inside the workspace to resolve outside it.

## Issue Context
Resolve the real workspace root and verify every existing path component remains beneath it. For create/write operations, validate the real parent directory and reject symlink components; avoid validation/use races where practical.

## Fix Focus Areas
- electron/ipc/multiai.cjs[1239-1280]
- electron/multiai-renderer.js[3147-3162]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +731 to +734
function roundOutcome(roundMessages, token) {
const replies = roundMessages.filter(m => m.role === 'assistant' || m.role === 'pass');
if (!replies.length) return { converged: false, allPassed: false };
const tokenCount = replies.filter(m => m.role === 'assistant' && startsWithToken(m.text, token)).length;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Failures permit false convergence 🐞 Bug ≡ Correctness

roundOutcome() evaluates only assistant and PASS messages, excluding errors and participants that
never replied. If one seat fails while the remaining seats pass or emit the stop token, the run
incorrectly announces that every participant converged and ends early.
Agent Prompt
## Issue description
Round convergence is calculated over received assistant/PASS messages rather than the complete expected participant set. Failed, absent, or benched seats can therefore be ignored when deciding that everyone converged.

## Issue Context
Pass the expected active participant identities or count into the outcome calculation. Do not report convergence or all-passed unless every expected seat produced a qualifying response for that round.

## Fix Focus Areas
- electron/ipc/multiai-core.cjs[729-738]
- electron/ipc/multiai.cjs[889-901]
- electron/ipc/multiai.cjs[966-974]
- tests/electron/ipc/multiai-core.test.js[170-184]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread electron/ipc/multiai.cjs
Comment on lines +857 to +860
if (!agents.length) {
if (all.length) note(chatId, 'No provider tabs are enabled in Settings, so every seat is running on its own route.');
agents = all.slice();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Disabled browser agents restored 🐞 Bug ☼ Reliability

When every configured seat resolves to a disabled browser provider, runClassic() undoes the
filtering by restoring the original un-runnable roster and dispatches requests to tabs the user
explicitly disabled. Those sends then wait up to 30 seconds for unavailable BrowserViews before
failing, rather than rejecting the run immediately.
Agent Prompt
## Issue description

`runClassic` filters out browser-route agents whose provider tabs are disabled, but restores the full roster when the filtered result is empty. This defeats the Settings provider-disable switch, schedules sends for unavailable BrowserViews, and produces delayed failures instead of rejecting the run immediately.

## Issue Context

Keep the filtered roster authoritative. API and CLI seats are independent of the browser-provider toggle, but if filtering leaves no runnable agent, record or report a clear explanation and terminate before starting provider sends rather than treating disabled browser seats as runnable.

## Fix Focus Areas

- electron/ipc/multiai.cjs[845-861]
- electron/providers/sender.cjs[54-67]
- tests/electron/ipc/multiai-handlers.test.js[544-552]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread electron/ipc/multiai.cjs
// ---- variants: local CLI seats over REST ----
if (method === 'GET' && p === '/v1/multiai/variants') { sendJSON(res, 200, await variants({ probe: url.searchParams.get('probe') === '1' })); return true; }
if (method === 'POST' && p === '/v1/multiai/variants/ask') { sendJSON(res, 200, await askVariant(abortOnDisconnect(res, body))); return true; }
if (method === 'POST' && p === '/v1/multiai/variants/ask-many') { sendJSON(res, 200, await askVariants(body)); return true; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

4. Disconnected fan-out keeps running 🐞 Bug ☼ Reliability

The ask-many REST route does not attach disconnect cancellation, unlike the single-variant route.
Up to eight abandoned CLI calls can continue for as long as 20 minutes and occupy the two shared
per-provider slots, blocking chat and variant work.
Agent Prompt
## Issue description
`POST /v1/multiai/variants/ask-many` starts multiple CLI operations without retaining their handles or aborting them when the HTTP client disconnects. Abandoned calls continue consuming shared CLI concurrency slots.

## Issue Context
Allow `askVariants()` to expose every started handle, register one disconnect listener for the request, and abort all active or queued handles. Preserve per-variant result behavior for connected clients.

## Fix Focus Areas
- electron/ipc/multiai.cjs[1550-1574]
- electron/providers/variants.cjs[142-173]
- electron/providers/cli.cjs[618-649]
- electron/providers/cli.cjs[801-809]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +979 to +982
const dir = scratchDir(cwd);
const level = workspace && workspace.root && workspace.tools && workspace.tools !== 'none' ? workspace.tools : 'none';
const full = `${systemPrompt || (level !== 'none' ? workspaceSystemPrompt(workspace) : DISCUSSION_SYSTEM_PROMPT)}\n\n${prompt}`;
if (full.length > AGY_PROMPT_MAX) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

5. Oversized prompts leak scratch 🐞 Bug ☼ Reliability

runAntigravity() creates its per-run scratch directory before validating prompt length, then
returns early without cleanup when the prompt is oversized. Every such rejected request permanently
leaves a multiai-cli/run-* directory and mcp_empty.json behind.
Agent Prompt
## Issue description
Antigravity creates a scratch directory before its prompt-size check. The oversized-prompt return bypasses the cleanup registered later on the normal execution path.

## Issue Context
Validate the assembled prompt before creating scratch state, or explicitly remove the directory before returning the rejected handle. Add a regression test confirming no run directory remains.

## Fix Focus Areas
- electron/providers/cli.cjs[978-1016]
- tests/electron/providers/antigravity.test.js[1-58]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread electron/ipc/multiai.cjs
Comment on lines +825 to +826
const result = await sendAux(chat, judge, core.buildSummaryPrompt({ topic: brief, messages: chat.messages }));
record(chatId, { who: `State summary · ${judge.name} (auto)`, text: result.response, provider: judge.provider, agentId: judge.id, role: 'summary', cycle: chat.cycle || 1, round: 0 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

6. Stop misses automatic summaries 🐞 Bug ☼ Reliability

If Stop is pressed while maybeAutoSummary awaits sendAux, the summary request is neither raced
against the run signal nor tracked for abort, so the run remains blocked until it completes and then
records a summary after being stopped. Automatic summaries can therefore keep a cancelled browser
request active and append unexpected output to the transcript.
Agent Prompt
## Issue description
Automatic summaries await `sendAux` directly. Unlike normal turns, this path is not raced with the chat cancellation signal and is not added to `inFlight`, so `multiai-stop-run` cannot promptly end or abort it.

## Issue Context
`runClassic` calls `maybeAutoSummary` between rounds. `stopChat` only aborts tracked entries, while the browser implementation of `sendAux` directly awaits `sender.sendMessageToProvider` and then `maybeAutoSummary` records its result without checking cancellation.

## Fix Focus Areas
- electron/ipc/multiai.cjs[109-123]
- electron/ipc/multiai.cjs[485-515]
- electron/ipc/multiai.cjs[814-829]
- electron/ipc/multiai.cjs[966-979]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A confirmed availability-key mismatch breaks Antigravity variant execution, and the review-panel script has validated argument/cleanup issues that can lead to incorrect or messy runs.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR brings the Multi-AI feature branch up to date by expanding Multi-AI Chat’s local-CLI “variants” support (including a standalone MCP server), improving workspace safety/behavior, adding review/tour tooling, and significantly increasing automated coverage around these flows.

Changes:

  • Add MCP tooling for Multi-AI Chat transcripts (multiai_*) and local CLI variants (cli_variants / ask_variant / ask_variants), plus a standalone variants MCP server.
  • Introduce/expand core modules for CLI variants, workspace git-branch management, and provider cancellation behavior, with extensive new tests.
  • Add scripts and documentation for a guided tour and a local-CLI “review panel” workflow; bump versioning to 5.1.0.
File summaries
File Description
tests/mcp/variants-server.test.js Tests standalone variants MCP server behavior (no IPC).
tests/mcp/tools-multiai.test.js Tests MCP multiai_* tools’ IPC translation and rendering.
tests/mcp/tools-cli.test.js Tests MCP cli_* tools output formatting and zod constraints.
tests/electron/providers/workspace.test.js Tests git workspace branch/prepare/commit behaviors.
tests/electron/providers/variants.test.js Tests CLI variant resolution, validation, and OpenAI surface.
tests/electron/providers/sender.test.js Tests queued-send cancellation and abortActive behavior.
tests/electron/providers/cli.test.js Tests CLI discovery/parsing/quoting/concurrency behaviors.
tests/electron/providers/antigravity.test.js Tests Antigravity model parsing and core integration.
tests/electron/multiai-tour.test.js Validates guided tour selectors/columns against markup.
tests/electron/ipc/multiai-core.test.js Expands Multi-AI core tests (store, prompts, agents, blind rounds).
tests/electron/api/ws-variants.test.js Tests WebSocket variants actions (variants/ask_variant/ask_variants).
tests/electron/api/routes.test.js Tests /v1/models ordering including extra cli: models.
tests/byok/providers.test.js Tests reasoningEffort mapping for OpenAI/Anthropic providers.
tests/byok/http.test.js Tests plain http:// support for local OpenAI-compatible endpoints.
src/mcp/variants-server.js Standalone MCP server for local CLI variants (no app needed).
src/mcp/tools-multiai.js MCP tools for Multi-AI Chat transcripts/actions via IPC.
src/mcp/tools-cli.js MCP tools for local CLI variants via IPC or direct backend.
src/mcp/index.js Registers new MCP tool modules; uses package.json version.
scripts/review-panel.mjs Script to run a “panel” review via Proxima REST + local CLIs.
scripts/record-tour.mjs Script to record the guided tour into a GIF via ffmpeg.
README.md Adds top-level Multi-AI Chat overview and docs pointer.
package.json Version bump; adds variants MCP bin/script; build unpack additions.
electron/providers/workspace.cjs Workspace git-ops module (branching/commit/diff description).
electron/providers/variants.cjs CLI variants resolution, validation, ask, and model listing.
electron/providers/sender.cjs Adds active-send tracking, abortActive, and best-effort tab model label.
electron/providers/engines/perplexity-engine.js Adds user abort support for in-flight requests.
electron/providers/engines/gemini-engine.js Adds user abort support + frame diagnostics in extraction.
electron/providers/engines/claude-engine.js Adds user abort support for in-flight requests.
electron/providers/engines/chatgpt-engine.js Adds user abort support for in-flight requests.
electron/providers/catalog.cjs Adds cached, shaped models.dev catalog support.
electron/providers/api.cjs Adds abortActive() to abort in-flight provider sends.
electron/preload.cjs Exposes Multi-AI IPC APIs to renderer; extends byokSaveKey args.
electron/package.json Version bump for electron package metadata.
electron/multiai-tour.js Implements the Multi-AI guided tour overlay.
electron/main-v2.cjs Instance hygiene, single-instance lock, REST extra models, Multi-AI IPC plumbing, fork updater guard, CLI cleanup on quit.
electron/ipc/settings.cjs Extends byok-save-key IPC to accept endpoint/displayName options.
electron/ipc/core.cjs Adds variants-server config path to MCP config output.
electron/api/ws-server.cjs Adds WS actions for variants/ask_variant/ask_variants.
electron/api/routes.cjs Adds extra models injection into /v1/models response.
electron/api/rest-api.cjs Reads version from package.json; safe sendJSON; route extensions; broader body parsing methods; listening callback guard.
electron/api/byok/providers/openai.cjs Adds reasoning_effort mapping + completion budget widening.
electron/api/byok/providers/anthropic.cjs Adds extended-thinking budget mapping from reasoningEffort.
electron/api/byok/providers/_http.cjs Adds http:// transport support for local endpoints.
docs/review/session-review.md Captured review output session notes for the branch.
docs/MULTIAI.md Full reference documentation for Multi-AI Chat, variants, tools, and APIs.
CHANGELOG.md Adds 5.1.0 fork release notes.
assets/multiai-icon.svg Adds Multi-AI icon asset.
Review details
  • Files reviewed: 54/56 changed files
  • Comments generated: 4
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +126 to +127
const kind = core.CLI_PROVIDERS[agent.provider].bin;
if (!cli.availability()[kind]) throw err(503, `${core.providerLabel(agent.provider)} is not installed (no \`${kind}\` on PATH)`);
Comment thread scripts/review-panel.mjs
const cli = require(path.join(ROOT, 'electron', 'providers', 'cli.cjs'));

const argv = process.argv.slice(2);
const flag = (n, d) => { const i = argv.indexOf('--' + n); return i === -1 ? d : argv[i + 1]; };
Comment thread scripts/review-panel.mjs
Comment on lines +195 to +201
temporary = path.join(path.dirname(ROOT), `.${path.basename(ROOT)}-review-${Date.now()}`);
// On a branch, not detached. workspace.prepare() cuts its chat branch from
// the *default* branch whenever HEAD is detached or already on multiai/*,
// so a detached worktree had the seats reviewing main while the brief
// described this branch — they read a different revision than the one they
// were shown, and said so.
git('worktree', 'add', '-b', `proxima-review-${Date.now()}`, temporary, 'HEAD');
Comment thread src/mcp/index.js
Comment on lines 263 to 267
server: 'Proxima MCP Server',
version: '5.0.0',
version: PKG_VERSION,
architecture: 'modular',
modules: ['ipc-bridge', 'helpers', 'pipeline', 'tools-chat', 'tools-code', 'tools-search', 'tools-content', 'tools-utility', 'tools-workflow'],
enabledProviders: Array.from(enabled),
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants