Skip to content

Add cmux browser repl: a Playwright-shaped browser REPL for agents - #15570

Closed
lawrencecchen wants to merge 398 commits into
mainfrom
feat-browser-repl-parity
Closed

lawrencecchen wants to merge 398 commits into
mainfrom
feat-browser-repl-parity

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds cmux browser repl, a persistent JavaScript REPL that agents use to drive cmux browser panes. It has one Playwright-shaped API with native input, budgeted accessibility snapshots and site tools. Design: docs/browser-repl/README.md; driver contract and guards: docs/browser-repl/driver-protocol.md.

  • API: Playwright page, locators, keyboard, mouse and events, plus tabs, snapshot, screenshot, cookie-bearing fetch, fs/path/os, session, secrets and sites. Named sessions keep variables and tabs; cmux browser repl mcp serves a session over stdio MCP.
  • Snapshot: accessibility tree with refs bound to DOM nodes; nested and cross-origin frames and open and closed shadow roots inline; diffs with context; interactive and viewport modes. Output is budgeted (20,000 characters printed, 25,000 per call, the rest spilled to a file).
  • Input and focus: native trusted events; hidden tabs render in an offscreen window. Agent work does not move the user's key window, workspace, pane, terminal focus or text: replayed keys never reach the terminal or menus, Tab cannot move focus out of a page, and dialogs, file choosers and popups that the agent's own input causes go to the agent.
  • Tab states: tabs.list() reports live, hibernated, waking or crashed. A call on a hibernated tab wakes it off screen (bounded wait); a crashed tab fails with an error that says to call page.reload().
  • Guards (native, outside agent code): secrets are fill-only and redacted everywhere, including captures and typed TOTP codes; the domain policy covers every frame, fetch redirect hop and subresource; page and agent clipboard writes in session-created tabs go to a per-tab clipboard, never the system clipboard; each session has a private temp directory; cells, fetches and timers are bounded.
  • Site tools: Google (Gmail, Docs, Sheets, Slides, Drive, Calendar, Search), YouTube, Slack, Notion, LinkedIn, X, GitHub, Linear, Jira, page assets, WebMCP and a secure sign-in sheet. Writes that reach other people are drafts until { confirm: true }.

Evidence

  • tests/browser-parity/gate.sh --app --app-runs 2 on a fleet build of the final head (results in the merge receipt comment).
  • 156 differential cases against two reference agent browsers: 0 cases worse (docs/browser-repl/parity-report.md, docs/browser-repl/edge-cases.md). Excluded: raw CDP (WebKit has none).
  • Snapshot performance at or ahead of a Playwright-based MCP server at p50 on large pages (docs/browser-repl/performance.md).
  • Security: parallel static scans of the diff, two rounds, then fixes with red-first tests; remaining medium and low findings are in the follow-up issue.
  • Live checks on a fleet Mac: focus unchanged through agent clicks, keys, Tab and Shift+Tab in focused and background workspaces; hibernated and force-discarded tabs wake; crashed tabs recover with page.reload().

Review notes

  • Private WebKit SPI behind runtime checks: _frames:, resource-load delegate, _tryClose, _doAfterProcessingAllPendingMouseEvents:, _doAfterActivityStateUpdate:, _simulateMouseMove:, _WKContentWorldConfiguration.allowAccessToClosedShadowRoots, WebKit feature flags for the async Clipboard API.
  • The clipboard command redirect swizzles +[NSPasteboard pasteboardWithName:] for WebKit callers only, bounded to one command.
  • Accepted gap: a page with a user gesture can still write the system clipboard through a frame whose document has not loaded (documented in Guards).
  • REPL-driven tabs skip the insecure-HTTP prompt. browser.repl.* socket methods are not allowlisted for remote relay.
  • The browser-parity Node suite is not part of CI; it runs through gate.sh on the fleet. CI adds only the CmuxBrowser package tests (about 15 s) and one app-host suite.

Changelog

  • Added: cmux browser repl, a persistent browser automation REPL for agents with native input, budgeted accessibility snapshots, guarded secrets and clipboard, and site tools.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 388 files, which is 88 over the limit of 300.

To get a review, reduce the PR to 300 files or fewer by splitting it into smaller PRs or changing its base branch.

Usage-priced reviews support at most 300 files.

⚙️ Run configuration
  • Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 9614f822-af17-4f84-81e2-5e3070a9784e
📥 Commits

Reviewing files that changed from the base of the PR and between 161e1cc and 89c84cb.

📒 Files selected for processing (388)
  • CLI/CMUXCLI+BrowserRepl.swift
  • CLI/CMUXCLI+TaskHelp.swift
  • CLI/cmux.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Control/BrowserKeyboardEvent.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Download/BrowserSuggestedFilenameOverriding.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Input/BrowserWebKitKeyDownDispatch.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Input/SyntheticKeyEventFactory.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplAgentUserScript.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplBoundary.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplBoundedWindow.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplCaptureMask.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplContentRuleLists.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplCookieClearScope.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplCookieMatch.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplDomainPolicy.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplDownloadLedger.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplDriver.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFetcher.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSandbox.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFrame.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFrameGate.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFrameRegistry.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplHeldKeys.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplJSThread.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplKeyStroke.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplMouseEventPlan.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplPDFPrinter.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplPageClipboard.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplPasteboardRedirect.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplPointerOwner.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplPublicSuffixList.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplRoutedRequests.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplRuntimeBundle.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplScriptProbe.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSecretStore.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSecretTarget.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSessionRegistry.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplTabOwnership.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplTabWaker.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplTextCommit.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplTimerScheduler.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplTypedSecrets.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplWatchdog.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplWorkspaceBinding.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/NSPasteboard+BrowserReplClipboardItems.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/WKWebsiteDataStore+BrowserReplID.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Screenshot/BrowserOffscreenRenderPanel.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/BrowserOffscreenRenderHost.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebView+AutomationInput.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebView+ScriptedDownloads.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebView.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/WKWebView+AutomationFocusContainment.swift
  • Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/WKWebView+BrowserUserAgentPolicy.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplAgentUserScriptTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplBoundaryTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplBoundedWindowTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplCaptureMaskTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplClipboardItemsTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplContentRuleListsTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplCookieClearScopeTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplCookieMatchTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplDataStoreIDTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplDomainPolicyTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplDownloadLedgerTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplDragPasteboardTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplFileSandboxTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplFileSystemTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplFocusContainmentTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplFrameGateTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplFrameRegistryTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplHeldKeysTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplInputMappingTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplKeyResendTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplPDFPrinterTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplPageClipboardTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplPasteboardRedirectTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplPointerOwnerTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplPopupPolicyTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplPublicSuffixListTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplRoutedRequestsTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplRuntimeBundleTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplSecretRedactionTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplSecretTargetTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplSessionLifecycleTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplSessionResourceTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplSessionTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplTabOwnershipTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplTabWakeTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplTestSupport.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplTextCommitTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplTimerSchedulerTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplTypedSecretsTests.swift
  • Packages/macOS/CmuxBrowser/Tests/CmuxBrowserTests/Repl/BrowserReplWorkspaceBindingTests.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Debug/ControlCommandCoordinator+DebugV1.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Coordinator/Debug/ControlDebugContext.swift
  • Packages/macOS/CmuxControlSocket/Sources/CmuxControlSocket/Wire/ControlCommandExecutionPolicy.swift
  • Packages/macOS/CmuxControlSocket/Tests/CmuxControlSocketTests/ControlCommandContextTestStubs+Debug.swift
  • Packages/macOS/CmuxRemoteWorkspace/Tests/CmuxRemoteWorkspaceTests/RemoteRelayBrowserReplPolicyTests.swift
  • Resources/Localizable.xcstrings
  • Resources/browser-repl/agent-tools.js
  • Resources/browser-repl/api.js
  • Resources/browser-repl/guide.md
  • Resources/browser-repl/manifest.json
  • Resources/browser-repl/page-agent.js
  • Resources/browser-repl/page-clipboard.js
  • Resources/browser-repl/repl-host.js
  • Resources/browser-repl/runtime-core.js
  • Resources/browser-repl/sites/auth-fill.js
  • Resources/browser-repl/sites/browser-auth.js
  • Resources/browser-repl/sites/github.js
  • Resources/browser-repl/sites/gmail.js
  • Resources/browser-repl/sites/google-accounts.js
  • Resources/browser-repl/sites/google-calendar.js
  • Resources/browser-repl/sites/google-docs.js
  • Resources/browser-repl/sites/google-drive.js
  • Resources/browser-repl/sites/google-editors.js
  • Resources/browser-repl/sites/google-search.js
  • Resources/browser-repl/sites/google-sheets.js
  • Resources/browser-repl/sites/google-slides.js
  • Resources/browser-repl/sites/google.js
  • Resources/browser-repl/sites/jira.js
  • Resources/browser-repl/sites/linear.js
  • Resources/browser-repl/sites/linkedin.js
  • Resources/browser-repl/sites/loader.js
  • Resources/browser-repl/sites/notion.js
  • Resources/browser-repl/sites/page-assets.js
  • Resources/browser-repl/sites/slack.js
  • Resources/browser-repl/sites/webmcp.js
  • Resources/browser-repl/sites/x.js
  • Resources/browser-repl/sites/youtube.js
  • Resources/browser-repl/snapshot.js
  • Resources/browser-repl/vendor/acorn.js
  • Resources/browser-repl/vendor/playwright-injected.js
  • Resources/browser-repl/vendor/playwright-locator-utils.js
  • Sources/AppDelegate.swift
  • Sources/Panels/BrowserDiscardRestoreHeal.swift
  • Sources/Panels/BrowserNavigationDelegate.swift
  • Sources/Panels/BrowserPanel+PrewarmedWebViewAdoption.swift
  • Sources/Panels/BrowserPanel+WebContentTermination.swift
  • Sources/Panels/BrowserPanel.swift
  • Sources/Panels/BrowserPanelView.swift
  • Sources/Panels/BrowserPopupWindowController.swift
  • Sources/Panels/BrowserRepl/BrowserReplCapture.swift
  • Sources/Panels/BrowserRepl/BrowserReplCredentialRequest.swift
  • Sources/Panels/BrowserRepl/BrowserReplDriverGuards.swift
  • Sources/Panels/BrowserRepl/BrowserReplFrameTree.swift
  • Sources/Panels/BrowserRepl/BrowserReplNativeInput.swift
  • Sources/Panels/BrowserRepl/BrowserReplResourceLoadObserver.swift
  • Sources/Panels/BrowserRepl/BrowserReplTabAttachment.swift
  • Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift
  • Sources/TerminalController+BrowserRepl.swift
  • Sources/TerminalController+Capabilities.swift
  • Sources/TerminalController+ControlDebugContext.swift
  • Sources/TerminalController+ControlSocketAsync.swift
  • Sources/Workspace+PanelLifecycle.swift
  • Sources/Workspace.swift
  • THIRD_PARTY_LICENSES.md
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/BrowserReplRenderHostTests.swift
  • docs/browser-repl/README.md
  • docs/browser-repl/driver-protocol.md
  • docs/browser-repl/edge-cases.md
  • docs/browser-repl/parity-report.md
  • docs/browser-repl/performance.md
  • docs/browser-repl/reference-c-parity.md
  • docs/browser-repl/site-tools.md
  • docs/cli-contract.md
  • scripts/localization-allowed-omissions.json
  • tests/browser-parity/README.md
  • tests/browser-parity/capabilities.json
  • tests/browser-parity/diff/cases/10-globals.mjs
  • tests/browser-parity/diff/cases/20-page.mjs
  • tests/browser-parity/diff/cases/30-locator.mjs
  • tests/browser-parity/diff/cases/40-input.mjs
  • tests/browser-parity/diff/cases/50-tabs-dialogs-content.mjs
  • tests/browser-parity/diff/cases/70-edge.mjs
  • tests/browser-parity/diff/cases/80-sessions.mjs
  • tests/browser-parity/diff/fixtures/alert-load.html
  • tests/browser-parity/diff/fixtures/busy.html
  • tests/browser-parity/diff/fixtures/closer.html
  • tests/browser-parity/diff/fixtures/detach.html
  • tests/browser-parity/diff/fixtures/editor.html
  • tests/browser-parity/diff/fixtures/files.html
  • tests/browser-parity/diff/fixtures/iframe-nav.html
  • tests/browser-parity/diff/fixtures/infinite.html
  • tests/browser-parity/diff/fixtures/inputs.html
  • tests/browser-parity/diff/fixtures/lab.html
  • tests/browser-parity/diff/fixtures/log.js
  • tests/browser-parity/diff/fixtures/nest-inner.html
  • tests/browser-parity/diff/fixtures/nest.html
  • tests/browser-parity/diff/fixtures/next.html
  • tests/browser-parity/diff/fixtures/overlay.html
  • tests/browser-parity/diff/fixtures/permissions.html
  • tests/browser-parity/diff/fixtures/popups.html
  • tests/browser-parity/diff/fixtures/sandbox-inner.html
  • tests/browser-parity/diff/fixtures/sandbox.html
  • tests/browser-parity/diff/fixtures/spa.html
  • tests/browser-parity/diff/fixtures/storage.html
  • tests/browser-parity/diff/fixtures/sw.html
  • tests/browser-parity/diff/fixtures/ws.html
  • tests/browser-parity/diff/fixtures/zoom.html
  • tests/browser-parity/diff/lib.mjs
  • tests/browser-parity/diff/reference-b-runner.ts
  • tests/browser-parity/diff/report.mjs
  • tests/browser-parity/diff/results/.gitignore
  • tests/browser-parity/diff/results/cmux-dev.json
  • tests/browser-parity/diff/results/cmux.json
  • tests/browser-parity/diff/results/reference-a.json
  • tests/browser-parity/diff/results/reference-b.json
  • tests/browser-parity/diff/run.mjs
  • tests/browser-parity/diff/server.mjs
  • tests/browser-parity/fixtures/agent-frame.html
  • tests/browser-parity/fixtures/agent-tools.html
  • tests/browser-parity/fixtures/aria.html
  • tests/browser-parity/fixtures/closed-shadow.html
  • tests/browser-parity/fixtures/compact.html
  • tests/browser-parity/fixtures/corpus/bbc.html
  • tests/browser-parity/fixtures/corpus/bbc.oracle.json
  • tests/browser-parity/fixtures/corpus/books.html
  • tests/browser-parity/fixtures/corpus/books.oracle.json
  • tests/browser-parity/fixtures/corpus/github.html
  • tests/browser-parity/fixtures/corpus/github.oracle.json
  • tests/browser-parity/fixtures/corpus/gt.js
  • tests/browser-parity/fixtures/corpus/hackernews.html
  • tests/browser-parity/fixtures/corpus/hackernews.oracle.json
  • tests/browser-parity/fixtures/corpus/mdn-iframe.html
  • tests/browser-parity/fixtures/corpus/mdn-iframe.oracle.json
  • tests/browser-parity/fixtures/corpus/mdn.html
  • tests/browser-parity/fixtures/corpus/mdn.oracle.json
  • tests/browser-parity/fixtures/corpus/npr.html
  • tests/browser-parity/fixtures/corpus/npr.oracle.json
  • tests/browser-parity/fixtures/corpus/reference-a-sizes.json
  • tests/browser-parity/fixtures/corpus/vercel.html
  • tests/browser-parity/fixtures/corpus/vercel.oracle.json
  • tests/browser-parity/fixtures/corpus/wikipedia.html
  • tests/browser-parity/fixtures/corpus/wikipedia.oracle.json
  • tests/browser-parity/fixtures/dialogs.html
  • tests/browser-parity/fixtures/dynamic.html
  • tests/browser-parity/fixtures/files.html
  • tests/browser-parity/fixtures/frame-inner.html
  • tests/browser-parity/fixtures/frames.html
  • tests/browser-parity/fixtures/index.html
  • tests/browser-parity/fixtures/input.html
  • tests/browser-parity/fixtures/log.js
  • tests/browser-parity/fixtures/nest-l1.html
  • tests/browser-parity/fixtures/nest-l2.html
  • tests/browser-parity/fixtures/nest-l3.html
  • tests/browser-parity/fixtures/nest-top.html
  • tests/browser-parity/fixtures/search-bing.html
  • tests/browser-parity/fixtures/search-duckduckgo.html
  • tests/browser-parity/fixtures/shadow.html
  • tests/browser-parity/fixtures/states.html
  • tests/browser-parity/fixtures/stress/frame.html
  • tests/browser-parity/fixtures/stress/stress.html
  • tests/browser-parity/fixtures/surface.html
  • tests/browser-parity/fixtures/targets.html
  • tests/browser-parity/fixtures/visibility.html
  • tests/browser-parity/gate.sh
  • tests/browser-parity/goldens/01-snapshot.json
  • tests/browser-parity/goldens/02-aria.json
  • tests/browser-parity/goldens/03-states.json
  • tests/browser-parity/goldens/04-actions-diff.json
  • tests/browser-parity/goldens/05-input.json
  • tests/browser-parity/goldens/06-frames.json
  • tests/browser-parity/goldens/07-shadow.json
  • tests/browser-parity/goldens/08-dialogs.json
  • tests/browser-parity/goldens/09-dialogs-held.json
  • tests/browser-parity/goldens/10-files.json
  • tests/browser-parity/goldens/11-files-held.json
  • tests/browser-parity/goldens/12-navigation.json
  • tests/browser-parity/goldens/13-tabs.json
  • tests/browser-parity/goldens/14-screenshots.json
  • tests/browser-parity/goldens/15-locators.json
  • tests/browser-parity/goldens/16-network-console.json
  • tests/browser-parity/goldens/17-refs.json
  • tests/browser-parity/goldens/18-print.json
  • tests/browser-parity/goldens/19-clipboard.json
  • tests/browser-parity/goldens/20-session.json
  • tests/browser-parity/goldens/21-fs.json
  • tests/browser-parity/goldens/22-api-surface.json
  • tests/browser-parity/goldens/23-insecure-http.json
  • tests/browser-parity/goldens/24-visibility.json
  • tests/browser-parity/goldens/25-nested-frames.json
  • tests/browser-parity/goldens/26-closed-shadow.json
  • tests/browser-parity/goldens/27-corpus.json
  • tests/browser-parity/goldens/28-compact.json
  • tests/browser-parity/goldens/29-targets.json
  • tests/browser-parity/goldens/30-stress.json
  • tests/browser-parity/goldens/31-frame-calls.json
  • tests/browser-parity/goldens/32-agent-tools.json
  • tests/browser-parity/goldens/33-markdown-corpus.json
  • tests/browser-parity/goldens/34-cookie-scope.json
  • tests/browser-parity/goldens/35-pointer-owner.json
  • tests/browser-parity/goldens/36-session-tabs.json
  • tests/browser-parity/goldens/37-session-context.json
  • tests/browser-parity/goldens/38-cookie-guards.json
  • tests/browser-parity/goldens/39-policy-rules.json
  • tests/browser-parity/lib/corpus.mjs
  • tests/browser-parity/lib/dev-driver.mjs
  • tests/browser-parity/lib/fixture-server.mjs
  • tests/browser-parity/lib/native-boundary.mjs
  • tests/browser-parity/lib/normalize.mjs
  • tests/browser-parity/lib/oracle.mjs
  • tests/browser-parity/lib/public-suffix.mjs
  • tests/browser-parity/lib/references.mjs
  • tests/browser-parity/lib/test-dirs.mjs
  • tests/browser-parity/perf/bench.mjs
  • tests/browser-parity/perf/chrome-refs.mjs
  • tests/browser-parity/perf/report.mjs
  • tests/browser-parity/perf/results/after-cmux-dev.json
  • tests/browser-parity/perf/results/after-cmux.json
  • tests/browser-parity/perf/results/before-cmux-dev.json
  • tests/browser-parity/perf/results/before-cmux.json
  • tests/browser-parity/perf/results/ref-chrome.json
  • tests/browser-parity/perf/results/ref-reference-a.json
  • tests/browser-parity/perf/results/round2after-cmux.json
  • tests/browser-parity/perf/results/round2before-cmux.json
  • tests/browser-parity/perf/results/round2ref-chrome.json
  • tests/browser-parity/perf/tokens.mjs
  • tests/browser-parity/reference/reference-a-api-surface.txt
  • tests/browser-parity/reference/reference-b-api-surface.txt
  • tests/browser-parity/reference/site-surface.txt
  • tests/browser-parity/run.mjs
  • tests/browser-parity/scenarios/01-snapshot.js
  • tests/browser-parity/scenarios/02-aria.js
  • tests/browser-parity/scenarios/03-states.js
  • tests/browser-parity/scenarios/04-actions-diff.js
  • tests/browser-parity/scenarios/05-input.js
  • tests/browser-parity/scenarios/06-frames.js
  • tests/browser-parity/scenarios/07-shadow.js
  • tests/browser-parity/scenarios/08-dialogs.js
  • tests/browser-parity/scenarios/09-dialogs-held.js
  • tests/browser-parity/scenarios/10-files.js
  • tests/browser-parity/scenarios/11-files-held.js
  • tests/browser-parity/scenarios/12-navigation.js
  • tests/browser-parity/scenarios/13-tabs.js
  • tests/browser-parity/scenarios/14-screenshots.js
  • tests/browser-parity/scenarios/15-locators.js
  • tests/browser-parity/scenarios/16-network-console.js
  • tests/browser-parity/scenarios/17-refs.js
  • tests/browser-parity/scenarios/18-print.js
  • tests/browser-parity/scenarios/19-clipboard.js
  • tests/browser-parity/scenarios/20-session.js
  • tests/browser-parity/scenarios/21-fs.js
  • tests/browser-parity/scenarios/22-api-surface.js
  • tests/browser-parity/scenarios/23-insecure-http.js
  • tests/browser-parity/scenarios/24-visibility.js
  • tests/browser-parity/scenarios/25-nested-frames.js
  • tests/browser-parity/scenarios/26-closed-shadow.js
  • tests/browser-parity/scenarios/27-corpus.js
  • tests/browser-parity/scenarios/28-compact.js
  • tests/browser-parity/scenarios/29-targets.js
  • tests/browser-parity/scenarios/30-stress.js
  • tests/browser-parity/scenarios/31-frame-calls.js
  • tests/browser-parity/scenarios/32-agent-tools.js
  • tests/browser-parity/scenarios/33-markdown-corpus.js
  • tests/browser-parity/scenarios/34-cookie-scope.js
  • tests/browser-parity/scenarios/35-pointer-owner.js
  • tests/browser-parity/scenarios/36-session-tabs.js
  • tests/browser-parity/scenarios/37-session-context.js
  • tests/browser-parity/scenarios/38-cookie-guards.js
  • tests/browser-parity/scenarios/39-policy-rules.js
  • tests/browser-parity/sites/.gitignore
  • tests/browser-parity/sites/drafts.test.mjs
  • tests/browser-parity/sites/google-editors.test.mjs
  • tests/browser-parity/sites/google-mail-calendar.test.mjs
  • tests/browser-parity/sites/google-workspace.test.mjs
  • tests/browser-parity/sites/harness.mjs
  • tests/browser-parity/sites/live-diff.mjs
  • tests/browser-parity/sites/mock-editors.mjs
  • tests/browser-parity/sites/mock-sites.mjs
  • tests/browser-parity/sites/page-tools.test.mjs
  • tests/browser-parity/sites/social.test.mjs
  • tests/browser-parity/sites/work-apps.test.mjs
  • tests/browser-parity/sites/youtube-search.test.mjs
  • tests/browser-parity/unit/agent-tools.test.mjs
  • tests/browser-parity/unit/budget.test.mjs
  • tests/browser-parity/unit/capabilities.test.mjs
  • tests/browser-parity/unit/diff.test.mjs
  • tests/browser-parity/unit/mcp.test.mjs
  • tests/browser-parity/unit/native-boundary.test.mjs
  • tests/browser-parity/unit/perf.test.mjs
  • tests/browser-parity/unit/runtime.test.mjs
  • tests/browser-parity/unit/tab-ownership.test.mjs
  • tests/test-execution.toml
  • tests/test_browser_repl_cli_io.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@lawrencecchen lawrencecchen changed the title Add cmux browser repl with Aside and ChatGPT for Chrome parity Add cmux browser repl: one Playwright-based agent browser API Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

CI failure attribution

CI passes on 89c84cbb3f (run 37195523844 attempt 1).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's.

@lawrencecchen
lawrencecchen marked this pull request as ready for review September 30, 2026 18:21
@cursor

cursor Bot commented Sep 30, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

1 similar comment
@cursor

cursor Bot commented Sep 30, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Dogfood tours of 89c84cbb

browser-notifications-tour at 89c84cbb, on its merge 4395ed98 that CI built: failure (run)

browser-notifications-tour at 89c84cbb

Key frames of browser-notifications-tour at 89c84cb 04-browser-split 12-omnibar-suggestions 20-notifications-menu 27-end

Tours are picked by the paths globs in dogfood/scenarios/*.json; a Dogfood-tours: a, b line in the description picks them instead (none turns this off). Look at every frame before merging: a green tour only means no step failed.

@cursor

cursor Bot commented Sep 30, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cubic-dev-ai cubic-dev-ai Bot 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.

60 issues found across 535 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift">

<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift:44">
P1: The sandbox authorizes a path and then uses it in separate filesystem calls, leaving a symlink-swap TOCTOU gap. Use descriptor-relative operations with no-followed directory descriptors, or revalidate the opened file before exposing data or mutating it.

(Based on your team's feedback about descriptor-relative filesystem operations.)</violation>

<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift:59">
P2: `writeFile` silently treats missing or invalid base64 as an empty file and reports success. Reject malformed payloads with `EINVAL` before opening or truncating the destination.</violation>

<violation number="3" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift:105">
P1: `rm` follows an in-sandbox symlink because it operates on the canonical target returned by `path(.write)`. Preserve the lexical final component and unlink the symlink itself after authorizing the resolved target.</violation>

<violation number="4" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift:134">
P0: `rename` and `copyFile` remove an existing destination before the source operation succeeds. A missing source or `from == to` can destroy data, and `to == "."` bypasses `rm`’s root guard; reject root endpoints and use source-first atomic replacement instead.</violation>
</file>

<file name="Resources/browser-repl/sites/linear.js">

<violation number="1" location="Resources/browser-repl/sites/linear.js:64">
P1: This guard only rejects documents whose first token is `mutation`; a leading GraphQL comment lets a mutation reach Linear without a draft or confirmation. Strip GraphQL comments or parse the document before rejecting mutation operations.</violation>
</file>

<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift">

<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift:210">
P1: A close can queue cleanup before an already-submitted `beginEval`; `beginEval` does not re-check `closed`, so it can start a new evaluation after cleanup and leave its continuation unresolved when the stop block runs. Reject the state on the JS thread before proceeding with context setup.</violation>

<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift:238">
P1: This timeout cannot interrupt synchronous JavaScript because it queues `finishEval` on the thread currently executing `evalFunction.call(...)`. A cell such as `while (true) {}` therefore hangs the evaluation and blocks `close()` as well; use an interruptible JavaScript execution/watchdog path.</violation>

<violation number="3" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift:239">
P1: The timeout only completes the Swift continuation; it leaves the JavaScript `__cmuxReplEval` promise running. Because `repl-host.js` serializes cells through one pending queue, a cell that times out while awaiting an unresolved promise permanently wedges all later evaluations; cancel the JS cell or reset its runtime/context on timeout.</violation>

<violation number="4" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift:358">
P1: `setTimer` can crash the app for a finite delay outside `Int64` range, such as `setTimeout(() => {}, 1e20)`. Clamp the delay to the supported timer range before converting it to `Int64`.</violation>
</file>

<file name="Resources/browser-repl/sites/gmail.js">

<violation number="1" location="Resources/browser-repl/sites/gmail.js:44">
P2: `readThreadBody` treats any message-body link containing `view=att` as an attachment. Restrict discovery to Gmail attachment containers so crafted or ordinary body links cannot be listed and fetched as attachments.</violation>

<violation number="2" location="Resources/browser-repl/sites/gmail.js:144">
P1: `finishSend` searches the whole document for `Message sent`, so drafted text or existing thread content can confirm the send before Gmail does. Scope this check to a new Gmail alert; otherwise the later tab cleanup can cancel the pending send.</violation>
</file>

<file name="Resources/browser-repl/sites/browser-auth.js">

<violation number="1" location="Resources/browser-repl/sites/browser-auth.js:46">
P2: This check accepts non-text inputs such as checkbox, radio, submit, and date controls. Restrict `HTMLInputElement` to the supported text-like types (or require the DOM type to match `f.type`) before marking the element for credential fill.</violation>

<violation number="2" location="Resources/browser-repl/sites/browser-auth.js:50">
P1: `browserAuth` validates only the top-level origin, so a locator in a cross-origin iframe can receive the credentials while the sheet names the parent page. Reject fields whose holder frame has a different origin, or pass the holder origin through the native request and verify it before filling.</violation>
</file>

<file name="Resources/browser-repl/sites/slack.js">

<violation number="1" location="Resources/browser-repl/sites/slack.js:26">
P1: This request trusts `team.url` from localStorage and can post the workspace token to any origin. Validate the URL as an HTTPS Slack host before appending the token and fetching, so malformed or tampered workspace metadata cannot exfiltrate credentials.</violation>

<violation number="2" location="Resources/browser-repl/sites/slack.js:117">
P1: A draft without `team` can post to a different workspace than the one active when the user reviewed it: changing Slack's active workspace before confirmation changes `lastActiveTeamId`, and line 117 resolves it again. Bind the concrete workspace (and channel ID) when creating the draft, or require an explicit stable workspace ID for posts.</violation>
</file>

<file name="tests/browser-parity/compare/chatgpt-live.ts">

<violation number="1" location="tests/browser-parity/compare/chatgpt-live.ts:195">
P1: Parity cleanup closes every tab returned by the current-workspace list, including tabs that existed before the run. Track the IDs opened by this script and close only those IDs.</violation>
</file>

<file name="Resources/browser-repl/sites/webmcp.js">

<violation number="1" location="Resources/browser-repl/sites/webmcp.js:61">
P2: This makes a valid page tool named like `draft-1-abcdef` impossible to call, and can dispatch a matching name as an unrelated confirmation. Use an unambiguous confirmation path or verify an existing draft before treating the name as a draft ID.

(Based on your team's feedback about user-representable string sentinels.)</violation>

<violation number="2" location="Resources/browser-repl/sites/webmcp.js:66">
P1: This trusts an untrusted page's `readOnlyHint` as the confirmation boundary, so a mutating WebMCP tool can mark itself read-only and send or change data without a draft. Treat the annotation as informational and require host-side classification or confirmation for page tools.</violation>

<violation number="3" location="Resources/browser-repl/sites/webmcp.js:68">
P1: The WebMCP draft keeps the caller's mutable `input` object by reference, allowing the payload to change after the preview and before confirmation. Snapshot or validate the input when creating the draft, then use that snapshot for both the preview and confirmed execution.</violation>
</file>

<file name="Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift">

<violation number="1" location="Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift:340">
P1: `tabs.close` accepts targets from `tabs.list({ all: true })`, but this call uses the session workspace for every target. Resolve `panel.workspaceId` before closing; otherwise a foreign tab remains open after the driver emits `tab.closed`.</violation>

<violation number="2" location="Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift:384">
P2: `tab.bringToFront` silently no-ops for tabs listed from another workspace because this lookup uses the session workspace. Resolve the workspace containing `panel.workspaceId` and select the panel there.</violation>

<violation number="3" location="Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift:531">
P2: This retry loop can return success after all 50 document evaluations fail. Track a successful evaluation and throw or continue until the timeout when none succeeds, so navigation cannot resolve before the requested load state.</violation>

<violation number="4" location="Sources/Panels/BrowserRepl/WebKitBrowserReplDriver.swift:1081">
P2: A failed mouse-down permanently claims the tab's pointer for this session because release is deferred only for `up`. Clear the pointer claim on every failed down path so later sessions cannot hang waiting for a button that was never delivered.</violation>
</file>

<file name="Sources/TerminalController+BrowserRepl.swift">

<violation number="1" location="Sources/TerminalController+BrowserRepl.swift:116">
P1: Named session lookup ignores the resolved workspace, so reusing a session name from another workspace silently drives the original workspace's tabs. Scope the registry key by workspace ID or reject a workspace mismatch before evaluating.</violation>
</file>

<file name="Resources/browser-repl/sites/google.js">

<violation number="1" location="Resources/browser-repl/sites/google.js:58">
P1: Slides exports use the wrong Google endpoint shape here. Use `/presentation/d/${ref.id}/export/${format}` for `presentation` refs (preserving `authuser` as a query parameter), while retaining the query-form endpoint for Docs and Sheets.</violation>
</file>

<file name="Sources/Panels/BrowserRepl/BrowserReplCredentialRequest.swift">

<violation number="1" location="Sources/Panels/BrowserRepl/BrowserReplCredentialRequest.swift:59">
P1: The secret-bearing fill runs in the same content world exposed to arbitrary agent evaluations, so an agent can install a setter or event hook and capture the credentials. Execute the fill through an app-owned world or another mechanism that does not share mutable JS state with REPL code.</violation>
</file>

<file name="tests/browser-parity/compare/chatgpt-formats.mjs">

<violation number="1" location="tests/browser-parity/compare/chatgpt-formats.mjs:104">
P1: This key does not identify a stable frame or document: it combines a per-call frame position with a per-document local ref. When iframe visibility/order changes or the page navigates, an old `node_id` is reused for a different element, so subsequent ref-based automation can act on the wrong control; track stable frame/document generations and invalidate mappings when they change.</violation>
</file>

<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplTimerScheduler.swift">

<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplTimerScheduler.swift:157">
P1: The scheduler commits every due timer before invoking any callback, so an earlier callback cannot cancel a later due timer. Keep due entries cancellable until each callback is dispatched, or dispatch one due timer at a time and re-check its registration after each callback.</violation>
</file>

<file name="CLI/cmux.swift">

<violation number="1" location="CLI/cmux.swift:18144">
P1: The `repl` branch silently ignores an explicitly supplied surface handle. `cmux browser --surface <surface> repl ...` can therefore run against the focused workspace instead of the requested browser context; reject surface-qualified REPL commands or translate the handle to its workspace before dispatch.</violation>
</file>

<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebView+AutomationInput.swift">

<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebView+AutomationInput.swift:93">
P2: Automation drops never notify WebKit's original drag source when they end. Preserve the `source` passed to `beginDraggingSession` and invoke its `draggingSession(_:endedAt:operation:)` callback; otherwise the page's `dragend` lifecycle is skipped and drag state can remain active.</violation>

<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/WebView/CmuxWebView+AutomationInput.swift:233">
P1: Session teardown clears the bookkeeping without sending modifier-release events, leaving WebKit with stuck modifiers after a REPL disconnect or exception. Replay a release/`flagsChanged` event for each held key before removing it from the owner.</violation>
</file>

<file name="Sources/Panels/BrowserRepl/BrowserReplCapture.swift">

<violation number="1" location="Sources/Panels/BrowserRepl/BrowserReplCapture.swift:47">
P1: This edge-only limit still permits a 268-megapixel screenshot and can exhaust the app's memory. Apply a total pixel-area cap before taking the WebKit snapshot, not only per-dimension clamps.</violation>

<violation number="2" location="Sources/Panels/BrowserRepl/BrowserReplCapture.swift:225">
P2: The cookie path check ignores the required path-boundary rule, so `/account` cookies match `/accounting`. Match an exact path, a slash-terminated cookie path, or a prefix followed by `/`.</violation>
</file>

<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSessionRegistry.swift">

<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSessionRegistry.swift:89">
P1: A long-running evaluation is closed at the idle deadline. `BrowserReplSession` records `lastUsed` when evaluation starts, so an evaluation longer than `idleTimeout` makes this condition false and closes its active JS thread, despite the comment saying long evaluations should be kept; track in-flight evaluations or refresh usage before expiring.</violation>

<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSessionRegistry.swift:93">
P1: This expiry path can close a newly created session belonging to the same name. The callback releases the registry lock before the name-based reset, so a concurrent reset/recreate can install a replacement before this call; revalidate the timer ID and session identity under the lock before removing and closing it.</violation>
</file>

<file name="Sources/Panels/BrowserRepl/BrowserReplTabAttachment.swift">

<violation number="1" location="Sources/Panels/BrowserRepl/BrowserReplTabAttachment.swift:283">
P1: Visibility changes must reevaluate the render host instead of always releasing it. Selecting the tab in a non-key window loses automation focus, while switching away stops hidden-tab rendering until the next command; call `keepRendering()` here so it chooses the live pane only when appropriate.</violation>

<violation number="2" location="Sources/Panels/BrowserRepl/BrowserReplTabAttachment.swift:358">
P1: `detachAll` must release pointer waiters and clear `pointerOwner`; otherwise closing a tab while another session is waiting for its pointer serializes that session forever.</violation>
</file>

<file name="Resources/browser-repl/sites/notion.js">

<violation number="1" location="Resources/browser-repl/sites/notion.js:131">
P2: Appending nested Markdown lists flattens every item at the page root because the parser discards indentation and creates no child relationships. Preserve list depth when building `content` operations, or reject nested list input instead of silently changing its structure.</violation>

<violation number="2" location="Resources/browser-repl/sites/notion.js:143">
P1: This adapter lets `options.origin` redirect Notion API calls and confirmed page edits away from `https://www.notion.so`, so a mistaken or injected option can transmit page content to an arbitrary site. Reject non-Notion origins or always use the fixed Notion origin.</violation>
</file>

<file name="Resources/browser-repl/sites/google-calendar.js">

<violation number="1" location="Resources/browser-repl/sites/google-calendar.js:59">
P2: Date-only ISO inputs are parsed as UTC, but this route uses local getters. Normalize date-only values as calendar dates or use UTC components so `events({ date: "2026-09-30" })` selects the requested day.</violation>

<violation number="2" location="Resources/browser-repl/sites/google-calendar.js:100">
P1: The guest path can report success when the invitation dialog never exposes `Send`, leaving guests uninformed. Do not swallow this wait failure; require and click the invitation control, or explicitly verify an equivalent sent state.</violation>
</file>

<file name="Resources/browser-repl/api.js">

<violation number="1" location="Resources/browser-repl/api.js:424">
P1: Google exports use the current tab's cookies instead of the `page` being exported. Pass the exported page's target ID/cookie context into the fetch used by `createExporter`, otherwise `page.exportContent()` fails or can export from the wrong account for background tabs.</violation>
</file>

<file name="Sources/Panels/BrowserRepl/BrowserReplNativeInput.swift">

<violation number="1" location="Sources/Panels/BrowserRepl/BrowserReplNativeInput.swift:95">
P1: These conversions can crash cmux before `Int32(clamping:)` runs: a valid JSON wheel delta outside `Int` range traps during `Int(...)` conversion. Validate finite values and clamp the `Double` to the `Int32` range before converting so invalid agent input returns safely instead of terminating the app.</violation>
</file>

<file name="Resources/browser-repl/sites/loader.js">

<violation number="1" location="Resources/browser-repl/sites/loader.js:191">
P2: `parseA1Range` accepts invalid A1 ranges such as `A0`, `A1:B0`, and `B2:A1`; `googleSheets.read` then passes the resulting negative or reversed indexes to `slice`, silently returning incorrect or empty rows. Validate 1-based row numbers and range ordering before returning the parsed range.</violation>

<violation number="2" location="Resources/browser-repl/sites/loader.js:366">
P1: Drafts retain mutable caller objects, so Slack and Calendar can send changed values after the user approved the original preview. Snapshot the input before invoking the draft factory and close the send operation over that snapshot.</violation>
</file>

<file name="Resources/browser-repl/sites/page-assets.js">

<violation number="1" location="Resources/browser-repl/sites/page-assets.js:83">
P2: `list()` truncates inline SVG markup that `bundle()` later persists, producing incomplete SVG files for large icons. Preserve the full markup for bundling or mark truncated entries and refuse to write them instead of treating them as complete assets.</violation>

<violation number="2" location="Resources/browser-repl/sites/page-assets.js:121">
P2: `bundle()` can overwrite a downloaded asset named `manifest.json` when it writes its manifest. Reserve the manifest filename before allocating asset filenames so the returned asset remains intact.</violation>
</file>

<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSandbox.swift">

<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSandbox.swift:64">
P1: The canonical-path check is vulnerable to a symlink TOCTOU race: an attacker can replace a checked parent after `resolve` returns and redirect the subsequent filesystem operation outside the sandbox. Perform these operations through descriptor-relative handles with no-follow constraints, or revalidate while holding the relevant directory descriptors.

(Based on your team's feedback about descriptor-relative filesystem operations.)</violation>

<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSandbox.swift:65">
P2: The read-only download exception is bypassed for every downloaded file under the temporary directory because the writable `additionalRoots` check runs first. Reject write access to any path in `readableFiles` before accepting `root` or `additionalRoots`.</violation>
</file>

<file name="CLI/CMUXCLI+BrowserRepl.swift">

<violation number="1" location="CLI/CMUXCLI+BrowserRepl.swift:114">
P2: Large code, especially newline-heavy code, can exceed the socket request buffer after JSON escaping and fail as a transport error. Call `ensureTextFitsSocketRequest` for every cell before sending it.</violation>

<violation number="2" location="CLI/CMUXCLI+BrowserRepl.swift:174">
P2: This reads the entire stdin stream before enforcing the 15 MiB limit, allowing a large or unbounded pipe to consume the CLI’s memory. Read bounded chunks and fail as soon as the limit is crossed.</violation>
</file>

<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFetcher.swift">

<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFetcher.swift:160">
P2: `storeCookies` drops `Secure` `Set-Cookie` responses received over HTTP, including the loopback HTTP origins this browser supports. Parse Secure cookies against an HTTPS URL, as `BrowserCookieBuilder` does, while retaining the response URL for cookie scope.</violation>

<violation number="2" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFetcher.swift:237">
P1: `browserRepl(from:)` does not reliably preserve the `HttpOnly` flag when converting the Playwright shape back to `HTTPCookie`. Build HttpOnly cookies through the existing Set-Cookie reparse path so fetch-written authentication cookies remain hidden from page JavaScript.</violation>
</file>

<file name="Resources/browser-repl/sites/linkedin.js">

<violation number="1" location="Resources/browser-repl/sites/linkedin.js:125">
P1: The compose check validates only the first 40 characters, so a truncated or modified remainder can still be posted after confirmation. Compare the normalized full composer text with the drafted text before clicking Post.</violation>
</file>

<file name="Resources/browser-repl/sites/x.js">

<violation number="1" location="Resources/browser-repl/sites/x.js:85">
P2: `statusId` accepts arbitrary strings that merely end in digits, so malformed post IDs can silently target another post. Anchor the accepted bare-ID and X/Twitter status-URL forms before creating a tweet URL or reply draft.</violation>

<violation number="2" location="Resources/browser-repl/sites/x.js:143">
P2: The compose safety check validates only the first 40 characters, so X can publish a draft whose remaining text differs from the confirmed preview. Compare the complete whitespace-normalized composer text before clicking Post.</violation>
</file>

<file name="Resources/browser-repl/page-agent.js">

<violation number="1" location="Resources/browser-repl/page-agent.js:1066">
P2: File choosers opened by file inputs inside shadow roots are associated with the shadow host instead of the input because this document-level listener uses the retargeted `event.target`. Read the original target from `event.composedPath()[0]` before checking for `HTMLInputElement`, so shadow-root uploads resolve the correct handle.</violation>
</file>

<file name="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplKeyStroke.swift">

<violation number="1" location="Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplKeyStroke.swift:83">
P2: Control-modified digits and punctuation still carry their printable base character. The runtime sends no text for these keys, but this branch excludes Control, so native replay can insert `1` or `;` into editable fields; clear printable characters for Control before the later letter-to-control conversion.</violation>
</file>

<file name="Resources/browser-repl/sites/google-drive.js">

<violation number="1" location="Resources/browser-repl/sites/google-drive.js:59">
P2: `googleDrive.export` cannot export a Google document, sheet, or presentation supplied as a Drive URL when `{ kind }` is provided: `g.parse` leaves the URL as `kind: "file"`, so this branch always throws. Allow a supplied Google kind to classify Drive URLs (while retaining the uploaded-file error when no kind is supplied), or change the documented API.</violation>
</file>

<file name="Resources/browser-repl/sites/auth-fill.js">

<violation number="1" location="Resources/browser-repl/sites/auth-fill.js:9">
P2: Shadow-root credential fields cannot be filled: `document.querySelector` only searches the document tree, while the preceding locator flow accepts elements inside shadow roots. Resolve the marked element through the agent world's shadow-root traversal (including the closed-root support used by the browser API) rather than querying only the document.</violation>
</file>

Note: This PR contains a large number of files. cubic selects up to 200 of the highest-priority eligible files for this review, so some files may not have been reviewed.

Re-trigger cubic

if fileManager.fileExists(atPath: to) {
try fileManager.removeItem(atPath: to)
}
try fileManager.moveItem(atPath: from, toPath: to)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0: rename and copyFile remove an existing destination before the source operation succeeds. A missing source or from == to can destroy data, and to == "." bypasses rm’s root guard; reject root endpoints and use source-first atomic replacement instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplFileSystem.swift, line 134:

<comment>`rename` and `copyFile` remove an existing destination before the source operation succeeds. A missing source or `from == to` can destroy data, and `to == "."` bypasses `rm`’s root guard; reject root endpoints and use source-first atomic replacement instead.</comment>

<file context>
@@ -0,0 +1,205 @@
+            if fileManager.fileExists(atPath: to) {
+                try fileManager.removeItem(atPath: to)
+            }
+            try fileManager.moveItem(atPath: from, toPath: to)
+            return NSNull()
+        case "copyFile":
</file context>

Comment thread Resources/browser-repl/sites/linear.js Outdated
},
// Any read-only GraphQL query; mutations go through the Linear UI.
async query(text, variables) {
if (/^\s*mutation\b/i.test(String(text))) throw new S.SiteError("write_requires_draft", "linear.query: mutations are not run by site tools; make the change in the Linear page");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: This guard only rejects documents whose first token is mutation; a leading GraphQL comment lets a mutation reach Linear without a draft or confirmation. Strip GraphQL comments or parse the document before rejecting mutation operations.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/linear.js, line 64:

<comment>This guard only rejects documents whose first token is `mutation`; a leading GraphQL comment lets a mutation reach Linear without a draft or confirmation. Strip GraphQL comments or parse the document before rejecting mutation operations.</comment>

<file context>
@@ -0,0 +1,71 @@
+        },
+        // Any read-only GraphQL query; mutations go through the Linear UI.
+        async query(text, variables) {
+          if (/^\s*mutation\b/i.test(String(text))) throw new S.SiteError("write_requires_draft", "linear.query: mutations are not run by site tools; make the change in the Linear page");
+          return q(String(text), variables);
+        },
</file context>

}
guard let self else { return }
let milliseconds = timeout.components.seconds * 1000 + timeout.components.attoseconds / 1_000_000_000_000_000
self.thread.perform {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: This timeout cannot interrupt synchronous JavaScript because it queues finishEval on the thread currently executing evalFunction.call(...). A cell such as while (true) {} therefore hangs the evaluation and blocks close() as well; use an interruptible JavaScript execution/watchdog path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift, line 238:

<comment>This timeout cannot interrupt synchronous JavaScript because it queues `finishEval` on the thread currently executing `evalFunction.call(...)`. A cell such as `while (true) {}` therefore hangs the evaluation and blocks `close()` as well; use an interruptible JavaScript execution/watchdog path.</comment>

<file context>
@@ -0,0 +1,490 @@
+            }
+            guard let self else { return }
+            let milliseconds = timeout.components.seconds * 1000 + timeout.components.attoseconds / 1_000_000_000_000_000
+            self.thread.perform {
+                self.finishEval(id: evalID, error: "Error: REPL evaluation timed out after \(milliseconds)ms")
+            }
</file context>

guard let self else { return }
let milliseconds = timeout.components.seconds * 1000 + timeout.components.attoseconds / 1_000_000_000_000_000
self.thread.perform {
self.finishEval(id: evalID, error: "Error: REPL evaluation timed out after \(milliseconds)ms")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The timeout only completes the Swift continuation; it leaves the JavaScript __cmuxReplEval promise running. Because repl-host.js serializes cells through one pending queue, a cell that times out while awaiting an unresolved promise permanently wedges all later evaluations; cancel the JS cell or reset its runtime/context on timeout.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Packages/macOS/CmuxBrowser/Sources/CmuxBrowser/Repl/BrowserReplSession.swift, line 239:

<comment>The timeout only completes the Swift continuation; it leaves the JavaScript `__cmuxReplEval` promise running. Because `repl-host.js` serializes cells through one pending queue, a cell that times out while awaiting an unresolved promise permanently wedges all later evaluations; cancel the JS cell or reset its runtime/context on timeout.</comment>

<file context>
@@ -0,0 +1,490 @@
+            guard let self else { return }
+            let milliseconds = timeout.components.seconds * 1000 + timeout.components.attoseconds / 1_000_000_000_000_000
+            self.thread.perform {
+                self.finishEval(id: evalID, error: "Error: REPL evaluation timed out after \(milliseconds)ms")
+            }
+        }
</file context>

const shown = (await box.innerText()).replace(/\s+/g, " ");
if (bodyStart && !shown.includes(bodyStart.replace(/\s+/g, " "))) throw new S.SiteError("compose_mismatch", "gmail.send: the compose window did not receive the drafted body; nothing was sent");
await page.locator('div[role="button"][data-tooltip^="Send"], div[role="button"][aria-label^="Send"]').last().click();
await t.waitIn(page, () => /Message sent/.test(document.body.innerText), undefined, { signIn: SIGN_IN, name: "gmail", timeout: 30000, what: "Gmail to confirm the message was sent" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: finishSend searches the whole document for Message sent, so drafted text or existing thread content can confirm the send before Gmail does. Scope this check to a new Gmail alert; otherwise the later tab cleanup can cancel the pending send.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/gmail.js, line 144:

<comment>`finishSend` searches the whole document for `Message sent`, so drafted text or existing thread content can confirm the send before Gmail does. Scope this check to a new Gmail alert; otherwise the later tab cleanup can cancel the pending send.</comment>

<file context>
@@ -0,0 +1,205 @@
+        const shown = (await box.innerText()).replace(/\s+/g, " ");
+        if (bodyStart && !shown.includes(bodyStart.replace(/\s+/g, " "))) throw new S.SiteError("compose_mismatch", "gmail.send: the compose window did not receive the drafted body; nothing was sent");
+        await page.locator('div[role="button"][data-tooltip^="Send"], div[role="button"][aria-label^="Send"]').last().click();
+        await t.waitIn(page, () => /Message sent/.test(document.body.innerText), undefined, { signIn: SIGN_IN, name: "gmail", timeout: 30000, what: "Gmail to confirm the message was sent" });
+        // Gmail holds a sent message for its undo window in this page; keep
+        // the tab until the Undo action is gone.
</file context>
Suggested change
await t.waitIn(page, () => /Message sent/.test(document.body.innerText), undefined, { signIn: SIGN_IN, name: "gmail", timeout: 30000, what: "Gmail to confirm the message was sent" });
await t.waitIn(page, () => [...document.querySelectorAll('[role="alert"], .bAq')].some((e) => /Message sent/.test(e.innerText)), undefined, { signIn: SIGN_IN, name: "gmail", timeout: 30000, what: "Gmail to confirm the message was sent" });

const inlineSvgs = svgs.map((s, i) => {
const label = s.getAttribute("aria-label") || (s.querySelector("title") || {}).textContent || s.id || "svg-" + (i + 1);
const markup = s.outerHTML;
return { name: String(label).trim().slice(0, 60), markup: markup.length > arg.maxSvgChars ? markup.slice(0, arg.maxSvgChars) + "<!-- truncated -->" : markup };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: list() truncates inline SVG markup that bundle() later persists, producing incomplete SVG files for large icons. Preserve the full markup for bundling or mark truncated entries and refuse to write them instead of treating them as complete assets.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/page-assets.js, line 83:

<comment>`list()` truncates inline SVG markup that `bundle()` later persists, producing incomplete SVG files for large icons. Preserve the full markup for bundling or mark truncated entries and refuse to write them instead of treating them as complete assets.</comment>

<file context>
@@ -0,0 +1,171 @@
+    const inlineSvgs = svgs.map((s, i) => {
+      const label = s.getAttribute("aria-label") || (s.querySelector("title") || {}).textContent || s.id || "svg-" + (i + 1);
+      const markup = s.outerHTML;
+      return { name: String(label).trim().slice(0, 60), markup: markup.length > arg.maxSvgChars ? markup.slice(0, arg.maxSvgChars) + "<!-- truncated -->" : markup };
+    });
+    return { pageUrl: location.href, assets: [...found.values()], inlineSvgs };
</file context>

let bare = domain.hasPrefix(".") ? String(domain.dropFirst()) : domain
guard host == bare || host.hasSuffix("." + bare) else { return false }
let path = url.path.isEmpty ? "/" : url.path
guard path.hasPrefix(cookie.path) else { return false }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The cookie path check ignores the required path-boundary rule, so /account cookies match /accounting. Match an exact path, a slash-terminated cookie path, or a prefix followed by /.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Sources/Panels/BrowserRepl/BrowserReplCapture.swift, line 225:

<comment>The cookie path check ignores the required path-boundary rule, so `/account` cookies match `/accounting`. Match an exact path, a slash-terminated cookie path, or a prefix followed by `/`.</comment>

<file context>
@@ -0,0 +1,251 @@
+        let bare = domain.hasPrefix(".") ? String(domain.dropFirst()) : domain
+        guard host == bare || host.hasSuffix("." + bare) else { return false }
+        let path = url.path.isEmpty ? "/" : url.path
+        guard path.hasPrefix(cookie.path) else { return false }
+        return !cookie.isSecure || url.scheme == "https" || isLoopback(host)
+    }
</file context>

if (!loc || typeof loc.count !== "function") throw new S.SiteError("invalid", `browserAuth.request: field ${f.id}: selector: expected a selector string or a locator`);
if ((await loc.count()) !== 1) return { status: "locator_invalid", locator_error: { field_id: f.id, reason: "not_unique" } };
if (!(await loc.isVisible())) return { status: "locator_invalid", locator_error: { field_id: f.id, reason: "not_user_visible" } };
const tag = await loc.evaluate((el) => (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && !el.disabled && !el.readOnly);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This check accepts non-text inputs such as checkbox, radio, submit, and date controls. Restrict HTMLInputElement to the supported text-like types (or require the DOM type to match f.type) before marking the element for credential fill.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/browser-auth.js, line 46:

<comment>This check accepts non-text inputs such as checkbox, radio, submit, and date controls. Restrict `HTMLInputElement` to the supported text-like types (or require the DOM type to match `f.type`) before marking the element for credential fill.</comment>

<file context>
@@ -0,0 +1,92 @@
+            if (!loc || typeof loc.count !== "function") throw new S.SiteError("invalid", `browserAuth.request: field ${f.id}: selector: expected a selector string or a locator`);
+            if ((await loc.count()) !== 1) return { status: "locator_invalid", locator_error: { field_id: f.id, reason: "not_unique" } };
+            if (!(await loc.isVisible())) return { status: "locator_invalid", locator_error: { field_id: f.id, reason: "not_user_visible" } };
+            const tag = await loc.evaluate((el) => (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) && !el.disabled && !el.readOnly);
+            if (!tag) return { status: "locator_invalid", locator_error: { field_id: f.id, reason: "not_editable_text_field" } };
+            // The frame that holds the element (a frameLocator chain ends in a child frame).
</file context>

// Calls a tool. readOnlyHint tools run now; others return a draft that
// call(draftId, { confirm: true }) runs. Options: { page }.
async call(name, input, options = {}) {
if (typeof name === "string" && /^draft-\d+-[0-9a-f]+$/.test(name)) return t.write("webmcp", "call", name, input);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: This makes a valid page tool named like draft-1-abcdef impossible to call, and can dispatch a matching name as an unrelated confirmation. Use an unambiguous confirmation path or verify an existing draft before treating the name as a draft ID.

(Based on your team's feedback about user-representable string sentinels.)

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/webmcp.js, line 61:

<comment>This makes a valid page tool named like `draft-1-abcdef` impossible to call, and can dispatch a matching name as an unrelated confirmation. Use an unambiguous confirmation path or verify an existing draft before treating the name as a draft ID.

(Based on your team's feedback about user-representable string sentinels.) </comment>

<file context>
@@ -0,0 +1,82 @@
+        // Calls a tool. readOnlyHint tools run now; others return a draft that
+        // call(draftId, { confirm: true }) runs. Options: { page }.
+        async call(name, input, options = {}) {
+          if (typeof name === "string" && /^draft-\d+-[0-9a-f]+$/.test(name)) return t.write("webmcp", "call", name, input);
+          const page = options.page || t.currentPage();
+          const { tools } = await list(page);
</file context>

out.push({ type: "code", properties: { title: [[code.join("\n")]], language: [[m[1] || "Plain Text"]] } });
} else if ((m = /^(#{1,3})\s+(.*)$/.exec(line))) out.push({ type: ["header", "sub_header", "sub_sub_header"][m[1].length - 1], properties: { title: richText(m[2]) } });
else if ((m = /^\s*[-*]\s+\[( |x|X)\]\s+(.*)$/.exec(line))) out.push({ type: "to_do", properties: { title: richText(m[2]), checked: [[m[1] === " " ? "No" : "Yes"]] } });
else if ((m = /^\s*[-*]\s+(.*)$/.exec(line))) out.push({ type: "bulleted_list", properties: { title: richText(m[1]) } });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Appending nested Markdown lists flattens every item at the page root because the parser discards indentation and creates no child relationships. Preserve list depth when building content operations, or reject nested list input instead of silently changing its structure.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/notion.js, line 131:

<comment>Appending nested Markdown lists flattens every item at the page root because the parser discards indentation and creates no child relationships. Preserve list depth when building `content` operations, or reject nested list input instead of silently changing its structure.</comment>

<file context>
@@ -0,0 +1,246 @@
+        out.push({ type: "code", properties: { title: [[code.join("\n")]], language: [[m[1] || "Plain Text"]] } });
+      } else if ((m = /^(#{1,3})\s+(.*)$/.exec(line))) out.push({ type: ["header", "sub_header", "sub_sub_header"][m[1].length - 1], properties: { title: richText(m[2]) } });
+      else if ((m = /^\s*[-*]\s+\[( |x|X)\]\s+(.*)$/.exec(line))) out.push({ type: "to_do", properties: { title: richText(m[2]), checked: [[m[1] === " " ? "No" : "Yes"]] } });
+      else if ((m = /^\s*[-*]\s+(.*)$/.exec(line))) out.push({ type: "bulleted_list", properties: { title: richText(m[1]) } });
+      else if ((m = /^\s*\d+[.)]\s+(.*)$/.exec(line))) out.push({ type: "numbered_list", properties: { title: richText(m[1]) } });
+      else if ((m = /^>\s?(.*)$/.exec(line))) out.push({ type: "quote", properties: { title: richText(m[1]) } });
</file context>

@cubic-dev-ai cubic-dev-ai Bot 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.

7 issues found across 13 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Resources/browser-repl/sites/google-drive.js">

<violation number="1" location="Resources/browser-repl/sites/google-drive.js:31">
P2: The closed tooltip taxonomy corrupts titles for Drive file types it does not list, such as shortcuts or other uploaded types. Preserve the row-name/innerText fallback whenever the suffix is unrecognized, rather than returning the full `<name> <type>` tooltip as `title`.</violation>
</file>

<file name="Resources/browser-repl/sites/linkedin.js">

<violation number="1" location="Resources/browser-repl/sites/linkedin.js:89">
P2: The feed readiness check can pass before a parseable keyed post exists. Match the wait selector to `readCards()` so unkeyed or partially-rendered list items do not cause `feed()` to return an empty result.</violation>
</file>

<file name="Resources/browser-repl/sites/slack.js">

<violation number="1" location="Resources/browser-repl/sites/slack.js:56">
P2: Signed-out Slack operations now wait the full 30 seconds before reporting `not_signed_in`. Detect Slack's sign-in page during bootstrap or use a bounded/fail-fast signed-out path instead of swallowing the entire workspace-config wait.</violation>
</file>

<file name="Resources/browser-repl/sites/notion.js">

<violation number="1" location="Resources/browser-repl/sites/notion.js:149">
P2: The cached origin is never re-probed after its session becomes unauthorized, so a persistent REPL session can report `not_signed_in` even when the other supported Notion origin still has the active session. Include the alternate origin whenever the cached origin returns 401, then update `apiOrigin` to the successful origin.</violation>

<violation number="2" location="Resources/browser-repl/sites/notion.js:150">
P2: The origin fallback only survives a 401: a hard failure probing app.notion.com (page.goto timeout or DNS failure) propagates out of calls() and never tries www.notion.so, and acceptance is judged from rs[0] alone, so a later call in the list returning 401 on the first origin won't retry the fallback either. Wrap each origin attempt in try/catch and continue on error, and accept an origin when all calls succeeded.</violation>
</file>

<file name="Resources/browser-repl/sites/google.js">

<violation number="1" location="Resources/browser-repl/sites/google.js:102">
P2: The `\n{3,}` collapse runs on every Markdown export (googleDocs.read defaults to format "md"), not only when data: definitions were actually removed, so documents without images get 3+-blank-line runs rewritten — including inside fenced code blocks, where the newlines are code content, not layout. Gate the normalization on the definition strip having changed the text.</violation>
</file>

<file name="tests/browser-parity/sites/google-workspace.test.mjs">

<violation number="1" location="tests/browser-parity/sites/google-workspace.test.mjs:22">
P3: This deepEqual freezes `googleDocs.read` (md) output that keeps the image reference `![][image1]` while the strip in `google.js` `exportText` removes the `[image1]: <data:...>` definition. The returned Markdown therefore contains a dangling, permanently unresolved image reference. If the intent of dropping inline image data is clean text for agents, the reference lines should be removed along with the definitions (or converted to a plain marker); otherwise an agent consuming this Markdown renders a broken image slot. The new "drop inline image data" test only asserts the absence of `data:image`, so it would pass either way.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const TYPES = ["Google Docs", "Google Sheets", "Google Slides", "Google Forms", "Google Drawings", "Google Sites", "Google Apps Script", "Shared folder", "Folder", "PDF", "Image", "Video", "Audio", "Microsoft Word", "Microsoft Excel", "Microsoft PowerPoint", "Text", "Archive", "Unknown"];
const tip = clean((el.querySelector("[data-tooltip]") || {}).getAttribute ? el.querySelector("[data-tooltip]").getAttribute("data-tooltip") : "");
const type = TYPES.find((x) => tip === x || tip.endsWith(" " + x)) || null;
let title = type ? clean(tip.slice(0, tip.length - type.length)) : tip;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The closed tooltip taxonomy corrupts titles for Drive file types it does not list, such as shortcuts or other uploaded types. Preserve the row-name/innerText fallback whenever the suffix is unrecognized, rather than returning the full <name> <type> tooltip as title.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/google-drive.js, line 31:

<comment>The closed tooltip taxonomy corrupts titles for Drive file types it does not list, such as shortcuts or other uploaded types. Preserve the row-name/innerText fallback whenever the suffix is unrecognized, rather than returning the full `<name> <type>` tooltip as `title`.</comment>

<file context>
@@ -9,6 +9,36 @@
+                const TYPES = ["Google Docs", "Google Sheets", "Google Slides", "Google Forms", "Google Drawings", "Google Sites", "Google Apps Script", "Shared folder", "Folder", "PDF", "Image", "Video", "Audio", "Microsoft Word", "Microsoft Excel", "Microsoft PowerPoint", "Text", "Archive", "Unknown"];
+                const tip = clean((el.querySelector("[data-tooltip]") || {}).getAttribute ? el.querySelector("[data-tooltip]").getAttribute("data-tooltip") : "");
+                const type = TYPES.find((x) => tip === x || tip.endsWith(" " + x)) || null;
+                let title = type ? clean(tip.slice(0, tip.length - type.length)) : tip;
+                if (!title) title = clean((el.innerText || "").split("\n")[0]);
+                out.push({ id, title, type, url: "https://drive.google.com/open?id=" + id });
</file context>
Suggested change
let title = type ? clean(tip.slice(0, tip.length - type.length)) : tip;
let title = type ? clean(tip.slice(0, tip.length - type.length)) : clean((el.querySelector(".name, [data-column-field='6'], strong") || {}).textContent || (el.innerText || "").split("\n")[0]);

async function cards(url, kind, limit) {
return t.withTab(url, async (page) => {
t.assertSignedIn("linkedin", page, SIGN_IN);
await t.waitIn(page, (k) => (k === "feed" ? !!document.querySelector('[data-urn^="urn:li:activity"], [data-id^="urn:li:activity"], main [role="listitem"] [data-testid="expandable-text-box"]') : !!document.querySelector("main a[href*='/in/'], main a[href*='/company/']") || /No results/i.test(document.body.innerText)), kind, { signIn: SIGN_IN, name: "linkedin", what: "LinkedIn results", timeout: 30000 });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The feed readiness check can pass before a parseable keyed post exists. Match the wait selector to readCards() so unkeyed or partially-rendered list items do not cause feed() to return an empty result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/linkedin.js, line 89:

<comment>The feed readiness check can pass before a parseable keyed post exists. Match the wait selector to `readCards()` so unkeyed or partially-rendered list items do not cause `feed()` to return an empty result.</comment>

<file context>
@@ -72,7 +86,7 @@
         return t.withTab(url, async (page) => {
           t.assertSignedIn("linkedin", page, SIGN_IN);
-          await t.waitIn(page, (k) => (k === "feed" ? !!document.querySelector('[data-urn^="urn:li:activity"], [data-id^="urn:li:activity"]') : !!document.querySelector("main a[href*='/in/'], main a[href*='/company/']") || /No results/i.test(document.body.innerText)), kind, { signIn: SIGN_IN, name: "linkedin", what: "LinkedIn results", timeout: 30000 });
+          await t.waitIn(page, (k) => (k === "feed" ? !!document.querySelector('[data-urn^="urn:li:activity"], [data-id^="urn:li:activity"], main [role="listitem"] [data-testid="expandable-text-box"]') : !!document.querySelector("main a[href*='/in/'], main a[href*='/company/']") || /No results/i.test(document.body.innerText)), kind, { signIn: SIGN_IN, name: "linkedin", what: "LinkedIn results", timeout: 30000 });
           let got = await page.evaluate(readCards, { kind, limit });
           for (let i = 0; i < 6 && got.length < limit; i++) {
</file context>
Suggested change
await t.waitIn(page, (k) => (k === "feed" ? !!document.querySelector('[data-urn^="urn:li:activity"], [data-id^="urn:li:activity"], main [role="listitem"] [data-testid="expandable-text-box"]') : !!document.querySelector("main a[href*='/in/'], main a[href*='/company/']") || /No results/i.test(document.body.innerText)), kind, { signIn: SIGN_IN, name: "linkedin", what: "LinkedIn results", timeout: 30000 });
await t.waitIn(page, (k) => (k === "feed" ? !!document.querySelector('[data-urn^="urn:li:activity"], [data-id^="urn:li:activity"], main [role="listitem"][componentkey] [data-testid="expandable-text-box"]') : !!document.querySelector("main a[href*='/in/'], main a[href*='/company/']") || /No results/i.test(document.body.innerText)), kind, { signIn: SIGN_IN, name: "linkedin", what: "LinkedIn results", timeout: 30000 });

t.withTab(APP + "/robots.txt", async (page) => {
if (!(await page.evaluate(hasConfig))) {
await page.goto(APP + "/client", { waitUntil: "load", timeout: 45000 });
await t.waitIn(page, hasConfig, undefined, { timeout: 30000, what: "Slack's web client to load the workspaces", name: "slack" }).catch(() => {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Signed-out Slack operations now wait the full 30 seconds before reporting not_signed_in. Detect Slack's sign-in page during bootstrap or use a bounded/fail-fast signed-out path instead of swallowing the entire workspace-config wait.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/slack.js, line 56:

<comment>Signed-out Slack operations now wait the full 30 seconds before reporting `not_signed_in`. Detect Slack's sign-in page during bootstrap or use a bounded/fail-fast signed-out path instead of swallowing the entire workspace-config wait.</comment>

<file context>
@@ -38,8 +39,26 @@
+        t.withTab(APP + "/robots.txt", async (page) => {
+          if (!(await page.evaluate(hasConfig))) {
+            await page.goto(APP + "/client", { waitUntil: "load", timeout: 45000 });
+            await t.waitIn(page, hasConfig, undefined, { timeout: 30000, what: "Slack's web client to load the workspaces", name: "slack" }).catch(() => {});
+          }
+          return body((fn, arg) => page.evaluate(fn, arg));
</file context>

Comment thread Resources/browser-repl/sites/notion.js Outdated
Comment on lines +149 to +155
for (const o of options.origin ? [options.origin] : apiOrigin ? [apiOrigin] : API_ORIGINS) {
rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId });
if (rs[0] && rs[0].status !== 401) {
if (!options.origin) apiOrigin = o;
break;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The cached origin is never re-probed after its session becomes unauthorized, so a persistent REPL session can report not_signed_in even when the other supported Notion origin still has the active session. Include the alternate origin whenever the cached origin returns 401, then update apiOrigin to the successful origin.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/notion.js, line 149:

<comment>The cached origin is never re-probed after its session becomes unauthorized, so a persistent REPL session can report `not_signed_in` even when the other supported Notion origin still has the active session. Include the alternate origin whenever the cached origin returns 401, then update `apiOrigin` to the successful origin.</comment>

<file context>
@@ -141,8 +141,18 @@
       async function calls(list, options = {}) {
-        const rs = await t.inOrigin(origin(options), notionCall, { calls: list, userId: options.userId });
+        let rs;
+        for (const o of options.origin ? [options.origin] : apiOrigin ? [apiOrigin] : API_ORIGINS) {
+          rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId });
+          if (rs[0] && rs[0].status !== 401) {
</file context>
Suggested change
for (const o of options.origin ? [options.origin] : apiOrigin ? [apiOrigin] : API_ORIGINS) {
rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId });
if (rs[0] && rs[0].status !== 401) {
if (!options.origin) apiOrigin = o;
break;
}
}
const origins = options.origin ? [options.origin] : apiOrigin ? [apiOrigin, ...API_ORIGINS.filter((o) => o !== apiOrigin)] : API_ORIGINS;
for (const o of origins) {
rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId });
if (rs[0] && rs[0].status !== 401) {
if (!options.origin) apiOrigin = o;
break;
}
}

async function calls(list, options = {}) {
let rs;
for (const o of options.origin ? [options.origin] : apiOrigin ? [apiOrigin] : API_ORIGINS) {
rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The origin fallback only survives a 401: a hard failure probing app.notion.com (page.goto timeout or DNS failure) propagates out of calls() and never tries www.notion.so, and acceptance is judged from rs[0] alone, so a later call in the list returning 401 on the first origin won't retry the fallback either. Wrap each origin attempt in try/catch and continue on error, and accept an origin when all calls succeeded.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/notion.js, line 150:

<comment>The origin fallback only survives a 401: a hard failure probing app.notion.com (page.goto timeout or DNS failure) propagates out of calls() and never tries www.notion.so, and acceptance is judged from rs[0] alone, so a later call in the list returning 401 on the first origin won't retry the fallback either. Wrap each origin attempt in try/catch and continue on error, and accept an origin when all calls succeeded.</comment>

<file context>
@@ -141,8 +141,18 @@
-        const rs = await t.inOrigin(origin(options), notionCall, { calls: list, userId: options.userId });
+        let rs;
+        for (const o of options.origin ? [options.origin] : apiOrigin ? [apiOrigin] : API_ORIGINS) {
+          rs = await t.inOrigin(o, notionCall, { calls: list, userId: options.userId });
+          if (rs[0] && rs[0].status !== 401) {
+            if (!options.origin) apiOrigin = o;
</file context>

let text = await response.text();
// Google's Markdown export embeds each image as a data: reference
// definition (most of a document with images); keep the references.
if (format === "md") text = text.replace(/^[ \t]*\[[^\]]+\]:[ \t]*<data:[^>]*>[ \t]*\n?/gm, "").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: The \n{3,} collapse runs on every Markdown export (googleDocs.read defaults to format "md"), not only when data: definitions were actually removed, so documents without images get 3+-blank-line runs rewritten — including inside fenced code blocks, where the newlines are code content, not layout. Gate the normalization on the definition strip having changed the text.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/google.js, line 102:

<comment>The `\n{3,}` collapse runs on every Markdown export (googleDocs.read defaults to format "md"), not only when data: definitions were actually removed, so documents without images get 3+-blank-line runs rewritten — including inside fenced code blocks, where the newlines are code content, not layout. Gate the normalization on the definition strip having changed the text.</comment>

<file context>
@@ -96,7 +96,11 @@
+    let text = await response.text();
+    // Google's Markdown export embeds each image as a data: reference
+    // definition (most of a document with images); keep the references.
+    if (format === "md") text = text.replace(/^[ \t]*\[[^\]]+\]:[ \t]*<data:[^>]*>[ \t]*\n?/gm, "").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
+    return { title, text };
   }
</file context>
Suggested change
if (format === "md") text = text.replace(/^[ \t]*\[[^\]]+\]:[ \t]*<data:[^>]*>[ \t]*\n?/gm, "").replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
if (format === "md") {
const stripped = text.replace(/^[ \t]*\[[^\]]+\]:[ \t]*<data:[^>]*>[ \t]*\n?/gm, "");
if (stripped !== text) text = stripped.replace(/\n{3,}/g, "\n\n").replace(/\n+$/, "\n");
}


test("googleDocs.read: Markdown by default, title from the export's file name", async () => {
const r = await s.value('sites.googleDocs.read("https://docs.google.com/document/d/DOC1/edit")');
assert.deepEqual(r, { title: "Design Notes", text: "# Design Notes\n\nThe **plan**, in brief.\n\n![][image1]\n" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This deepEqual freezes googleDocs.read (md) output that keeps the image reference ![][image1] while the strip in google.js exportText removes the [image1]: <data:...> definition. The returned Markdown therefore contains a dangling, permanently unresolved image reference. If the intent of dropping inline image data is clean text for agents, the reference lines should be removed along with the definitions (or converted to a plain marker); otherwise an agent consuming this Markdown renders a broken image slot. The new "drop inline image data" test only asserts the absence of data:image, so it would pass either way.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/browser-parity/sites/google-workspace.test.mjs, line 22:

<comment>This deepEqual freezes `googleDocs.read` (md) output that keeps the image reference `![][image1]` while the strip in `google.js` `exportText` removes the `[image1]: <data:...>` definition. The returned Markdown therefore contains a dangling, permanently unresolved image reference. If the intent of dropping inline image data is clean text for agents, the reference lines should be removed along with the definitions (or converted to a plain marker); otherwise an agent consuming this Markdown renders a broken image slot. The new "drop inline image data" test only asserts the absence of `data:image`, so it would pass either way.</comment>

<file context>
@@ -19,10 +19,16 @@ test("googleAccounts.list: signed-in accounts with their uid, from ListAccounts"
 test("googleDocs.read: Markdown by default, title from the export's file name", async () => {
   const r = await s.value('sites.googleDocs.read("https://docs.google.com/document/d/DOC1/edit")');
-  assert.deepEqual(r, { title: "Design Notes", text: "# Design Notes\n\nThe **plan**, in brief.\n" });
+  assert.deepEqual(r, { title: "Design Notes", text: "# Design Notes\n\nThe **plan**, in brief.\n\n![][image1]\n" });
   assert.equal((await s.value('sites.googleDocs.read("https://docs.google.com/document/d/DOC1/edit", { format: "txt" })')).text, "Design Notes\n\nThe plan, in brief.\n");
 });
</file context>

@cursor

cursor Bot commented Oct 1, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@cubic-dev-ai cubic-dev-ai Bot 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.

29 issues found across 27 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Resources/browser-repl/sites/google-sheets.js">

<violation number="1" location="Resources/browser-repl/sites/google-sheets.js:29">
P2: Empty rows pass validation and are reported as successfully written with an invalid range. Reject rows with zero cells before calculating `width`.</violation>

<violation number="2" location="Resources/browser-repl/sites/google-sheets.js:35">
P2: `write` accepts row zero even though it is not valid A1 notation. Reject non-positive row numbers before selecting the range.</violation>

<violation number="3" location="Resources/browser-repl/sites/google-sheets.js:120">
P3: An unknown `options.gid` silently falls back to the first tab (`book[0]`) and returns cells, while an unknown `options.sheet` throws `not_found`. An agent that asks `cells(sheet, { gid: "7" })` on a file without that tab gets the first sheet's data with no error. Throw `not_found` here, matching the `options.sheet` branch.</violation>

<violation number="4" location="Resources/browser-repl/sites/google-sheets.js:153">
P2: On a multi-tab sheet `append` measures the last non-empty row on the tab selected by `options.sheet`/`options.gid`, but the paste itself lands in the URL-gid tab: `selectRange` only sets the name box within the active sheet and never switches tabs. The verification then re-reads the `options`-selected tab via `api.cells`, so the tool writes one tab and verifies another (reporting `verified: false`, or worse confirming a coincidental match). The same mismatch exists for `write`/`clear` when `options.sheet` differs from the URL's `#gid`.</violation>
</file>

<file name="tests/browser-parity/scenarios/33-markdown-corpus.js">

<violation number="1" location="tests/browser-parity/scenarios/33-markdown-corpus.js:30">
P2: The `squash(want.name) !== own` gate silently skips every link whose rendered text differs from Chrome's accessible name — links with aria-label, icon links (innerText empty), and alt-named links — so those links are never checked against `md` and a regression that drops their label leaves `missing-links` empty. The header claims "every link Chrome lists that this engine shows keeps its text," but the check only covers links whose name equals innerText. Drop the name-equality gate and verify the rendered text of every link that has some; for textless icon links additionally check `flat.includes(squash(want.name))`.</violation>

<violation number="2" location="tests/browser-parity/scenarios/33-markdown-corpus.js:36">
P3: `noMarkup` only rejects `div`, `span`, `script`, and the `\u0000` frame marker, while the header promises "no HTML or frame placeholder is left outside code". A leak of any other tag — `<a href=…>`, `<main>`, `<nav>`, `<article>`, `<img>`, `<b>/<i>`, or raw `<iframe>` text — passes the golden check and would not be caught by this scenario. Broaden the tag set (e.g., `a|iframe|main|nav|article|b|i|em|strong|h1|h2|p|ul|ol`) or the leak test to a generic `/</?[a-z][\w-]*\b/` on the code-stripped text, or narrow the header claim to what the assertion actually enforces.</violation>
</file>

<file name="tests/browser-parity/unit/agent-tools.test.mjs">

<violation number="1" location="tests/browser-parity/unit/agent-tools.test.mjs:111">
P3: `withRepl` creates `dir` and `sessionTmp` outside the `try`, so if `createDevBrowser()`, `createNodeHost()`, or `createBrowserRepl()` throws (e.g., WebKit missing on the machine), the `finally` never runs and both temp directories leak on every failed run. Move cleanup ownership over the setup by creating `dir`/`sessionTmp` inside the `try` (with the cleanup in `finally`) or by guarding the setup with its own try/finally.</violation>

<violation number="2" location="tests/browser-parity/unit/agent-tools.test.mjs:361">
P3: This test is meant to guarantee every proof resolves (comment: "every proof names a scenario key or a unit test that exists"), but a `sites/*.test.mjs` proof only matches the placeholder regex and is never checked against the filesystem, so a doc row naming a non-existent sites test passes. Resolve it the same way as the other branches: read the `sites` directory and assert the named test files exist.</violation>
</file>

<file name="Resources/browser-repl/sites/google-editors.js">

<violation number="1" location="Resources/browser-repl/sites/google-editors.js:96">
P2: `parseDeck` only extracts direct text shapes, so grouped-shape and table text disappears from slide reads and replacement verification. Traverse all text-bearing slide descendants, including `p:grpSp` and table `p:graphicFrame` elements.</violation>

<violation number="2" location="Resources/browser-repl/sites/google-editors.js:122">
P2: `exportParts` reports an unauthenticated redirect as an unexpected non-ZIP export. Preserve the final response URL or detect the sign-in page and raise `not_signed_in`, so callers receive the documented recovery guidance.</violation>
</file>

<file name="Resources/browser-repl/runtime-core.js">

<violation number="1">
P2: This hook feeds redacted driver data back into runtime state, not only into user-visible output. When a registered secret appears in a URL or redirect, `page.url()` becomes the masked, nonexistent URL, so later URL-dependent operations target the wrong address; preserve raw payloads for internal bookkeeping and redact only copies crossing the REPL/output boundary.</violation>
</file>

<file name="Resources/browser-repl/repl-host.js">

<violation number="1" location="Resources/browser-repl/repl-host.js:310">
P2: The catch leaves a frozen or non-writable `Error` with its original message, so callers formatting returned `r.exception` can print a registered secret despite `r.error` being masked. Replace the exception with a redacted `Error` when mutation fails, or otherwise guarantee that the returned exception is safe to format.</violation>
</file>

<file name="Resources/browser-repl/sites/google-slides.js">

<violation number="1" location="Resources/browser-repl/sites/google-slides.js:28">
P2: `replace` counts speaker-note matches but the Find and replace action does not edit speaker notes, so it cannot fulfill its “every occurrence in the deck” contract. Either support note editing or exclude notes from the count and document the scope.</violation>

<violation number="2" location="Resources/browser-repl/sites/google-slides.js:35">
P2: This branch marks the edit verified without checking the deck whenever the replacement contains the search text. Verify the transformed slide content instead, so failed or partial Replace all operations cannot be reported as successful.</violation>
</file>

<file name="Resources/browser-repl/sites/google-drive.js">

<violation number="1" location="Resources/browser-repl/sites/google-drive.js:14">
P1: `trashNow` drops the account selector from parsed file URLs. Append `?authuser=${ref.uid}` when `ref.uid` is present, otherwise confirmed trash requests for non-default accounts open account 0.</violation>

<violation number="2" location="Resources/browser-repl/sites/google-drive.js:94">
P1: `create` loses the selected Google account in its returned URL. Include the account query so callers can read, edit, or trash a file created with `{ uid: 1 }` without falling back to account 0.</violation>
</file>

<file name="Resources/browser-repl/sites/google-docs.js">

<violation number="1" location="Resources/browser-repl/sites/google-docs.js:18">
P2: `count` is case-sensitive even though these operations use Google Docs' case-insensitive Find and Replace. Mixed-case matches are reported with the wrong count, and `insertAfter` can pass its uniqueness check before replacing multiple matches; count matches using the same case-insensitive semantics as the editor.</violation>
</file>

<file name="Resources/browser-repl/api.js">

<violation number="1" location="Resources/browser-repl/api.js:569">
P1: The fetch domain policy checks only the initial URL, so redirects bypass `allowedDomains`, `prohibitedDomains`, and IP blocking. Enforce the policy on every redirect hop in the fetch transport and on the final response URL before exposing it.</violation>

<violation number="2" location="Resources/browser-repl/api.js:592">
P2: Redacting serialized JSON before parsing can corrupt JSON when a registered secret contains JSON punctuation. Parse the response first and recursively redact returned values without replacing structural characters.</violation>
</file>

<file name="docs/browser-repl/site-tools.md">

<violation number="1">
P3: The new write rule ("Private to only me" files run at once) contradicts the Confirmation taxonomy paragraph, which still states "Every cmux write is in 'always confirm'" and "cmux has no delete, share, permission, purchase or account-creation tool". With this change, a write to a private Google file runs without any draft/confirmation, and googleDrive.trash is a delete tool. Amend the taxonomy paragraph to record the private-file exception and the trash tool so the two sections agree.</violation>

<violation number="2">
P2: This new section documents implemented editing tools, but the inventory table and Decision 6 elsewhere in the same file still say the opposite: Google Docs "Edits: not implemented (decision 6)", Google Sheets "Writes: decision 6", and Decision 6 describes the now-implemented feature in future tense ("A cmux version would be a draft of the diff, confirmed, applied with real input in the document tab"). Update those rows and remove or strike Decision 6 so the doc does not tell readers the tools do not exist.</violation>
</file>

<file name="Resources/browser-repl/agent-tools.js">

<violation number="1" location="Resources/browser-repl/agent-tools.js:94">
P1: `blockIPAddresses(true)` can be bypassed with integer, hexadecimal, or octal IPv4 spellings because `isIPHost` does not recognize them. Canonicalize the parsed host with a complete IPv4/IPv6 parser before applying the block.</violation>

<violation number="2" location="Resources/browser-repl/agent-tools.js:288">
P3: This branch is unreachable: `"DIALOG"` is in the `SKIP` set, and the preceding `if (SKIP.has(tag(el))) return true;` returns before `el.open` is ever consulted. Open `<dialog>` content (modals) is therefore always omitted from `page.markdown()` even though the code clearly intends to include it (closed dialogs are already hidden by their default `display: none`). Swap the two checks so an open dialog is exempted from `SKIP`.</violation>

<violation number="3" location="Resources/browser-repl/agent-tools.js:931">
P1: `session.record()` writes raw screenshots, so a visible secret-filled field is persisted visually in PNG and `run.png`. Mask or omit secret-bearing frames before writing recording files.</violation>

<violation number="4" location="Resources/browser-repl/agent-tools.js:1044">
P2: `session.storageState()` reads cookies and localStorage through `session.call`, which the new `afterCall` hook runs through `redactValue`. When a registered secret value appears inside a stored cookie or localStorage item, the export writes `<secret:name>` in place of the real value, and `session.setStorageState()` (`cookies.set` / `localStorage.setItem`) then stores that literal masked string back — silently corrupting the auth state the round-trip is supposed to preserve. The same redaction feeds `api.js` `fetchWithCookies`: its dev-driver path (`host.fetchHandlesCookies` unset) builds the Cookie header from the redacted `cookies.get` result, so requests carry `<secret:name>` instead of the cookie. Masking belongs on output (prints, reads, files), not on data that is later reused as input; exclude `cookies.get`/localStorage reads used for storage state from masking, or keep a masked↔original mapping so `setStorageState` restores the true values.</violation>

<violation number="5" location="Resources/browser-repl/agent-tools.js:1057">
P1: `storageState({ path })` writes registered secret values unchanged when they appear in cookies or localStorage. Reject secret-bearing persistence or write a redacted artifact so storage-state files cannot disclose credentials.</violation>

<violation number="6" location="Resources/browser-repl/agent-tools.js:1364">
P2: `page.scroll` relies on fixed sleeps and polling, so it can return before smooth scrolling settles and adds an unnecessary one-second cap. Await a scroll-settled signal or use a cancellation-aware wait tied to the actual scroll state.</violation>
</file>

<file name="tests/browser-parity/sites/live-diff.mjs">

<violation number="1" location="tests/browser-parity/sites/live-diff.mjs:250">
P3: The new `logged in` and `cookie found` substrings also match errors that assert the user IS signed in (e.g. "already logged in", "cookie found for the wrong origin"), which get reported as "user not signed in" and hide real regressions in the parity doc. Restrict the additions to sign-out phrasing ("not logged in", "logged out", "no cookie found") or add a negation qualifier.</violation>
</file>

<file name="tests/browser-parity/sites/mock-editors.mjs">

<violation number="1" location="tests/browser-parity/sites/mock-editors.mjs:161">
P2: Docs Find and replace skips table cells. Include each `b.rows` cell in the replacement and count so replacements in tables are reflected in subsequent exports.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

const g = S.shared.google;
const created = new Set();
async function trashNow(ref) {
return t.withTab(`https://docs.google.com/${ref.kind}/d/${ref.id}/edit`, async (page) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: trashNow drops the account selector from parsed file URLs. Append ?authuser=${ref.uid} when ref.uid is present, otherwise confirmed trash requests for non-default accounts open account 0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/google-drive.js, line 14:

<comment>`trashNow` drops the account selector from parsed file URLs. Append `?authuser=${ref.uid}` when `ref.uid` is present, otherwise confirmed trash requests for non-default accounts open account 0.</comment>

<file context>
@@ -9,6 +9,17 @@
       const g = S.shared.google;
+      const created = new Set();
+      async function trashNow(ref) {
+        return t.withTab(`https://docs.google.com/${ref.kind}/d/${ref.id}/edit`, async (page) => {
+          await t.waitIn(page, () => !!document.querySelector("#docs-file-menu"), undefined, { signIn: [/^https:\/\/accounts\.google\.com\//], name: "googleDrive.trash", what: "the editor's File menu", timeout: 45000 });
+          await page.locator("#docs-file-menu").click();
</file context>
Suggested change
return t.withTab(`https://docs.google.com/${ref.kind}/d/${ref.id}/edit`, async (page) => {
return t.withTab(`https://docs.google.com/${ref.kind}/d/${ref.id}/edit${ref.uid !== undefined ? `?authuser=${ref.uid}` : ""}`, async (page) => {

await input.press("Enter");
await t.sleep(800);
created.add(id);
return { id, url: `https://docs.google.com/${kind}/d/${id}/edit`, title };

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: create loses the selected Google account in its returned URL. Include the account query so callers can read, edit, or trash a file created with { uid: 1 } without falling back to account 0.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/sites/google-drive.js, line 94:

<comment>`create` loses the selected Google account in its returned URL. Include the account query so callers can read, edit, or trash a file created with `{ uid: 1 }` without falling back to account 0.</comment>

<file context>
@@ -65,6 +76,38 @@
+            await input.press("Enter");
+            await t.sleep(800);
+            created.add(id);
+            return { id, url: `https://docs.google.com/${kind}/d/${id}/edit`, title };
+          });
+        },
</file context>
Suggested change
return { id, url: `https://docs.google.com/${kind}/d/${id}/edit`, title };
return { id, url: `https://docs.google.com/${kind}/d/${id}/edit${q}`, title };

const page = state.current && !state.current.isClosed() ? state.current : null;
const base = page && /^https?:/.test(page.url()) ? page.url() : undefined;
const url = new core.URL(String(input && input.url ? input.url : input), base).href;
if (session.agentTools) session.agentTools.checkURL("fetch", url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: The fetch domain policy checks only the initial URL, so redirects bypass allowedDomains, prohibitedDomains, and IP blocking. Enforce the policy on every redirect hop in the fetch transport and on the final response URL before exposing it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/api.js, line 569:

<comment>The fetch domain policy checks only the initial URL, so redirects bypass `allowedDomains`, `prohibitedDomains`, and IP blocking. Enforce the policy on every redirect hop in the fetch transport and on the final response URL before exposing it.</comment>

<file context>
@@ -560,11 +560,13 @@
       const page = state.current && !state.current.isClosed() ? state.current : null;
       const base = page && /^https?:/.test(page.url()) ? page.url() : undefined;
       const url = new core.URL(String(input && input.url ? input.url : input), base).href;
+      if (session.agentTools) session.agentTools.checkURL("fetch", url);
       const headers = {};
       const src = init.headers || {};
</file context>

}
}
const state = { cookies, origins: [...origins].map(([origin, localStorage]) => ({ origin, localStorage })) };
if (options.path) fs.writeFileSync(options.path, JSON.stringify(state, null, 2));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: storageState({ path }) writes registered secret values unchanged when they appear in cookies or localStorage. Reject secret-bearing persistence or write a redacted artifact so storage-state files cannot disclose credentials.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/agent-tools.js, line 1057:

<comment>`storageState({ path })` writes registered secret values unchanged when they appear in cookies or localStorage. Reject secret-bearing persistence or write a redacted artifact so storage-state files cannot disclose credentials.</comment>

<file context>
@@ -0,0 +1,1433 @@
+        }
+      }
+      const state = { cookies, origins: [...origins].map(([origin, localStorage]) => ({ origin, localStorage })) };
+      if (options.path) fs.writeFileSync(options.path, JSON.stringify(state, null, 2));
+      return state;
+    }
</file context>

try {
const r = await session.call("tab.screenshot", { targetId: page._targetId, format: "png" });
const file = path.join(recorder.dir, `frame-${String(++recorder.frames).padStart(4, "0")}.png`);
fs.writeFileSync(file, Buffer.from(r.base64, "base64"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: session.record() writes raw screenshots, so a visible secret-filled field is persisted visually in PNG and run.png. Mask or omit secret-bearing frames before writing recording files.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/agent-tools.js, line 931:

<comment>`session.record()` writes raw screenshots, so a visible secret-filled field is persisted visually in PNG and `run.png`. Mask or omit secret-bearing frames before writing recording files.</comment>

<file context>
@@ -0,0 +1,1433 @@
+      try {
+        const r = await session.call("tab.screenshot", { targetId: page._targetId, format: "png" });
+        const file = path.join(recorder.dir, `frame-${String(++recorder.frames).padStart(4, "0")}.png`);
+        fs.writeFileSync(file, Buffer.from(r.base64, "base64"));
+        recorder.frameFiles.push(file);
+        return file;
</file context>

for (const [, t] of unit[2].matchAll(/`([^`]+)`/g)) {
assert.equal(all.filter((x) => x.startsWith(t.replace(/…$/, ""))).length, 1, `unit/${unit[1]}: "${t}" names no single test`);
}
} else assert.ok(/^sites\/\*\.test\.mjs$/.test(part), `unknown proof "${part}" in: ${row}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This test is meant to guarantee every proof resolves (comment: "every proof names a scenario key or a unit test that exists"), but a sites/*.test.mjs proof only matches the placeholder regex and is never checked against the filesystem, so a doc row naming a non-existent sites test passes. Resolve it the same way as the other branches: read the sites directory and assert the named test files exist.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/browser-parity/unit/agent-tools.test.mjs, line 361:

<comment>This test is meant to guarantee every proof resolves (comment: "every proof names a scenario key or a unit test that exists"), but a `sites/*.test.mjs` proof only matches the placeholder regex and is never checked against the filesystem, so a doc row naming a non-existent sites test passes. Resolve it the same way as the other branches: read the `sites` directory and assert the named test files exist.</comment>

<file context>
@@ -0,0 +1,364 @@
+        for (const [, t] of unit[2].matchAll(/`([^`]+)`/g)) {
+          assert.equal(all.filter((x) => x.startsWith(t.replace(/…$/, ""))).length, 1, `unit/${unit[1]}: "${t}" names no single test`);
+        }
+      } else assert.ok(/^sites\/\*\.test\.mjs$/.test(part), `unknown proof "${part}" in: ${row}`);
+    }
+  }
</file context>


// One REPL session on the dev driver, with the app's output cap.
async function withRepl(fn, { maxOutput } = {}) {
const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "cmux-repl-bu-")));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: withRepl creates dir and sessionTmp outside the try, so if createDevBrowser(), createNodeHost(), or createBrowserRepl() throws (e.g., WebKit missing on the machine), the finally never runs and both temp directories leak on every failed run. Move cleanup ownership over the setup by creating dir/sessionTmp inside the try (with the cleanup in finally) or by guarding the setup with its own try/finally.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/browser-parity/unit/agent-tools.test.mjs, line 111:

<comment>`withRepl` creates `dir` and `sessionTmp` outside the `try`, so if `createDevBrowser()`, `createNodeHost()`, or `createBrowserRepl()` throws (e.g., WebKit missing on the machine), the `finally` never runs and both temp directories leak on every failed run. Move cleanup ownership over the setup by creating `dir`/`sessionTmp` inside the `try` (with the cleanup in `finally`) or by guarding the setup with its own try/finally.</comment>

<file context>
@@ -0,0 +1,364 @@
+
+// One REPL session on the dev driver, with the app's output cap.
+async function withRepl(fn, { maxOutput } = {}) {
+  const dir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "cmux-repl-bu-")));
+  const sessionId = `bu-${process.pid}-${Math.random().toString(36).slice(2, 8)}`;
+  const browser = await createDevBrowser();
</file context>

@@ -0,0 +1,333 @@
# Site tools (`sites`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The new write rule ("Private to only me" files run at once) contradicts the Confirmation taxonomy paragraph, which still states "Every cmux write is in 'always confirm'" and "cmux has no delete, share, permission, purchase or account-creation tool". With this change, a write to a private Google file runs without any draft/confirmation, and googleDrive.trash is a delete tool. Amend the taxonomy paragraph to record the private-file exception and the trash tool so the two sections agree.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At docs/browser-repl/site-tools.md, line 165:

<comment>The new write rule ("Private to only me" files run at once) contradicts the Confirmation taxonomy paragraph, which still states "Every cmux write is in 'always confirm'" and "cmux has no delete, share, permission, purchase or account-creation tool". With this change, a write to a private Google file runs without any draft/confirmation, and googleDrive.trash is a delete tool. Amend the taxonomy paragraph to record the private-file exception and the trash tool so the two sections agree.</comment>

<file context>
@@ -131,6 +131,45 @@ error is a `SiteError` with a `code`: `invalid`, `not_signed_in`,
+| `googleDrive.trash(url)` | the editor's File > Move to trash | delete |
+
+Rule for writes (ChatGPT's confirmation taxonomy, [9] edits others can see):
+a write first opens the file's editor and reads its Share button. If it
+says "Private to only me", nobody else sees the edit and it runs at once.
+Otherwise, including when the sharing cannot be read, the write returns a
</file context>


// Verdict from two summaries.
function verdict(op, c, a, cap) {
const unavailable = (r) => r.code === "not_signed_in" || /not signed in|sign in|invalid_auth|not_authed|unauthorized|login|logged in|cookie found|cookies missing|session may be expired/i.test(r.error || "");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: The new logged in and cookie found substrings also match errors that assert the user IS signed in (e.g. "already logged in", "cookie found for the wrong origin"), which get reported as "user not signed in" and hide real regressions in the parity doc. Restrict the additions to sign-out phrasing ("not logged in", "logged out", "no cookie found") or add a negation qualifier.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At tests/browser-parity/sites/live-diff.mjs, line 250:

<comment>The new `logged in` and `cookie found` substrings also match errors that assert the user IS signed in (e.g. "already logged in", "cookie found for the wrong origin"), which get reported as "user not signed in" and hide real regressions in the parity doc. Restrict the additions to sign-out phrasing ("not logged in", "logged out", "no cookie found") or add a negation qualifier.</comment>

<file context>
@@ -247,7 +247,7 @@ async function signedIn() {
 // Verdict from two summaries.
 function verdict(op, c, a, cap) {
-  const unavailable = (r) => r.code === "not_signed_in" || /not signed in|sign in|invalid_auth|not_authed|unauthorized|login/i.test(r.error || "");
+  const unavailable = (r) => r.code === "not_signed_in" || /not signed in|sign in|invalid_auth|not_authed|unauthorized|login|logged in|cookie found|cookies missing|session may be expired/i.test(r.error || "");
   if (!c.ok && unavailable(c)) return { verdict: "cmux-unavailable", note: "user not signed in to cmux" };
   if (!a.ok && a.code === "no_tool") return c.ok ? { verdict: "cmux-better", note: "Aside has no tool for this read" } : { verdict: "cmux-worse", note: `cmux failed: ${c.code || ""} ${redact(c.error)}` };
</file context>
Suggested change
const unavailable = (r) => r.code === "not_signed_in" || /not signed in|sign in|invalid_auth|not_authed|unauthorized|login|logged in|cookie found|cookies missing|session may be expired/i.test(r.error || "");
const unavailable = (r) => r.code === "not_signed_in" || /not signed in|not logged in|logged out|sign in|login|invalid_auth|not_authed|unauthorized|cookies missing|no cookie found|session (may be )?expired/i.test(r.error || "");

Comment on lines +288 to +289
if (tag(el) === "DIALOG" && el.open) return false;
const cs = style(el);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: This branch is unreachable: "DIALOG" is in the SKIP set, and the preceding if (SKIP.has(tag(el))) return true; returns before el.open is ever consulted. Open <dialog> content (modals) is therefore always omitted from page.markdown() even though the code clearly intends to include it (closed dialogs are already hidden by their default display: none). Swap the two checks so an open dialog is exempted from SKIP.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At Resources/browser-repl/agent-tools.js, line 288:

<comment>This branch is unreachable: `"DIALOG"` is in the `SKIP` set, and the preceding `if (SKIP.has(tag(el))) return true;` returns before `el.open` is ever consulted. Open `<dialog>` content (modals) is therefore always omitted from `page.markdown()` even though the code clearly intends to include it (closed dialogs are already hidden by their default `display: none`). Swap the two checks so an open dialog is exempted from `SKIP`.</comment>

<file context>
@@ -0,0 +1,1433 @@
+    };
+    const skipped = (el, ctx) => {
+      if (SKIP.has(tag(el))) return true;
+      if (tag(el) === "DIALOG" && el.open) return false;
+      const cs = style(el);
+      if (cs.display === "none" || cs.contentVisibility === "hidden") return true;
</file context>
Suggested change
if (tag(el) === "DIALOG" && el.open) return false;
const cs = style(el);
if (tag(el) === "DIALOG" && el.open) return false;
if (SKIP.has(tag(el))) return true;

@cursor

cursor Bot commented Oct 2, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

lawrencecchen and others added 16 commits October 1, 2026 19:04
A grid cell that drops text arriving without a keydown or a composition
(Google Sheets' editor in WebKit) and reads pastes from clipboardData.
Red: the app inserts text without a composition and pastes by inserting
text, so no paste event fires.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tools eval, snapshot, screenshot, tabs and reset run in one REPL session
(--session, mcp by default) over browser.repl.eval and browser.repl.reset.
The JSON-RPC handling is a socket-free type; a node test drives the built
CLI against a fake control socket when PARITY_CMUX_CLI is set.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A default save carried every cookie in the user's profile. It now covers
the registrable domain of the current tab (page.context().storageState()
of that page); { all: true } keeps the whole-profile save and { urls }
is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d paste

Google Sheets' cell editor in WebKit starts an edit only on a keydown or a
composition: insertText reached the DOM but not the sheet, and Meta+V
inserted text with no paste event. insertText now sets marked text and
confirms it (compositionstart, beforeinput/input, compositionend, all
trusted). Copy, Cut and Paste run WebKit's own editing commands with the
general pasteboard lookup redirected to a private pasteboard holding the
tab's clipboard, so pages get trusted clipboard events with
clipboardData and the system clipboard stays untouched.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
session.configure({ userAgent, extraHTTPHeaders, permissions, proxy })
applies Playwright browser-context options to the tabs a session drives:
the user agent overrides the destination policy, headers go on main-frame
GET navigations through the user-agent restart path, camera, microphone,
geolocation and notification requests answer from the granted list, and a
proxy puts tabs opened afterwards in a private data store routed through
it. The domain policy also compiles to a WebKit content rule list, so
images, scripts, XHR, WebSockets and iframes from other domains are
blocked too. browser-use-parity proofs may name diff cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Each capture first sets -webkit-text-security: disc on text fields whose
value holds a secret and on elements whose own text holds one, in every
frame and shadow root, and restores them after.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…te is dropped

The mock Sheets editor now reads a paste from clipboardData, as Sheets does
in WebKit, and records whether each edit was pasted or typed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With Meta+V now a trusted paste carrying the tab's clipboard, one paste of
the rows as TSV writes the whole range. When the export does not show the
values, the cells are typed as before. The dev driver dispatches a paste
event with clipboardData, as the app's paste does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…refixed requests

A navigation the driver starts carries session.configure's headers from
the first request, so no policy restart races the navigation's wait.
WebKit sanitizes pasted HTML, so the trusted-paste case checks the bold
text rather than the markup.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…one input event

A composition into a text field fired three input events where Playwright's
fill fires one (04-actions-diff trusted-log on the app). Fields get a plain
insert again; contenteditable editors such as Sheets' cell editor get the
composition.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
All 158 cases recorded on the tagged app at 0a2c79e, including the new
Sheets-style editor, trusted paste and session.configure cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Moves the driver's ObjectIdentifier-based data store id into the package
unchanged (WKWebsiteDataStore.browserReplID) so the test can reach it.
dataStoreID came from the store's ObjectIdentifier, whose address a later
store takes once the first is freed, so tabs.open({ dataStore }) with an
old id could open a tab in another store. Each store now carries a UUID as
an associated object.
insertText recorded the secret for other sessions' masks before the
check, so a value refused for its domain still became a mask other
sessions saw. The record now happens in checkTarget after the check, on
the same main-actor turn as the commit, so it still precedes typing.
- A window a user's tab opens while it handles the session's own input
  becomes a background tab for that session (tab.created, userOwned) under
  its domain policy, instead of a key popup window over the user's work; it
  stays the user's and is never closed with the session.
- frame.evaluate counts as the session's action (el.click() that opens
  confirm() no longer shows cmux's dialog), as does a wake or a crash
  recovery; a navigation's input window ends when it commits, so a dialog
  from the user's own click while the page loads stays the user's.
- The wake's commit wait ends on an invalidated web view instead of spinning
  on the main actor, and a cancelled call stops waiting.
- cmux browser press runs Command+A/C/X/V/Z on the web view itself, since
  the resend of its keys to the Edit menu is dropped.
- tab.handleEvents, tabs.activate and tab.bringToFront no longer wake a tab;
  the domain policy judges a hibernated tab's recorded URL before a wake
  loads it; tab.reload of a crashed or hibernated tab keeps its timeout and
  waitUntil.
- The dev driver mirrors frame.evaluate and the commit-bounded navigation
  window.
@lawrencecchen lawrencecchen changed the title Add cmux browser repl: one Playwright-based agent browser API Add cmux browser repl: a Playwright-shaped browser REPL for agents Oct 4, 2026
@cursor

cursor Bot commented Oct 4, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

Since dcd478a the runtime names the page's tab on every cookie call, so
the guard on tabs showing a blocked page also refused cookies.get and
cookies.set: with allowedDomains(www.parent.test) the agent could not set a
cookie for the allowed host from a page that showed another site
(38-cookie-guards set-allowed-host, red on cmux-dev and the app). get and set
use the tab only to pick its data store and are judged by URL and cookie
domain; cookies.clear takes its scope from the tab's site and stays guarded.
…ry directory

Red: the dev host has no private temporary directory, and the runtime nests
cmux-browser-repl/<session> inside the session's own (18-print on the app).
The test only ever removes a directory under <tmp>/cmux-browser-repl.
…sts remove only their own directories

The session's os.tmpdir() is already its own private directory
(<tmp>/cmux-browser-repl/<session>-<random>-tmp, mode 0700), but images,
exports, recordings and spills added cmux-browser-repl/<session> inside it
again (18-print on the app). They now go straight into it; the native
session still removes it at close only when empty, so the files stay. The
dev host gets the same private directory, so cmux-dev runs as the app does.

Test cleanup: every recursive removal in tests/browser-parity goes through
lib/test-dirs.mjs, which removes only a directory this process made with
mkdtemp under the system temporary directory and refuses the temporary
directory itself, /tmp, the home directory, the working directory and their
parents. (An earlier version of the new test removed the shared temporary
directory.) The cmux backend runs each scenario in a new working directory
instead of the checkout, and 21-fs no longer deletes files it did not make.
Red: every frame.evaluate, the runtime's own agent-world reads and polls
included, held the session's action window for the whole call, so during a
long evaluation the user's own dialogs and popups in that tab went to the agent.
…ater popups in the background

- Only page-world frame.evaluate (the agent's own script) opens the
  session's action window, and for at most a second on the injected clock;
  the runtime's agent-world reads and polls hold none, so during a long
  evaluation the user's own dialogs and popups stay the user's.
- A window the page of a driven tab opens through the browser's path while
  the user is not working in that tab (not shown and focused in the key
  window of the active app), such as window.open after an await in the
  agent's click handler, opens as a background tab told to no session,
  never as a key popup window over the user's work.
- tab.reload's wait after a wake uses the call's remaining time.
- The dev driver mirrors the page-world, one-second window.
Merge-main commit by scripts/merge-main.sh.
Merged by scripts/merge-main.sh: origin/main at 161e1cc.

Resolved conflicts:
- Resources/Localizable.xcstrings: xcstrings key-level union
- cmux.xcodeproj/project.pbxproj: union of added entries, then normalize-pbxproj.py

Merge-main-previous-head: 1bf6df0
Merge-main-base: 161e1cc
@github-actions

github-actions Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

merge-gate: ci-status is not successful on 89c84cb. A fresh merge-override: comment from a write-access collaborator is required for: CLA Assistant, ci-status. For every check, name it and link a main run that fails the same check or write 'not on main', then add a real sentence explaining why it is safe.

…callback budget

The flaky watchdog test raced a 0 ms setTimeout against the next cell.
When the timer's callback was queued on the session thread after the
next cell became current but before that cell began, the watchdog
counted the callback as the cell's work and never stopped it, so the
cell timed out. The test now fires the timer by hand behind a held
thread, which reproduces the race every time (red), and the idle case
no longer waits on a real timer.
…at cell

A cell became current when it was submitted, so a timer or event
callback that the thread ran before reaching the cell was attributed to
it. The watchdog exempts a run under the current cell from the callback
budget, so a runaway callback queued ahead of a cell held the thread
until that cell timed out. The session now marks a cell begun on the
thread and attributes runs only to a begun cell.
The pending-timer test awaited a 1 ms timer after the flood and assumed
the flood's callbacks had run; it now awaits a promise the flood's last
callback settles, then schedules the full 10,000 timers again (none
comes due), which proves every slot was released. The hung restore and
the distant deadline in the tab-wake tests suspend until their task is
cancelled instead of sleeping for an hour.
The plain Copy/Cut test and two Paste tests ran WebKit's commands
against the real general pasteboard, so a broken redirect would have
written or read the person's clipboard, and the Paste tests skipped
their checks when the real clipboard's change count was low. They now
run inside the stand-in, which also answers +generalPasteboard, and the
Paste tests give the system change count explicitly so they always
exercise the paste. The real general pasteboard is only looked up for
identity and change counts, never read, written or cleared.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Continued as #17256: one commit with the same tree as this branch's head 89c84cb, because the CLA action cannot read more than 250 commits of one pull request (this PR has 392). History, reviews and red-first commits stay here. Follow-ups: #17253.

lawrencecchen added a commit that referenced this pull request Oct 4, 2026
…17256)

`cmux browser repl` is a persistent JavaScript REPL that agents use to drive
cmux browser panes: Playwright page, locator, keyboard and mouse semantics with
native trusted input, budgeted accessibility snapshots with diffs, tabs,
cookie-bearing fetch, a sandboxed fs, named sessions, an MCP server mode and
site tools. Guards live outside agent code: fill-only secrets with redaction
and capture masking, a domain policy over every frame and fetch hop, a per-tab
clipboard for session-created tabs, private per-session temp directories and
bounded cells, fetches and timers. Agent work never moves the user's focus,
hibernated tabs wake on use, and crashed tabs report how to recover.

Squashed from #15570 (392 commits; the
CLA action cannot read more than 250 commits of one pull request). Same tree as
that branch's head.
lawrencecchen added a commit that referenced this pull request Oct 4, 2026
…EPL (S0a)

Three-way merge of main's browser REPL (3b15ee4, #17256) into the
cmux-next copy. Base: #15570 at 3add499, the tree that b8c82aa moved
(path-only edits confirmed by a diff). Main's changes come in; cmux-next's
host adaptations stay.

Runtime (cmux-tui/crates/cmux-browser-host/js):
- Main's behavior and fixes: cell queue without a promise chain, per-cell
  cancel by id, cell console that drops a cancelled cell's late output,
  timer delay clamp and the per-session timer bound, output hard cap,
  session-private tmpdir for spill, exports and records, frozen host,
  driver and agent-tools hooks, __cmuxNative and the runtime namespace
  deleted before the first cell, fetch credentials (include, same-origin,
  omit), tab state (live, hibernated, waking, crashed), storageState and
  setStorageState scoped to the page's data store, IDN and trailing-dot host
  names in domain patterns, YouTube caption host check, page-clipboard.js,
  and the site tool changes (linear, jira, loader, webmcp drafts, auth-fill
  field checks, gmail, google docs/sheets/slides, notion, youtube).
- Kept for cmux-next until the native ABI slice (S0b, decision D1): the
  host's secretSet/secretList/secretDelete/policyNarrow/policyGet/
  policyLog/policyCheck natives, {__secret: name} handles, locator.fill
  with the handle in the page agent's fill call, narrow-only policy from
  the VM, host-side policy log and check, registrableDomain for sites,
  "stat" for lstatSync (the host has no lstat op yet).
- One blocked-navigation event name, main's: navigation.blocked
  {targetId, url, reason}. The Rust host emits neither name today.

Suite (tests/browser-parity): main's scenarios 34-39 and goldens, unit
tests (tab-ownership, runtime, budget, mcp, agent-tools), sites tests
(drafts and the rest), lib/test-dirs.mjs with its cleanup guard,
lib/public-suffix.mjs, fixture-server and oracle changes, and the dev
driver's tab ownership, data stores, hibernation and cookie scope. Every
harness still builds its REPL through createHostedRepl (reference-host.mjs).
The CLI backends now run each scenario in a new working directory.
Not taken: main's native-boundary.mjs and its unit test (they implement
main's ABI; they merge with reference-host.mjs into one oracle in S0b), and
the comparison material that #16818 moved to the private repository
(diff/, capabilities.json, reference/, chrome refs, references.mjs,
reference-a sizes, parity-report.md, performance.md, reference-c-parity.md).

Docs (plans/cmux-next/browser-repl): main's README, driver-protocol (Guards,
hibernated and crashed tabs, native host contract), edge-cases and
site-tools, with each cmux-next difference on a line that starts with
"cmux-next:".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lawrencecchen added a commit that referenced this pull request Oct 4, 2026
…EPL (S0a)

Three-way merge of main's browser REPL (3b15ee4, #17256) into the
cmux-next copy. Base: #15570 at 3add499, the tree that b8c82aa moved
(path-only edits confirmed by a diff). Main's changes come in; cmux-next's
host adaptations stay.

Runtime (cmux-tui/crates/cmux-browser-host/js):
- Main's behavior and fixes: cell queue without a promise chain, per-cell
  cancel by id, cell console that drops a cancelled cell's late output,
  timer delay clamp and the per-session timer bound, output hard cap,
  session-private tmpdir for spill, exports and records, frozen host,
  driver and agent-tools hooks, __cmuxNative and the runtime namespace
  deleted before the first cell, fetch credentials (include, same-origin,
  omit), tab state (live, hibernated, waking, crashed), storageState and
  setStorageState scoped to the page's data store, IDN and trailing-dot host
  names in domain patterns, YouTube caption host check, page-clipboard.js,
  and the site tool changes (linear, jira, loader, webmcp drafts, auth-fill
  field checks, gmail, google docs/sheets/slides, notion, youtube).
- Kept for cmux-next until the native ABI slice (S0b, decision D1): the
  host's secretSet/secretList/secretDelete/policyNarrow/policyGet/
  policyLog/policyCheck natives, {__secret: name} handles, locator.fill
  with the handle in the page agent's fill call, narrow-only policy from
  the VM, host-side policy log and check, registrableDomain for sites,
  and an early jira check of home.atlassian.com from the host's patterns
  (the host has no URL check op yet). lstatSync sends main's "lstat" op;
  the Rust host answers EINVAL for it until the fs rules slice (S1).
- One blocked-navigation event name, main's: navigation.blocked
  {targetId, url, reason}. The Rust host emits neither name today.

Suite (tests/browser-parity): main's scenarios 34-37 and goldens, unit
tests (tab-ownership, runtime, budget, mcp, agent-tools), sites tests
(drafts and the rest), lib/test-dirs.mjs with its cleanup guard,
lib/public-suffix.mjs, fixture-server and oracle changes, and the dev
driver's tab ownership, data stores, hibernation and cookie scope. Every
harness still builds its REPL through createHostedRepl (reference-host.mjs).
The CLI backends now run each scenario in a new working directory.
Assertions on the host's guard texts stay cmux-next's (a refused secret
names the frame URL; a TOTP test computes the code with reference-host's
totp). The runtime's compact site list gains co.at and or.at.
Not taken: main's native-boundary.mjs and its unit test, and scenarios
38-cookie-guards and 39-policy-rules (they need main's ABI and the cookie
guards in the boundary; they come with the one-oracle merge in S0b), and
the comparison material that #16818 moved to the private repository
(diff/, capabilities.json, reference/, chrome refs, references.mjs,
reference-a sizes, parity-report.md, performance.md, reference-c-parity.md).

Docs (plans/cmux-next/browser-repl): main's README, driver-protocol (Guards,
hibernated and crashed tabs, native host contract), edge-cases and
site-tools, with each cmux-next difference on a line that starts with
"cmux-next:".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
austinywang added a commit that referenced this pull request Oct 5, 2026
…t screens (#17230)

* cloud welcome: introducing cmux cloud window, shown once, five layouts to compare from help (wip)

* cloud onboarding: glass welcome window, lowercase and machine focus layouts, sidebar intro with banner and reasons, enablement view takes plain values (wip)

* cloud onboarding: one welcome design (machine focus, all lowercase), sidebar intro titles by plan, 6pt buttons, drop the comparison layouts

* cloud onboarding: review fixes (glass behind compiler guard, welcome considered once per launch, size after hosting, no return shortcut, comments, orphaned string)

* cloud welcome: ignore the titlebar safe area (fixes a layout-loop crash on open), three reasons

* cloud tab intro: title first, no icon

* cloud tab intro: app icon banner back, lock badge while the plan needs pro

* cloud tab intro: dark app icon in the banner

* fix(cloud): wait for display helper readiness (#17132)

* fix(cloud): explain unavailable display actions

Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace.

— unregistered

* fix(cloud): explain unavailable display actions

Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace.

— unregistered

* fix(cloud): reject cross-machine display opens before projection

* fix(cloud): probe legacy desktop VMs for displays

* fix(cloud): initialize wallpapers for new displays

* fix(cloud): fail closed before display double-click opens

* fix(cloud): replay repeated display ownership hints

* test(cloud): cover display helper refresh

* fix(cloud): refresh installed display helper

* test(cloud): require display service readiness probe

* fix(cloud): wait for display helper readiness

* fix(cloud): keep display creation retryable

* fix(cloud): authorize dynamic display ports

* fix(cloud): let new display retry guest discovery

* Revert "fix(cloud): let new display retry guest discovery"

This reverts commit 0fe008ed3f6fd78338be9e9a2f8161d37984cc0f.

* test(cloud): sidebar visibility filter must keep display creation state

Moves applyingDeviceVisibility next to SurfaceCatalogSnapshot in
CmuxSurfaceCatalogModel (no behavior change) so it is testable, and adds
a failing regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): keep display creation state through the sidebar filter

applyingDeviceVisibility rebuilt SurfaceCatalogSnapshot from scratch and
dropped displayCreationMachines, staleMachineIDs and display memberships,
so every desktop VM's New Display row reported additional displays as
unavailable. Filter a copy instead so all per-machine state survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(surfaces): a split with no room opens as a tab in the target pane

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): optimistic display row; fall back to a tab when a split has no room

New Display now shows a "Starting display…" row from the click until the
guest answers, driven by the catalog's in-flight creation set.

Opening a sidebar resource splits the focused pane; once split admission
had no room left, the open failed with "Could not create the pane:
noSpace". SurfacePaneFactory now opens it as a tab in the pane that would
have been split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(surfaces): refused split is typed; sidebar gestures open a tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(surfaces): scope the no-room tab fallback to sidebar gestures

The factory now throws a typed noSpace instead of opening a tab, so the
layout replay and socket open verbs keep a truthful refused split. Sidebar
opens and New Display use openPreferringSplit, which retries as a tab in
the pane that would have been split. The empty Displays row no longer
shows beside the optimistic Starting display row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): display creation needs no discovery round trip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(cloud): open the display pane at the click and skip pre-create discovery

New Display paid two VM exec round trips (list, then create, about 2s
each) before any pane appeared. The create reply is already the full guest
catalog, so the list is dropped. The pane now opens at the click with a
native Starting display state and is adopted by the projection once the
guest assigns the display; creation failure closes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): harden the reserved display pane

Drop a second click before it opens a pane; bind the reservation to the
created display id so no other projection adopts it; keep creating when
the pane cannot open; leave the display in the pool when the person closed
the pane; show the failure on a pane the socket refuses to close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): additional displays serve noVNC beside the primary desktop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): make additional displays reachable on the VM private address

Displays 2+ ran websockify on 127.0.0.1 while display 1 listens on [::].
The client reaches every display through the private address, so each new
display's first noVNC connection was refused and only recovered after a
timeout. Bind like display 1 (Xvnc stays -localhost), replace proxies an
older helper left on loopback, and drop the open-port call added for
display ports: it changed no routing and cost a control-plane request,
plus a desktop heal exec for display 1, on every open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): stale proxy match covers only this display's websockify

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): match stale display proxies by argv, not pgrep regex

pgrep's ERE has no (?:...) groups, so the stale websockify match failed,
the old loopback listener kept the port, and the rebound proxy exited.
Read /proc argv for this user's websockify on this display's ports.
Verified on a live VM: displays 2 and 3 now accept the noVNC websocket on
the private address, one proxy each, X sessions untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): a failed first desktop connection retries before failing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): retry a display's first connection; warm the carrier at create

noVNC stops at Connect after an initial connect failure; its reconnect only
follows a session that once connected. Restored and new displays could lose
that race while the proxy or guest listener started, leaving "Failed to
connect to server". Retry the route's first connection three times
(0.5s/1s/2s on the injected clock) before showing the failure card.

The first display open on a machine spent 12-18s starting its browser
carrier after the guest exec. Start it alongside the create request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): an explicit Retry gets its own quiet first-connection retries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): run the guest display script test on the main actor

CloudGuestDisplayScript is @MainActor; the test called it from a
nonisolated context and broke the CmuxCloud package test build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): per-runner build roots only on fleet Macs without glaeda (#17239)

* test(ci): Blacksmith keeps the shared root; reruns keep a per-runner root (red)

Blacksmith macOS runners report RUNNER_ENVIRONMENT=self-hosted, so they got
per-runner roots: every build started cold and no seed could be adopted
(job 111334766861). take-product-canonical-root.sh also refused a product
built at a per-runner root on a fleet Mac without glaeda.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): per-runner roots only on fleet Macs without glaeda

canonical-build-root.sh derived /private/tmp/cmux-ci-<runner> for every
self-hosted runner without the glaeda helper, which included Blacksmith's
ephemeral macOS runners: their builds started cold and their seeds were
never adopted. Derive it only when the fleet directory exists. Let
take-product-canonical-root.sh accept such a root when glaeda isn't there
to hold it, so app-host test reruns build where the product was compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): repair main's guards, localization parity and cmuxTests compile (#17207)

* fix(ci): align owned-build-state guards with per-runner canonical roots

#17168 (496195b) derives a canonical root per self-hosted runner, exports
CMUX_CI_CANONICAL_ROOT from the build-slot step and drops the shared
/private/tmp/cmux-ci fallback in test-e2e's owned-state step. Three tests in
tests/test_ci_owned_build_state.py still assumed the old contract, turning
"CI fast guards" red on main.

Update them to the new contract: the slot step exports the root, a
per-runner root reads its own store, and a missing root reads nothing. The
owned-state step now fails with a clear message when the root is missing, and
rejects a root nested under a cmux-ci-* prefix (e.g. cmux-ci-a/../x), so the
looser glob cannot map the store outside the Mac's package directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(l10n): translate the New Machine plan loading strings

#17135 added machines.new.plan.loading, .retry and .error with the English
text copied into every locale, so `localization_catalog.py check` reports 24
parity errors and "Fast static checks" fails on every PR. Translate them for
all catalog locales; Retry reuses common.retry's wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(l10n): match the catalog's plan terminology in de, ko and zh-Hant

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tests): repair cmuxTests compile after the sidebar reorder change

#17070 moved RightSidebarModeBarDragLayout into the CmuxSidebar package,
but RightSidebarTabCustomizationTests still relied on the app module
exporting it, and CloudMachineOrderingTests put `try` calls inside an `&&`
in #expect, which the macro rejects ("operator can throw but expression is
not marked with 'try'"). Import CmuxSidebar and hoist the throwing lookups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tests): hoist the second throwing lookup out of #expect in CloudMachineOrderingTests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): satisfy dogfood-build runner guards and the Swift warning budget

#17206 turned dogfood-build into a macOS build, which tripped three guards:
no pinned Xcode, no fork branch in runs-on, and no static-preflight
dependency. Add `scripts/select-ci-xcode.sh`, the standard owner and fork-PR
branches ahead of the existing selector, and `needs: static-preflight`. The
job's `if` already excludes forks, so manaflow-ai PRs keep the same runner.

#17070's `RightSidebarModeBarDragController.coordinateSpace` is read from
a Sendable geometry closure and broke the zero-warning budget; it is a plain
String constant, so mark it `nonisolated`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): a root the glaeda hook exported wins over the per-runner root

Fails on main: canonical-build-root.sh treats an exported /private/tmp/cmux-ci
as unset and derives /private/tmp/cmux-ci-<runner>, so main-compile-probe
refuses the root glaeda placed it in (runs 37157423969, 37155786981).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): honor the canonical root the glaeda hook exported

#17168 (496195b) derives /private/tmp/cmux-ci-<runner> on every self-hosted
runner unless CMUX_CI_CANONICAL_ROOT names a non-default root. glaeda's
runner hook holds root 1 (/private/tmp/cmux-ci) for a compile job and
exports exactly that, so the job then built somewhere glaeda does not hold,
and main-compile-probe refused it: "glaeda placed this job in
/private/tmp/cmux-ci, not /private/tmp/cmux-ci-cmux13s-mac-mini-glaeda".

Let any exported root win. A self-hosted job with no exported root still
gets its per-runner root, so #17168's isolation for unplaced runners stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): an unplaced self-hosted job keeps the shared seeded root

Fails on the branch: canonical-build-root.sh derives /private/tmp/cmux-ci-<runner>,
whose fingerprint never matches main's seeds, so new aws runners
(aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4) compiled cold and timed out at
35 minutes in compile admission (run 37158561950, both attempts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): build unplaced self-hosted jobs at the shared seeded root

#17168 gave every self-hosted job without an exported root its own
/private/tmp/cmux-ci-<runner>. That root is part of the cache fingerprint,
so main's DerivedData seeds and the owned build state never match it, and the
prepare step clears it each job: every new aws runner compiled cold and hit
the 35-minute admission limit (aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4 on
run 37158561950).

Go back to /private/tmp/cmux-ci when no root is exported. glaeda's hook still
exports root N for the jobs it places, so those stay isolated. Unplaced
concurrent runners on one Mac share the root again, as before #17168, until
glaeda places those jobs too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): the pool picker never picks an owned label it was not configured with

Fails on main: simple_pool_picker adds every glaeda-<class>-xcode-* label it
sees on a runner, and glaeda-std-xcode-26.3 (ten aws EC2 runners, five per
Mac) sorts before the minis' 26.6, so compile admission ran there and timed
out at 35 minutes while the minis were idle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): pick only configured owned pools, not any owned-looking label

simple_pool_picker added every glaeda-<class>-xcode-* label it saw on an
online runner to the configured CI_OWNED_POOL_SLOTS pools, then tried them
in string order. The aws EC2 Macs carry glaeda-std-xcode-26.3 (ten runners,
five per Mac), which sorts before the minis' glaeda-std-xcode-26.6, so PR
compile admission landed on contended EC2 runners and timed out at 35
minutes while the minis sat idle. Use only the configured pools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): the pool picker reads every page of organization runners

Fails on main: LiveState.runners reads one page of 100, but manaflow-ai has
509 runners and the first page holds the aws Macs and no idle mini, so the
picker never saw the mini fleet (run 37166910798 fell back to Blacksmith with
29 idle minis online).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): read every page of runners in the pool picker

LiveState.runners read only the first page of 100 organization runners.
manaflow-ai has about 500, and the first page holds the aws Macs but no idle
mini, so the picker never saw the mini fleet: it took the aws 26.3 label
while that was discoverable, and fell back to Blacksmith once it was not.
Read up to ten pages. Against live data the picker now sees 38 online
glaeda-std-xcode-26.6 minis (33 free) and picks them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: clear the Cloud sidebar warnings that broke the Swift warning budget

Compile admission on a mini (run 37167089830) built cleanly but failed the
zero-warning budget on three warnings from the Cloud sidebar work:
`SurfaceCatalog.shared` used as a default argument of two @MainActor
CloudWorkspaceSidebarPresentation entry points (evaluated nonisolated), and an
implicit `self` in CloudTreeOutlineView's machine-lift reopen closure.
Resolve `.shared` inside the main-actor body and make the capture explicit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): preserve canonical root and translation wording

* test(ci): align canonical root guard with main behavior

* test: use isolated catalogs in Cloud sidebar fixtures

Pass each test fixture catalog through the sidebar snapshot factory and direct presentation assertion so Cloud machine metadata is read from the catalog the test populated.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* test: model loading Cloud sidebar state

Keep device projections visible while their catalog rows are restoring, and assert that a reserved loading card shows machine identity before its directory appears after adoption.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* test: use a terminal panel for cloud directory settings

The sidebar detail test supplies a reported directory, so give it a real terminal panel instead of a loading card that correctly suppresses directory metadata.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* fix(cloud): restore device directory fallback lost in main merge

The merge of main took #17107's presentation file and dropped the
fallback from 27a8d804fdf, so a device projection whose catalog row is
still restoring rendered no directory and
SidebarCloudWorkspaceBadgeTests.deviceNameIsVisibleBesideItsDirectory
failed on b6ea6632cb1 (the regression run is that CI failure).

Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>

* ci: require a written merge override for red CI (#17217)

* ci: require merge-gate before merges

* Add API-only merge gate for pull requests

* test: cover merge-gate override decisions

* ci: wire merge-gate tests and push timing

* ci: pin merge gate workflow source

* Reject read-only override authors

* ci: harden merge gate runner and author checks

* ci: keep merge gate on hosted capacity

* ci: fail closed when collaborator lookup fails

* ci: reject stale successes during reruns

* ci: fail closed for untimestamped queued checks

* ci: fail closed for untimestamped pending statuses

* ci: verify matching override failure evidence

* test: cover mismatched override failure evidence

* ci: choose newest merge gate check run

* ci: require compile evidence for main-fix merges (#16993)

* test: reproduce main-fix merging without compile evidence

The installed helper bypasses all checks under --main-fix. The regression records that it merges with no compile checks present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: require build evidence for main fixes

gh-merge-green --main-fix now requires successful Release, Debug and Swift test target build steps on the exact PR head. Existing Swift test failures are waivable only when their parsed issue records match the same Swift test step on the exact base SHA; the audit comment records each match and unrelated failures remain fatal.

CPU, memory and disk checks: the validator uses one bounded 90-second GitHub request per call, caps captured output at 32 MiB, writes audit bodies to temporary files, and does not retain logs, caches or stores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ci: harden merge gate freshness and evidence

* ci: require per-check override reasons

* ci: complete merge gate review fixes

* docs(ci): document merge-gate rollout order

* ci: make merge helper rollout repairable

* ci: point merge helper refusals to repair runbook

* ci: resolve merge helper symlink for main fixes

* docs: remove stale main-fix PR reference

* fix: avoid pipefail false negatives in merge helper

* fix: keep merge gate events fresh per pull request

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Share one vCPU and memory pool across a plan's Cloud VMs (#17238)

* test(vms): reproduce missing shared Cloud VM resource pool

* feat(vms): share one vCPU and memory pool across a plan's Cloud VMs

Pro, Team (per paid seat), and Founder's Edition get up to 5 active VMs
sharing 20 vCPUs and 40 GB RAM, with machines up to the 32 GB xl row.
Max gets 80 vCPUs and 160 GB RAM with machines up to the 64 GB 2xl row.
The repository enforces the pool next to the active-VM count, under the
same transaction and billing lock, on create, Base open/reset, paused
resume, CPU/memory resize, and fork. Pricing, docs, app, and iOS copy
now describe pooled resources.

* Pooled VMs: Pro overrides stop at 32 GB; drop unused iOS pool strings

* test(vms): reproduce leaked compute claim and stale-plan resume pool

* fix(vms): give back unused compute claims and resume against the caller's pool

* docs(pricing): describe the Team pool per paid seat and localize pool strings in every locale

* Cloud VM: snapshot create honors Idempotency-Key (#17244)

* test(cloud-vm): snapshot create dedups by idempotency key (red)

A retry of POST /api/vm/:id/snapshot with the same Idempotency-Key must return
the first snapshot and take no second one; a retry while the first runs is
refused as in progress; the same key with another name is a conflict; a failed
attempt frees the key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud-vm): snapshot create dedups by Idempotency-Key

POST /api/vm/:id/snapshot now reads Idempotency-Key. A new ledger table,
cloud_vm_snapshot_requests (one row per machine and key, additive migration
20261004120000), records a pending attempt before the provider call and the
provider snapshot after it. A retry with the same key returns the first
snapshot and records no second usage event; a retry while the first runs gets
409 vm_snapshot_in_progress (retryable); the same key with another name gets
409 vm_snapshot_idempotency_conflict; a failed attempt frees the key; a pending
row older than 15 minutes (route budget 600 s) is taken over by a retry.
Requests without a key behave as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(cloud-vm): smoke --snapshot-check proves snapshot idempotency

With --create, takes one snapshot of the throwaway smoke machine twice with the
same Idempotency-Key, requires the same snapshotId, deletes that snapshot,
then destroys the machine as before. No fixed VM id and no printed token: the
smoke mints its own throwaway user session.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Preserve selected tab when closing another surface (#16645)

* Preserve selected tab when closing another surface

* Only select the closing pane's tab when that pane is focused

BonsplitController.selectTab also focuses the pane, so calling it for an
unfocused pane moved focus into the pane where a tab closed, the focus theft
this change is meant to stop. Bonsplit already keeps the selection when an
unselected tab closes; the shouldCloseTab change is the actual fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs: detail tmux help options (#16780)

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test: guard managed contributor difficulty labels (#16448)

* test: guard managed contributor difficulty labels

* test: reject invalid difficulty descriptions

* test: exercise difficulty label sync requests

---------

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): repair main's Cloud app-host failures from #17132 and #17103 (#17250)

* test(cloud): expect the display helper readiness loop

#17132 replaced the one-line `list || exit 1` probe with a bounded
retry loop and updated the package test, but cmuxTests still asserted
the old line, so CloudDisplayCatalogTests.guestCommandShape failed on
main (run 37186487936, shard 4/7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): make the restored-display retry test a desktop

#17132's initialDesktopFailureRetries configured port 6902 without a
resource ID, so CloudBrowserAccessState.isDesktop was false and
desktopConnectionDidChange ignored the failure: no quiet retry, and the
following navigations[1] trapped and crashed the shard 5 app host on
main (run 37186487936). Give it the .display identity additional
displays carry and require the retry before indexing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): a foreign display click shows one synchronous hint

#17103 made a display click on another machine's workspace reject
synchronously through showDisplayOpenHint, so no tree operation starts
and waitForOpen() waited out the 60 s suite limit on main (run
37186487936, shards 2 and 3). Assert no operation ran and that the
rejection is shown once; this stays red until the double-click stops
repeating the hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): show a foreign display's ownership hint once per double-click

AppKit delivers a double-click's first click to handleSingleClick,
which already shows the ownership hint, and #17103 also showed it from
handleDoubleClick, so the user got the same rejection twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): never show upstream tree failures in the Machines status row

#17103 started presenting treeError verbatim so its ownership hints
would survive, but treeError also carries raw upstream failures
(LocalizedError descriptions, create output), so a URL with query
parameters reached the toolbar text, hover help and copy menu.
MachinesCloudStatusTests.emptyStatusHasNoProgressPresentation caught it
on main (run 37186487936, shard 7).

Hints now travel through their own onHint sink (falling back to
onFailure for other callers), the panel records them as trusted copy,
and the status row shows the tree error verbatim only when it is that
hint; everything else gets the safe recovery message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): keep merge-gate diagnostics on the exact PR (#17248)

* fix: keep merge-gate diagnostics on the exact PR

* fix: print repair guidance for gate diagnostics

* fix: allow merge-gate PR comments when permitted

* fix: reject mismatched merge-gate event identities

* fix: keep merge-gate alive on comment errors

* fix: keep the cmux-cua credential out of the Codex argv (#17252)

* test: codex wrapper must not put the cmux-cua credential in argv

The Codex wrapper passes the cmux-cua socket credential as
-c mcp_servers.cmux-cua.env.CMUX_CUA_SOCKET_AUTH_TOKEN=<value>, so any
local user can read it with ps. These assertions require the value to be
absent from argv, Codex to receive it in its environment, and the MCP
server to receive it through env_vars. The fake Codex now builds the MCP
server environment the way Codex does (allow-list + env_vars + env).

* fix: keep the cmux-cua credential out of the Codex argv

The wrapper passed the socket credential as a Codex -c env override, which
put the value in the codex process argv where any local user can read it
with ps. The wrapper now exports CMUX_CUA_SOCKET_AUTH_TOKEN in the parent
shell before exec and emits mcp_servers.cmux-cua.env_vars so Codex forwards
the variable by name to the MCP server (Codex env_vars, openai/codex#5246).
Codex's default shell_environment_policy excludes *TOKEN* names from agent
shell commands. Attachment stays fail-closed when no credential resolves.

* fix: keep codex wrapper overrides when the subcommand gets -c (#17257)

* test: codex wrapper overrides must survive subcommand -c flags

Codex declares -c/--config, --enable, and --disable as clap global
arguments. When a subcommand such as exec or resume also receives one,
Codex 0.159.3 keeps only the subcommand-level values, so the wrapper's
cmux-cua MCP config, hook config, and --disable computer_use placed
before the subcommand are dropped. The fake Codex now applies that rule,
and new cases cover exec, resume, mixed root and subcommand -c, and a
literal prompt after --.

* fix: keep codex wrapper overrides when the subcommand gets -c

Codex declares -c/--config, --enable, and --disable as clap global
arguments and keeps only the subcommand-level values when a subcommand
also receives one. The wrapper put its cmux-cua MCP config, hook config,
and --disable computer_use before the user's argv, so codex exec -c ...
or codex resume ... -c ... dropped all of them. The wrapper now moves the
user's subcommand-level global arguments, in order, in front of the
subcommand. Codex reads one root-level list with the same precedence as
before. Tokens after -- stay in place, and interactive launches without a
subcommand are unchanged.

* Add cmux browser repl: a Playwright-shaped browser REPL for agents (#17256)

`cmux browser repl` is a persistent JavaScript REPL that agents use to drive
cmux browser panes: Playwright page, locator, keyboard and mouse semantics with
native trusted input, budgeted accessibility snapshots with diffs, tabs,
cookie-bearing fetch, a sandboxed fs, named sessions, an MCP server mode and
site tools. Guards live outside agent code: fill-only secrets with redaction
and capture masking, a domain policy over every frame and fetch hop, a per-tab
clipboard for session-created tabs, private per-session temp directories and
bounded cells, fetches and timers. Agent work never moves the user's focus,
hibernated tabs wake on use, and crashed tabs report how to recover.

Squashed from https://github.com/manaflow-ai/cmux/pull/15570 (392 commits; the
CLA action cannot read more than 250 commits of one pull request). Same tree as
that branch's head.

* Cloud: VM file operation routes (port of #16936) (#17254)

* Cloud: VM file operation routes (port of #16936), missing path answers 404

Ports the web part of https://github.com/manaflow-ai/cmux/pull/16936
(feat-cmux-next) to main: /api/vm/[id]/fs/[operation] (list, read, stat,
write, mkdir, remove) with the Freestyle driver and gateway methods.

Includes the fix from feat-cmux-next: Freestyle removes a missing path with
success, so removeVmFile stats first and answers a missing file with
404 vm_file_not_found (any other stat failure, a missing VM included, stays a
provider failure).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cloud): stat/read/dir of a missing VM path must answer vm_file_not_found

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): stat/read/dir of a missing VM path answer 404 vm_file_not_found

The staging rehearsal of #17254 showed stat of a removed file answering 502
vm_cloud_service_unavailable. Map the guest ENOENT on every file read, as
remove already did, and title the error 'File not found'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Stop calling the legacy Subrouter during account deletion (#17273)

* test: account deletion must not call the retired legacy Subrouter

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Stop calling the legacy Subrouter during account deletion

The legacy Subrouter at subrouter.cmux.dev is being retired. Account
deletion now skips the legacy revoke phase and needs no legacy env vars.
Local mapping rows are still deleted, and a legacy_delete_pending
tombstone from an older deployment resumes at the hosted checkpoint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Split the account DELETE handler under the complexity limit

DELETE delegates to deleteAccount and named phase helpers that share one
progress record, so its complexity drops from 46 to under 20 and its
grandfathered baseline entry is removed. Behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: remove merge gate and restore exact-head merging (#17275)

* Cloud: private network routes (port of #16948) (#17255)

* Cloud: private network routes (port of #16948), missing firewall rule answers 404

Ports the web part of https://github.com/manaflow-ai/cmux/pull/16948
(feat-cmux-next) to main: /api/vm/firewall (list, get, create, delete),
/api/vm/network and /api/vm/tunnel/network/[operation], with the Freestyle
driver, gateway and private-network workflow pieces.

Includes the fix from feat-cmux-next: a firewall rule that is not in the
caller's network answers 404 vm_firewall_rule_not_found (a provider 404 race
on delete too); a missing VM endpoint stays vm_not_found.

Stacked on the file-routes port (#17254).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(cloud): move firewall endpoint parsing into services/vms/firewallEndpoint

No behavior change; makes the parser testable without the route.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cloud): firewall must accept normal CIDR prefixes and team-owned VM endpoints

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): firewall accepts normal CIDR prefixes and team-owned VM endpoints

The staging rehearsal of #17255 found two defects. validCidr compared the
prefix with net.isIP(), which returns the family (4 or 6), so any IPv4
prefix above /4 was refused; it now uses canonicalCidr. The vmId ownership
check looked the VM up in the personal scope, but every new VM is
team-owned, so vmId endpoints were vm_not_found; the route now resolves the
account scope when a vmId is named, and the VM must be the caller's own
(the firewall edits the caller's network). The provider now gets only the
rule fields: the Freestyle driver spreads its input into the request body,
so userId and provider were sent to Freestyle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cloud): firewall rules must name a caller resource as destination; get/delete must find VM rules

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): decide firewall rule ownership on the shared provider account

The Freestyle account is shared by every cmux user, so the API decides
whose a rule is. Reading and deleting: the rule names at least one resource
and every resource it names is the caller's. get and delete now read the
rule by id; they searched only the network listing, so a rule that named a
VM and a CIDR was created (201) and then could not be found or deleted. The
list merges the network listing with one listing per caller VM. Creating:
the destination must be a caller resource (400 vm_invalid_firewall_rule),
because a destination of only an address range or the public Internet would
reach other tenants' machines. Every firewall call resolves the account
scope like the other VM routes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cloud): firewall refuses unknown endpoint fields as owned and sends canonical CIDRs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): unknown firewall endpoint fields are never owned; send canonical CIDRs

Security review P2: the provider adds selectors as new optional fields, and
an unknown one could name another tenant's resource, so a rule with an
unknown endpoint field is not the caller's. Review P3: send the canonical
range so a valid non-canonical CIDR does not fail at the provider.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): title vm_firewall_rule_not_found 'Firewall rule not found'

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cloud): firewall needs a 100-rule cap, a bounded list, and a per-user rate limit

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): cap firewall rules at 100, bound the unfiltered list, rate-limit mutations

These routes are new on a provider account shared by every cmux user.
Create refuses at 100 owned rules (409 vm_firewall_rule_limit). An
unfiltered list reads the network plus at most the 10 newest live VMs, one
provider call each; older VMs list with ?vmId. Create and delete are
throttled per user with the Vercel firewall rule CMUX_VM_FIREWALL_RATE_LIMIT_ID
(no other VM mutation route has a limiter, so this follows the team-invite
limiter: fail closed when the firewall is unavailable, fail open and report
when the rule is unset or removed).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* remote-tmux: stop a torn-down control stream from feeding the reconnected one (#16897)

* remote-tmux: failing test for a torn-down stream feeding the next one

A control stream torn down for a reconnect keeps delivering what its reader
had already buffered. The test holds the main actor while a first client
writes 560 KB, starts a reconnect, and expects none of those bytes to reach
the connection.

* remote-tmux: stop a torn-down stream from feeding the next one

Cancelling the task that reads a control client's stdout does not empty the
reader's buffer, so chunks the old client had already written were still
ingested after the teardown. Once the reconnect had respawned, a leftover
command result was taken for the new client's attach reply. The connection
then never asked for windows and the mirror stayed blank for good.

Each read loop now stops as soon as its process generation is no longer the
current one, and closes its reader.

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>

* remote-tmux: keep a window whose Dock has panels when its mirrors move out (#17237)

* remote-tmux: failing test for a docked terminal closed when its window's mirrors move

* remote-tmux: keep a window whose Dock has panels when its mirrors move out

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>

* Expose the workspace task-status lane to custom sidebars (#17245)

Custom sidebars could not read a workspace's task-status lane. cmux already
resolves one per workspace and the control socket can pin it, but the
interpreter data context carried no field for it, so a sidebar had no way to
group or colour rows by whether a workspace needs attention.

`workspaces[i].status` now carries the resolved lane as its raw wire value:
todo, working, needs-attention, review or done. The snapshot takes it as a
required parameter so a dropped wiring breaks the build rather than reporting
a silent "todo".


Claude-Session: https://claude.ai/code/session_0113SqtxGQwjHjzFw8mkSgwU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Clear restored agent notifications once the agent is gone (#17067)

* test: prune read notifications of agents that died with the previous app

An agent alive at quit dies without SessionEnd, so its last "Completed in"
notification is restored on every launch and shown as the workspace's
latest sidebar summary even though no agent is running. The stale-agent
sweep must drop it once the pane has no agent again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: drop restored agent notifications once the agent does not return

Notifications persist across relaunch so an unseen agent result is not
lost, but an agent that was alive when cmux quit dies without SessionEnd.
Nothing clears its notification afterwards: agent PIDs are not restored,
so the 30s stale-PID sweep has no dead PID to catch.

Restore now records the notifications of local panes that hosted an agent
(resume binding or restorable agent snapshot). The stale-agent sweep
removes the read ones when the pane has no agent PID again; unread ones
survive until read, and a pane the agent resumes into is handed back to
its hooks. Remote terminals are skipped since their agent can outlive the
app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: keep restored notifications while the resume is in flight

Covers a read notification posted after restore (never tracked) and a
pane whose restored resume has not reported an agent PID yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: defer restored notification prune while the resume is in flight

The 30-second sweep can run before an auto-resumed agent reports its PID.
Skip panes whose restored command is still in flight, using the
coordinator's existing ownsInFlightRestoredCommand contract. Also look up
tracked notifications by id instead of scanning the store per panel, and
mark the value-only snapshot helper nonisolated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: keep read non-agent notifications on a restored agent pane

The restore tracks every notification persisted on a pane that hosted an
agent, so a read `cmux notify` banner on that pane is pruned with the
agent's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: track only agent-produced notifications on restored agent panes

Use the persisted `isAgentEvent` provenance instead of panel ownership so
a `cmux notify` banner on a pane that hosted an agent is not retired with
the agent's result. Unknown provenance restores as agent-produced, matching
TerminalNotificationStore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Send sidebar links through the external-open rules (#7397)

* browser: apply external-open rules to sidebar links

The sidebar's pull-request and port links (SwiftUI and AppKit rows, and
the open-all-pull-requests action) opened in the embedded browser whenever
that preference was on, without consulting the URL rules that route a site
to the system browser. Sites listed there cannot work in the embedded web
view at all, so a rule now wins over the embedded preference on those
paths, the same way it does for a click inside a page.

* browser: require a user event before a link escapes to the system browser

WebKit reports a script calling click() on an anchor as .linkActivated,
the same as a real click, so the external-open rules on their own let a
page hand itself a system-browser open at a moment of its choosing. The
navigation-typed escape now also requires an AppKit event in flight (a
key, left-mouse, or middle-mouse event) and never intercepts a download,
on every path that consults the rules: the main navigation delegate, the
target=_blank UI delegate, and both popup delegates. The context menu's
Open Link in New Tab is a gesture by construction and says so.

The event check is a bound rather than a proof: NSApp.currentEvent says
an event is being dispatched, not that this navigation is the thing the
user asked for. Middle-clicks arrive as otherMouse events and count.

* browser: e2e coverage for external-open link routing

BrowserExternalOpenRoutingUITests drives real WebKit link activations
through the socket browser.click against a local fixture and asserts
routing at the delegate layer, where popup-vs-link-activation behavior
actually diverges and unit tests cannot reach. Escapes are captured to a
file through the existing DEBUG-only UI-test sink
(CMUX_UI_TEST_CAPTURE_EXTERNAL_OPEN_PATH) from the external-navigation
handler's default opener, so CI never opens Safari. Four cases: a matched
link click escapes while the embedded page stays put; an unmatched click
navigates embedded; a scripted window.open to a matched host never
escapes; a target=_blank form POST to a matched host stays embedded.

The shared BrowserFixtureSocketTestCase gains subclass hooks for launch
arguments and environment, falls back from the in-process socket client
to nc -U and then the bundled cmux CLI, disables hidden-webview discarding
for the backgrounded UI-test host, and polls browser.wait through the
cold-start content-process transient.

* browser: click links for real in the external-open UI tests

A link now leaves for the system browser only while a real input event
is in flight, and the socket browser.click runs JavaScript, so the matched
and unmatched link cases click through accessibility the way a person
does. The scripted popup and form cases keep the socket click, since a
scripted action is what they test.

* browser: keep only the sidebar links, drop the in-page activation change

The in-page half of the external-open rules has landed separately. What remains here is the sidebar: pull-request and port links follow the rules, with tests for the matcher.

* browser: use a rule the pattern safety check accepts in the port-link test

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ci: require merge checks only when their workflows exist (#17284)

* test: cover merging repos without aggregate CI workflow

* fix: make merge checks conditional on base workflows

* ci: fall back to ancestor evidence for --main-fix (#17277)

* ci: use nearest ancestor for main-fix evidence

* ci: constrain ancestor evidence to path-filtered changes

* ci: inspect renamed paths in ancestor evidence

* ci: bound ancestor evidence traversal

* ci: reject incomplete ancestor path comparisons

* fix(session): sweep stale scrollback replay files (#16056)

* test(session): cover replay sweep and permissions

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): sweep stale scrollback replay files

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): sweep stale replay files before restore

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): remove synchronous replay sweep from app init

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): gate restore on off-main replay cleanup

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(app): keep replay sweep off startup critical path

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): skip replay sweep under XCTest

* fix(session): harden retained replay files during stale sweep

* docs(session): keep crash-recovery gate semantics accurate

* test(session): cover legacy replay permissions and sweep filters

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Cover dotted Claude project dir in session directory search (#4939)

* docs: clarify Claude project dir decode asymmetry

* test: cover dotted Claude project dir in session directory scope

* test: scope Claude session roots per task instead of process env

Swift Testing runs suites in parallel, so setting CLAUDE_CONFIG_DIR process-wide could leak into other tests. A DEBUG-only TaskLocal override keeps the fixture root local to the test's task tree.

* test: cover Claude cwd-filter lookup without a DEBUG seam

Widen the Claude candidate enumerator and its two types to internal so the test reaches them through @testable import, per the no-test-debug-seam review rule. The test checks .claude/worktrees and .worktrees cwds and that dot-preserving and other project dirs are excluded.

---------

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Answer the tmux session commands Claude Code calls (#13632)

* Answer the tmux session commands Claude Code calls

Claude Code's tmux backend tears down and reattaches its agent panes with
kill-session, switch-client, new-session -A and show-options -g prefix. The
compatibility layer rejected all four, so a Teams session failed with
"Unsupported tmux compatibility command" once it got past creating panes.

A tmux session is a cmux workspace, which has-session and new-session already
assume, so kill-session closes that workspace, switch-client selects it, and
new-session -A attaches to it when it exists instead of creating a duplicate.
show-options now answers from a table, and prefix reports the C-b that a
default tmux client would.

The sequence test drives all four through the real shim. Its fake socket also
now unwraps the capability envelope that the shell integration adds inside a
cmux terminal, so the test reports the behavior it checks rather than a JSON
decode error; that unwrapping moved into the shared helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Reject a socket payload that is not a request

The fake servers indexed request["method"] straight off json.loads, so a
payload that decoded to null, a list, or an object with a non-string method
raised inside the handler thread and surfaced as an unrelated CLI error. They
now answer those with an error line, which names the real problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: reject empty fake socket request methods

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: reject a failed tmux session lookup and kill-session -a

A workspace.list error must not become a new session, and kill-session -a
must not close the caller.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep a failed tmux session lookup from creating a workspace

new-session -A treated every resolution error as a missing session, and
kill-session ignored -a and closed the caller. A missing session still
creates; a lookup failure and an unsupported flag now fail first.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix custom-sidebar nil-comparison so optional-guarded views render (#7943) (#7974)

* Add failing test: sidebar nil-comparison yields nothing (#7943)

In a custom sidebar, `x != nil` / `x == nil` against a bound optional field
does not evaluate to true/false — it evaluates to nothing. Interpolation
renders empty, ternaries always take the else branch, and `if x != nil`
guards are never taken, so optional-guarded views never render.

This commit adds only the regression test (no fix) so CI shows it red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix sidebar nil-comparison to evaluate to a Bool (#7943)

The interpreter's value model had no null case and no `nil`-literal
evaluator branch, so `nil` evaluated to a host `SwiftValue?` of `nil` — the
same value that means "expression unsupported / no value". `evalInfix` then
bailed on the comparison, so `x != nil` / `x == nil` produced nothing:
interpolation rendered empty, ternaries always took the else branch, and
`if x != nil` guards were never taken. Optional-guarded views (including the
shipped status-board.swift / finder.swift examples) silently drew nothing.

- Add `SwiftValue.null` for the `nil` literal and for comparing an absent
  optional field against `nil` (distinct from host `nil` = "no value").
- Evaluate `NilLiteralExprSyntax` to `.null`.
- Handle `==` / `!=` before the operand guards, coalescing an absent operand
  (host `nil`) and the `nil` literal to `.null`, so the comparison yields a
  Bool that is true/false when present and false/true when absent.

Fixes #7943

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: preserve nil comparison evaluation failures

* fix: preserve nested nil comparison misses

* fix: preserve parenthesized nil comparison misses

* fix: handle nil optional binding and equality budget

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>

* fix: refresh merge helper and honor neutral checks (#17294)

* test: cover neutral checks and helper checkout refresh

* test: tolerate absent git diagnostics

* fix: refresh clean main checkout before merging

* test: cover cloud welcome close shortcut ownership

* fix: route cloud welcome close shortcut to its window

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Leo <cheerleaderleo@outlook.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: BlueRaddish <jeeholife2@gmail.com>
Co-authored-by: EJ <ej@campbell.name>
Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-authored-by: Philipp Mochine <philipp@mochine.de>
Co-authored-by: mys <wowpotato@naver.com>
Co-authored-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Sungho Park <relilau00@gmail.com>
Co-authored-by: Darío Kondratiuk <dariokondratiuk@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mark Xian <mark-xian@foxmail.com>
Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com>
austinywang added a commit that referenced this pull request Oct 5, 2026
…visible (#17127)

* cloud: enable cloud for pro, upgrade for free, keep the button when the panel rebuilds

the cloud tab and settings > cloud now show enable cloud only when the plan
includes cloud and upgrade for free accounts. the plan answer lives on the
account flow (per account) instead of the panel's view state, so switching
sidebar modes no longer drops the button back to "checking your cmux plan…".

* cloud: stop waiting on a slow plan check, title enable cloud machines, debug plan override

* cloud: cloud.fill for the pro-required screen, settings shows only the enable row until cloud is on, enable cloud machines title

* cloud: drop the debug plan override

* cloud: keep prominent cloud buttons visible in an inactive window, keep a known plan through failed or cancelled checks, recheck settings on account change

* cloud gate: newest plan check wins, drop answers for a switched account

* fix cloud billing plan concurrency and state ownership

* fix cloud plan localization parity

* fix cloud entitlement detection for team plans

* avoid stale cloud entitlements across team changes

* report billing refresh success to settings

* preserve account flow billing refresh contract

* cloud onboarding: introducing cmux cloud welcome, and cloud enablement screens (#17230)

* cloud welcome: introducing cmux cloud window, shown once, five layouts to compare from help (wip)

* cloud onboarding: glass welcome window, lowercase and machine focus layouts, sidebar intro with banner and reasons, enablement view takes plain values (wip)

* cloud onboarding: one welcome design (machine focus, all lowercase), sidebar intro titles by plan, 6pt buttons, drop the comparison layouts

* cloud onboarding: review fixes (glass behind compiler guard, welcome considered once per launch, size after hosting, no return shortcut, comments, orphaned string)

* cloud welcome: ignore the titlebar safe area (fixes a layout-loop crash on open), three reasons

* cloud tab intro: title first, no icon

* cloud tab intro: app icon banner back, lock badge while the plan needs pro

* cloud tab intro: dark app icon in the banner

* fix(cloud): wait for display helper readiness (#17132)

* fix(cloud): explain unavailable display actions

Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace.

— unregistered

* fix(cloud): explain unavailable display actions

Show the existing Cloud failure message when New Display is unavailable, and explain the machine ownership restriction when a display is opened into another Cloud workspace.

— unregistered

* fix(cloud): reject cross-machine display opens before projection

* fix(cloud): probe legacy desktop VMs for displays

* fix(cloud): initialize wallpapers for new displays

* fix(cloud): fail closed before display double-click opens

* fix(cloud): replay repeated display ownership hints

* test(cloud): cover display helper refresh

* fix(cloud): refresh installed display helper

* test(cloud): require display service readiness probe

* fix(cloud): wait for display helper readiness

* fix(cloud): keep display creation retryable

* fix(cloud): authorize dynamic display ports

* fix(cloud): let new display retry guest discovery

* Revert "fix(cloud): let new display retry guest discovery"

This reverts commit 0fe008ed3f6fd78338be9e9a2f8161d37984cc0f.

* test(cloud): sidebar visibility filter must keep display creation state

Moves applyingDeviceVisibility next to SurfaceCatalogSnapshot in
CmuxSurfaceCatalogModel (no behavior change) so it is testable, and adds
a failing regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): keep display creation state through the sidebar filter

applyingDeviceVisibility rebuilt SurfaceCatalogSnapshot from scratch and
dropped displayCreationMachines, staleMachineIDs and display memberships,
so every desktop VM's New Display row reported additional displays as
unavailable. Filter a copy instead so all per-machine state survives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(surfaces): a split with no room opens as a tab in the target pane

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): optimistic display row; fall back to a tab when a split has no room

New Display now shows a "Starting display…" row from the click until the
guest answers, driven by the catalog's in-flight creation set.

Opening a sidebar resource splits the focused pane; once split admission
had no room left, the open failed with "Could not create the pane:
noSpace". SurfacePaneFactory now opens it as a tab in the pane that would
have been split.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(surfaces): refused split is typed; sidebar gestures open a tab

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(surfaces): scope the no-room tab fallback to sidebar gestures

The factory now throws a typed noSpace instead of opening a tab, so the
layout replay and socket open verbs keep a truthful refused split. Sidebar
opens and New Display use openPreferringSplit, which retries as a tab in
the pane that would have been split. The empty Displays row no longer
shows beside the optimistic Starting display row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): display creation needs no discovery round trip

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* perf(cloud): open the display pane at the click and skip pre-create discovery

New Display paid two VM exec round trips (list, then create, about 2s
each) before any pane appeared. The create reply is already the full guest
catalog, so the list is dropped. The pane now opens at the click with a
native Starting display state and is adopted by the projection once the
guest assigns the display; creation failure closes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): harden the reserved display pane

Drop a second click before it opens a pane; bind the reservation to the
created display id so no other projection adopts it; keep creating when
the pane cannot open; leave the display in the pool when the person closed
the pane; show the failure on a pane the socket refuses to close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): additional displays serve noVNC beside the primary desktop

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): make additional displays reachable on the VM private address

Displays 2+ ran websockify on 127.0.0.1 while display 1 listens on [::].
The client reaches every display through the private address, so each new
display's first noVNC connection was refused and only recovered after a
timeout. Bind like display 1 (Xvnc stays -localhost), replace proxies an
older helper left on loopback, and drop the open-port call added for
display ports: it changed no routing and cost a control-plane request,
plus a desktop heal exec for display 1, on every open.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): stale proxy match covers only this display's websockify

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): match stale display proxies by argv, not pgrep regex

pgrep's ERE has no (?:...) groups, so the stale websockify match failed,
the old loopback listener kept the port, and the rebound proxy exited.
Read /proc argv for this user's websockify on this display's ports.
Verified on a live VM: displays 2 and 3 now accept the noVNC websocket on
the private address, one proxy each, X sessions untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): a failed first desktop connection retries before failing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): retry a display's first connection; warm the carrier at create

noVNC stops at Connect after an initial connect failure; its reconnect only
follows a session that once connected. Restored and new displays could lose
that race while the proxy or guest listener started, leaving "Failed to
connect to server". Retry the route's first connection three times
(0.5s/1s/2s on the injected clock) before showing the failure card.

The first display open on a machine spent 12-18s starting its browser
carrier after the guest exec. Start it alongside the create request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): an explicit Retry gets its own quiet first-connection retries

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): run the guest display script test on the main actor

CloudGuestDisplayScript is @MainActor; the test called it from a
nonisolated context and broke the CmuxCloud package test build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): per-runner build roots only on fleet Macs without glaeda (#17239)

* test(ci): Blacksmith keeps the shared root; reruns keep a per-runner root (red)

Blacksmith macOS runners report RUNNER_ENVIRONMENT=self-hosted, so they got
per-runner roots: every build started cold and no seed could be adopted
(job 111334766861). take-product-canonical-root.sh also refused a product
built at a per-runner root on a fleet Mac without glaeda.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): per-runner roots only on fleet Macs without glaeda

canonical-build-root.sh derived /private/tmp/cmux-ci-<runner> for every
self-hosted runner without the glaeda helper, which included Blacksmith's
ephemeral macOS runners: their builds started cold and their seeds were
never adopted. Derive it only when the fleet directory exists. Let
take-product-canonical-root.sh accept such a root when glaeda isn't there
to hold it, so app-host test reruns build where the product was compiled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): repair main's guards, localization parity and cmuxTests compile (#17207)

* fix(ci): align owned-build-state guards with per-runner canonical roots

#17168 (496195b) derives a canonical root per self-hosted runner, exports
CMUX_CI_CANONICAL_ROOT from the build-slot step and drops the shared
/private/tmp/cmux-ci fallback in test-e2e's owned-state step. Three tests in
tests/test_ci_owned_build_state.py still assumed the old contract, turning
"CI fast guards" red on main.

Update them to the new contract: the slot step exports the root, a
per-runner root reads its own store, and a missing root reads nothing. The
owned-state step now fails with a clear message when the root is missing, and
rejects a root nested under a cmux-ci-* prefix (e.g. cmux-ci-a/../x), so the
looser glob cannot map the store outside the Mac's package directory.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(l10n): translate the New Machine plan loading strings

#17135 added machines.new.plan.loading, .retry and .error with the English
text copied into every locale, so `localization_catalog.py check` reports 24
parity errors and "Fast static checks" fails on every PR. Translate them for
all catalog locales; Retry reuses common.retry's wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(l10n): match the catalog's plan terminology in de, ko and zh-Hant

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tests): repair cmuxTests compile after the sidebar reorder change

#17070 moved RightSidebarModeBarDragLayout into the CmuxSidebar package,
but RightSidebarTabCustomizationTests still relied on the app module
exporting it, and CloudMachineOrderingTests put `try` calls inside an `&&`
in #expect, which the macro rejects ("operator can throw but expression is
not marked with 'try'"). Import CmuxSidebar and hoist the throwing lookups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tests): hoist the second throwing lookup out of #expect in CloudMachineOrderingTests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): satisfy dogfood-build runner guards and the Swift warning budget

#17206 turned dogfood-build into a macOS build, which tripped three guards:
no pinned Xcode, no fork branch in runs-on, and no static-preflight
dependency. Add `scripts/select-ci-xcode.sh`, the standard owner and fork-PR
branches ahead of the existing selector, and `needs: static-preflight`. The
job's `if` already excludes forks, so manaflow-ai PRs keep the same runner.

#17070's `RightSidebarModeBarDragController.coordinateSpace` is read from
a Sendable geometry closure and broke the zero-warning budget; it is a plain
String constant, so mark it `nonisolated`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): a root the glaeda hook exported wins over the per-runner root

Fails on main: canonical-build-root.sh treats an exported /private/tmp/cmux-ci
as unset and derives /private/tmp/cmux-ci-<runner>, so main-compile-probe
refuses the root glaeda placed it in (runs 37157423969, 37155786981).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): honor the canonical root the glaeda hook exported

#17168 (496195b) derives /private/tmp/cmux-ci-<runner> on every self-hosted
runner unless CMUX_CI_CANONICAL_ROOT names a non-default root. glaeda's
runner hook holds root 1 (/private/tmp/cmux-ci) for a compile job and
exports exactly that, so the job then built somewhere glaeda does not hold,
and main-compile-probe refused it: "glaeda placed this job in
/private/tmp/cmux-ci, not /private/tmp/cmux-ci-cmux13s-mac-mini-glaeda".

Let any exported root win. A self-hosted job with no exported root still
gets its per-runner root, so #17168's isolation for unplaced runners stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): an unplaced self-hosted job keeps the shared seeded root

Fails on the branch: canonical-build-root.sh derives /private/tmp/cmux-ci-<runner>,
whose fingerprint never matches main's seeds, so new aws runners
(aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4) compiled cold and timed out at
35 minutes in compile admission (run 37158561950, both attempts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): build unplaced self-hosted jobs at the shared seeded root

#17168 gave every self-hosted job without an exported root its own
/private/tmp/cmux-ci-<runner>. That root is part of the cache fingerprint,
so main's DerivedData seeds and the owned build state never match it, and the
prepare step clears it each job: every new aws runner compiled cold and hit
the 35-minute admission limit (aws-m4pro-9-glaeda-3, aws-m4pro-8-glaeda-4 on
run 37158561950).

Go back to /private/tmp/cmux-ci when no root is exported. glaeda's hook still
exports root N for the jobs it places, so those stay isolated. Unplaced
concurrent runners on one Mac share the root again, as before #17168, until
glaeda places those jobs too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): the pool picker never picks an owned label it was not configured with

Fails on main: simple_pool_picker adds every glaeda-<class>-xcode-* label it
sees on a runner, and glaeda-std-xcode-26.3 (ten aws EC2 runners, five per
Mac) sorts before the minis' 26.6, so compile admission ran there and timed
out at 35 minutes while the minis were idle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): pick only configured owned pools, not any owned-looking label

simple_pool_picker added every glaeda-<class>-xcode-* label it saw on an
online runner to the configured CI_OWNED_POOL_SLOTS pools, then tried them
in string order. The aws EC2 Macs carry glaeda-std-xcode-26.3 (ten runners,
five per Mac), which sorts before the minis' glaeda-std-xcode-26.6, so PR
compile admission landed on contended EC2 runners and timed out at 35
minutes while the minis sat idle. Use only the configured pools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(ci): the pool picker reads every page of organization runners

Fails on main: LiveState.runners reads one page of 100, but manaflow-ai has
509 runners and the first page holds the aws Macs and no idle mini, so the
picker never saw the mini fleet (run 37166910798 fell back to Blacksmith with
29 idle minis online).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): read every page of runners in the pool picker

LiveState.runners read only the first page of 100 organization runners.
manaflow-ai has about 500, and the first page holds the aws Macs but no idle
mini, so the picker never saw the mini fleet: it took the aws 26.3 label
while that was discoverable, and fell back to Blacksmith once it was not.
Read up to ten pages. Against live data the picker now sees 38 online
glaeda-std-xcode-26.6 minis (33 free) and picks them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: clear the Cloud sidebar warnings that broke the Swift warning budget

Compile admission on a mini (run 37167089830) built cleanly but failed the
zero-warning budget on three warnings from the Cloud sidebar work:
`SurfaceCatalog.shared` used as a default argument of two @MainActor
CloudWorkspaceSidebarPresentation entry points (evaluated nonisolated), and an
implicit `self` in CloudTreeOutlineView's machine-lift reopen closure.
Resolve `.shared` inside the main-actor body and make the capture explicit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): preserve canonical root and translation wording

* test(ci): align canonical root guard with main behavior

* test: use isolated catalogs in Cloud sidebar fixtures

Pass each test fixture catalog through the sidebar snapshot factory and direct presentation assertion so Cloud machine metadata is read from the catalog the test populated.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* test: model loading Cloud sidebar state

Keep device projections visible while their catalog rows are restoring, and assert that a reserved loading card shows machine identity before its directory appears after adoption.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* test: use a terminal panel for cloud directory settings

The sidebar detail test supplies a reported directory, so give it a real terminal panel instead of a loading card that correctly suppresses directory metadata.

Co-authored-by: Austin Wang <austinwang115@gmail.com>

* fix(cloud): restore device directory fallback lost in main merge

The merge of main took #17107's presentation file and dropped the
fallback from 27a8d804fdf, so a device projection whose catalog row is
still restoring rendered no directory and
SidebarCloudWorkspaceBadgeTests.deviceNameIsVisibleBesideItsDirectory
failed on b6ea6632cb1 (the regression run is that CI failure).

Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>

* ci: require a written merge override for red CI (#17217)

* ci: require merge-gate before merges

* Add API-only merge gate for pull requests

* test: cover merge-gate override decisions

* ci: wire merge-gate tests and push timing

* ci: pin merge gate workflow source

* Reject read-only override authors

* ci: harden merge gate runner and author checks

* ci: keep merge gate on hosted capacity

* ci: fail closed when collaborator lookup fails

* ci: reject stale successes during reruns

* ci: fail closed for untimestamped queued checks

* ci: fail closed for untimestamped pending statuses

* ci: verify matching override failure evidence

* test: cover mismatched override failure evidence

* ci: choose newest merge gate check run

* ci: require compile evidence for main-fix merges (#16993)

* test: reproduce main-fix merging without compile evidence

The installed helper bypasses all checks under --main-fix. The regression records that it merges with no compile checks present.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: require build evidence for main fixes

gh-merge-green --main-fix now requires successful Release, Debug and Swift test target build steps on the exact PR head. Existing Swift test failures are waivable only when their parsed issue records match the same Swift test step on the exact base SHA; the audit comment records each match and unrelated failures remain fatal.

CPU, memory and disk checks: the validator uses one bounded 90-second GitHub request per call, caps captured output at 32 MiB, writes audit bodies to temporary files, and does not retain logs, caches or stores.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ci: harden merge gate freshness and evidence

* ci: require per-check override reasons

* ci: complete merge gate review fixes

* docs(ci): document merge-gate rollout order

* ci: make merge helper rollout repairable

* ci: point merge helper refusals to repair runbook

* ci: resolve merge helper symlink for main fixes

* docs: remove stale main-fix PR reference

* fix: avoid pipefail false negatives in merge helper

* fix: keep merge gate events fresh per pull request

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Share one vCPU and memory pool across a plan's Cloud VMs (#17238)

* test(vms): reproduce missing shared Cloud VM resource pool

* feat(vms): share one vCPU and memory pool across a plan's Cloud VMs

Pro, Team (per paid seat), and Founder's Edition get up to 5 active VMs
sharing 20 vCPUs and 40 GB RAM, with machines up to the 32 GB xl row.
Max gets 80 vCPUs and 160 GB RAM with machines up to the 64 GB 2xl row.
The repository enforces the pool next to the active-VM count, under the
same transaction and billing lock, on create, Base open/reset, paused
resume, CPU/memory resize, and fork. Pricing, docs, app, and iOS copy
now describe pooled resources.

* Pooled VMs: Pro overrides stop at 32 GB; drop unused iOS pool strings

* test(vms): reproduce leaked compute claim and stale-plan resume pool

* fix(vms): give back unused compute claims and resume against the caller's pool

* docs(pricing): describe the Team pool per paid seat and localize pool strings in every locale

* Cloud VM: snapshot create honors Idempotency-Key (#17244)

* test(cloud-vm): snapshot create dedups by idempotency key (red)

A retry of POST /api/vm/:id/snapshot with the same Idempotency-Key must return
the first snapshot and take no second one; a retry while the first runs is
refused as in progress; the same key with another name is a conflict; a failed
attempt frees the key.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud-vm): snapshot create dedups by Idempotency-Key

POST /api/vm/:id/snapshot now reads Idempotency-Key. A new ledger table,
cloud_vm_snapshot_requests (one row per machine and key, additive migration
20261004120000), records a pending attempt before the provider call and the
provider snapshot after it. A retry with the same key returns the first
snapshot and records no second usage event; a retry while the first runs gets
409 vm_snapshot_in_progress (retryable); the same key with another name gets
409 vm_snapshot_idempotency_conflict; a failed attempt frees the key; a pending
row older than 15 minutes (route budget 600 s) is taken over by a retry.
Requests without a key behave as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(cloud-vm): smoke --snapshot-check proves snapshot idempotency

With --create, takes one snapshot of the throwaway smoke machine twice with the
same Idempotency-Key, requires the same snapshotId, deletes that snapshot,
then destroys the machine as before. No fixed VM id and no printed token: the
smoke mints its own throwaway user session.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Preserve selected tab when closing another surface (#16645)

* Preserve selected tab when closing another surface

* Only select the closing pane's tab when that pane is focused

BonsplitController.selectTab also focuses the pane, so calling it for an
unfocused pane moved focus into the pane where a tab closed, the focus theft
this change is meant to stop. Bonsplit already keeps the selection when an
unselected tab closes; the shouldCloseTab change is the actual fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* docs: detail tmux help options (#16780)

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test: guard managed contributor difficulty labels (#16448)

* test: guard managed contributor difficulty labels

* test: reject invalid difficulty descriptions

* test: exercise difficulty label sync requests

---------

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): repair main's Cloud app-host failures from #17132 and #17103 (#17250)

* test(cloud): expect the display helper readiness loop

#17132 replaced the one-line `list || exit 1` probe with a bounded
retry loop and updated the package test, but cmuxTests still asserted
the old line, so CloudDisplayCatalogTests.guestCommandShape failed on
main (run 37186487936, shard 4/7).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): make the restored-display retry test a desktop

#17132's initialDesktopFailureRetries configured port 6902 without a
resource ID, so CloudBrowserAccessState.isDesktop was false and
desktopConnectionDidChange ignored the failure: no quiet retry, and the
following navigations[1] trapped and crashed the shard 5 app host on
main (run 37186487936). Give it the .display identity additional
displays carry and require the retry before indexing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(cloud): a foreign display click shows one synchronous hint

#17103 made a display click on another machine's workspace reject
synchronously through showDisplayOpenHint, so no tree operation starts
and waitForOpen() waited out the 60 s suite limit on main (run
37186487936, shards 2 and 3). Assert no operation ran and that the
rejection is shown once; this stays red until the double-click stops
repeating the hint.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): show a foreign display's ownership hint once per double-click

AppKit delivers a double-click's first click to handleSingleClick,
which already shows the ownership hint, and #17103 also showed it from
handleDoubleClick, so the user got the same rejection twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cloud): never show upstream tree failures in the Machines status row

#17103 started presenting treeError verbatim so its ownership hints
would survive, but treeError also carries raw upstream failures
(LocalizedError descriptions, create output), so a URL with query
parameters reached the toolbar text, hover help and copy menu.
MachinesCloudStatusTests.emptyStatusHasNoProgressPresentation caught it
on main (run 37186487936, shard 7).

Hints now travel through their own onHint sink (falling back to
onFailure for other callers), the panel records them as trusted copy,
and the status row shows the tree error verbatim only when it is that
hint; everything else gets the safe recovery message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): keep merge-gate diagnostics on the exact PR (#17248)

* fix: keep merge-gate diagnostics on the exact PR

* fix: print repair guidance for gate diagnostics

* fix: allow merge-gate PR comments when permitted

* fix: reject mismatched merge-gate event identities

* fix: keep merge-gate alive on comment errors

* fix: keep the cmux-cua credential out of the Codex argv (#17252)

* test: codex wrapper must not put the cmux-cua credential in argv

The Codex wrapper passes the cmux-cua socket credential as
-c mcp_servers.cmux-cua.env.CMUX_CUA_SOCKET_AUTH_TOKEN=<value>, so any
local user can read it with ps. These assertions require the value to be
absent from argv, Codex to receive it in its environment, and the MCP
server to receive it through env_vars. The fake Codex now builds the MCP
server environment the way Codex does (allow-list + env_vars + env).

* fix: keep the cmux-cua credential out of the Codex argv

The wrapper passed the socket credential as a Codex -c env override, which
put the value in the codex process argv where any local user can read it
with ps. The wrapper now exports CMUX_CUA_SOCKET_AUTH_TOKEN in the parent
shell before exec and emits mcp_servers.cmux-cua.env_vars so Codex forwards
the variable by name to the MCP server (Codex env_vars, openai/codex#5246).
Codex's default shell_environment_policy excludes *TOKEN* names from agent
shell commands. Attachment stays fail-closed when no credential resolves.

* fix: keep codex wrapper overrides when the subcommand gets -c (#17257)

* test: codex wrapper overrides must survive subcommand -c flags

Codex declares -c/--config, --enable, and --disable as clap global
arguments. When a subcommand such as exec or resume also receives one,
Codex 0.159.3 keeps only the subcommand-level values, so the wrapper's
cmux-cua MCP config, hook config, and --disable computer_use placed
before the subcommand are dropped. The fake Codex now applies that rule,
and new cases cover exec, resume, mixed root and subcommand -c, and a
literal prompt after --.

* fix: keep codex wrapper overrides when the subcommand gets -c

Codex declares -c/--config, --enable, and --disable as clap global
arguments and keeps only the subcommand-level values when a subcommand
also receives one. The wrapper put its cmux-cua MCP config, hook config,
and --disable computer_use before the user's argv, so codex exec -c ...
or codex resume ... -c ... dropped all of them. The wrapper now moves the
user's subcommand-level global arguments, in order, in front of the
subcommand. Codex reads one root-level list with the same precedence as
before. Tokens after -- stay in place, and interactive launches without a
subcommand are unchanged.

* Add cmux browser repl: a Playwright-shaped browser REPL for agents (#17256)

`cmux browser repl` is a persistent JavaScript REPL that agents use to drive
cmux browser panes: Playwright page, locator, keyboard and mouse semantics with
native trusted input, budgeted accessibility snapshots with diffs, tabs,
cookie-bearing fetch, a sandboxed fs, named sessions, an MCP server mode and
site tools. Guards live outside agent code: fill-only secrets with redaction
and capture masking, a domain policy over every frame and fetch hop, a per-tab
clipboard for session-created tabs, private per-session temp directories and
bounded cells, fetches and timers. Agent work never moves the user's focus,
hibernated tabs wake on use, and crashed tabs report how to recover.

Squashed from https://github.com/manaflow-ai/cmux/pull/15570 (392 commits; the
CLA action cannot read more than 250 commits of one pull request). Same tree as
that branch's head.

* Cloud: VM file operation routes (port of #16936) (#17254)

* Cloud: VM file operation routes (port of #16936), missing path answers 404

Ports the web part of https://github.com/manaflow-ai/cmux/pull/16936
(feat-cmux-next) to main: /api/vm/[id]/fs/[operation] (list, read, stat,
write, mkdir, remove) with the Freestyle driver and gateway methods.

Includes the fix from feat-cmux-next: Freestyle removes a missing path with
success, so removeVmFile stats first and answers a missing file with
404 vm_file_not_found (any other stat failure, a missing VM included, stays a
provider failure).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cloud): stat/read/dir of a missing VM path must answer vm_file_not_found

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): stat/read/dir of a missing VM path answer 404 vm_file_not_found

The staging rehearsal of #17254 showed stat of a removed file answering 502
vm_cloud_service_unavailable. Map the guest ENOENT on every file read, as
remove already did, and title the error 'File not found'.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Stop calling the legacy Subrouter during account deletion (#17273)

* test: account deletion must not call the retired legacy Subrouter

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Stop calling the legacy Subrouter during account deletion

The legacy Subrouter at subrouter.cmux.dev is being retired. Account
deletion now skips the legacy revoke phase and needs no legacy env vars.
Local mapping rows are still deleted, and a legacy_delete_pending
tombstone from an older deployment resumes at the hosted checkpoint.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* Split the account DELETE handler under the complexity limit

DELETE delegates to deleteAccount and named phase helpers that share one
progress record, so its complexity drops from 46 to under 20 and its
grandfathered baseline entry is removed. Behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* ci: remove merge gate and restore exact-head merging (#17275)

* Cloud: private network routes (port of #16948) (#17255)

* Cloud: private network routes (port of #16948), missing firewall rule answers 404

Ports the web part of https://github.com/manaflow-ai/cmux/pull/16948
(feat-cmux-next) to main: /api/vm/firewall (list, get, create, delete),
/api/vm/network and /api/vm/tunnel/network/[operation], with the Freestyle
driver, gateway and private-network workflow pieces.

Includes the fix from feat-cmux-next: a firewall rule that is not in the
caller's network answers 404 vm_firewall_rule_not_found (a provider 404 race
on delete too); a missing VM endpoint stays vm_not_found.

Stacked on the file-routes port (#17254).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(cloud): move firewall endpoint parsing into services/vms/firewallEndpoint

No behavior change; makes the parser testable without the route.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cloud): firewall must accept normal CIDR prefixes and team-owned VM endpoints

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): firewall accepts normal CIDR prefixes and team-owned VM endpoints

The staging rehearsal of #17255 found two defects. validCidr compared the
prefix with net.isIP(), which returns the family (4 or 6), so any IPv4
prefix above /4 was refused; it now uses canonicalCidr. The vmId ownership
check looked the VM up in the personal scope, but every new VM is
team-owned, so vmId endpoints were vm_not_found; the route now resolves the
account scope when a vmId is named, and the VM must be the caller's own
(the firewall edits the caller's network). The provider now gets only the
rule fields: the Freestyle driver spreads its input into the request body,
so userId and provider were sent to Freestyle.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cloud): firewall rules must name a caller resource as destination; get/delete must find VM rules

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): decide firewall rule ownership on the shared provider account

The Freestyle account is shared by every cmux user, so the API decides
whose a rule is. Reading and deleting: the rule names at least one resource
and every resource it names is the caller's. get and delete now read the
rule by id; they searched only the network listing, so a rule that named a
VM and a CIDR was created (201) and then could not be found or deleted. The
list merges the network listing with one listing per caller VM. Creating:
the destination must be a caller resource (400 vm_invalid_firewall_rule),
because a destination of only an address range or the public Internet would
reach other tenants' machines. Every firewall call resolves the account
scope like the other VM routes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cloud): firewall refuses unknown endpoint fields as owned and sends canonical CIDRs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): unknown firewall endpoint fields are never owned; send canonical CIDRs

Security review P2: the provider adds selectors as new optional fields, and
an unknown one could name another tenant's resource, so a rule with an
unknown endpoint field is not the caller's. Review P3: send the canonical
range so a valid non-canonical CIDR does not fail at the provider.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): title vm_firewall_rule_not_found 'Firewall rule not found'

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(cloud): firewall needs a 100-rule cap, a bounded list, and a per-user rate limit

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(cloud): cap firewall rules at 100, bound the unfiltered list, rate-limit mutations

These routes are new on a provider account shared by every cmux user.
Create refuses at 100 owned rules (409 vm_firewall_rule_limit). An
unfiltered list reads the network plus at most the 10 newest live VMs, one
provider call each; older VMs list with ?vmId. Create and delete are
throttled per user with the Vercel firewall rule CMUX_VM_FIREWALL_RATE_LIMIT_ID
(no other VM mutation route has a limiter, so this follows the team-invite
limiter: fail closed when the firewall is unavailable, fail open and report
when the rule is unset or removed).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* remote-tmux: stop a torn-down control stream from feeding the reconnected one (#16897)

* remote-tmux: failing test for a torn-down stream feeding the next one

A control stream torn down for a reconnect keeps delivering what its reader
had already buffered. The test holds the main actor while a first client
writes 560 KB, starts a reconnect, and expects none of those bytes to reach
the connection.

* remote-tmux: stop a torn-down stream from feeding the next one

Cancelling the task that reads a control client's stdout does not empty the
reader's buffer, so chunks the old client had already written were still
ingested after the teardown. Once the reconnect had respawned, a leftover
command result was taken for the new client's attach reply. The connection
then never asked for windows and the mirror stayed blank for good.

Each read loop now stops as soon as its process generation is no longer the
current one, and closes its reader.

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>

* remote-tmux: keep a window whose Dock has panels when its mirrors move out (#17237)

* remote-tmux: failing test for a docked terminal closed when its window's mirrors move

* remote-tmux: keep a window whose Dock has panels when its mirrors move out

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>

* Expose the workspace task-status lane to custom sidebars (#17245)

Custom sidebars could not read a workspace's task-status lane. cmux already
resolves one per workspace and the control socket can pin it, but the
interpreter data context carried no field for it, so a sidebar had no way to
group or colour rows by whether a workspace needs attention.

`workspaces[i].status` now carries the resolved lane as its raw wire value:
todo, working, needs-attention, review or done. The snapshot takes it as a
required parameter so a dropped wiring breaks the build rather than reporting
a silent "todo".


Claude-Session: https://claude.ai/code/session_0113SqtxGQwjHjzFw8mkSgwU

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Clear restored agent notifications once the agent is gone (#17067)

* test: prune read notifications of agents that died with the previous app

An agent alive at quit dies without SessionEnd, so its last "Completed in"
notification is restored on every launch and shown as the workspace's
latest sidebar summary even though no agent is running. The stale-agent
sweep must drop it once the pane has no agent again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: drop restored agent notifications once the agent does not return

Notifications persist across relaunch so an unseen agent result is not
lost, but an agent that was alive when cmux quit dies without SessionEnd.
Nothing clears its notification afterwards: agent PIDs are not restored,
so the 30s stale-PID sweep has no dead PID to catch.

Restore now records the notifications of local panes that hosted an agent
(resume binding or restorable agent snapshot). The stale-agent sweep
removes the read ones when the pane has no agent PID again; unread ones
survive until read, and a pane the agent resumes into is handed back to
its hooks. Remote terminals are skipped since their agent can outlive the
app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: keep restored notifications while the resume is in flight

Covers a read notification posted after restore (never tracked) and a
pane whose restored resume has not reported an agent PID yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: defer restored notification prune while the resume is in flight

The 30-second sweep can run before an auto-resumed agent reports its PID.
Skip panes whose restored command is still in flight, using the
coordinator's existing ownsInFlightRestoredCommand contract. Also look up
tracked notifications by id instead of scanning the store per panel, and
mark the value-only snapshot helper nonisolated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: keep read non-agent notifications on a restored agent pane

The restore tracks every notification persisted on a pane that hosted an
agent, so a read `cmux notify` banner on that pane is pruned with the
agent's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: track only agent-produced notifications on restored agent panes

Use the persisted `isAgentEvent` provenance instead of panel ownership so
a `cmux notify` banner on a pane that hosted an agent is not retired with
the agent's result. Unknown provenance restores as agent-produced, matching
TerminalNotificationStore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Send sidebar links through the external-open rules (#7397)

* browser: apply external-open rules to sidebar links

The sidebar's pull-request and port links (SwiftUI and AppKit rows, and
the open-all-pull-requests action) opened in the embedded browser whenever
that preference was on, without consulting the URL rules that route a site
to the system browser. Sites listed there cannot work in the embedded web
view at all, so a rule now wins over the embedded preference on those
paths, the same way it does for a click inside a page.

* browser: require a user event before a link escapes to the system browser

WebKit reports a script calling click() on an anchor as .linkActivated,
the same as a real click, so the external-open rules on their own let a
page hand itself a system-browser open at a moment of its choosing. The
navigation-typed escape now also requires an AppKit event in flight (a
key, left-mouse, or middle-mouse event) and never intercepts a download,
on every path that consults the rules: the main navigation delegate, the
target=_blank UI delegate, and both popup delegates. The context menu's
Open Link in New Tab is a gesture by construction and says so.

The event check is a bound rather than a proof: NSApp.currentEvent says
an event is being dispatched, not that this navigation is the thing the
user asked for. Middle-clicks arrive as otherMouse events and count.

* browser: e2e coverage for external-open link routing

BrowserExternalOpenRoutingUITests drives real WebKit link activations
through the socket browser.click against a local fixture and asserts
routing at the delegate layer, where popup-vs-link-activation behavior
actually diverges and unit tests cannot reach. Escapes are captured to a
file through the existing DEBUG-only UI-test sink
(CMUX_UI_TEST_CAPTURE_EXTERNAL_OPEN_PATH) from the external-navigation
handler's default opener, so CI never opens Safari. Four cases: a matched
link click escapes while the embedded page stays put; an unmatched click
navigates embedded; a scripted window.open to a matched host never
escapes; a target=_blank form POST to a matched host stays embedded.

The shared BrowserFixtureSocketTestCase gains subclass hooks for launch
arguments and environment, falls back from the in-process socket client
to nc -U and then the bundled cmux CLI, disables hidden-webview discarding
for the backgrounded UI-test host, and polls browser.wait through the
cold-start content-process transient.

* browser: click links for real in the external-open UI tests

A link now leaves for the system browser only while a real input event
is in flight, and the socket browser.click runs JavaScript, so the matched
and unmatched link cases click through accessibility the way a person
does. The scripted popup and form cases keep the socket click, since a
scripted action is what they test.

* browser: keep only the sidebar links, drop the in-page activation change

The in-page half of the external-open rules has landed separately. What remains here is the sidebar: pull-request and port links follow the rules, with tests for the matcher.

* browser: use a rule the pattern safety check accepts in the port-link test

---------

Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ci: require merge checks only when their workflows exist (#17284)

* test: cover merging repos without aggregate CI workflow

* fix: make merge checks conditional on base workflows

* ci: fall back to ancestor evidence for --main-fix (#17277)

* ci: use nearest ancestor for main-fix evidence

* ci: constrain ancestor evidence to path-filtered changes

* ci: inspect renamed paths in ancestor evidence

* ci: bound ancestor evidence traversal

* ci: reject incomplete ancestor path comparisons

* fix(session): sweep stale scrollback replay files (#16056)

* test(session): cover replay sweep and permissions

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): sweep stale scrollback replay files

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): sweep stale replay files before restore

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): remove synchronous replay sweep from app init

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): gate restore on off-main replay cleanup

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(app): keep replay sweep off startup critical path

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>

* fix(session): skip replay sweep under XCTest

* fix(session): harden retained replay files during stale sweep

* docs(session): keep crash-recovery gate semantics accurate

* test(session): cover legacy replay permissions and sweep filters

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Cover dotted Claude project dir in session directory search (#4939)

* docs: clarify Claude project dir decode asymmetry

* test: cover dotted Claude project dir in session directory scope

* test: scope Claude session roots per task instead of process env

Swift Testing runs suites in parallel, so setting CLAUDE_CONFIG_DIR process-wide could leak into other tests. A DEBUG-only TaskLocal override keeps the fixture root local to the test's task tree.

* test: cover Claude cwd-filter lookup without a DEBUG seam

Widen the Claude candidate enumerator and its two types to internal so the test reaches them through @testable import, per the no-test-debug-seam review rule. The test checks .claude/worktrees and .worktrees cwds and that dot-preserving and other project dirs are excluded.

---------

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Answer the tmux session commands Claude Code calls (#13632)

* Answer the tmux session commands Claude Code calls

Claude Code's tmux backend tears down and reattaches its agent panes with
kill-session, switch-client, new-session -A and show-options -g prefix. The
compatibility layer rejected all four, so a Teams session failed with
"Unsupported tmux compatibility command" once it got past creating panes.

A tmux session is a cmux workspace, which has-session and new-session already
assume, so kill-session closes that workspace, switch-client selects it, and
new-session -A attaches to it when it exists instead of creating a duplicate.
show-options now answers from a table, and prefix reports the C-b that a
default tmux client would.

The sequence test drives all four through the real shim. Its fake socket also
now unwraps the capability envelope that the shell integration adds inside a
cmux terminal, so the test reports the behavior it checks rather than a JSON
decode error; that unwrapping moved into the shared helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Reject a socket payload that is not a request

The fake servers indexed request["method"] straight off json.loads, so a
payload that decoded to null, a list, or an object with a non-string method
raised inside the handler thread and surfaced as an unrelated CLI error. They
now answer those with an error line, which names the real problem.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test: reject empty fake socket request methods

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: reject a failed tmux session lookup and kill-session -a

A workspace.list error must not become a new session, and kill-session -a
must not close the caller.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep a failed tmux session lookup from creating a workspace

new-session -A treated every resolution error as a missing session, and
kill-session ignored -a and closed the caller. A missing session still
creates; a lookup failure and an unsupported flag now fail first.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix custom-sidebar nil-comparison so optional-guarded views render (#7943) (#7974)

* Add failing test: sidebar nil-comparison yields nothing (#7943)

In a custom sidebar, `x != nil` / `x == nil` against a bound optional field
does not evaluate to true/false — it evaluates to nothing. Interpolation
renders empty, ternaries always take the else branch, and `if x != nil`
guards are never taken, so optional-guarded views never render.

This commit adds only the regression test (no fix) so CI shows it red.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix sidebar nil-comparison to evaluate to a Bool (#7943)

The interpreter's value model had no null case and no `nil`-literal
evaluator branch, so `nil` evaluated to a host `SwiftValue?` of `nil` — the
same value that means "expression unsupported / no value". `evalInfix` then
bailed on the comparison, so `x != nil` / `x == nil` produced nothing:
interpolation rendered empty, ternaries always took the else branch, and
`if x != nil` guards were never taken. Optional-guarded views (including the
shipped status-board.swift / finder.swift examples) silently drew nothing.

- Add `SwiftValue.null` for the `nil` literal and for comparing an absent
  optional field against `nil` (distinct from host `nil` = "no value").
- Evaluate `NilLiteralExprSyntax` to `.null`.
- Handle `==` / `!=` before the operand guards, coalescing an absent operand
  (host `nil`) and the `nil` literal to `.null`, so the comparison yields a
  Bool that is true/false when present and false/true when absent.

Fixes #7943

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: preserve nil comparison evaluation failures

* fix: preserve nested nil comparison misses

* fix: preserve parenthesized nil comparison misses

* fix: handle nil optional binding and equality budget

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Leo Li <cheerleaderleo@outlook.com>

* fix: refresh merge helper and honor neutral checks (#17294)

* test: cover neutral checks and helper checkout refresh

* test: tolerate absent git diagnostics

* fix: refresh clean main checkout before merging

* test: cover cloud welcome close shortcut ownership

* fix: route cloud welcome close shortcut to its window

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Leo <cheerleaderleo@outlook.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: BlueRaddish <jeeholife2@gmail.com>
Co-authored-by: EJ <ej@campbell.name>
Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-authored-by: Philipp Mochine <philipp@mochine.de>
Co-authored-by: mys <wowpotato@naver.com>
Co-authored-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Sungho Park <relilau00@gmail.com>
Co-authored-by: Darío Kondratiuk <dariokondratiuk@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mark Xian <mark-xian@foxmail.com>
Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com>

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Leo <cheerleaderleo@outlook.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Co-authored-by: BlueRaddish <jeeholife2@gmail.com>
Co-authored-by: EJ <ej@campbell.name>
Co-authored-by: ejc3 <ejc3@users.noreply.github.com>
Co-authored-by: Philipp Mochine <philipp@mochine.de>
Co-authored-by: mys <wowpotato@naver.com>
Co-authored-by: Alejandro Florez <soyeladice@gmail.com>
Co-authored-by: Sungho Park <relilau00@gmail.com>
Co-authored-by: Darío Kondratiuk <dariokondratiuk@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Mark Xian <mark-xian@foxmail.com>
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.

1 participant