Skip to content

Merge upstream cmux main @ 4e9d779888 (2026-10-01) - #61

Merged
rajinsyed merged 529 commits into
mainfrom
sync-upstream-2026-10-01
Oct 3, 2026
Merged

rajinsyed merged 529 commits into
mainfrom
sync-upstream-2026-10-01

Conversation

@rajinsyed

@rajinsyed rajinsyed commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Merges 259 first-parent upstream cmux commits (upstream main 2026-09-29 → 2026-09-30, through 4e9d779888). The headline upstream change is shared terminal sizing: a terminal viewed from the Mac, an iPhone, or another Mac through Devices now has one grid, sized by default to the smallest viewer, with bounds drawn in the divider color and Connected Devices… on iOS. User-facing notes and the one open decision are in SUPERMUX-UPGRADES.md (2026-10-01 section).

Conflicts

25 files conflicted, resolved per SUPERMUX.md: upstream's code plus the re-applied SUPERMUX fences. Most were mechanical (a fork line next to a new upstream argument, dependency, enum case, or CI if:). The ones that needed judgment:

  • Notification history encoding. Upstream added isAgentEvent to NotificationFeedHistoryRecord, whose fork fence carries explicit CodingKeys/init(from:). Without adding the key there, the flag would be silently dropped from persisted history; it is now encoded and decoded.
  • Workspace close (--force). Upstream added a running-process confirmation to the socket, control and mobile close paths. It stays upstream's and runs before the fork's empty-home close; in TerminalController+MobileWorkspaceList.swift the fork fence is split in two around it.
  • iOS native scroll. The fork's enqueueScrollMechanicsDelta keeps its structure and now applies upstream's gridDisplayScale, matching upstream's own delta path for grids scaled to fit.
  • iOS Feed vs. Notifications. Upstream's new agent Feed hides the Notifications tab by default, which would hide the fork's project-aware notification rows. A new fence (#601) keeps the Notifications tab visible beside the Feed; recorded as an open decision.
  • New upstream switch. TerminalCopyAction.swift (new upstream file) switches exhaustively over the built-in actions; a fence (#600) adds .newClaudeHarness.
  • SidebarWidthPolicyTests.swift moved to Swift Testing upstream; the right-sidebar minimum-width fences are re-expressed as #expect. RemoteDaemonRPCClientTimeoutIsolationTests.swift: upstream rewrote the second test without a PTY attach, so only the first test keeps the fork's queue fence.
  • Localizable.xcstrings and project.pbxproj: the clone's installed merge drivers are older copies and bailed, so both were merged manually (per-key union with upstream's current driver; union of added entries, then normalize-pbxproj.py + check-pbxproj.sh). All 172 fork-added and 254 upstream-added pbxproj ids are present.

Registry

Follow-up commits

  • fix(supermux): restore upstream catalog entries the previous merge left stale: the 2026-09-30 merge kept older wording for 18 upstream-owned strings (for example "Couldn't reach Stack" on sign-in errors) and re-added 21 keys upstream had deleted. Every non-supermux. key now equals upstream except the iOS: redial at once after the Mac's idle timeout instead of probing the dead session #84 exception.
  • docs(supermux): upgrade notes for the 2026-10-01 upstream merge.

Testing

  • Tagged Mac Debug build: CMUX_DEV_BACKEND_MODE=local CMUX_RELOAD_NO_GLOBAL_CLI_LINKS=1 CARGO_PROFILE_RELEASE_BUILD_OVERRIDE_STRIP=false ./scripts/reload.sh --tag sync-up succeeded (merge commit; not launched).
  • iOS simulator build: xcodebuild -workspace ios/cmux.xcworkspace -scheme cmux-ios -configuration Debug -destination 'platform=iOS Simulator,name=iPhone 18 Pro' build succeeded.
  • ./scripts/test-unit.sh build-for-testing (compile only, no tests run): the app target, including the restored catalog, compiles, and all 1,199 cmuxTests files were compiled with no errors in fork-touched files. The test target still fails on five files that are byte-identical to upstream main (CLIVMTransferTests, CloudWorkspaceLiveProjectionTests, LastSurfaceClosePreferenceTests, PaneResizeShortcutTests, WorkspaceCloseTabsContextMenuTests). That is upstream's own break at 4e9d779888, with open fixes upstream (test: restore cmuxTests compile on main manaflow-ai/cmux#16285, #16245, #16306); the next merge should pick it up.
  • swift test: SupermuxKit 1057/1058 in the full run (the timing-sensitive closeRetainsEscalationUntilAnIgnoringProcessIsKilled failed while the Mac build loaded the machine; its suite passes 16/16 alone), SupermuxMobileCore 88/88, SupermuxMobileKit 225/225, SupermuxMobileUI 205/205.
  • CI guard scripts that read the edited workflows pass: test_ci_guard_workflow_structure.py, test_ci_workflow_guards_are_wired.py, test_ci_reusable_workflow_permissions.py, test_ci_workflow_path_filter_parity.py. test_ci_change_areas.py fails the same way on a pristine upstream tree here (environmental).

Not verified: app-hosted suites (not run locally by policy), any live behavior in the tagged Mac app or on a phone. Worth a dogfood pass: iPhone scrollback on a shared grid larger than the phone (the fork's bounded scroll geometry still uses the unscaled cell height), the Feed and Notifications tabs together, and a Devices mirror resizing its source terminal.

Changelog

Changed: Supermux now includes upstream cmux through 2026-09-30, including shared terminal sizing across Mac, iPhone and Devices mirrors, Connected Devices on iOS, and the iOS Feed (shown beside Notifications)

Review in cubic

lawrencecchen and others added 30 commits September 30, 2026 05:32
The main merge kept this branch's older pointer; main's sources need
BonsplitContrastPalette and TabPresence from the newer one.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test: cover SwiftPM warning in package lane tolerance

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

* fix: ignore warning text in package lane error check

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

---------

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

* Improve setup prerequisite errors

* Tighten GhosttyKit lock handling

* Clarify Xcode setup preflight

* Sanitize setup error output

* Print sanitized lock mkdir errors

* Register GhosttyKit lock before cleanup

* Harden GhosttyKit lock diagnostics

* Move the Xcode preflight ahead of setup mutations

Main now preflights the Metal toolchain before any setup mutation, and it
already stops a Command Line Tools-only machine. Run one xcodebuild -version
check first so a missing Xcode or an unaccepted license gets a direct message,
and drop the developer-directory path glob, which could reject valid Xcode
installs.

Co-authored-by: archit-goyal <go4archit@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Retry when a stale GhosttyKit lock disappears

---------

Co-authored-by: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The subnav read teams from the Hexclave SDK cache, which the dashboard's
own team mutations never refresh, so a deleted team stayed in the Settings
subnav beside every team page. It now reads the team catalog query that
create, rename, leave, and delete already invalidate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test: cover current base ref for registry guard

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

* fix: resolve registry guard base ref at runtime

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>
The current card said "$50/mo" while the others said "$50 per month".
The picker now takes the subscription's Stripe price as data and renders
every card as amount + "per month" (per seat for Team), adding "billed
annually" for yearly prices.

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

* test: cover popover closes whose didClose never arrives

reaches popoverWillClose but never popoverDidClose. The click-away test
simulates that fault and fails on main: nothing bounds the close, so the
presenter stays closing with isShown true and the next toggle cannot
present a new popover. A second test fails the same way and pins that a
repeated willClose keeps the first deadline. The toggle-close test pins
that a programmatic close is never reported as an external dismissal,
whether didClose or the deadline ends it.

The presenter takes the clock its close deadline will run on, so the
tests advance a manual clock instead of sleeping; SidebarTestManualClock
gains a sleeper count for deadline-bounded polls.

Also cover the checklist section restoring close animation when its
popover survives an anchor reparent.

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

* fix(sidebar): finish popover closes whose didClose never arrives

#14895 found that on some owned Mac minis an animated NSPopover close
reaches popoverWillClose but never popoverDidClose, and fixed only the
anchor-detach close by turning its animation off. An ordinary user close
(click-away, Esc, toggle) still animates, so on those hosts the presenter
stayed closing with isShown true: the next toggle closed the stuck popover
again instead of presenting, and a click-away was never written back.

popoverWillClose now arms a one-second deadline, the close fade plus a
margin, on a MainActorDeferredActionScheduler. A repeated willClose for
the same popover keeps the first deadline. If didClose has not arrived by
then, the presenter abandons the stuck popover (drops its delegate,
closes it without animation, orders its window out) and runs the same
completion didClose would. The next popover is new, animates normally, and
gets its own hosting controller so a late teardown of the abandoned one
cannot take its content view. Notifications from an abandoned popover are
ignored. Animation stays on everywhere else.

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

* fix(sidebar): re-present cancels a pending close fallback; drop test seams

Showing a hidden popover again now cancels the close fallback armed by
its earlier willClose, so the stale deadline cannot abandon the popover
that is visible again. Tests read the presenter's popover through
@testable import instead of DEBUG-only accessors.

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

* dogfood: tour the checklist popover's click-away and toggle closes

Opens the checklist popover from the sidebar summary line, closes it by
clicking away and by toggling, and reopens it after each close, so the
PR media shows the presenter never stays stuck closing.

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

* fix(sidebar): re-present also resets the programmatic-close flag

A programmatic close superseded by a re-present no longer marks the
next click-away as programmatic. The re-present test now checks the
fallback was cancelled, the reparent test closes its popover, and the
dogfood tour clicks away farther from the popover.

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

* fix(sidebar): ignore a superseded close's late didClose after re-present

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

* dogfood: click the checklist summary line by position

The summary line is not reachable by identifier from the UI test, so the
tour clicks where it draws and records the sidebar tree for diagnosis.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Add forward-only submodule CI guard

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

* fix: route the submodule guard job through the runner variables

The new workflow-guard-submodule-forward-only job pinned
runs-on: ubuntu-24.04, which the repo-variable guard rejects: runner
choice has to stay a repo-variable flip so Blacksmith and the paid
overflow pool can be swapped without editing workflows. Use the same
expression the five sibling jobs in this file already use, which also
keeps fork pull requests on a hosted runner.

One defect, three red checks: CI fast guards,
guards / workflow-guard-tests / ci and
guards / workflow-guard-tests / preflight all failed on this single line.

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

* Fix submodule guard comparison and routing

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

* Use shallow synthetic parent for submodule guard

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

---------

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

* test: pay the first-exec check for fixture executables before timing them

macOS 26 blocks the first run of every newly written executable, a copy
included, while syspolicyd assesses it (Gatekeeper scan, notarization
lookup, XProtect): 0.15-0.4 s on an idle Mac, seconds on a loaded fleet
mini. The Claude wrapper tests wrote fresh fakes for every case and ran
them first inside the wrapper's own budgets, 1 s for the hook settings
generator and 0.75 s for the claude --help probe. When the first-run check
ate the budget the wrapper fell back to minimal hooks or cached an empty
catalog, and the lane failed with a different message each time: 12 of 45
glaeda runs, 0 of 138 Blacksmith runs. The mutual shim test ran fresh
wrapper copies and shims first inside each 5 s guard the same way.

Every fixture now exits at once under CMUX_TEST_PRIME_EXEC and is run once
that way when written, as #15768 did for the Hermes fixtures. No budget or
guard changes. The cancellation case records a failure instead of raising
ProcessLookupError when the wrapper exits before the interrupt, so one bad
case no longer hides the rest of the results.

Refs manaflow-ai/cmux#15488

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

* test: prime the per-run wrapper copies in the Claude hooks test

Review follow-ups:
- run_wrapper and the env and auth probes copy the wrapper to a fresh path
  on every call, and several checks time that copy's first exec under a 2 s
  or 5 s process timeout. Prime each copy with PATH=/usr/bin:/bin, where
  the wrapper finds no claude and exits before writing anything; the
  runner's PATH could reach a real claude.
- The cancellation check waited only for the help PID log to exist, but
  the shell creates it before printf writes both PIDs; wait for both lines.

Refs manaflow-ai/cmux#15488

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…anup failure (#15958)

* test: a receipt whose PID another executable reused must read as stale

App-host receipts stay on disk after the host exits, and a shared runner
can reuse that PID within minutes: in full-suite run 36680987910 shard
5/7 the PID space wrapped twice during the job. The new case gives the
receipt's PID to /bin/sleep, which does not hold the receipt descriptor,
and requires cleanup to succeed without authorizing or signaling it. On
the current script it fails with the CI message: app-host receipt does
not match the PID executable vnode.

Refs manaflow-ai/cmux#15488

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

* fix(ci): treat a reused app-host receipt PID as a stale receipt

cmux_verify_app_host_receipt failed hard when the receipt's PID was alive
but ran another executable, while the same file already treats that as
exited elsewhere. The receipt descriptor is O_CLOEXEC, so no exec keeps
it: a PID that runs another executable and does not hold the receipt is a
new process. Such a receipt now verifies as stale (2), which authorizes
and signals nothing; a PID that still holds the receipt under another
executable still fails, and the live-target scan still fails on any app
host without a verified receipt. Shard 5/7 of run 36680987910 failed
cleanup this way after all 711 tests passed.

Refs manaflow-ai/cmux#15488

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

* fix(ci): count a receipt PID as reused only when lsof confirms it

The reuse check took any failure of the receipt descriptor check as
proof, including lsof failing or returning malformed data, so an
uninspectable PID under another executable read as a stale receipt
(review on #15958). The descriptor check now returns 3 when a live PID
confirmably does not hold the receipt (lsof exit 1), and only that or an
exited PID counts as reuse; every other inspection failure still fails
cleanup. A new case makes lsof fail on the receipt query for a reused PID
and requires cleanup to fail; the looser check passes it as stale.

Refs manaflow-ai/cmux#15488

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

* fix(ci): require lsof to list a PID before calling its receipt absent

lsof exits 1 for an error as well as for a search that found nothing, so
an empty filtered query did not show that a live PID lacks the receipt
(review on #15958). The descriptor check now returns 3 only when lsof
also lists the PID's open files and none is the receipt descriptor; an
exit 1 without that listing stays an inspection failure. A new case makes
both lsof queries exit 1 for a reused PID and requires cleanup to fail;
the previous check passed it as stale.

Refs manaflow-ai/cmux#15488

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

* test(ci): cover receipt checks that must not read as PID reuse

Three cases the receipt-absence check got wrong:
- lsof names the PID but lists no open files, which proves nothing;
- the receipt is open on a descriptor other than the recorded one, so the
  PID still holds it;
- the reused PID exits between the receipt query and the listing, which is
  an exit, not an inspection failure.

Refs manaflow-ai/cmux#15488

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

* fix(ci): read receipt absence only from a full listing of the PID

- Count the receipt as held on any descriptor, and require the listing to
  name at least one open file before it shows the receipt is gone.
- Return "exited" when the PID dies between the two lsof queries.
- Report lsof's diagnostic when the reuse check cannot decide, and log
  each receipt it skips as stale.
- Merge the two comments that described different return codes.

Refs manaflow-ai/cmux#15488

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

* fix(ci): keep lsof's diagnostic when listing a receipt PID fails

The reuse check already prints what it captured when it cannot decide, so
the listing no longer throws lsof's own error away.

Refs manaflow-ai/cmux#15488

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

* test(ci): drive the recovery verifier with a reused receipt PID

Removing the reuse check from cmux_verify_stale_app_host_receipt left every
case green. Recovery reads receipts an earlier job left, where PID reuse is
likeliest, so cover it: the receipt reads as stale and the PID is not
signaled. Without the check the case fails with the vnode mismatch.

Refs manaflow-ai/cmux#15488

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* test: require a pinned agent-chat CI type check

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

* ci: type-check agent-chat with pinned TypeScript

Pin TypeScript 5.9.2, invoke the installed compiler, and run a separate preflight type check after a single frozen install. Include DOM.AsyncIterable for the existing ReadableStream iteration.

Builds on manaflow-ai/cmux#12351 and manaflow-ai/cmux#12201.

Co-Authored-By: Austin Wang <austinwang115@gmail.com>
Co-Authored-By: Somesh Lingwal <someshlingwal1@gmail.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>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
Co-authored-by: Somesh Lingwal <someshlingwal1@gmail.com>
…g the Release build (#15944)

* test: a stalled cmux-tui client download must not hang the installer

Serve the manifest from a local TLS server that answers and then sends
nothing, which is what a dead HTTP/2 stream looks like to curl, and
require the real installer and curl to give up within the attempt
budget. Without a stall bound the installer waits until a 60 s watchdog
kills it; in CI run 36685498203 the same stall held the Release build's
helper install for 46 minutes until the job timed out.

Refs manaflow-ai/cmux#15488

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

* fix(ci): bound each cmux-tui client download attempt by progress

curl's --retry had no connect timeout or low-speed limit, and its
retries reuse the stalled connection, so a dead HTTP/2 stream held one
attempt for about twenty minutes. The Release build of full-suite run
36685498203 spent 46 minutes in Install Release helpers and was
cancelled at its 60 minute limit, failing macOS status. Give each
attempt its own curl process with a connect timeout, a 60 s stall
limit and a 10 minute ceiling, as download-with-retry.sh does.

Refs manaflow-ai/cmux#15488

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

* fix(ci): reject malformed download budgets and bound the stall-server wait

Validate CMUX_TUI_CLIENT_DOWNLOAD_ATTEMPTS and
CMUX_TUI_CLIENT_DOWNLOAD_STALL_SECONDS as positive integers with a clear
error, and wait for the test's stall server by deadline with an explicit
failure instead of an iteration count (review on #15944).

Refs manaflow-ai/cmux#15488

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

* fix(ci): drop the per-attempt time cap and assert why a stalled download ends

Review on #15944. The 600 s ceiling restarted each 40 MB slice from byte
zero, so a slow but moving developer download that used to finish now
failed; the stall bound alone ends a dead stream. A slice download that
fails every attempt exits instead of falling through to a misleading
sha256 mismatch, and a malformed budget names its variable and value.
The stall test now requires curl's stall error (28) on both attempts, the
installer's 'after 2 attempts' error and two server connections, so an
early failure for any other reason no longer passes it.

Refs manaflow-ai/cmux#15488

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#14302 changed `MobileAuthComposition.tokenStore` to return `.memory`
instead of `.none` when the bundle identifier does not resolve, so
authenticated operations no longer trap inside the Stack SDK. The
assertion in `missingAppIdentityCannotPersistTokens()` still required
`.none`, so the iOS simulator lane fails on main and on every pull
request that routes it.

Require `.memory` instead. The test's intent is unchanged: a missing
app identity must not select a store that persists credentials or
shares them with another bundle, and memory storage does neither.

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

* Fix four automation-blocking regressions in the iOS connectivity gate

Found by driving the relay-only release gate end to end on real staging:

- MobileIrxSettingsController.irohSettingsUpdates() never yielded the
  initial snapshot, so the gate runner's path-policy check subscribed
  after the transport settled and hung to its deadline.
- The irx settingsSnapshot() never populated selectedTransportPath (it
  stayed .unavailable while an admitted relay session was live); a new
  IrxConnection.selectedPath() accessor feeds it, classifying a relay as
  managed only when it matches a signed credential, fail-closed.
- No script set CMUX_MOBILE_SOAK_OPEN_SELECTED_WORKSPACE, so gate-mode
  launches sat on the workspace list and readiness starved on
  selectedTerminalID; mobile-dev-launch now defaults it on in gate mode.
- The What's New sheet covers the workspace UI on every fresh automated
  install; a DEBUG-only CMUX_UITEST_SUPPRESS_WHATS_NEW knob suppresses
  presentation without touching acknowledgement markers.

Also logs each path-check snapshot so the next silent stall names itself.

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

* Add the iOS e2e gate: two-machine workflow draft and terminal driver

scripts/e2e/ios-e2e-run.sh drives sign-in-adjacent terminal use on a live
paired sim/Mac and asserts BOTH sides of every step (Vision OCR of the
rendered screen; the tagged Mac socket for what the real shell executed):
echo round trip, output burst plus verified scrollback, alt-screen
enter/exit, Ctrl-C, background/foreground replay, and input after
reconnect. scripts/e2e/mac-host.sh holds a CI Mac runner on a done-file
with a hard timeout (no GitHub API polling). ios-e2e.yml is the 4-job
two-runner topology (Tailscale as control plane only); its pull_request
trigger stays commented out until the check is approved for promotion.

Verified twice back to back on tag e2eci against remote staging over the
real relay, after the in-app gate probe passed with path=managed_relay.

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

* Fix iOS e2e workflow guard requirements

* Fix iOS e2e workflow review blockers

* Use guard-compatible workflow trigger syntax

* ci(ios-e2e): run a fresh per-run backend on a Blacksmith Linux runner

Replace the shared dev-backend VM stub with a backend job that builds and
serves this revision's web/, iroh-v2 and presence Workers (Durable Objects in
local workerd) and Postgres, published on the tailnet by Tailscale Serve.
The macOS jobs start in parallel and wait on its health endpoints; the iOS
job releases both holders over Tailscale SSH. node_modules live on Blacksmith
sticky disks and the web build is cached by the web/ tree SHA.

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

* ci(ios-e2e): keep the PR trigger note outside the on: block

The macOS runner guard reads every line under on:, so the commented
pull_request note made it refuse the new block-mapping trigger.

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

* Run iOS E2E Mac and simulator on one runner

* Include backend port lease helper in CI

* Keep iOS E2E backend client in cmux repo

* Install AXe for iOS E2E runner

* Fix GCP backend status output

* ci(ios-e2e): per-run backend with its own Iroh relay, OIDC-only tailnet

Replace the shared dev-VM backend with a backend job on a Blacksmith Linux
runner: iroh-v2 and presence in local workerd, Postgres, and the upstream
iroh-relay 1.0.2 (the version cmux-relay wraps), published by Tailscale
Serve. iroh-v2 signs relay credentials with a per-run key for the per-run
relay, so no production relay key reaches CI. No web/: nothing on the path
under test calls it.

Every tailnet join uses GitHub OIDC in the ios-e2e environment instead of the
stored Tailscale OAuth secret.

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

* docs(ios-e2e): document the per-run backend, relay and OIDC trust boundary

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

* ci(ios-e2e): extract iroh-relay by its archive path

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

* ci(ios-e2e): no cross-branch-writable caches in the secret-holding backend job

Blacksmith sticky-disk keys are repository-wide, so another branch could plant
node_modules code that this job runs with the Stack server key. Use
branch-scoped actions/cache for Bun's package cache instead, and verify the
relay tarball's digest on every run, not only on download.

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

* ci(ios-e2e): give the backend's tailscaled a state directory for tailscale cert

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

* ci(ios-e2e): run wrangler on Node 22

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

* ci(ios-e2e): tear the per-run backend down on every exit

backend-up.sh down stops the relay and both wrangler/workerd trees, removes
the Postgres container and Serve listeners, and deletes the files holding the
Stack server key, the per-run relay key and the TLS key. It tolerates a
partial up; the workflow runs it after the log upload, always.

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

* ci(ios-e2e): serve the backend with a fixed-name certificate, no per-run issuance

A per-run Tailscale certificate cost 37 s and would exhaust Let's Encrypt's
weekly limit for the tailnet domain; a private CA cannot serve the relay
because the iOS Iroh client checks built-in public roots. One certificate for
cmux-e2e-backend.<tailnet>, issued once and stored in the ios-e2e environment,
now covers all origins. No node keeps the name; runners map it in /etc/hosts.
tls-forward.mjs terminates TLS on the tailnet address and forwards bytes;
Postgres serves the same certificate. down and unhosts remove the hosts lines.

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

* ci(ios-e2e): verify Postgres TLS up front; skip the route checkout on dispatch

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

* ci(ios-e2e): refuse to join the tailnet from a runner that already runs Tailscale

On a self-hosted fleet host the Tailscale action would re-register the host's
own node under tag:ci and log it out at job end, dropping the host from the
tailnet. Both joins now fail first, labeled infra-preflight, on such a host.

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

* ci(ios-e2e): boot the newest iPhone on the newest iOS runtime; cache Tailscale on macOS

The last iPhone-family device type was an iPod touch with no runtime on the
image, so the simulator never booted.

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

* ci(ios-e2e): suppress What's New and open the paired terminal on launch

Run 36681682225 signed in and paired, then the fresh install stopped on the
What's New sheet and the driver never reached a terminal. The launcher only
enables both switches in release-gate mode; set them for this lane.

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

* test(ci): cover per-run iOS E2E backend scripts

* fix(ci): use approved macOS fallback for iOS E2E

* fix(ci): route fork iOS E2E runs to macOS 26

* test(ci): cover backend cleanup without origin config

* fix(ci): allow backend cleanup without origin name

* test(ci): cover backend host cleanup without origin config

* fix(ci): allow host cleanup without origin name

* test(ci): require machine-readable iOS E2E failures

* fix(ci): emit parseable iOS E2E failure markers

* test(ci): cover empty backend hold state

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

* fix(ci): make backend preflight failures reachable

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

* revert(ios): keep transport selection fail-closed

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

---------

Co-authored-by: Abdulaziz Albahar <67667005+azooz2003-bit@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Austin Wang <38676809+austinywang@users.noreply.github.com>
Co-authored-by: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
* test(agent-chat): cover duplicate Claude child close

Add a regression for two parallel Claude spawn-tool children. The second child must remain running after the first child's PostToolUse and duplicate SubagentStop events.

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

* fix(agent-chat): ignore duplicate Claude subagent stop

Claude brackets child runs with the spawn tool's PostToolUse, and its SubagentStop hook arrives as a second close without a request id. Skip that duplicate event only for Claude so the existing FIFO fallback and SubagentStart/SubagentStop behavior for Codex, pi, and OMP remain unchanged. This source-specific guard is smaller and safer than adding deduplication state to closeChild.

Correct the custom sidebar documentation to describe FIFO closing and the request id field the code actually decodes.

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

* test(agent-chat): cover Claude background child lifecycle

Add a regression showing that a Claude child must stay running after the spawn tool returns and settle only when SubagentStop arrives.

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

* fix(agent-chat): close Claude children on subagent stop

Claude's spawn tool PostToolUse fires when a foreground or background child detaches, so it is not a completion signal. Ignore it for Claude and close the child on the SubagentStop hook instead. Keep the existing post-tool and FIFO behavior for Codex, pi, and OMP, and document the corrected lifecycle.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(ios): cover terminal composer input traits

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

* fix(ios): disable composer text rewriting traits

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

---------

Signed-off-by: Alejandro Florez <soyeladice@gmail.com>
* Deduplicate Cloud attachment recovery requests

* test: cover idempotent cloud mirror fixture close

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

* fix: make cloud mirror fixture close idempotent

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

---------

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

* test: pin the Cloud sidebar's chrome to the right sidebar's own metrics

The Cloud surfaces sit inside the right sidebar but cannot import the app
target that owns `RightSidebarChromeMetrics`, so each of them carries its
own copy of the numbers. The copies have drifted: the Cloud banners use a
12pt outer inset against the sidebar's 8 and a 5pt vertical against its 4,
and the Cloud tree reserves a 12pt trailing column against the sidebar's 6.

`CloudSidebarChromeMetrics` states the sidebar's numbers once for the
package. These tests run in the app target, where both types are visible,
so the copy cannot drift from the original without failing. They are red
until the Cloud tree is moved onto the shared trailing column.

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

* fix: put the Cloud sidebar on the right sidebar's chrome columns

The Cloud banners sat on a 12pt outer inset while the mode bar, the Vault
grouping pills and the Vault search row sit on 8, so the Cloud surfaces
stepped in from the sidebar they are part of. The Cloud tree reserved a
12pt trailing column against the sidebar's 6, so machine rows stopped
short of the header's controls and titles truncated 6pt early.

All of these now read `CloudSidebarChromeMetrics` instead of repeating a
literal. `CloudTreeLayoutMetrics.referenceInset` and the spacing lab's
`referenceInset` follow `CloudTreeRowGrid`'s trailing padding rather than
restating it, which is what would otherwise have let the outline document
and the hosted row content drift apart on this change.

The operation activity row keeps its 8pt vertical padding: it carries a
progress indicator rather than a line of text, and shortening it is a
look rather than a mismatch.

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

* fix(cloud): keep titleWidth's expectation on the inset, align two more sidebar rows

Review of #15307 caught a suite left red by the previous commit:
`titleWidthReservesControls` asserted a literal 240, which is
`420 - 92 - 76 - 12`. Moving `referenceInset` to the row grid's 6 makes
that 246. The assertion now derives from `referenceInset` so the sidebar's
chrome can move again without rewriting arithmetic here.

The doc comments on `CloudTreeLayoutMetrics` and in that test claimed the
outline document reserves this inset. It does not: `documentWidth` is
`max(0, viewportWidth)`, and `titleWidth` has no caller outside the test.
Both now say what is true, which is that the default keeps a dormant helper
from being wired up at a stale number.

Two more rows in the same vertical stack were still on a 10pt outer inset
while the banners beside them moved to 8: the team picker's fleet status row
and the "machines unavailable" notice. Aligned both.

The three Cloud views that live in the app target now read
`RightSidebarChromeMetrics` directly instead of the package's copy. The copy
exists because CmuxCloud cannot import the app target; app-target code has
no such problem, and pointing it at the copy invents drift where there was
none.

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

* test(cloud): state the title-width clamp instead of leaning on the old inset

`titleWidth(rowWidth: 180, …)` came out at exactly zero only because the
reference inset was 12, so the assertion read as a clamp test and was
really arithmetic. Moving the inset to the sidebar's 6 made it 6 and the
test failed with no clamping behaviour changed.

Now the narrow case is written against `referenceInset` like the wide one,
and the clamp gets its own case at a width that actually underflows.

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

* fix(cloud): make CloudSidebarChromeMetrics a value, not a static namespace

scripts/lint_swift_namespaces.py rejects an all-static public surface. The
metrics are now an Equatable, Sendable struct with a .sidebar instance for
the numbers the app ships.

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

* fix(cloud): align the team-change error row with the chrome bar above it

The Cloud header row went on `.rightSidebarChromeBar()`, which insets it by
`RightSidebarChromeMetrics.barHorizontalPadding` (8). The team-change error row
sits directly under it and kept a hardcoded 10, so the message and its Close
button stood 2pt inboard of the header they belong to. Read both paddings from
the metric instead, which is what the rest of this branch does.

The vertical value is unchanged at 4; it now names the metric rather than
repeating the number.

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

* fix(cloud): put the tree's trailing column on the header's own inset

Review of this branch found the tree moved to the wrong column. The commit
that introduced CloudSidebarChromeMetrics took the tree's trailing column
from headerTrailingPadding (6) on the theory that the header above it sits
there. It does not. CloudTeamPickerHeader calls a plain
rightSidebarChromeBar(), whose trailingPadding defaults to
barHorizontalPadding (8). The two call sites that opt into 6 are the mode bar
and the Vault grouping bar, neither of which is above the Cloud tree.

So the row hover buttons went from 4pt inboard of the header's refresh and +
buttons to 2pt outboard of them: still misaligned, and the doc comment
claimed they lined up. The branch also contradicted itself, since the
team-change error row was aligned to 8 two commits later for exactly the
reason the tree was aligned to 6.

The tree now follows barHorizontalPadding, so the header, the error row and
the row accessories all stop on the same column. headerTrailingPadding had no
reader left and is removed rather than kept as a copy documenting a
relationship that does not hold.

Also in the same stack, MachinesCloudStatus was still on a literal 10. It is
the status slot rendered directly under the error row, so the header read
8 / 8 / 10 after the previous commit. It now names the metrics.

CloudTreeMachineBand takes trailingPadding - 2, whose headroom fell from 10
to 4 when the column moved off 12. The DEBUG spacing lab's slider starts at
0, so that expression can go negative; clamped at 0.

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

* fix(cloud): preserve machine name ends when narrow

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

* fix(cloud): collapse machine actions in a narrow header

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* test: require xcstrings refusals to preserve both sides

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

* fix: materialize xcstrings merge conflicts

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

* fix: reject malformed catalog shapes visibly

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

* docs: clarify generated-file merge drivers

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

* test: preserve separate xcstrings conflict hunks

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

* test: isolate xcstrings conflict hunk assertions

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

* fix: preserve multi-hunk xcstrings conflicts

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

* fix(xcstrings): close the last fail-open write, correct the driver-trust note

A review of this branch found one refusal path that still ends where the bug
started. `_materialize_conflict` caught only `OSError` around a call that both
renders and writes, so a failure to open `%A` printed to stderr and returned
normally, and a non-`OSError` from `conflict_text` escaped `main` entirely.
Either way the process exits nonzero with `%A` byte-identical to ours: git
records the path as unmerged, the file on disk has no markers, and it reads as
"no disagreement here" and gets staged. That is the exact shape of #15415, so
the claim that every refusal writes a visible conflict was not yet true.

Now the text is rendered before the file is opened, both steps catch
`Exception`, and when a conflict cannot be written at all `_blank` truncates
`%A` to zero bytes. A driver cannot ask git to abort, so an empty file is the
loudest signal left, and it is never valid JSON, so anything that parses the
catalog fails rather than accepting ours as a merge of theirs. Reopening can
fail for the same reason the first write did; then the message names the file
and says it is still ours. The success write gets the same treatment, since a
failure there loses just as quietly.

Two tests, both confirmed to fail against the previous driver: a refusal whose
render raises leaves `%A` empty rather than ours, and a `%A` that cannot be
written at all reports that it must not be committed. A third test for the
reported ours-equals-base shape passed against the old driver as well, so it
had no power and is not here.

The `.gitattributes` paragraph this branch added was also wrong. It said both
drivers run from the working tree, but `scripts/install-git-hooks.sh` installs
reviewed copies into `$GIT_COMMON_DIR/cmux-merge-drivers` and configures
absolute paths to those, which is what `tests/test_install_git_hooks.py`
asserts. The note now describes that, and why catch-up pins the drivers to
false instead.

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

* test: cover xcstrings conflict containment and omissions

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

* test: pin per-key conflicts and all fallback sections

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

* fix: materialize xcstrings conflicts per key

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

* fix(xcstrings): refuse per-key conflicts that share a line

The per-key conflict path replaced text from the start of the key's
*line*, and never checked that two replacement ranges were disjoint. On
a catalog whose `strings` keys do not each sit on their own line that
lost data:

  - a compacted catalog dropped the `{"sourceLanguage":...,"strings":{`
    prefix into neither side of the conflict, so a human resolving it
    had to retype it;
  - two conflicting keys on one line clobbered each other, truncating
    ours' text for the second key and emitting a `||||||| base` with no
    opening marker.

Both now raise, which `main()` already catches and degrades to the
lossless whole-file conflict the driver used before per-key conflicts
existed. No catalog in the repo has shared-line keys today, but
`.gitattributes` registers this driver for every `*.xcstrings`, the
schema lint has no layout rule, and the docstring advertises compacted
leaf objects, so one hand-written catalog is enough.

Found by a review subagent on this PR; both cases are pinned by tests
that fail without the guards.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…reen (#14858)

* Add built-in copy actions for working directory, project root, and screen

cmux.copyWorkingDirectory, cmux.copyProjectRoot, and cmux.copyScreen work as
surface tab bar buttons, cmux.json actions (including shortcuts), and command
palette entries. Every entrypoint runs TerminalCopyActionRunner, which writes
through the terminal pasteboard service and leaves the clipboard untouched
(with a beep) when there is nothing to copy.

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

* Fix copy-action test inference and replace the static text namespace

TerminalCopyText becomes String extensions to satisfy the package
namespace-type convention, and the config test gives its compactMap an
explicit element type.

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

* Address review: per-panel provenance, bounded root walk, stale-write guard

- Copy actions target one terminal panel and read its directory through
  effectivePanelDirectory, so remote, cloud, and remote-tmux panels copy
  only a directory their host reported, and a missing or non-terminal
  target beeps instead of copying another pane's directory. The palette
  and shortcut paths pass the focused panel explicitly.
- The git root walk runs off the cooperative pool with a 1.5 s deadline,
  and the delayed write only lands if the clipboard is unchanged since
  the action started.
- Soften the workTreeRoot and visibleScreenClipboardText docs, replace a
  vacuous config assertion, add provenance and stale-write tests, and
  document the remote fallback on the custom-commands page.

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

* Give the project-root walk its own queue and a 5 s budget

The first bounded version shared the git status queue and a 1.5 s
deadline that included queue wait; under CI load the walk timed out and
the tests saw nil. The walk now runs on a user-initiated global queue.
The stale-write guard already keeps a late result from clobbering a
newer copy, so the deadline only needs to cover a hung mount.

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

* Bound the project-root wait, stay quiet when a newer copy wins, resolve mirror panes

- workTreeRoot runs on a dedicated serial BoundedBlockingRunner: the
  caller resumes from whichever of the walk or a timer finishes first,
  and a call made while a walk is stuck on a hung mount returns nil
  (copy falls back to the working directory) instead of parking another
  thread.
- copyToStandardClipboard(_:ifUnchangedSince:) returns the write status;
  the runner no longer beeps when the user's newer copy wins.
- Copy actions resolve their target through terminalInputTarget, so a
  focused remote-tmux window container copies its active inner pane's
  screen and remote cwd.

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

* Test that a copy-action shortcut passes through without a terminal

With a browser panel focused, a bound cmux.copyWorkingDirectory,
cmux.copyProjectRoot, or cmux.copyScreen shortcut currently beeps and
reports the keystroke handled, so the browser never sees it. This test
expects the shared action path to report it unhandled and leave the
clipboard alone. It fails until the next commit.

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

* Let copy-action shortcuts pass through when no terminal is focused

The shared built-in action path now reports a copy action unhandled,
without a beep, when the focused panel isn't a terminal. A bound
shortcut's keystroke reaches the browser or other focused panel instead
of being swallowed. The plus-menu and group-menu callers already beep on
an unhandled action, and the palette and tab bar button keep calling the
runner directly, so they still beep when there's nothing to copy. A
terminal with nothing to copy still consumes the shortcut.

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

* Tell a timed-out project-root walk apart from "not in a repository"

workTreeRoot returned nil for three different cases: no repository, the
walk timed out, or an earlier walk was still stuck. Copy Project Root
copied the working directory for all three, so after one walk hung on a
network mount every later press silently copied the cwd as if it were
the root. BoundedBlockingRunner now reports finished, timedOut, or busy,
workTreeRoot returns GitWorkTreeRootLookup (root, notInRepository,
unavailable), and the action only falls back to the working directory
for notInRepository. It beeps when the lookup is unavailable.

Review fixes on the shared action path:
- A copy action run from the group menu calls onExecuted even when there
  was nothing to copy, so the menu restores the selection it moved to
  the anchor workspace.
- The group menu beeps when a copy action can't find a terminal, the way
  the plus menu already does, since the action path no longer beeps there.
- The runner and root tests use their own runner instead of the shared
  static one, so they no longer depend on test order. The passthrough
  test drops its NSPasteboard change-count check, which any other process
  on the host could bump.

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

* Add a changelog line for the copy actions

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

* Move the changelog line to Unreleased; steady the busy-runner test

The changelog line had landed under the released 0.64.24 section. The
busy-runner test gave its blocked job 50 ms to start; on a loaded
machine the job could miss the deadline and never run, so the next call
found the runner idle. It now waits 500 ms and asserts the runner is
busy before the second call.

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

* test: add copy actions dogfood tour

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

* test: target copy action palette row

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
TeamsClient.shared is set at app start; tests that authenticate a fake
coordinator never bootstrap it, and the sign-in poll dereferenced nil and
timed out unrelated Cloud suites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(ssh): reconcile agent status on projector startup

* test(ssh): cover initial agent status projection

* test(ssh): isolate status projection on main actor
* test: cover SOS socket replies

* fix: preserve SOS replies in socket input
…5973)

* test(flags): cover malformed review dates

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

* fix(flags): collect registries and reject invalid dates

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

* feat(flags): report review lead time

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

* ci(flags): file review drift issues

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

* test(flags): reproduce scheduled report false all-clears

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

* fix(flags): keep scheduled review drift truthful

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

* fix(flags): open an issue when report fails

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
teamleaderleo and others added 10 commits September 30, 2026 18:28
…t (#16267)

* test(ci): cmux-tui artifact publishing must run in the artifacts environment

#16171 put the cmux-tui publish job in the release environment, whose
policy allows only main and v* tags, so helper-branch pin publishes
fail before any step runs.

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

* fix(ci): cmux-tui artifact publishing runs in the artifacts environment

The artifacts environment holds only the R2 upload credentials and
allows main, feat-cmux-next and cmux-tui-pin-* helper branches, so
daemon pin publishes work again while signing, Sparkle, Homebrew and
Apple secrets stay in release (main and v* tags only).

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cloud): reusing a projection at its current placement changes nothing

Reconcile reprojects every missing placement through SurfaceCatalog.project.
When the reused pane already carries that placement, attachRemoteView still
removes and reinserts it, bumps the projection revision twice, and requests
the next reconcile of the same machine. Any disagreement between the plan
and project() then becomes a main-actor livelock, which is how nightly
b36a9b3 spun at 98% CPU and grew to tens of GB (fixed at the plan level by
#16025). Fails on main: projectionVersions advances by 2.

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

* fix(cloud): reattaching a projection's current placement is a no-op

attachRemoteView rewrote a reused projection even when its remote workspace
and tab were already the requested ones: it removed and reinserted it
(clearing and resetting the panel directory, rerunning sidebar git probes,
bumping the guest routing revision twice) and requested another reconcile of
the machine. Since reconcile itself reprojects through project(), any plan
that reports a shown pane as missing became an endless main-actor loop.

Return early when the coordinates are unchanged, and apply a real change as
one projections assignment so observers never see the pane unprojected.

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

* fix(cloud): setting a projection's current remote placement is a no-op

Same guard as attachRemoteView for setRemotePlacement: skip views whose
coordinates already match, and apply real changes as one projections
assignment. Unchanged placements no longer bump the projection revision or
post a catalog change that wakes the device layout coordinator. The test now
states its fixture precondition explicitly.

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

* test(cloud): reconciling one graph stops when every pass requests another

A consumer that requests another reconcile without changing the accepted
graph keeps CloudWorkspaceProjectionCoordinator's loop running forever on
the main actor, which is how nightly b36a9b3 hung at 100% CPU and grew to
tens of GB. Fails on main: the loop runs until the test stub stops asking
(1000 passes).

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

* fix(cloud): bound reconciliation passes over one accepted graph

CloudWorkspaceProjectionCoordinator re-ran reconcile while anything kept
requesting it, with no progress check. Any consumer that asks for another
pass without changing the graph (attachRemoteView before this PR, a plan
that reports a shown pane as missing in #16025) held the main actor forever:
nightly b36a9b3 pinned a core, grew to tens of GB, and could not even run
its updater.

Count passes over the same accepted CloudVMState. A converging graph needs
two or three; after eight, stop, report a Sentry warning, and wait for the
next graph or request, which starts a new count.

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

* fix(cloud): bound reconciliation by progress, not by passes over one graph

Review of the previous bound: counting every pass over an unchanged graph
could stop a reconcile that was still making progress (a staggered restore
of several bound workspaces re-requests the same graph), stranding panes
until the next graph.

CloudWorkspaceReconcileBudget now stops after three consecutive passes that
start from the same graph, projection revision and bindings (a pass that
changed nothing cannot make the next one different), with a hard ceiling of
64 passes per graph for a loop that rewrites projections every pass, as
nightly b36a9b3 did. Non-convergence is reported once per graph.

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

* fix(cloud): report projection non-convergence once per daemon generation

Review: keying the dedupe on the full CloudVMState retained a whole graph per
machine for the process lifetime (cancel never cleared it) and still reported
once per revision. Key on the cursor generation, include generation and
revision in the event, and clear it when the machine is cancelled.

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

* chore(l10n): document the French Actions discovery titles as invariant

Same change as #16175: main's localization parity check fails on
actions.discovery.menuTitle and dialogTitle (fr is identical to English),
which blocks this PR's static preflight and every gate behind it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ope) (#16260)

* fix: share OpenCodePaths with the CLI through CMUXAgentLaunch

#16229 made CLI/cmux.swift call OpenCodePaths, but the enum lived in
Sources/SessionIndexModels.swift, which only the app target compiles, so
the CLI target fails with "cannot find 'OpenCodePaths' in scope". Move
the unchanged path logic into CMUXAgentLaunch, which the app, the CLI and
cmuxTests already import, and make its two entry points public.

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

* Pass the temporary-config flag to the Codex provider override parser

#16201 made providerOverrides(from:) skip provider entries when the caller
uses a temporary CODEX_HOME, but read `usesTemporaryConfig`, a parameter of
build(configToml:usesTemporaryConfig:) that is not in scope there, so the CLI
no longer compiles. Pass the flag through.

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

* test: match temporary Codex config argument scope

* Make OpenCodePaths a value type to satisfy package conventions

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

* ci: cmux-tui artifact publishing runs in its own artifacts environment (#16267)

* test(ci): cmux-tui artifact publishing must run in the artifacts environment

#16171 put the cmux-tui publish job in the release environment, whose
policy allows only main and v* tags, so helper-branch pin publishes
fail before any step runs.

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

* fix(ci): cmux-tui artifact publishing runs in the artifacts environment

The artifacts environment holds only the R2 upload credentials and
allows main, feat-cmux-next and cmux-tui-pin-* helper branches, so
daemon pin publishes work again while signing, Sparkle, Homebrew and
Apple secrets stay in release (main and v* tags only).

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

---------

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

* fix(cloud): make Cloud workspace reconciliation always settle (#16158)

* test(cloud): reusing a projection at its current placement changes nothing

Reconcile reprojects every missing placement through SurfaceCatalog.project.
When the reused pane already carries that placement, attachRemoteView still
removes and reinserts it, bumps the projection revision twice, and requests
the next reconcile of the same machine. Any disagreement between the plan
and project() then becomes a main-actor livelock, which is how nightly
b36a9b3 spun at 98% CPU and grew to tens of GB (fixed at the plan level by
#16025). Fails on main: projectionVersions advances by 2.

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

* fix(cloud): reattaching a projection's current placement is a no-op

attachRemoteView rewrote a reused projection even when its remote workspace
and tab were already the requested ones: it removed and reinserted it
(clearing and resetting the panel directory, rerunning sidebar git probes,
bumping the guest routing revision twice) and requested another reconcile of
the machine. Since reconcile itself reprojects through project(), any plan
that reports a shown pane as missing became an endless main-actor loop.

Return early when the coordinates are unchanged, and apply a real change as
one projections assignment so observers never see the pane unprojected.

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

* fix(cloud): setting a projection's current remote placement is a no-op

Same guard as attachRemoteView for setRemotePlacement: skip views whose
coordinates already match, and apply real changes as one projections
assignment. Unchanged placements no longer bump the projection revision or
post a catalog change that wakes the device layout coordinator. The test now
states its fixture precondition explicitly.

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

* test(cloud): reconciling one graph stops when every pass requests another

A consumer that requests another reconcile without changing the accepted
graph keeps CloudWorkspaceProjectionCoordinator's loop running forever on
the main actor, which is how nightly b36a9b3 hung at 100% CPU and grew to
tens of GB. Fails on main: the loop runs until the test stub stops asking
(1000 passes).

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

* fix(cloud): bound reconciliation passes over one accepted graph

CloudWorkspaceProjectionCoordinator re-ran reconcile while anything kept
requesting it, with no progress check. Any consumer that asks for another
pass without changing the graph (attachRemoteView before this PR, a plan
that reports a shown pane as missing in #16025) held the main actor forever:
nightly b36a9b3 pinned a core, grew to tens of GB, and could not even run
its updater.

Count passes over the same accepted CloudVMState. A converging graph needs
two or three; after eight, stop, report a Sentry warning, and wait for the
next graph or request, which starts a new count.

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

* fix(cloud): bound reconciliation by progress, not by passes over one graph

Review of the previous bound: counting every pass over an unchanged graph
could stop a reconcile that was still making progress (a staggered restore
of several bound workspaces re-requests the same graph), stranding panes
until the next graph.

CloudWorkspaceReconcileBudget now stops after three consecutive passes that
start from the same graph, projection revision and bindings (a pass that
changed nothing cannot make the next one different), with a hard ceiling of
64 passes per graph for a loop that rewrites projections every pass, as
nightly b36a9b3 did. Non-convergence is reported once per graph.

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

* fix(cloud): report projection non-convergence once per daemon generation

Review: keying the dedupe on the full CloudVMState retained a whole graph per
machine for the process lifetime (cancel never cleared it) and still reported
once per revision. Key on the cursor generation, include generation and
revision in the event, and clear it when the machine is cancelled.

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

* chore(l10n): document the French Actions discovery titles as invariant

Same change as #16175: main's localization parity check fails on
actions.discovery.menuTitle and dialogTitle (fr is identical to English),
which blocks this PR's static preflight and every gate behind it.

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

---------

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

* fix(tests): name the app's window-chrome sidebar options explicitly

#11539 reverted #14991's qualification in SidebarWidthPolicyTests, so
SidebarMaterialOption.sidebar is ambiguous between CmuxSettings and the
app's typealias to WindowChromeSidebarMaterialOption. Use the
WindowChrome names again, as #14991 did.

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: Leo Li <cheerleaderleo@outlook.com>
Co-authored-by: Austin Wang <austinwang115@gmail.com>
* test: cover authorized dev relay limit bypass

* fix: exempt authorized dev relay clients from limits

---------

Co-authored-by: lawrencecchen <54008264+lawrencecchen@users.noreply.github.com>
* test: reproduce late Agent Chat startup stealing session focus

* fix: keep late Agent Chat startup replies from changing selection

* Make Agent Chat fork and handoff actions recoverable (#15994)

* test: reproduce stuck Agent Chat fork and handoff lifecycle

* fix: make Agent Chat fork and handoff actions recoverable

* fix(agent-chat): deduplicate reconnecting session actions

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

---------

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

* test: reproduce Stop doing nothing before startup acknowledgement

* fix: cancel pending agent starts by request ID and recover queued drafts

* fix: preserve startup cancellation through provider initialization

Latch Pi startup cancellation across initialization and retain request-to-session identity after request cache expiry so delayed Stop retries still reach the created session. Extend the server fixture for expired request IDs.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(ci): notarization must use the team App Store Connect API key

The nightly DMG and Computer Use helper notarization tests now require
notarytool to authenticate with --key/--key-id/--issuer, a mode-600 key
file decoded from ASC_API_KEY_P8_BASE64, no Apple ID credentials, and
deletion of the decoded key on exit.

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

* ci: notarize Mac builds with the team App Store Connect API key

Release, nightly/RC and the v0.64.25 repair workflow now authenticate
notarytool with --key/--key-id/--issuer instead of an Apple ID and
app-specific password. scripts/ci/lib/notary-auth.sh decodes
ASC_API_KEY_P8_BASE64 into a mode-600 file inside the caller's private
temp dir, which an EXIT trap deletes. The notarize scripts fail before
any upload when a key value is missing.

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

* test(ci): read the fake notary key mode on Linux and macOS

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Merges 259 first-parent upstream commits (upstream main 2026-09-29 to
2026-09-30), including shared terminal sizing across Mac, iPhone and
Devices mirrors, Connected Devices on iOS, the iOS agent Feed, copy
built-in actions, and guarded workspace close (--force).

25 files conflicted; resolved per SUPERMUX.md (upstream code plus the
re-applied SUPERMUX fences). Semantic fixes folded in:
- NotificationFeedHistoryRecord's explicit CodingKeys/init(from:) now
  carry upstream's new isAgentEvent (it would otherwise be dropped on
  encode).
- TerminalCopyAction.swift gains a fenced .newClaudeHarness arm (#600).
- iOS keeps the Notifications tab visible by default beside the new Feed
  (#601, open decision in SUPERMUX-UPGRADES.md).
- iOS native-scroll delta applies upstream's gridDisplayScale.

Registry: rows updated for moved code, #504 path moved under Debug/,
#600 and #601 added (517-599 are held for the
remote-workspaces branch). scripts/supermux-check-touchpoints.sh passes.
…ft stale

The 2026-09-30 merge kept older wording for 18 upstream-owned strings
(for example "Couldn't reach Stack" on sign-in errors, where upstream now
says "Couldn't reach cmux") and re-added 21 keys upstream had deleted.
Touchpoint #4b allows the fork to change only supermux.* keys plus the
#84 exception, so the 18 values now match upstream/main and the 21
unreferenced keys are removed. Only member spans changed; the rest of
the catalog keeps its formatting.

After this, every non-supermux key equals upstream/main except
settings.search.alias.setting.app.workspace-inherit-working-directory
(#84).
What a supermux user notices after merging upstream main @ 4e9d779:
shared terminal sizing across Mac, iPhone and Devices mirrors, Connected
Devices on iOS, the iOS Feed beside the fork's Notifications tab, guarded
workspace close, the new ⌃⌥⌘= and V shortcuts, watch-outs, and the one
open decision (Feed vs. Notifications tab).
@rajinsyed
rajinsyed deployed to cloud-vm-image-checks October 1, 2026 01:44 — with GitHub Actions Active

@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.

10 issues found across 1976 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=".github/workflows/ci-owned-pool-rescue.yml">

<violation number="1" location=".github/workflows/ci-owned-pool-rescue.yml:40">
P1: The periodic sweeper cannot discover these side-lane runs: it adopts marker artifacts and CI reruns, while these no-picker workflows upload no marker. Removing their `workflow_run` subscriptions leaves attempt-1 owned jobs without rescue; restore the subscriptions or add a marker the sweeper consumes.</violation>
</file>

<file name=".github/workflows/nightly.yml">

<violation number="1" location=".github/workflows/nightly.yml:1309">
P1: The `release` environment excludes `rc/**`, even though this workflow publishes every RC-branch push; RC runs will be blocked from signing and publishing. Add `rc/**` to the environment’s allowed deployment branches, or use an environment that permits RC refs.</violation>
</file>

<file name="Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ExplicitTerminalInput.swift">

<violation number="1" location="Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ExplicitTerminalInput.swift:56">
P1: The exactly-once route runs before this guard, so detached terminals can still receive and acknowledge input through `deliverExactlyOnce`. Check `terminalAllowsTraffic` before that call, as the paste paths do.</violation>
</file>

<file name="Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift">

<violation number="1" location="Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift:37">
P2: This check only gates calls before enqueueing; a scroll already pending when a detach event arrives is still dispatched after the in-flight request completes. Recheck attachment in `performTerminalScroll` before sending the RPC so queued TUI wheel input is not delivered after detach.</violation>
</file>

<file name="Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift">

<violation number="1" location="Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift:100">
P2: This marks `outer -> inner` as in-repository even when `inner` points to `/outside/secret`, because the final target component is never resolved. Resolve target symlink chains while retaining the dangling-target fallback.</violation>
</file>

<file name="Packages/macOS/CmuxCloudTui/Sources/CmuxCloudTui/CloudTuiFrontendProjectionRequests.swift">

<violation number="1" location="Packages/macOS/CmuxCloudTui/Sources/CmuxCloudTui/CloudTuiFrontendProjectionRequests.swift:21">
P2: When no projection exists, this omits the revision precondition, so concurrent first writes can both succeed and the later write can erase the earlier membership. Send revision `0` for `nil`; the caller can then retry the revision conflict against the latest projection.</violation>
</file>

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

<violation number="1" location="CLI/CMUXCLI+AgentHibernation.swift:21">
P2: This filter silently discards an incomplete `--workspace` option, so hibernate/wake proceeds without the requested workspace scope. Reject unparsed option tokens before sending the request.</violation>
</file>

<file name="Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift">

<violation number="1" location="Packages/macOS/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift:65">
P2: The identity migration drops `reply` when it rebuilds an item with a canonicalized workstream ID, so remote terminal replies disappear from loaded history after restart. Preserve `item.reply` in `normalizedWorkstreamItem`.</violation>
</file>

<file name=".github/contributor/welcome.md">

<violation number="1" location=".github/contributor/welcome.md:9">
P2: This promises CI will run touched UI tests, but the UI-test lane is currently disabled unless `CI_UI_TESTS_ENABLED=1`, and it does not dispatch tests for fork PRs. Clarify that app-host suites are selected automatically while UI-test coverage depends on the lane being enabled and the PR being eligible.</violation>
</file>

<file name="Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator+TeamSelection.swift">

<violation number="1" location="Packages/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator+TeamSelection.swift:92">
P2: This refresh can overwrite a team switch that completes while its fetch is in flight: the refresh applies its server selection without checking for a newer team mutation. Fence refresh results against team mutations or serialize refresh with selection so a stale response cannot restore the previous active scope.</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

# vars.CI_NIGHTLY_TRUSTED_RUNNER a glaeda-runner-* label, attempt 1 of every
# push or schedule run on main asks for that one trusted mini, so every such
# run is watched (owned_pool_rescue.NIGHTLY_WORKFLOW_PATH).
# The side lanes have no picker and are already covered by the periodic

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 periodic sweeper cannot discover these side-lane runs: it adopts marker artifacts and CI reruns, while these no-picker workflows upload no marker. Removing their workflow_run subscriptions leaves attempt-1 owned jobs without rescue; restore the subscriptions or add a marker the sweeper consumes.

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 .github/workflows/ci-owned-pool-rescue.yml, line 40:

<comment>The periodic sweeper cannot discover these side-lane runs: it adopts marker artifacts and CI reruns, while these no-picker workflows upload no marker. Removing their `workflow_run` subscriptions leaves attempt-1 owned jobs without rescue; restore the subscriptions or add a marker the sweeper consumes.</comment>

<file context>
@@ -32,19 +32,14 @@ run-name: ${{ (github.event_name == 'schedule' || github.event_name == 'workflow
 #   vars.CI_NIGHTLY_TRUSTED_RUNNER a glaeda-runner-* label, attempt 1 of every
 #   push or schedule run on main asks for that one trusted mini, so every such
 #   run is watched (owned_pool_rescue.NIGHTLY_WORKFLOW_PATH).
+# The side lanes have no picker and are already covered by the periodic
+# sweeper. They stay in the script's target rules for explicit dispatches, but
+# no longer wake a fresh rescue through workflow_run.
</file context>

build-sign-notarize-nightly:
# Production secrets: GitHub releases them only to protected refs (the
# environment's deployment branch policy: main, tags v*).
environment: release

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 release environment excludes rc/**, even though this workflow publishes every RC-branch push; RC runs will be blocked from signing and publishing. Add rc/** to the environment’s allowed deployment branches, or use an environment that permits RC refs.

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 .github/workflows/nightly.yml, line 1309:

<comment>The `release` environment excludes `rc/**`, even though this workflow publishes every RC-branch push; RC runs will be blocked from signing and publishing. Add `rc/**` to the environment’s allowed deployment branches, or use an environment that permits RC refs.</comment>

<file context>
@@ -1304,6 +1304,9 @@ jobs:
   build-sign-notarize-nightly:
+    # Production secrets: GitHub releases them only to protected refs (the
+    # environment's deployment branch policy: main, tags v*).
+    environment: release
     needs: [decide, build-nightly-ghostty-cli-helper, build-nightly-app, resolve-nightly-cmux-tui-client]
     if: needs.decide.outputs.should_build == 'true' && (github.event_name != 'schedule' || github.event.schedule == '47 8 * * *') && needs.decide.outputs.build_only != 'true'
</file context>

) {
return settlement == .delivered
}
guard terminalAllowsTraffic(surfaceID: terminalID.rawValue) 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.

P1: The exactly-once route runs before this guard, so detached terminals can still receive and acknowledge input through deliverExactlyOnce. Check terminalAllowsTraffic before that call, as the paste paths do.

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/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ExplicitTerminalInput.swift, line 56:

<comment>The exactly-once route runs before this guard, so detached terminals can still receive and acknowledge input through `deliverExactlyOnce`. Check `terminalAllowsTraffic` before that call, as the paste paths do.</comment>

<file context>
@@ -53,6 +53,7 @@ extension MobileShellComposite {
         ) {
             return settlement == .delivered
         }
+        guard terminalAllowsTraffic(surfaceID: terminalID.rawValue) else { return false }
         let target = workspaceMutationTarget(for: workspaceID)
         guard let client = target.client else { return false }
</file context>

/// stale scroll packets.
// SUPERMUX:end ios-terminal-alt-scroll-budget
public func scrollTerminal(surfaceID: String, lines: Double, col: Int, row: Int) async {
guard terminalAllowsTraffic(surfaceID: surfaceID) else { return }

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 only gates calls before enqueueing; a scroll already pending when a detach event arrives is still dispatched after the in-flight request completes. Recheck attachment in performTerminalScroll before sending the RPC so queued TUI wheel input is not delivered after detach.

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/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TerminalScrollDelivery.swift, line 37:

<comment>This check only gates calls before enqueueing; a scroll already pending when a detach event arrives is still dispatched after the in-flight request completes. Recheck attachment in `performTerminalScroll` before sending the RPC so queued TUI wheel input is not delivered after detach.</comment>

<file context>
@@ -34,6 +34,7 @@ extension MobileShellComposite {
     /// stale scroll packets.
     // SUPERMUX:end ios-terminal-alt-scroll-budget
     public func scrollTerminal(surfaceID: String, lines: Double, col: Int, row: Int) async {
+        guard terminalAllowsTraffic(surfaceID: surfaceID) else { return }
         // Screen-anchored sessions own primary-screen scrolling: the gesture
         // already moved the local mirror's viewport over locally accumulated
</file context>

// The parent is resolved, not the target: the target may not exist, and
// every real component above it does.
let parent = target.deletingLastPathComponent().resolvingSymlinksInPath().standardizedFileURL
let resolved = parent.appendingPathComponent(target.lastPathComponent).standardizedFileURL.path

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 marks outer -> inner as in-repository even when inner points to /outside/secret, because the final target component is never resolved. Resolve target symlink chains while retaining the dangling-target fallback.

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/CMUXAgentLaunch/Sources/CMUXAgentLaunch/WorktreeSeed/WorktreeSeedRepository.swift, line 100:

<comment>This marks `outer -> inner` as in-repository even when `inner` points to `/outside/secret`, because the final target component is never resolved. Resolve target symlink chains while retaining the dangling-target fallback.</comment>

<file context>
@@ -74,12 +74,30 @@ public struct WorktreeSeedRepository: Sendable {
+        // The parent is resolved, not the target: the target may not exist, and
+        // every real component above it does.
+        let parent = target.deletingLastPathComponent().resolvingSymlinksInPath().standardizedFileURL
+        let resolved = parent.appendingPathComponent(target.lastPathComponent).standardizedFileURL.path
         if resolved == resolvedRootPath { return true }
         return resolved.hasPrefix(resolvedRootPath.hasSuffix("/") ? resolvedRootPath : resolvedRootPath + "/")
</file context>

Comment on lines +21 to +22
if let expectedProjectionRevision {
fields["expected_projection_revision"] = String(expectedProjectionRevision)

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: When no projection exists, this omits the revision precondition, so concurrent first writes can both succeed and the later write can erase the earlier membership. Send revision 0 for nil; the caller can then retry the revision conflict against the latest projection.

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/CmuxCloudTui/Sources/CmuxCloudTui/CloudTuiFrontendProjectionRequests.swift, line 21:

<comment>When no projection exists, this omits the revision precondition, so concurrent first writes can both succeed and the later write can erase the earlier membership. Send revision `0` for `nil`; the caller can then retry the revision conflict against the latest projection.</comment>

<file context>
@@ -0,0 +1,26 @@
+            "generation": generation,
+            "projection": projection,
+        ]
+        if let expectedProjectionRevision {
+            fields["expected_projection_revision"] = String(expectedProjectionRevision)
+        }
</file context>
Suggested change
if let expectedProjectionRevision {
fields["expected_projection_revision"] = String(expectedProjectionRevision)
fields["expected_projection_revision"] = String(expectedProjectionRevision ?? 0)

) throws {
let (workspaceRaw, afterWorkspace) = parseOption(args, name: "--workspace")
let (surfaceOption, remaining) = parseOption(afterWorkspace, name: "--surface")
let positional = remaining.filter { !$0.hasPrefix("-") }

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 filter silently discards an incomplete --workspace option, so hibernate/wake proceeds without the requested workspace scope. Reject unparsed option tokens before sending the request.

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 CLI/CMUXCLI+AgentHibernation.swift, line 21:

<comment>This filter silently discards an incomplete `--workspace` option, so hibernate/wake proceeds without the requested workspace scope. Reject unparsed option tokens before sending the request.</comment>

<file context>
@@ -0,0 +1,56 @@
+    ) throws {
+        let (workspaceRaw, afterWorkspace) = parseOption(args, name: "--workspace")
+        let (surfaceOption, remaining) = parseOption(afterWorkspace, name: "--surface")
+        let positional = remaining.filter { !$0.hasPrefix("-") }
+        guard let surfaceRaw = surfaceOption ?? positional.first,
+              !surfaceRaw.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
</file context>

@@ -47,6 +60,9 @@ public struct WorkstreamItem: Identifiable, Codable, Sendable, Equatable {
public var status: WorkstreamStatus
public var payload: WorkstreamPayload
public var context: WorkstreamContext?
/// The terminal response associated with this exact event, when one was
/// submitted from mobile or another remote surface.
public var reply: WorkstreamReply?

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 identity migration drops reply when it rebuilds an item with a canonicalized workstream ID, so remote terminal replies disappear from loaded history after restart. Preserve item.reply in normalizedWorkstreamItem.

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/CMUXAgentLaunch/Sources/CMUXAgentLaunch/Workstream/WorkstreamItem.swift, line 65:

<comment>The identity migration drops `reply` when it rebuilds an item with a canonicalized workstream ID, so remote terminal replies disappear from loaded history after restart. Preserve `item.reply` in `normalizedWorkstreamItem`.</comment>

<file context>
@@ -47,6 +60,9 @@ public struct WorkstreamItem: Identifiable, Codable, Sendable, Equatable {
     public var context: WorkstreamContext?
+    /// The terminal response associated with this exact event, when one was
+    /// submitted from mobile or another remote surface.
+    public var reply: WorkstreamReply?
     /// PID of the agent process that emitted the event (hook's parent
     /// pid). When non-nil, pending items get expired automatically as
</file context>


- [Start here](https://github.com/manaflow-ai/cmux/blob/main/docs/start-here.md) covers what reviewers look for, what CI runs for you, and what happens next.
- If the CLA check asks, reply with the sentence it gives you.
- You don't need to run the app-host or UI tests locally. CI runs the ones your diff touches once a maintainer approves the first workflow run.

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 promises CI will run touched UI tests, but the UI-test lane is currently disabled unless CI_UI_TESTS_ENABLED=1, and it does not dispatch tests for fork PRs. Clarify that app-host suites are selected automatically while UI-test coverage depends on the lane being enabled and the PR being eligible.

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 .github/contributor/welcome.md, line 9:

<comment>This promises CI will run touched UI tests, but the UI-test lane is currently disabled unless `CI_UI_TESTS_ENABLED=1`, and it does not dispatch tests for fork PRs. Clarify that app-host suites are selected automatically while UI-test coverage depends on the lane being enabled and the PR being eligible.</comment>

<file context>
@@ -6,5 +6,6 @@ A few things that help:
 
 - [Start here](https://github.com/manaflow-ai/cmux/blob/main/docs/start-here.md) covers what reviewers look for, what CI runs for you, and what happens next.
 - If the CLA check asks, reply with the sentence it gives you.
+- You don't need to run the app-host or UI tests locally. CI runs the ones your diff touches once a maintainer approves the first workflow run.
 - The [verification ladder](https://github.com/manaflow-ai/cmux/blob/main/docs/contributor-verification.md) shows which checks fit your change. Say in the description which ones you ran.
 - If we end up fixing the same problem another way, we'll credit you with a `Co-authored-by` trailer and link the fix here.
</file context>
Suggested change
- You don't need to run the app-host or UI tests locally. CI runs the ones your diff touches once a maintainer approves the first workflow run.
- CI selects touched app-host test suites automatically. UI tests run only when the UI-test lane is enabled and the PR is eligible; follow the [verification ladder](https://github.com/manaflow-ai/cmux/blob/main/docs/contributor-verification.md) for UI changes.

/// team and falls back like sign-in does otherwise.
func refreshTeams() async {
guard isAuthenticated else { return }
await refreshTeams(generation: sessionGeneration)

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 refresh can overwrite a team switch that completes while its fetch is in flight: the refresh applies its server selection without checking for a newer team mutation. Fence refresh results against team mutations or serialize refresh with selection so a stale response cannot restore the previous active scope.

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/Shared/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator+TeamSelection.swift, line 92:

<comment>This refresh can overwrite a team switch that completes while its fetch is in flight: the refresh applies its server selection without checking for a newer team mutation. Fence refresh results against team mutations or serialize refresh with selection so a stale response cannot restore the previous active scope.</comment>

<file context>
@@ -82,4 +82,13 @@ public extension AuthCoordinator {
+    /// team and falls back like sign-in does otherwise.
+    func refreshTeams() async {
+        guard isAuthenticated else { return }
+        await refreshTeams(generation: sessionGeneration)
+    }
 }
</file context>

rajinsyed added a commit that referenced this pull request Oct 1, 2026
# Conflicts:
#	SUPERMUX-TOUCHPOINTS.md
#	Sources/Devices/DeviceLink.swift
@rajinsyed
rajinsyed merged commit 9236e6b into main Oct 3, 2026
90 of 114 checks passed
@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Merge receipt for 454545940f, merged 2026-10-03 07:49:47 UTC

  • Not verified at merge: ci-status (failure), backend (failure), browser-skill (failure), CI fast guards (failure), Fast static checks (failure), guards (19) (failure), guest-install (failure), ios-tests (failure), linux-preflight (failure), package-conventions-lint (failure), suite-coverage (failure), system-keychain (failure), and 7 more
  • Verified: agent-session-web-resources, auth-refresh-tests, catalog-structure, CI timing, client, command-regressions, contract, cpp consumer, cpp package, detect-ios-changes, diagnostic-presentation, diff-sidecar-check, and 39 more
  • Skipped by policy: apply, Claude wrapper regressions, deploy-development, Dogfood build #​${{ github.event.pull_request.number }}, full-suite-coverage, ios-simulator, ios-simulator-build, macos, macOS admission gate, mobile-core-package, Notify IndexNow, remote-daemon, and 5 more
  • Full suite: runs on main after merge.

Labeled merged-unverified: if main breaks near this merge, look here first.

@rajinsyed
rajinsyed deleted the sync-upstream-2026-10-01 branch October 3, 2026 07:56
rajinsyed added a commit that referenced this pull request Oct 3, 2026
rajinsyed added a commit that referenced this pull request Oct 3, 2026
# Conflicts:
#	SUPERMUX-TOUCHPOINTS.md
#	Sources/Devices/DeviceLink.swift

This branch was successfully deployed

1 active deployment
cloud-vm-image-checks — 45454594 Deployed Oct 1, 2026 by rajinsyed via reachable #3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants