The status endpoint now reports, per account, how many sessions are
bound with traffic inside the routing load window, plus the account's
cooldown deadline. The coderouter CLI uses both to make sticky routing
and rate-limit cooldowns visible. The session-count read is display
only and fails to zero so it can never take the status endpoint down.
Pricing change: a team may connect and route up to 3 provider
accounts (subscriptions) for free. More than 3 requires an active
cmux Pro or Team subscription.
- New services/coderouter/entitlement.ts: free tier checked first
(one indexed count read); the Stripe read runs only for teams over
the limit. Both checks fail closed.
- Session issuance: replaces the flat Pro gate. Over-limit teams
without a subscription get 402 with the count in the message.
- Account add: connecting an account beyond the limit without a
subscription returns 402 before anything is stored. Re-importing
an account the team already has is always allowed, so a broken
account stays repairable on the free tier. The accounts POST
handler is now factory-built for dependency-injected tests.
- Billing-lapse token revocation stays unchanged: after a lapse the
CLI renews and the free tier re-qualifies teams with <= 3 accounts.
- New analytics: entitlement_basis on route_session_issued and a
coderouter_account_limit_reached event, both schema-whitelisted.
- CODEROUTER_HOSTED_PRO_REQUIRED=0 still disables all gating.
Boundary: exactly 3 accounts is free; the 4th needs Pro/Team.
A bound account in state 'refreshing' is healthy; its refresh lease
resolves in seconds. Treat it as usable for sticky reuse, and give a
sticky session a short bounded wait (4 x 500ms) when the refresh lease
is busy, instead of instantly moving the session and discarding its
prompt cache. Non-sticky requests keep the fail-fast move.
If the code deploys before the additive migration is applied, the
session-stickiness statements hit undefined_table (42P01). Treat that
as: no binding found, claim without the session-load ordering term, and
skip the pin. Routing behaves exactly like the pre-change code until
the migration lands, instead of erroring every Codex request. Covered
by a DB behavior test that renames the table away and back.
Port of subrouter PR #228 to the coderouter TypeScript data plane.
Problems in the current routing:
- selectAccountForRequest picks an account per request with three
independent statements (sweep, select, update). Nothing spans the
read-pick-write sequence, so parallel session starts read the same
snapshot and herd onto one account. That account burns quota first,
then every session reroutes to the next account at once.
- No session stickiness exists. Each request rotates to the
least-recently-used account, so the provider prompt cache is
discarded on nearly every turn and the whole prefix is re-billed
as uncached input across all accounts.
Fix:
- New coderouter_session_accounts table pins one agent session (the
Codex CLI session_id header) to one account.
- selectAccountForSession honors a usable binding first (sticky), and
moves a session only when its account is broken, cooling down,
removed, or already attempted in this request.
- New placements claim an account atomically in one statement with
FOR UPDATE SKIP LOCKED, ordered by fewest recently-active bound
sessions, then least-recently-used. Concurrent claims take
different accounts instead of the same snapshot argmax. When every
candidate row is locked, a blocking fallback claim accepts a
collision instead of failing the request.
- selectAccountForRequest (models, opencode) now uses the same atomic
claim, which closes its read-pick-write race too.
Tests: unit tests for the selector and the responses proxy, and
CMUX_DB_TEST-gated behavior tests that prove sequential spread,
concurrent non-herding, stickiness, move-on-cooldown, exclusion,
and last-write-wins binding against real Postgres.
The lane is fleet infrastructure we operate, not contributor guidance for this
codebase, so the prose now lives in cmuxterm-hq at
skills/infra/blacksmith-testbox/ beside macfleet and cloud-vm-ops. See
manaflow-ai/cmuxterm-hq#306.
Everything the box actually executes stays here: both workflows, the five
scripts/blacksmith-*.sh helpers, and the two guards that validate them. Only
tests/test_testbox_doc_blocks.sh follows the skill, because it validates that
prose, and hq gained a workflow so it keeps gating rather than quietly stopping.
CLAUDE.md keeps a pointer rather than dropping the subject, because an agent
starting cmux-tui Rust work reads this file and must still learn not to compile
on the Mac. Worth stating plainly: all nine trial agents found this skill from a
local file in this repo, and a pointer to another checkout is weaker
discoverability, particularly from a plain cmux clone.
Review follow-up: replace the 0/1 ternaries with an exhaustive switch into
DiagnosticConnectionMethod so a future third method becomes a compile error
instead of silently reporting as Tailscale.
Co-Authored-By: Claude Fable 5 <[email protected]>
Review follow-up: inject a fixed date instead of wall-clock Date() so
route-selection behavior in these tests is deterministic.
Co-Authored-By: Claude Fable 5 <[email protected]>
The bundle-isolation rollout (#9183) records iOS builds without the
X-Cmux-App-Namespace header as legacy. Those are the shipped pre-namespace
Beta binaries on the default lane, and they lost the official-namespace
default->{default,nightly} exception: discovery still listed a Nightly Mac,
but issuePairGrant denied it with target_not_pairable, surfacing on the
phone as Authorization failed while relay policy and reachability passed.
Legacy callers on the default lane now get the same default+nightly Mac
reach as official namespaced apps, applied through the single
canIOSBindingUseMac choke point so pair grants and proofed discovery agree.
Non-default legacy lanes keep exact tag matching, and canIOSBindingForgetMac
is split off the alias so the destructive forget path is not broadened.
Co-Authored-By: Claude Fable 5 <[email protected]>
Old Beta builds predate the X-Cmux-App-Namespace header and binding request
proofs, so the broker records them as legacy/default. They currently lose the
official-namespace default->nightly exception and get target_not_pairable
against Nightly Macs. These tests pin the intended compatibility: pairing and
discovery gain the fallback, non-default legacy lanes keep exact matching, and
the destructive forget_mac path is not broadened.
Co-Authored-By: Claude Fable 5 <[email protected]>
Revalidating the demo after the set-difference rewrite passed the build (2m13s
then 0.14s, box stopped, exit 0), but the org check afterwards showed its warmup
run still in_progress, holding a 32 vCPU runner. The skill calls cancelling a
required step and the script did not do it, so the one entry point most people
will run was the one leaking.
It records the run it approves and cancels it from the same EXIT trap that stops
the box, so a Ctrl-C cleans up both.
I added the cargo test line to the skill without running it. A concurrency
trial ran it and it fails on a fresh box: 104 failures, exit 101, all one root
cause. `blacksmith testbox run` gives a shell at umask 0002, so test directories
are created group-writable, and cmux-remote's secure-directory check correctly
rejects an ancestor writable by other users without the sticky bit.
Hosted CI runs at umask 022, so the suite passes there and fails here. The suite
is not umask-independent, which means this lane does not reproduce CI unless the
command says so. With `umask 022` prepended, 3504 tests pass in about 88s.
Builds and clippy are unaffected.
Two agents dispatching nine seconds apart deadlocked. The guard correctly
refused to guess between two identical waiting gates, then told the operator to
"approve yours in the UI". An agent has no UI, and nothing in the REST API binds
a run to a Testbox ID, so the recovery was unusable by the caller the lane is
built for. One trial burned 11 minutes and an extra box getting out of it.
Snapshot the waiting gates before dispatching and take the set difference after.
That is exact where a 120 second window is not, since another operator lands
inside any window you pick. On the rare true tie the instruction is now
deterministic and needs no browser: stop your box, re-snapshot, dispatch again.
Also record the trap the same trial nearly fell into. Correlating a box to a run
by timestamp looks right and is wrong: Blacksmith rewrites a box's CREATED value
as it hydrates, measured moving 05:54:43 to 05:55:03 to 05:58:48 on one box. The
authoritative binding is the RUN URL column, which appears only once the box is
ready.
SKILL.md, benchmark.md, and the demo script now share the one algorithm.
Every trial exercised the evidence path, so the skill only ever showed
cargo build. An agent's real task is usually cargo test or clippy, which use the
same warm target/ and need no stage helper; show them.
Move the trust-boundary reasoning to references/trust-boundary.md and keep the
operative summary inline. An agent about to build needs the rule and the
enforcement, not the full argument. SKILL.md is 11.7 KB, still above the 10 KB
guideline; the remaining bulk is the approval block, and the eighth trial proved
that moving an executable step away from the file that needs it strands a live
box, so it stays.
Ninth trial ran all eight documented blocks literally, in order, each exiting 0,
repairing nothing. Two cosmetic inaccuracies left, both mine.
The EXIT trap printed "Testbox <id> is still running; no stop was authorized"
at the end of a run whose ceremony had already stopped the box, because it
inferred liveness from CONFIRM_TESTBOX_STOP_SHA being unset rather than from
the inventory it had just captured. On a clean run that reads as a failure. It
checks list-at-exit.log now.
And I cited the changed-file overhead gap as "several times larger, 8.1s against
1.2s". It measured 1.48s this run with no change to the lane. The guidance not
to read that gap as sync overhead stands; the magnitude does not, so the text no
longer promises one.
Eighth trial ran the documented blocks literally, as instructed, and the
warmup block stranded a live 32 vCPU box. `grep -c .` exits 1 when the count is
zero, and the plan sets `set -euo pipefail`, so the assignment
`waiting_count="$(... | grep -c .)"` ended the script on the first poll. Zero
waiting runs is the normal state right after warmup returns, so the 150s poll
loop was unreachable dead code, and the exact failure commit 8d3edf5b28 set out
to fix happened again, one round later, in the fix itself. There is no error
text; the script simply stops, leaving a warmed box, an un-approved run, and a
receipt the trap correctly refuses to act on.
Guarded every count with `|| true` in both files.
tests/test_testbox_doc_blocks.sh now treats the plan as executable
documentation: it parses all 21 fenced bash blocks under the shell the plan
mandates, then executes each counting construct against an empty result to prove
it survives its normal first state. Verified red against the unguarded line and
green after. Syntax checking could never have caught this, which is why two
rounds of review missed it.
Two more from the same trial. benchmark.md never cancelled the warmup run, so
following it alone leaves the keepalive holding a runner until the 120 minute
timeout; it now cancels and polls to terminal state. And the setup artifact is
required evidence whose command sat in prose rather than a fenced block, so a
literal run never captured it.
Seventh trial ran smooth end to end, zero unexpected failures, and still found
that benchmark.md's warmup block goes straight from the receipt to
`status --wait`. The approval existed only in the surrounding prose, so anyone
copying the block parks at the environment gate, burns the full 15 minute
timeout, and strands a warmed 32 vCPU box. Prose next to a code block does not
protect someone running the code block. The approval, with the polling guard and
its own attempt transcript, is now in the code, along with the DISPATCH_EPOCH
capture it binds to. Removed the unreachable second warmup_status check while
there.
Also qualified the overhead column. The changed-file gap measured 8.1s against
1.2s for the other stages while all three synced strategy=skip, because that
stage's backup, edit, restore, and re-verify work runs inside the CLI call but
outside wall_seconds. It is real, it is not sync and transport, and
operations.md now says to compare overhead only across first-clean and
incremental-noop.
Every bash block in the plan is now syntax-checked as a unit.
Sixth trial reported the run as not smooth, and it was right. The guard I wrote
in SKILL.md Step 3 queries for waiting runs the instant warmup returns. GitHub
had not surfaced the run yet, so zero matched, and the guard printed "more than
one run waiting; approve yours in the UI" and exited. Zero runs is not
ambiguity, it is "not yet". Under benchmark.md's set -e that abort left a live,
un-approved box on the org inventory with its receipt already written, and the
plan's own trap is designed to refuse to stop it.
The guard now polls for 150s, distinguishes zero from two-or-more, and says
which happened. It also tells you that an abort means you already own a running
box, so stop it before retrying or the next dispatch leaves two boxes and one
receipt.
The trap also lied about why it gave up: it printed "warmup returned no owned
Testbox receipt" when the receipt existed and the real cause was
CONFIRM_TESTBOX_STOP_SHA being unset, which is the normal state. It now reports
the condition that actually held, and names the box you must stop yourself. Its
inventory file was called list-after-warmup-failure.log even on the success
path; it is list-at-exit.log now.
Two smaller ones. Record every approval attempt separately, because a retry that
overwrites the first hides a live unapproved box from the evidence pack. And
warm-before-reading has an exception for evidence runs, whose preflight and
receipt must exist before warmup; three agents in a row hit that ordering.
Finally, changed-file rebuilds cmux-remote as well as cmux-tui, so its ~8s is a
small-edit figure, not a single-crate floor.
Fifth trial ran the lane smoothly end to end: every documented command exited
as documented, no retries, no procedural guessing, and the only non-zero exits
were the two the docs predict. 132.25s clean, 0.17s no-op, 8.03s changed-file,
box and runner both proven gone. The artifact check added last round worked: it
recorded a 746 MB binary, 48 bytes larger in the changed-file stage.
Two defects were mine from the previous commit. The artifact prose said
cmux-tui-testbox-<run-id> while the command beside it correctly said
cmux-tui-testbox-setup-<run-id>, so following the prose downloads nothing. And
required field 3 asked for an "identity run ID" for a run the same document
forbids issuing; it now says there is no such run and names the real transcripts.
The third is older and worse. operations.md promised two clocks and described
the second as the local CLI transcript covering sync, transport, and queueing.
No pack ever contained it: stage `wall_seconds` is measured on the box around
cargo, which is why it sits milliseconds above `/usr/bin/time -p` real rather
than well above it, and three separate agents nearly cited it as CLI wall time.
The plan now records the real local clock per stage in <stage>.cli-wall.txt, and
operations.md says plainly that a pack without it cannot measure Testbox
overhead at all.
Fourth trial agent ran the plan with zero non-zero exits and no retries
(127.03s clean, 0.17s no-op, 8.02s changed-file, box and runner proven gone),
then found the ordering defect that matters most: benchmark.md printed the
three-stage loop BEFORE the pin instruction, so following that file top to
bottom benchmarks main rather than your branch. SKILL.md had the right order,
so the two documents disagreed. The pin now has its own section ahead of the
loop, and says plainly that running the loop first measures main.
The stage helper now verifies the build produced cmux-tui/target/debug/cmux-tui
and records its size in each stage JSON. A zero exit proves cargo was happy, not
that anything was produced, and the box is destroyed before anyone can check.
Also: name the setup artifact and give the gh run download command, instead of
"keep the artifact URL" with no artifact name; document that gh run cancel takes
about five minutes to land, so in_progress right after cancelling is not a
failure; and explain the two surfaces that look contradictory after a stop, that
status still prints a completed row while list shows nothing, and that
setup-identity.json says X64 where stage records say x86_64 for the same host.
MacSurfaceGalleryPreviewView (#if DEBUG) builds TerminalPickerMenuActions
without the required selectSimulatorStream closure, so every Debug iOS
build from main fails compiling CmuxMobileShellUI while Release archives
skip the file and stay green. Missed in the PR 10072 partial merge, same
family as #10287/#10290/#10295.
Co-Authored-By: Claude Fable 5 <[email protected]>
Third agent ran the whole plan with no failure, no retry, and every exit code
the docs predict, including the intended 75 from PREVIEW: 129.97s clean, 0.18s
no-op, 9.28s changed-file, box and runner both proven gone. What it found was
that the evidence pack the plan produces cannot be fully trusted.
`blacksmith auth whoami` writes to stderr, so the plan's bare invocation put no
org identity in the pack while the field list implied one; it captures stderr
now. `timings.json` hardcoded schema 2 while stage records self-report schema 3,
and operations.md still described stage records as schema 2, so the wrapper now
carries stage_record_schema and the prose matches the code.
The guard naming a historical evidence directory pointed at a path that does not
exist and could not: `blacksmith-testbox-e40704611ac35f4ffa153` is the first 13
characters of that SHA glued to its last 8, and it lacks the mandatory suffix. A
guard you cannot resolve protects nothing, so both files now state the rule
itself: never write into an evidence directory you did not create this run. The
superseded 161.47s/8.28s/9.13s numbers go with it; three trials have since
measured this lane directly.
The collision-avoidance branch was dead code. It tested for
blacksmith-testbox-<sha> while every directory the plan creates is
blacksmith-testbox-<sha>-<suffix>, so the timestamp path never fired and two
runs of one SHA from one PID collided. It globs the suffix now.
Finally, Blacksmith's CREATED column is a last-transition time, not a creation
time; the same box reported three different values across hydrating, ready, and
stopped. Say so, and point at the status --wait transcript for elapsed time.
Second independent agent, given only "produce defensible benchmark evidence",
found the skill from CLAUDE.md and ran the full plan first try: 129.37s clean,
0.17s no-op, 9.69s changed-file, box and runner both confirmed gone. The
receipt-bound cleanup fixed in the previous commit worked for it. Five things
still failed it.
Two were code. The post-stop poll wrote to status-before-stop.log, so the
evidence pack's own pre-stop record was overwritten with post-stop state and an
auditor would misread when the box existed; it writes status-after-stop.log now.
And cleanup-preview.json paired warmup_ref with the benchmarked source_sha,
so the destruction record named a SHA that is not on the ref beside it; it
records both refs now, schema 2, and the regression test asserts it.
Three were documentation. benchmark.md bills itself as the full plan but never
mentioned the deployment gate, so its warmup block deadlocks for 15 minutes on
`status --wait`; it now points at the guarded approval and warns against
approving workflow_runs[0]. The PREVIEW cleanup exits 75 on success, which the
same plan's mandated `set -e` turns into an abort right before cleanup; both
call sites say so and show the `set +e` wrapper. And SKILL.md claimed stopping
the box ends the warmup run, which is false: the measured run sat in_progress
for four minutes afterwards holding a 32 vCPU runner, so cancelling is now a
required step rather than a hedge.
makeSecondaryClient picked routes with a bare storedReconnectRoutes call,
so with Connection Method = Tailscale the multi-Mac aggregation pool still
pinned stored Iroh routes and dialed background-control sessions over
public paths and managed relays; broker-discovered secondaries took the
same path. Route through orderedReconnectRoutes, which applies the same
strict Tailscale requirement as the foreground dial: grant-authorized
Tailscale routes still aggregate, everything else fails closed and waits
for a new external edge.
Zero-touch Iroh discovery now returns no candidates while Tailscale is
selected, skipping broker round-trips whose iroh-only results could never
be dialed.
Co-Authored-By: Claude Fable 5 <[email protected]>
With Connection Method = Tailscale, background multi-Mac aggregation still
pins a stored Iroh route and opens an Iroh background-control session over
public paths and managed relays. makeSecondaryClient builds its routes
without the Tailscale requirement, bypassing the strict filter every other
dial path applies. Failing test only; the fix lands in the next commit.
Co-Authored-By: Claude Fable 5 <[email protected]>
A transport report today only implies the connection method and carrier
transport through whichever dial events survived the bounded ring; reading
one still needs a Settings screenshot. Record the configured method
(connectionMethodConfigured) at store construction and on every foreground
so any report window states it, decode the existing
connectionMethodPreferenceChanged value into the same readable method name,
and record foregroundTransportSelected with the active route's transport on
connect and on every route change, covering both state-then-route and
route-then-state connect flows plus mid-connection promotions.
Reports now carry lines like:
App feature event (Operation: connectionMethodConfigured, Method: Tailscale Only)
App feature event (Operation: foregroundTransportSelected, Transport: Iroh)
en+ja catalog entries for the new method and field names.
Co-Authored-By: Claude Fable 5 <[email protected]>
A subagent given only "compile cmux-tui on Linux" found the skill from
CLAUDE.md and followed it to real numbers, then reported what broke.
The receipt-bound cleanup could never succeed. #10303 pinned warmup to
--ref main, so the inventory REF column is always main, while the receipt
recorded source_ref, the branch being benchmarked. cleanup.sh compared the two
and exited 66 with "inventory ownership context differs from the warmup
receipt", for every legitimate user. That pushed operators toward a bare stop
and away from the ownership check the docs spend a page justifying. The receipt
now carries warmup_ref and source_ref separately and cleanup compares the
inventory against warmup_ref. tests/test_testbox_cleanup_receipt_ref.sh drives
the real script against a stubbed CLI; it reproduces the old exit 66 and passes
on the fix.
The approval snippet took workflow_runs[0]. Every run in this lane shares a
title and a main head branch, and the agent found a stranger's run waiting two
minutes ahead of its own, so the snippet would have approved someone else's
deployment gate. It now binds to a run created after your own dispatch and
refuses when more than one is waiting, matching the demo script's guard.
Also: `list --all` is not proof nothing is burning, because the keepalive holds
a 32 vCPU runner after the box is gone, so step 7 checks the run too and shows
`gh run cancel`; say which of the two cleanup paths is authoritative and when;
state that warmup does not block; put the --idle-timeout unit on the command;
and note that the aggregator is all-three-stages or nothing.
Running a script named "testbox demo" is the consent. The spend is bounded by
--idle-timeout and by the EXIT trap that stops the box, so a prompt whose answer
is always yes was noise. It also aborted confusingly with no TTY, because `read`
returns non-zero under `set -e`. Print what is about to happen instead.
Reading a runbook does not tell you what a persistent build box feels like.
scripts/blacksmith-testbox-demo.sh warms one, pins it to your pushed HEAD,
builds cmux-tui twice, and prints both times so the second build makes the
point the prose cannot.
It prints every remote command before running it, so the tour doubles as the
documentation. It stops the box it created from an EXIT trap, including on
Ctrl-C, and it only ever stops that box. Deployment approval matches runs
dispatched within two minutes of its own warmup and refuses to guess when more
than one is waiting, so it cannot approve someone else's run. --stages swaps
the two plain builds for the three measured stages.
The skill opened with its threat model and buried the commands, so an agent
about to compile cmux-tui learned the lane existed only after it had already
waited. Lead with the operating rule instead: warm your own box in the first
minute, before reading code, because the four minutes of hydration are four
minutes you can spend reading.
Then a seven-step CLI walkthrough covering warmup, deployment approval, pinning
the box to your commit, building or benchmarking, download, and stop. One box
per worktree and per agent, since `testbox run` synchronizes with
`rsync --delete` and two agents on one ID overwrite each other.
SKILL.md drops from 19.7 KB to 8 KB. Stage orchestration, the cleanup ceremony,
and how to read the two clocks move to references/operations.md; the full
evidence plan stays in benchmark.md. Index the skill in CLAUDE.md, which #10303
missed, so agents can find it at all.
Testing the broker's cross-commit path on a live box disproved a claim I wrote
into the skill. `blacksmith testbox run` synchronizes file contents, not
history: it makes one opportunistic `git fetch` of the benchmarked commit, falls
back to copying changed files, and skips even that once fingerprints match. A
local-only commit fails on the box with `upload-pack: not our ref`, leaving the
box on the `main` checkout the warmup job made, with candidate file contents
written over it. The stage helper then died on a raw `git rev-parse
<sha>^{tree}` error.
Require the pushed commit, and pin the box to it once per box with fetch plus
`git reset --hard` before the first stage. That makes the box an exact checkout
of the benchmarked revision, so a timing always names something anyone can
fetch, and an uncommitted edit can never be what got built. The helper now
verifies the commit is present and checked out, and prints the exact push and
pin commands instead of a git internals error.
Ubuntu 24.04 ships shellcheck 0.9, which reports trap-only functions as
SC2317; 0.10 renamed that to SC2329, which is what the file disabled. The new
lint job runs on the runner's shellcheck, so it flagged all 15 lines of the two
trap handlers. Name both codes.
The main CI suite is dispatch-only while CI is paused, so tests/test_ci_testbox_broker_guard.py
would not have run on a pull request that weakened the lane it guards. Give it
a small always-on workflow that also shellchecks the four helper scripts and
actionlints the warmup workflow with a checksum-pinned actionlint.
No path filter, deliberately. A change that moves or renames the guard is
exactly the change a path filter would let through.
The property that keeps candidate code out of the token-bearing job is a
workflow shape, and no runtime check can observe it without spending a 32 vCPU
box. Assert it statically: manual dispatch only, no input that selects a
source revision, a refs/heads/main guard ahead of begin-testbox, no repository
script or local composite action before the token, every checkout pinned to
github.sha, SHA-pinned actions, and a keepalive that runs from the checked-in
main script.
Verified red against the previous candidate-controlled workflow (two failures:
the candidate-selecting inputs and the missing main guard) and green here.
blacksmith testbox warmup resolves the workflow definition and the hydrated
source from one --ref, so a lane that warms a candidate branch runs that
branch's copy of the workflow before begin-testbox writes the Testbox auth
token into the job. A candidate could therefore delete its own guards.
Hydrate main only. The first step refuses any ref except refs/heads/main, and
no repository code runs before the token. A candidate revision reaches the box
afterwards through blacksmith testbox run, which syncs a maintainer's worktree
onto the warm VM and needs Blacksmith org credentials that already grant box
access, so it moves no trust boundary.
The hydrated commit and the benchmarked commit are now deliberately different.
The stage helper checks the setup marker for VM identity, runner class, and
toolchain completeness instead of source equality, records the hydrated ref and
SHA under a new "hydration" block, and still fails closed when the active Rust,
Cargo, or Zig differs from what warmed the caches.
The lane no longer needs BLACKSMITH_TESTBOX_REVIEWED_REF or
BLACKSMITH_TESTBOX_REVIEWED_SHA; the environment needs a deployment branch rule
of exactly main.
Tab identity was re-derived on every read from either the live surface or
the slot indexes, so each reader invented its own missing-data policy: the
projection failed hard, `rebuild_resource_indexes` silently dropped the tab,
and the browser branch required a live surface. A silent drop is the worst
of the three, because the next projection tombstones durable rows that are
still live.
`State::register_tab_identity` is now the writer every placement path uses,
including `insert_surface_checked` and the browser attach path, which
previously left identity to be harvested from the surface at the next index
rebuild. The reserved-placement install in `resource_project_terminal_selected`
still writes its own placement order, which `register_tab_identity` must not
reorder; it writes the same identity fields.
Every reader now takes identity from the topology: the index rebuild no
longer consults surfaces, the projection resolver reads the owner, and the
layout-undo token and active-tab lookup drop their surface fallbacks.
`State::ensure_tab_identity_coverage` runs at the projection boundary, so a
missing identity fails that mutation by name instead of erasing durable rows.
Browser tabs now project from their durable row when their runtime is gone,
matching terminals. That closes the same failure for a browser view whose
surface never materialized.
`ordered_terminal_tab_ids` demanded a live surface for every pane tab, while
the projection loop right below it already accepted a restored tab that had
no surface yet. Startup restores tabs before adoption, so an unadoptable host
leaves a tab with no surface, and the ordering pre-pass aborted the whole
projection with "pane references missing surface <slot>".
The abort happened inside the exit-detach commit that was supposed to prune
that terminal, so nothing was written and every later start repeated it. One
orphan host recovered, because removing it made the tree consistent; two or
more behind the same pane wedged the session forever.
Both call sites now resolve tab identity through one helper that prefers the
live surface and falls back to the durable indexes.
Also commit the durable exit before deleting the host record in the two
startup paths that proved a host dead. The record is the only evidence that
the host existed, so a failed commit must not erase it first.
A daemon restart that finds several unadoptable terminal hosts in the same
pane must still start. Today the first exit-detach projects the whole tree
while the other restored tab still has no surface, so startup aborts with
"pane references missing surface <slot>" and writes nothing, which makes the
session permanently unopenable.
Fourth and fifth Release-archive breaks from 04ff18eea6: WorkspaceDetailView
and its extensions reference store.selectedMacSurfaceID and
store.selectMacSurface(_:), which were never implemented on
MobileShellComposite (MobileShellCompositePreviewTests already encodes the
contract: starts nil, selectMacSurface sets it without touching
selectedTerminalID, workspace switch clears it). effectiveConnectionStatus
was private in WorkspaceDetailView.swift but used from
WorkspaceDetailView+Surfaces.swift, a different file.
Adds the stored property plus the clearing hook in selectedWorkspaceID.didSet,
the selectMacSurface mutator in MobileShellComposite+SurfaceFocus, and drops
the private modifier.
Co-authored-by: Claude Fable 5 <[email protected]>
supportsPanelArtifacts(in:) exposes MobileWorkspacePreview.ID publicly, but
the file imported CmuxMobileShellModel with a plain import, which is internal
under InternalImportsByDefault. The Release device archive rejects it (method
cannot be declared public because its parameter uses an internal type); Debug
simulator builds do not, which is why test-ios stayed green. Matches the 24
sibling files in this package that already use public import.
Co-authored-by: Claude Fable 5 <[email protected]>
* fix(ios): make artifact failure and scope switches exhaustive
04ff18eea6 added ChatArtifactError.unknown(code:) and the .panel loader
scope but missed two switches, so the cmux-ios scheme has not compiled
since Aug 14 and every iOS TestFlight upload since build 20260814211222
has failed. Adds the .unknown failure presentation (localized en/ja,
never blames connectivity) and routes .panel loader scope to the .panel
viewer scope.
Co-Authored-By: Claude Fable 5 <[email protected]>
* fix(ios): map unknown artifact errors in MarkdownSurfaceModel
The same 04ff18eea6 change left a third non-exhaustive switch in
MarkdownSurfaceModel.failure(for:), hidden behind the CmuxAgentChatUI
compile failure because CmuxMobileShellUI builds after it. Map
.unknown(code:) to .loadFailed(code:), whose doc contract is exactly
this case (the Mac answered with an unrecognized error), preserving the
code for display.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
CodeRabbit follow-ups: the UI test now also asserts the open-url capture
stays at exactly one entry through the settle window, and
debugSimulateCommandClick no longer exposes the open-url dispatch state
in its payload (nothing consumed it).
Co-Authored-By: Claude Fable 5 <[email protected]>
Root cause: one cmd-click could be handled by two independent open paths.
When the click landed on a link Ghostty recognizes (URL regex or OSC 8
hyperlink), Ghostty consumed the release and dispatched its open_url
action synchronously inside ghostty_surface_mouse_button; cmux routed it
through TerminalLinkOpenCoordinator. Back in handleCommandClickRelease,
the cmd-click word-path fallback ALSO ran: its guard deliberately lets
pointer-snapshot resolutions through when Ghostty consumed the release,
because consumption alone can also mean mouse reporting or prompt clicks.
Both paths then opened the same target, each with its own routing
decision, so with app.preferredEditor configured an image opened in both
Preview (coordinator, raw NSWorkspace) and the preferred editor
(fallback, PreferredEditorService) at once.
Fix, at the routing layer:
- GhosttyNSView now counts Ghostty open_url dispatches; the release path
snapshots the counter around the ghostty_surface_mouse_button call.
When it advanced, Ghostty owns the click's open and the word-path
fallback stays out entirely. The snapshot exception still applies to
consumed-but-not-link releases (mouse reporting, prompt clicks), so
cmd-click on paths inside TUIs keeps working.
- TerminalLinkOpenCoordinator routes local-file external opens through
the shared FileOpening seam (PreferredEditorService) instead of raw
NSWorkspace, so the single chosen handler is the preferred editor when
configured and the system default otherwise, matching every other file
entrypoint. Non-file URLs still open through the system opener.
Co-Authored-By: Claude Fable 5 <[email protected]>
Two failing behavior tests reproducing issue #10222:
- TerminalLinkOpenCoordinatorTests: with app.preferredEditor configured, a
local file that is not routed inside cmux must go through the
preferred-editor seam, never the raw system opener. Fails today because
TerminalLinkOpenCoordinator hands local files to NSWorkspace directly,
ignoring the preferred editor.
- TerminalCmdClickUITests: cmd-clicking an OSC 8 file hyperlink must reach
exactly one handler. Fails today because Ghostty consumes the click and
dispatches its open-url action AND the cmd-click word-path fallback also
opens the pointer's resolved path, delivering the same click to two
handlers (e.g. Preview and the preferred editor at once).
Co-Authored-By: Claude Fable 5 <[email protected]>
* test: reproduce ambiguous iOS dogfood identity
* fix(ios): pin dogfood auth identity end to end
* docs: use every Mac fleet slot for heavy work
* fix(ios): reject agent profile on physical iPhone gates
* fix(ios): preserve profile resolution and auth replacement
* fix(ios): stop tagged Mac before auth relaunch
* fix(auth): gate Mac session replacement on resolved credentials
* fix(auth): reuse matching tagged Mac sessions
* fix(ios): preserve queued builds and secret isolation
* fix(auth): restart stale Mac for default device launch
* fix(ios): preserve queue fallback checkout
* fix(auth): close profile and tagged app teardown gaps
* fix(auth): use actor-backed app termination wait
* fix(ios): clarify separate build and launch argument contracts
* fix(auth): use bootstrap-owned Mac auth readiness
* fix(auth): launch tagged Mac with workspace environment
* fix(ios): mark unauthenticated queue intent explicitly
* fix(ios): authenticate staged queue deliveries
preservingDefaults and the socket zoom test captured object(forKey:),
which reads through the registration domain; after BrowserPanel
registers browser fallbacks, restoring would persist a fallback for a
key that was never written. Snapshot the persistent domain instead.
Co-Authored-By: Claude Fable 5 <[email protected]>
Adds browser.defaultZoomLevel (0.25–5.0, default 1.0) as a shared
BrowserZoomSettings policy: applied at webview creation and prewarm
adoption, used by the Actual Size/reset action, editable in Settings >
Browser, configurable via cmux.json, and documented in the schema and
key reference. browser.zoom.set now also accepts an absolute numeric
zoom and a `surface` alias for surface_id, mirrored in the CLI
(`cmux browser zoom 0.8`).
Fixes#10155
Co-Authored-By: Claude Fable 5 <[email protected]>
* Prevent Mac sleep while agents run or iPhone is connected
Keeps the Mac reachable for the iOS app and long-running agent tasks by
holding a single PreventUserIdleSystemSleep power assertion (display
still sleeps) while either gate is active:
- power.preventSleepWhileAgentsRunning.enabled (default off): any
workspace has a registered agent PID (SleepyAgentCensus).
- power.preventSleepWhileMobileConnected.enabled (default on): at least
one iOS client connection (MobileHostConnectionRegistry).
Mechanism: new PowerAssertionHolder extracted from SleepyModeController
(which now uses two holders, behavior unchanged); new PreventSleepManager
recomputes a pure decision (PreventSleepDecision) on agent-model changes,
per-TabManager tabsPublisher emissions, mobile host status changes, and
settings flips, and acquires/releases the assertion idempotently.
Released on app termination.
Settings: two toggles in the Mobile section (Keep Awake group), catalog
keys in cmux.json, search aliases, EN+JA localization.
Validated empirically on a Mac mini (macOS 26.5): with 1-minute idle
sleep armed and protections removed the machine slept and dropped off
Tailscale (brief DarkWake windows only); with a headless process holding
this exact assertion, 157/157 reachability probes over 8 minutes passed
with zero sleep entries.
* Address review: fix stale-tab reconcile, gate agent census behind setting
- tabsPublisher emits during willSet, before tabManager.tabs commits.
Reconcile now counts agents from the emitting manager's new tab list
(override) instead of the stale census, so closing the last agent
workspace releases the sleep assertion immediately.
- Agent observation (tab subscriptions, per-workspace change streams,
census scan) now attaches only while the default-off agents setting
is enabled; disabled users pay no observer fanout.
- PreventSleepManager owns its own UserDefaults observer; AppDelegate
keeps only start()/stop() lifecycle calls (budget +2 accepted).
- Register the two Keep Awake rows in the settings search anchor
contract list (fixes SettingsRowAnchorResolutionTests).
* Make power assertion logger nonisolated, align subsystem with app convention
* Gate mobile keep-awake on authenticated connections; bound agent-event fanout
- MobileHostServiceStatus gains authenticatedConnectionCount (connections
with at least one authorized request, from clientIDsByConnectionID);
activeConnectionCount is unchanged for existing consumers. The service
posts mobileHostStatusDidChange on the unauthenticated->authenticated
transition and after authenticated-connection teardown so keep-awake
reconciles at both edges.
- PreventSleepManager's mobile gate now uses the authenticated count via
a lean accessor (no statusSnapshot route resolution per reconcile), so
unauthenticated LAN peers holding TCP sessions cannot pin the power
assertion.
- Per-agent runtime events no longer trigger app-wide sweeps: the manager
maintains per-workspace running-agent counts, updates only the emitting
model's count, and reconciles only when that count changes. Full rebuilds
happen only on topology/settings/mobile syncs. SleepyAgentCensus reverts
to main (helper no longer needed).
- Unit test: authenticatedConnectionCount tracks authorized connections
only (no double-count per connection, drops on close).
* Only record client IDs for verbs that required authorization; lean mobile reconcile
- onAuthorizedRequest fires for auth-exempt verbs too (authorizeRequest and
authorizationError both return nil for mobile.host.status), so a spoofed
client_id on the unauthenticated status verb was recorded and counted as
an authenticated connection, letting an unauthenticated LAN peer hold the
keep-awake assertion. Both listener sites now skip recording unless
requiresAuthorization(method:) is true.
- PreventSleepManager's mobile status observer now runs the constant-work
reconcile instead of a full topology sync; mobile events cannot change
workspace topology or agent observation.
* Count authenticated connections at the authorization gate, not the client-id hook
mobile.events.subscribe is authorized but intercepted before
onAuthorizedRequest, and subscribe requests need not carry client_id, so a
phone connected only for live events counted 0 and keep-awake released the
assertion mid-stream. Connections are now marked authenticated in the
authorizeRequest gate itself (any auth-required verb that passes), tracked
in a dedicated authorizedConnectionIDs set that backs
authenticatedConnectionCount; client-id bookkeeping stays as-is for
viewport cleanup. Set cleared on listener adoption/stop/debug-reset and on
connection close (with a status post so keep-awake drops promptly). Test
updated: client-id recording alone no longer counts, gate marking does.
* Refuse stale authenticated-connection inserts after mid-authorization disconnect
authorizationError suspends on network Stack verification; a client that
disconnects during that await has already been cleaned up by onClose, so an
unconditional insert in recordAuthorizedConnection would never be removed
and would pin the keep-awake assertion until the listener restarts. The
record now requires the connection to still be tracked (activeConnections
or the connection registry); record and removal both run on the main actor,
so either interleaving converges. Registry gains contains(id:). Test covers
the refused stale insert via a real liveness gate (never-started dummy
session registered through a DEBUG seam).
* Make PreventSleepDecision a value type instead of a top-level function
Mirrors MobileHostSyncDecision/MobileHostPortApplyOutcome: the pure policy
lives on a value type with stored inputs and an isDesired property, giving
the power feature a scoped API instead of a module-wide free function.
* Replace new debug seams with internal members reached via @testable import
Aziz test/debug-seam policy (cmux PR #6452 precedent): the two seams added
for authorized-connection tests are removed; recordAuthorizedConnection and
activeConnections are internal instead, and the test registers its
never-started dummy session directly.
* Add Keep Mac Awake control on Mac and iOS
* Tolerate headless app activation in caffeine UI test
* Fix iOS caffeine settings compilation
* Use mobile localization support in caffeine settings
* Remove reintroduced disabled toast setting
* Initialize task composer setting on iOS
* Test iOS Keep Mac Awake control
* Capture Keep Mac Awake menu evidence
* Fix iOS test Foundation import
* Revert "Fix iOS test Foundation import"
This reverts commit 37897fcf59.
* Harden mobile caffeine status handling
* Annotate caffeine RPC handlers on main actor
* Harden ambiguous mobile caffeine mutations
* Report failed caffeine mutations accurately
* Keep caffeine retry available after unknown failure
* Isolate caffeine controller injection on main actor
* Fence caffeine snapshots by state revision
---------
Co-authored-by: Abdulaziz Albahar <[email protected]>
- Dock setSurfaceResumeBinding no longer runs the mutating
managedAgentResumeBinding sync-on-read before the Codex acceptance
guard: a rejected incoming binding must not promote the effective
binding to managed state as a side effect of the check.
- Dock setSurfaceResumeBinding now drops the panel's transient
resume-session working-directory rescue when the binding replaces a
different session, mirroring the Workspace path, so close/reopen
cannot launch a replacement session in the previous session's cwd.
- The fork SessionStart/SessionEnd regression tests assert exact
resume-set / resume-clear request counts, so an extra publication
targeting the parent surface cannot slip past the last-request checks.
Co-Authored-By: Claude Fable 5 <[email protected]>
The Workspace group row in the new-workspace composer fell back to the
folder SF Symbol, nearly identical to the Directory row's folder.fill
directly above it. Use rectangle.3.group (the glyph already used for
group actions) so the row reads as a group of workspaces instead of a
second directory picker.
Co-authored-by: Claude Fable 5 <[email protected]>
Forked Claude tabs kept the parent session id: identity could be minted
from the --resume launch argument (process-scanner fork fallback), from
launch-argv sniffing in the hook CLI, and hooks could stamp identity onto
the workspace's focused pane. Fork-of-fork and close/reopen then resumed
the wrong conversation.
Now only an authoritative Claude SessionStart on its owning surface can
install or replace that surface's session identity:
- upsertAuthoritativeClaudeSessionStart installs the record and the
active-surface boundary in one locked transaction; stale late
startup/resume events are rejected unless the owner allows replacement
or the incoming process is demonstrably newer; /clear stays an
ordering barrier.
- Every Claude hook handler requires an authoritative delivery target;
focused-pane fallback can no longer receive another process's identity.
- The accepted SessionStart immediately publishes the surface resume
binding and projects it into the restorable-agent snapshot used by
close history and workspace restore, so a fork cannot retain its
parent's restore state.
- The Claude launch-argument identity fallback is removed from the
process scanner and hook CLI; the generic fork-parent fallback remains
for Codex, where the resumed id genuinely is the session id.
Test fixtures now model the new registration: SessionStart records the
pid on the hook record, so live-process evidence flows from the record,
never from --resume argv; a fail-closed hook that never wrote the store
file counts as "no record".
Closes#10156
Co-Authored-By: Claude Fable 5 <[email protected]>
Preserve one pre-Iroh Tailscale fallback when the registry refresh is Iroh-only, keep registry-published routes authoritative, and serialize forced method-switch reconnects.
* Add task composer workspace group routing
* Move task composer group helpers to file scope
* Preserve restored task composer workspace group
* Gate task composer groups on authoritative inventory
* Require explicit task group recovery
* Fix task group capability scoping
* Fail closed on incomplete group owners
* Invalidate stale group authority
Scale daemon uploads to the binary size, isolate bootstrap SSH traffic from stale ControlMasters, and clean remote writers and temporary files when transfers fail. Preserve process timeout details so workspace bootstrap errors remain actionable.
Follow-up to #10107: the Add account rows should copy-paste the per-tool
install commands (npx coderouter@latest add codex / add opencode) instead of
the cr routing commands, and render the command text visibly in the row.
Co-authored-by: chatmux-connections[bot] <chatmux-connections[bot]@users.noreply.github.com>
- Drop MoshRemoteIPMode's unused cliValue initializer and Codable
conformance: no CLI flag or persisted setting selects an IP mode, so
the speculative API only implied a configuration surface that does
not exist. Production always starts from .remote with the automatic
proxy fallback.
- Make the fish login-shell tests report an explicit skip via
.enabled(if:) instead of silently passing when fish is absent.
Co-Authored-By: Claude Fable 5 <[email protected]>
Add `attach` to claude_builtin_command_name so the wrapper classifies
`claude attach <id>` as a command invocation and execs the real CLI with argv
untouched, like agents/mcp/etc. Hook settings are meaningless for attach
anyway: hooks live in the already-running background writer process, and the
injected fresh --session-id is exactly what turned an attach into a new
session.
Co-Authored-By: Claude Fable 5 <[email protected]>
`claude attach <id>` (the attach door for --bg background sessions) is a real
subcommand but hidden from `claude --help`, so it's missing from the wrapper's
builtin-command list. The wrapper classifies it as a session entrypoint and
injects --session-id/--settings ahead of it, which makes the real CLI treat
"attach" as the [prompt] positional: instead of attaching, it mints a brand-new
session with "attach" pre-filled in the composer.
Test-only commit (two-commit regression policy): CI should go red here.
Co-Authored-By: Claude Fable 5 <[email protected]>
- Move iOS TestFlight into its own sidebar section, placed below coderouter
- Move coderouter Add account above Connected accounts
- Show copy-pastable commands for Codex (cr codex) and OpenCode (cr opencode)
with their logos instead of the generic npx/cmux add commands
Co-authored-by: chatmux-connections[bot] <chatmux-connections[bot]@users.noreply.github.com>
- Dashboard home now shows an iOS app card (next to CodeRouter) that
links to the TestFlight page.
- Remove the Stack <TeamSwitcher> organization picker from the
bottom-left account area so only the cmux account menu remains.
- Add dashboard.home iOS strings to all locales and update the account
menu tests for the retained menu surface.
Co-authored-by: pi (manaflow-ai) <[email protected]>
mosh-server executes the received command with execvp and no shell, so
the staged launcher must be ['/bin/sh', '-c', script] instead of one
'/bin/sh -c ...' string that execvp treats as a literal pathname. The
OpenSSH string form is unchanged.
When SSH_CONNECTION is unusable, fall back to Mosh's SSH-proxy address
resolution instead of local mode: local mode resolves the destination
via DNS and breaks SSH-config-only aliases such as port-forwarded Coder
workspaces. Validate only the SSH_CONNECTION shape Mosh actually parses
(four fields, numeric ports, usable server address, now including
loopback rejection), not the unused peer address.
Co-Authored-By: Claude Fable 5 <[email protected]>
mosh-server executes the remote command argv with execvp and no shell,
so a single '/bin/sh -c ...' string is treated as a literal pathname.
Also pin the address fallback to Mosh proxy resolution and validate
only the SSH_CONNECTION fields Mosh actually parses.
Co-Authored-By: Claude Fable 5 <[email protected]>
Release the Mac terminal viewport lease when the iOS scene deactivates, clear stale local viewport metadata, and restore fresh geometry on reactivation.
Advance Bonsplit to the restored legacy pasteboard payload
(manaflow-ai/bonsplit#219): pane/browser drop overlays decode
tab.id/kind/sourcePaneId/sourceProcessId again, so the blue drop zones
render and drops route. Also share one tab-drag capability registry
between workspaces and the Dock so bonsplit tab-strip destinations can
resolve drags that began in another controller, matching 0.64.22 where
any destination could decode the drag.
Fixes#10033
Co-Authored-By: Claude Fable 5 <[email protected]>
Ends cancelled tab drags immediately instead of after AppKit's snap-back
animation, so drag state is revoked before the next press.
Bonsplit PR: manaflow-ai/bonsplit#218
Co-Authored-By: Claude Fable 5 <[email protected]>
Tab drags silently never started when the press arrived with
clickCount >= 2 — the common click-to-select-then-immediately-drag flow.
Bonsplit PR: manaflow-ai/bonsplit#217
Fixes#10033
Co-Authored-By: Claude Fable 5 <[email protected]>
* Add failing tests for dropping onto a collapsed/empty group header
A workspace dragged from outside a group can only land inside it by
hitting an insertion gap that belongs to the group row scope. When the
group has no visible member row under its header - collapsed, or a group
whose only member is its anchor - the resolver treated the header bottom
edge as a group/root boundary and handed it to the ambiguous horizontal
lane rule, so the left half of the header silently planned a root slot
beside the group instead of inside it.
These tests pin the intended behavior and fail on the current resolver.
Co-Authored-By: Claude Opus 5 <[email protected]>
* Adopt into the group when a drag is released on its header row
A group header row's lower half is the group's own adopt zone: releasing
there should put the workspace inside the group. It already did while the
group's members were rendered below the header, because the resolver
confirmed the group scope by looking at the next visible row. When there
is no member row under the header - the group is collapsed, or its only
member is its anchor - that lookup failed and the drop fell through to
the ambiguous group/root horizontal lane rule, so the left half of the
header planned a root slot beside the group instead. The gap that would
have accepted the workspace is the one the collapse is hiding, leaving no
discoverable way to drop into a collapsed group.
Treat a group header as an unambiguous group target on its bottom edge
and stop consulting the next visible row there; only member rows can sit
on a real group/root boundary. The header's top half still plans the root
slot before the group, so inserting above a leading group keeps working.
The fix lands in the shared resolver, so the AppKit sidebar table, the
SwiftUI reorder overlay, and the mobile move-intent resolver all pick it
up from one path.
Co-Authored-By: Claude Opus 5 <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
* iPhone auth gate: hard-fail unauthenticated dogfood installs
Installed-but-signed-out was a docs rule (PR 10001) but not enforced;
agents kept handing off phone builds sitting on the login screen. Three
mechanical changes:
1. Hard-fail: scripts/mobile-dev-launch.sh device launches default to
--ensure-mac, and the post-launch readiness wait (mobile.rpc.ready with
the device's dogfood client id) is now the iPhone auth gate: failure
exits non-zero with the reason and exact retry command. --no-attach /
--no-sign-in / --no-setup / --no-launch on the device leg are refused
unless CMUX_ALLOW_UNAUTHENTICATED_INSTALL=1, which is human-only
(agents never set it; same convention as CMUX_ALLOW_LOCAL_XCODEBUILD).
ios/scripts/reload.sh forwards --no-attach explicitly and reports the
verified auth state in its device summary.
2. Verification verb: scripts/verify-iphone-auth.sh --tag <tag>
[--device-id <id>] relaunches the installed app WITHOUT injecting
credentials or a ticket and passes only if persisted sign-in + pairing
produce a usable RPC session with the tagged Mac (PASS/FAIL + reason +
retry command, no screenshots). It writes the same readiness receipt as
a gate pass.
3. Queue truthfulness: an auth-failed drain parks the entry in needs-auth/
(kept, retryable via the new `retry --tag <tag>` verb) instead of
failed/, and cmux notify reports the TRUE state ("VERIFIED signed in +
paired" vs "installed but SIGN-IN FAILED: <reason>" with the retry
command). A fresh readiness receipt is required to count a drain as
verified, so a launcher that lies with exit 0 still parks as
needs-auth. Unauthenticated enqueues need the human-only allowance,
recorded in the entry for the headless LaunchAgent drain.
Install invocations are marked CMUX_SANCTIONED_IPHONE_INSTALL=1 for the
cmuxterm-hq local-build-guards devicectl interceptor
(manaflow-ai/cmuxterm-hq#297), which refuses raw devicectl installs on the
personal iPhone; merge this PR before that one.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Address review: no-attach never reports verified; receipt freshness by removal
- ios/scripts/reload.sh: a --no-attach signed launch (human-authorized
opt-out) now reports UNVERIFIED in the device summary instead of
"verified signed in + paired" (the gate never ran on that path).
- iphone-install-queue.sh: remove any pre-existing readiness receipt
before the signed launch, so freshness is existence-after-removal
instead of a whole-second mtime comparison that a same-second stale
receipt could pass.
- verify-iphone-auth.sh: warn explicitly when the receipt cannot be
persisted after a PASS instead of staying silent.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Deferred delivery: locked/offline phone means enqueue-and-notify, exit 75
Agents burned hours in unlock-watcher loops (one takeover: 45min of
cycles; one script: 87 retries over 4 days). Now the device paths defer
promptly instead of retrying or watching:
- mobile-dev-launch.sh probes reachability ONCE before minting; an
offline phone notifies (best-effort cmux notify), prints the exact
retry command, and exits 75 (EX_TEMPFAIL). A launch that fails because
the phone is LOCKED gets the same treatment.
- ios/scripts/reload.sh: the queued outcome (phone unreachable at build
time) now notifies and exits 75 instead of 0 — queued-not-installed is
never reported as success. If the phone locks mid-delivery (launcher
exit 75 after install), the built signed app is parked in the install
queue, the user is notified to unlock, and the reload exits 75.
- iphone-install-queue.sh drain maps launcher exit 75 to keep-pending
(not needs-auth): a locked phone is a delivery deferral, not an auth
failure, and the LaunchAgent's periodic drain retries it.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Address review round 2: fail closed on unclearable receipt, truthful queue claim
- iphone-install-queue.sh: if a stale readiness receipt cannot be removed
before the signed launch, fail the entry instead of risking a verified
claim backed by an old file.
- ios/scripts/reload.sh: the locked-phone deferred path only prints
"queued; unlock to receive" (and exits 75) when the enqueue actually
succeeded; otherwise it errors with the manual retry command.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Preserve --no-attach in the deferred-delivery enqueue
A human-authorized --no-attach launch that hits a locked phone must queue
with the same intent; dropping the flag would escalate the queued drain
to the full ensure-mac paired flow.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
* Add failing test: sidebar description link is unreadable on the selected row
The link run in a workspace description renders in NSColor.linkColor, which
AppKit paints over the row palette. On an active row the sidebar selection
background is that same blue.
Refs https://github.com/manaflow-ai/cmux/issues/9596
* Give SidebarRowTextView ownership of link styling
AppKit paints .link runs in NSColor.linkColor and ignores the explicit
.foregroundColor the row applies, so a URL in a workspace description was
blue on the blue sidebar selection background when the row was selected.
SidebarRowPalette.attributed now moves every web link onto a private
.sidebarRowLink key and styles the run itself: the palette-derived color
plus an underline so it still reads as a link in both states. The active-row
color comes from sidebarSelectedWorkspaceForegroundNSColor, so a custom
sidebarSelectionColorHex stays legible. Covers the workspace description and
the metadata markdown blocks; SidebarRowTextView.linkURL(at:) reads the
private key, and the http(s)-only destination contract moves to a shared
SidebarRowWebLink.
Fixes https://github.com/manaflow-ai/cmux/issues/9596
* Cover the metadata markdown blocks in the row link tests
CodeRabbit: the new tests exercised customDescription only, while the same
SidebarRowPalette.attributed path also renders snapshot.metadataBlocks.
Adds active/inactive coverage asserting the row-owned .sidebarRowLink
destination, the palette foreground, the underline, and that AppKit's .link is
gone, plus an unsafe-scheme case.
* Measure only glyph pixels in the link raster assertions
CodeRabbit: minimumDistance scanned the whole bitmap, so the uniform selection
fill (itself a blue near NSColor.linkColor) could satisfy or break the
assertions without the link glyphs having any bearing on them. Background
pixels are now excluded, and the helper asserts some text pixels were found so
an empty raster cannot pass silently.
* Move sidebar link behavior onto the row text view
* Make sidebar link raster assertions antialiasing-safe
* Preserve sidebar link accessibility semantics
* Give row links actionable accessibility elements
* Test row link accessibility lifecycle
* Expose row links through accessibility tree
* Test stable and hidden row link lifecycles
* Preserve row link accessibility lifecycles
* Test row link semantic reuse and geometry
* Scope row link proxies to semantic owner
* Test pooled row link proxy release
* Release hidden row link accessibility state
* Test inline rename title restoration
* Preserve plain row text while hidden
* Test on-demand row link geometry
* Complete row link accessibility lifecycle
* Bridge row accessibility callbacks to main actor
* Test lazy row link accessibility materialization
* Defer detached row accessibility proxies
* Release pooled sidebar link proxies
* Harden sidebar AppKit lifecycle tests
* Stabilize sidebar rename field editor test
* Test sidebar link ownership lifecycles
* Repair extracted sidebar description compilation
* Fix sidebar link ownership lifecycle
* Test truncated sidebar links stay inaccessible
* Keep inline rename cancellation test focus-independent
* Hide truncated sidebar links from accessibility
* Apply one immutable sidebar link policy
* Keep sidebar description projection lightweight
* Keep sidebar link accessibility demand-driven
* Make sidebar retirement tests event-driven
* Test truncated sidebar link pointer hit
* Fix AppKit popover close test wait
* Update sidebar rename behavior test
* Make popover close wait state-driven
* Ignore truncated sidebar link glyphs
* Fix popover retirement close wait
* Fix checklist popover window lookup
* Test dark sidebar description color resolution
* Resolve sidebar row colors by appearance
* Test row palette preserves dynamic colors
* Preserve dynamic sidebar semantic colors
Agents kept handing off phone builds that were installed but sitting on
the login screen. Installed-but-signed-out is now explicitly a failed
install: every install must be verified past login and paired before
handoff, raw devicectl installs and --no-sign-in are banned for dogfood
builds, and unresolvable sign-in blockers must be reported with the
retry command.
Co-authored-by: Claude Fable 5 <[email protected]>
* test: cover file drops in global Dock sessions
* fix: route file drops through Dock containers
* refactor: harden Dock file-drop routing
* perf: focus Dock file-drop batches once
* fix: address Dock file-drop review findings
* fix: harden Dock drop routing and observation
* fix: finish Dock drop review follow-ups
* fix: keep preview metadata actor-independent
* fix: index Dock surface ownership
* fix: close Dock file-drop review gaps
* fix: compile Dock drop regression coverage
* test: require indexed Dock pane ownership
* test: cover Dock metadata ownership lifecycle
* refactor: centralize Dock preview metadata ownership
* test: cover late Dock drop target discovery
* fix: resolve late pane drop ownership
* test: cover Dock preview session round trip
* portal: invalidate the split-divider hit-test cache on nested subview insertion
The split-divider region cache trusted KVO of NSView.subviews to catch structural
changes, but addSubview does not reliably emit that KVO across macOS versions, so a
split view inserted into a nested container left the cache stale and hit-testing wrong.
Replace the root-only subview-id check with a structure fingerprint over all observed
views (root, its subviews, split ancestors), recomputed on the lookup path; the KVO
observers stay as an eager fast path but correctness no longer depends on them. Same
fix in the Browser portal, which duplicated the cache.
* portal: compare structure snapshots without intermediate arrays
* portal: cover deep-container split insertion and content-root replacement
The divider cache's structure snapshots only track the content root, its direct
children, and views that were split-related when the cache warmed up. Two gaps:
a split inserted under a container two levels deep changes no observed subview
list, and a replaced-but-still-alive content root passes validation because no
snapshot records which root it was built from. Both leave hit-testing on stale
empty regions. Failing tests first; the fix lands in the next commit.
* portal: validate the divider cache with a root-keyed full-tree split digest
The structure snapshots only covered the content root, its direct children,
and views that were split-related when the cache warmed, so a split inserted
under a deeper container changed no recorded subview list and the stale cache
kept winning. They also never recorded which root they were built from, so a
replaced-but-still-alive content root passed validation against the detached
tree. Replace the snapshots with a digest keyed to the root's identity that a
full-tree walk rebuilds on the lookup path: each split's identity, ancestor
chain, arranged subviews, orientation, and effective visibility. An insertion
under any container now misses the cache, while subview churn that cannot
affect dividers still reuses it, and the subviews KVO stays bounded to the
same views as before. Both portals share the digest through
PortalSplitDividerRegion.
* portal: prove cache hits skip large hierarchy traversal
* portal: invalidate divider caches at hierarchy mutations
* portal: scope hierarchy invalidation to cache roots
* portal: nest cache invalidation helpers
* portal: give hierarchy mutation routing one owner
* portal: hook every hierarchy mutation entrypoint
* test: align OMP resume environment expectation
* test: keep install command fixture inert
* test: cover app-host config path aliases
* ci: canonicalize app-host config evidence paths
* fix: resolve Xcode 26.3 Swift warnings
* fix: preserve Xcode 16 delegate compatibility
* test: bound portal hierarchy mutation hook work
* portal: make hierarchy invalidation generation-based
* test: cover no-op sorts and detached portal roots
* portal: preserve cache validity across sort and reattach
* test: cover detached nested portal subtree mutation
* test: calibrate portal mutation work against AppKit
* portal: distrust detached subtree hierarchy state
* fix: fail closed when portal hierarchy hooks are unavailable
* test: drop stale OMP path override from portal branch
* test: cover split-free subviews replacement
* portal: preserve split-free subviews fast path
* test: validate every warmed portal cache
* test: cover arranged split pane mutations
* portal: track arranged split pane mutations
* test: cover detached subtree parking path
* portal: revoke proofs at unindexed hierarchy boundaries
* test: isolate arranged pane mutation cases
* test: cover wrapped detached portal subtree
* portal: track detached indexed subtree mutations
* portal: keep mutation snapshots with their owner
* test: cover inactive portal proof lifetime
* portal: revoke proofs between cache lifetimes
* test: avoid arranged-pane teardown KVO trap
* test: cover detached portal reorder mutations
* test: restore split arrangement before teardown
* test: document detached cache boundary
* test: use valid arranged-pane lifecycles
---------
Co-authored-by: ejc3 <[email protected]>
Co-authored-by: austinpower1258 <[email protected]>
* test: reproduce cmux open PATH loss without Ghostty helper
* fix: keep bundled open first without Ghostty helper
* test: cover bundled bin in wrapper PATH invariant
* test: let fish PATH regression exit deterministically
* test: keep fish PATH coverage optional locally
* Test dashboard organization switching
* Add direct dashboard organization switching
* Test CodeRouter metrics failure alerting
* Alert on CodeRouter analytics query failures
* Test authorized CodeRouter organization discovery
* Expose authorized CodeRouter organizations
* Test permission-filtered dashboard organizations
* Filter dashboard organization switching by permission
* Test CodeRouter review regressions
* Fix CodeRouter organization navigation and failure states
* Test CodeRouter-only Team pricing benefit
* Test removal of unshipped Team benefits
* Test inherited Team cloud allowance
* Limit Team pricing claims to shipped CodeRouter
* Test final CodeRouter review regressions
* Preserve cached orgs and validate analytics JSON
* Test organization cache and response isolation
* Isolate and validate organization catalog state
* Test personal organization selection
* Preserve personal organization selection
* Test URL-scoped permitted organizations
* Scope the switcher to URL-permitted organizations
* Test authoritative organization fallback
* Use the authoritative selected CodeRouter organization
* Test live and personal organization selection
* Use live Stack organization selection
* Test organization cache refresh and switch failure
* Reconcile and report organization switches
* Test filtered selected organization recovery
* Test dashboard Team benefit copy
* Bound organization loading and correct Team upsell
* Test authored pricing locale boundary
* Keep organization authority current
* Test keyboard placement and normalized team scope
* Make organization switching keyboard reachable
* Test concurrent organization switch guard
* Serialize organization switches
* Test empty organization catalog recovery
* Recover from an empty organization catalog
* Update billing copy expectations
* Test organization network and update deadlines
* Bound organization query and update waits
* Keep timed-out organization mutations exclusive
* Test latest organization switch serialization
* Queue organization switches behind active mutations
* Offer safe reload recovery for stuck switches
* Cancel organization switch effects on unmount
* Reconcile failed queued organization switches
* Discard queued switches after a deadline
* Reconcile late switches without replacing navigation
* Make URL scope authoritative during organization switches
* Keep timed-out persistence safely blocked
* Release timed-out switches with late reconciliation
* Test analytics failure classes and account clearance
* Fix account layout and analytics failure handling
* Persist CodeRouter scope independently of Stack
* Scope CodeRouter organization preference per user
* Use portable state directory sync
* Close terminal host and resource atomically
* Satisfy hosted terminal close Clippy
* Test replayed terminal close isolation
* Keep terminal close replays effect-free
* Close durable terminals without runtimes
* Resolve terminal close replay before live state
* Hide terminal close state internals
* Test resource replay close isolation
* Keep resource close replays effect-free
* Fix terminal replay test identity borrows
* Tombstone detached terminals on explicit close
* Brand account UI as cmux
* Remove vendor term from account search metadata
* Revert "Remove vendor term from account search metadata"
This reverts commit 8d008f2514.
* Add regressions for private team usage metrics
* Add private team-scoped CodeRouter usage metrics
* Harden CodeRouter analytics isolation
* Match the deployed PostHog Endpoint dialect
* Make the usage window exactly 30 UTC days
* Accept both PostHog Endpoint row encodings
* Upgrade web to Next.js 16.3 instant navigation
* Add regressions for auth loading and Vault gating
* Gate Vault and restore Stack Auth rendering
* Add regressions for CodeRouter pricing and Vault gating
* Ship CodeRouter branding behind Vault pricing flag
* Keep Vault flag tests isolated
* Fix instant server and pricing test isolation
* Add iPhone simulator pane control
* Wire mobile simulator control files into app target
* Fix mobile simulator observer lookup
* Use IrohLib for endpoint signing
* Fix mobile simulator stream control
* Fix simulator stream tap policy lint
* Address simulator stream review feedback
iOS control-plane correctness: per-panel serialized start/stop operation
chains on the composite (background stop can no longer interleave a
foreground restart), failed start attempts settle back to idle instead of
spinning forever, simulator selection stops the previous panel before
starting the new one, and a pane's onDisappear deactivates only its own
panel so A->B switches keep B selected.
Ownership integrity: descriptor ownership is now tri-state; broadcast
payloads (state-sync rows, workspace lists) carry unknown ownership, so a
shared tick cannot flip the owning phone to view-only or locked mid-drag.
Passive simulator.state events merge descriptors without promoting idle
panels to starting.
Input and rendering: pointer events flow through one buffered stream drained
in order (no reordered began/moved/ended), and frame base64+image decode
moved off the main actor with forced decompression.
Mac host: advertised simulator capabilities gate on the same feature flag as
RPC dispatch, the stream coordinator prunes cached frames and workspace
mappings for closed panels, and a session whose frame delivery is refused
ends itself so the panel lock releases immediately.
Also drops @unchecked from the frame reader's now-checked Sendable, removes
a redundant initializer, renames the mapper's clamped: parameter to
allowsOutsideImage:, and adds focused tests for the new store behavior.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Move Iroh peer-identity derivation into CmuxIrohTransport
The app target imported IrohLib directly for SecretKey.fromBytes but never
links the IrohLib product, so the hosted test build failed with undefined
symbols for arm64 (the tagged reload only linked by toolchain accident).
CmuxIrohTransport owns the IrohLib dependency, so the derivation now lives
on CmxIrohIdentityMaterial there and the app consumes the public property.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Retry dogfood attach after startup teardown
* Improve mobile simulator tap precision
* Fix mobile simulator capability lint
* Fix mobile simulator input hit testing
* Fix simulator app switcher action
* Keep simulator frame stream live under backpressure
* Add simulator diagnostics telemetry
* Wire simulator diagnostics into macOS target
* Make simulator diagnostics queue test selectable
* Settle failed stream preflight and clear stale locked ownership
A start attempt that exits through the preflight guard (disconnected,
missing capability, or no client) now settles the activation spinner
back to idle, and a locked start rejection clears ownership remembered
from an earlier start so controls cannot stay live under the overlay.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Detect stalled simulator streams and self-heal on the phone
The dogfood freeze: the phone's transport died (direct path lost, relay
fallback, 30s idle timeout), the Mac cleanly released the stream, but the
pane kept showing the last frame under an 'iPhone Control' pill with no
way to tell it was dead. Nothing in the stream layer detected silence.
Mac side now emits simulator.state on a 5s cadence while a session is
active (capability simulator.keepalive.v1), so clients can treat event
silence as staleness without misreading a static Simulator screen; the
keepalive's frame-send request also retries a refused frame that no new
publication would retrigger. The phone arms a capability-gated watchdog
per active panel: a 15s interval with no frame or state event marks the
pane visibly stalled ('Reconnecting to Simulator' overlay, EN+JA) and
re-requests the stream through the serialized per-panel chain, retrying
each silent interval. Stalled is sticky until a fresh frame arrives so a
recovering pane cannot masquerade as live.
Also wires the previously dead includingSimulator parameter so simulator
capabilities are actually omitted when the feature flag is off.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Add consolidated on-disk AppLog split from network diagnostics
AppLog (CMUXMobileCore) persists the structured diagnostic ring to two
rotating 5MB files in Application Support, always on since events are
integer-encoded and privacy-safe: cmux-app.log carries everything
app-wide (simulator, browser, composer, render, plus the mirrored
string debug log) and cmux-network.log carries transport dials,
discovery, relay policy, paths, and session lifecycle; app lifecycle
and reachability context land in both. The composition root chains the
ring's event tap into AppLog ahead of the Sentry reporter and mirrors
MobileDebugLog's line stream, so one file tells the in-app story in
wall-clock order. Consecutive frame-pipeline events coalesce into a
'repeated xN' summary when the run breaks. Both files are shareable
from Iroh connection settings (strings EN+JA, plus the previously
uncataloged verbose-log strings).
Simulator diagnosis hardening: a stalled watchdog fire now records its
own DiagnosticSimulatorStreamLifecycle.stalled event (warning-level in
Sentry, so stalls become breadcrumbs and budgeted logs); identical
simulator.state keepalives short-circuit as .unchanged in the stream
store, feeding the watchdog without flooding the ring, breadcrumbs, or
disk every 5s; and the Mac keepalive only retries frames when a reader
exists, avoiding a readerMissing diagnostic per tick on panels without
a frame transport.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Await actor-isolated debug log line stream
Co-Authored-By: Claude Fable 5 <[email protected]>
* Move log sharing to a top-level Diagnostics settings section
The app log covers every feature and the network log covers all
connection diagnostics, so neither belongs on the Iroh screen; that
screen keeps only its connection report and verbose-connection-log
toggle.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Fall back to appending when log rotation fails
A failed .1 move followed by createFile truncated the current log in
place, erasing exactly the diagnostics a user might be about to share.
The writer now reopens the existing generation for appending and
retries rotation on later appends.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Discard unused frame-publication handler results
The two clear-handler calls kept their tokens implicitly, tripping the
zero-budget warning bucket for this new file in CI's warning gate.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Bound failed-rotation fallback: no header spam, no offset-0 writes
A sustained rotate failure now retries only after another full byte
budget accrues (threshold watermark instead of per-line attempts), the
fallback writes no extra session header, and a generation that cannot
be opened and positioned at its end disables writing instead of
overwriting from offset zero.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
* Add regression test: workspace search selection stays inside the search tab
Selecting a workspace from the search tab's results on iOS could strand
the app on the Workspaces list with no tab bar, no toolbar, no search
field, and a stale query filter: the selection deactivated search,
transitioned to the Workspaces tab, and pushed onto that tab's
NavigationStack while it was still off-window behind the search-field
dismissal, so the path recorded a push that never happened.
The test drives the exact repro: activate search, type a query that
filters to one workspace, tap the result, pop back, and require the app
to still be inside the search tab with usable bottom controls.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Open workspace search selections inside the search tab
Selecting a workspace from search results previously committed the draft
query, transitioned to the Workspaces tab, and pushed the detail onto
that tab's NavigationStack from onChange handlers that can run while the
stack is off-window mid search-dismissal. UIKit drops such pushes: the
path records the detail, so the root list stays up with the tab bar,
root toolbar, and compose button hidden and the committed query still
filtering the list, with no control left to escape (the reported stuck
state). Notification search never had this bug because it pushes results
onto its own stack.
Workspace search now does the same: the search tab's stack gets a real
path plus a workspace navigationDestination, and a tapped result pushes
there directly with no tab transition and no query commit, so popping
back lands on the live search results and the Workspaces tab keeps its
unfiltered list.
The pending cross-tab machinery still serves deeplinks and device-tree
selections made while searching; those consumes are now gated on the
destination stack being on screen (set by its onAppear/onDisappear), so
a deferred push replays from onAppear instead of landing off-window.
The layout-preview fixture mirrors the new shape (path-based search
stack, system back), and the minimized-search UI test is updated to the
new contract: popping back returns to search results, and the committed
filter check moves behind an explicit switch to the Workspaces tab.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Do not assert system search chrome over the pushed search detail
Whether iOS keeps the bottom search control visible above a detail
pushed inside the search tab is platform chrome, not part of the
selection contract; asserting its disappearance made the minimized-
search test fail against the in-stack push flow.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Name each assertion in the minimized-search test
The hosted runner only surfaces assertion messages, so each step of the
updated search-selection flow carries one to make failures diagnosable.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Fail fast with named steps in the minimized-search flow
The hosted runner surfaces XCTFail messages as annotations but not
XCTAssert messages; guard-and-fail makes the first broken step visible.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Expect the live search session after popping the selected workspace
Popping the detail returns to the active search (field and keyboard
restored), so the keyboard-dismissal expectation moves to the explicit
leave-search tab switch. Matches the verified on-device behavior.
Co-Authored-By: Claude Fable 5 <[email protected]>
* End the search session when a result is selected
Left presented across the push, the search field re-presents after
popping anchored to the navigation bar at the top instead of the search
tab's bottom control (reported from device dogfood). Deactivating on
select commits the query like every other search exit, so popping back
lands on the still-filtered results with the collapsed bottom control.
The regression test now pins the restored control to the bottom half
and rejects a top-anchored field after popping.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Finish the search round on the Workspaces tab after popping the detail
Popping back from a workspace opened via search previously left the app
on the deactivated search tab: the tinted search control read as a live
search, and the committed query kept filtering a list that looks
identical to the Workspaces root. Popping now returns selection to the
Workspaces tab and clears the committed query, so the search round ends
with the full list and no highlighted control. An explicit submit still
commits the query as the Workspaces filter; the minimized-search test
now exercises that path directly.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
* feat(ios): custom sort for the All Computers workspace list
Workspaces from different computers have no deterministic cross-Mac order
of their own, so make the aggregate order a device-local choice with three
modes: Automatic (foreground Mac first, then name — the old behavior),
Computer Order (user drags computers into a priority order; each computer
keeps its own sidebar order), and Recent Activity (one flat list across
every computer, latest lastActivityAt first).
Computer Order runs in the aggregation (through the macIDsInDisplayOrder
seam) so group sections and workspaces reorder together. Recent Activity
runs at the presentation layer only, because time interleaving cannot keep
group members contiguous; it presents flat and disables drag reorder (a
derived order has no spatial move to send).
The preference persists in MobileWorkspaceSortStore (injected UserDefaults,
mirroring the group-collapse store) and is exposed through a Sort By picker
in the workspace list filter menu, All Computers scope only, plus a
drag-to-reorder Computer Order sheet. First-time picks of Computer Order
auto-open the editor. Strings localized en+ja.
Co-Authored-By: Claude Fable 5 <[email protected]>
* test(ios): env-seed the layout fixture's sort mode for headless sim verification
Simulator windows on another Space cannot be tap-driven without stealing
the user's display, so the fixture accepts
CMUX_UITEST_WORKSPACE_LIST_PREVIEW_SORT and ..._SORT_PRIORITY to render
each All Computers sort mode for simctl screenshot verification, matching
the existing COUNT/GROUPS seeding knobs.
Co-Authored-By: Claude Fable 5 <[email protected]>
* fix(ios): sort menu gates on known computers, not visible machines
The sort section hid whenever fewer than two computers had workspace rows
on screen, so a paired-but-offline (or connection-wedged) secondary Mac
made the control undiscoverable exactly when cross-computer order matters.
Gate on distinct known computers instead: machines with visible rows plus
every paired Mac. The computer-order editor now also lists paired-but-
offline computers so they keep their slot while disconnected.
Co-Authored-By: Claude Fable 5 <[email protected]>
* fix(ios): show the sort menu regardless of computer count
The preference is worth setting before a second computer pairs, and any
count gate hides the control behind connection state (a wedged secondary
Mac already did once). All Computers scope alone decides visibility now.
Co-Authored-By: Claude Fable 5 <[email protected]>
* feat(ios): Mail-style illustrated sort tiles in a view-options card
Sort hidden inside a filter-icon menu was undiscoverable, and a text-only
'Computer Order' label communicates nothing. Replace the filter Menu with
a menu-styled popover whose top row is three drawn schematic tiles, each a
miniature of what the mode does to the list (computer sections; ranked
sections with drag grips; a flat time-stamped run), with Mail-style radio
checks. Selection re-sorts the list live behind the card. The computer-
order editor presents from the card itself, and the filter rows (read
state, machines) share it, keeping one entry point. UIMenu rows only
render text plus icon-sized images, hence a popover rather than a Menu.
Co-Authored-By: Claude Fable 5 <[email protected]>
* fix(ios): view-options card round 2 per dogfood
Drop the machines filter section (the title picker already owns computer
selection), make selecting Computer Order inert (the editor opens only
from the explicit Edit Computer Order row), inset the schematic rank
badges off the tile border, and use regular-weight selection checkmarks.
Co-Authored-By: Claude Fable 5 <[email protected]>
* fix(ios): pin the view-options card to regular-weight body text
The popover inherits the presenting toolbar button's font environment, so
rows rendered with toolbar weight; set body + regular at the card root.
Co-Authored-By: Claude Fable 5 <[email protected]>
* fix(ios): indent schematic rank badges further, shrink edit-row arrows
Co-Authored-By: Claude Fable 5 <[email protected]>
* fix(ios): rename the Automatic sort mode to Connected First
'Automatic' says nothing; 'Connected First' states the rule: the connected
computer leads, the rest follow by name. Raw value and persistence keep
the automatic spelling.
Co-Authored-By: Claude Fable 5 <[email protected]>
* feat(ios): automatic sort mode becomes Last Opened
'Automatic'/'Connected First' mislabeled the rule and the alphabetical
tail matched no user model. The mode is now Last Opened: the connected
computer counts as opened now, the rest order by when this device last
used them (pairing lastSeenAt as the device-local record), never-opened
computers alphabetical last. Raw value and persistence keep 'automatic'.
Co-Authored-By: Claude Fable 5 <[email protected]>
* fix(ios): review round — recompute on pairing refresh, robust editor seed
Cursor: the Last Opened order read pairedMacs.lastSeenAt but a pairing
refresh never rebuilt the derived list, so the aggregate order went stale
until the next workspace event; recompute on pairedMacs change (the
derivation already depended on pairedMacs for customization stamping).
Also seed the computer-order editor from the aggregated rows instead of
the filter-menu machine list, which empties below its two-machine floor
and would drop a singleton computer or mismatch the tail order.
Co-Authored-By: Claude Fable 5 <[email protected]>
---------
Co-authored-by: Claude Fable 5 <[email protected]>
* Add standalone iOS keyboard pinning lab
* Test rapid iOS keyboard dock reversals
* Unify iOS keyboard dock presentation
* Fix CLI compile break from classify() tuple access
https://github.com/manaflow-ai/cmux/pull/9804 landed
`FeedEventClassifier.classify(...).0` while classify() already returned
the named FeedEventClassification struct, so CLI/cmux.swift no longer
compiles on main (every app-host and tests-build-and-lag CI job fails
with "value of type 'FeedEventClassification' has no member '0'").
Use .hookEventName, matching the other call site.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Strengthen rapid keyboard dock coverage
* test(panes): drop stale MobileInjectedAttachStartupTests referencing removed API
The main merge replaced MobileStartupConnectionCoordinator's
connectInjectedAttach with the claim/finish lifecycle, and
DogfoodAttachPreparationTests already covers that lifecycle end to end.
The stale file kept the whole CmuxMobileShellUITests target from
compiling, so no package UI suite could run in CI.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Scope pairing scanner guidance copy onto MobilePairingScannerSheet
The caseless MobilePairingScannerGuidanceCopy enum (from #9493) trips the
namespace-enum rule in scripts/lint-ios-package-conventions.sh, turning the
package-conventions-lint job red for every branch that touches Packages/.
Co-Authored-By: Claude Fable 5 <[email protected]>
* Scope keyboard dock seam measurement to transitions
* Scope dock seam metric to keyboard transitions
* Test whole dock during keyboard reversal
* Isolate keyboard dock from terminal layout
* Animate hosted keyboard dock reflows
* Localize keyboard pinning lab name
---------
Co-authored-by: Claude Fable 5 <[email protected]>
Addresses bot-review findings on #7459.
- Type the split `orientation` as a two-case enum (horizontal|vertical)
instead of an unvalidated String. An orientation outside the wire
contract now fails closed at the conversion boundary (node -> nil ->
layout: null), the same path as an unparseable pane UUID. Emitted wire
JSON is unchanged for the two valid values (rawValue == the wire string).
- Fix the layout-conversion doc comment to describe what the code does: a
single unparseable leaf nils the ENTIRE workspace layout (deliberate
fail-closed; consumers fall back to the flat panes array), not just the
affected subtree. No behavior change.
- Replace the fixed time.sleep(1.0) in test_cli_tree_layout with a bounded
poll on `cmux --json tree` until the created workspaces materialize
(racy under CI load); reuses the existing _workspace_from_tree predicate.
Co-Authored-By: Claude Fable 5 <[email protected]>
`system.tree`'s `panes` array is flat — it lists a workspace's panes but
discards how they're arranged: which splits are vertical vs horizontal,
their ratios, and their nesting. That geometry is live in the workspace's
bonsplit controller (`treeSnapshot()`) but never reached the wire, so a
consumer that recreates a saved layout (e.g. an external snapshot/restore
tool) could only guess — multi-pane restores came out flat/horizontal.
Add an additive `layout` field to each workspace in the `system.tree`
payload: the split tree in the shape `--layout` already accepts
({direction, split, children} for a split; a {pane: {id, ref}} leaf
otherwise), so the tree cmux emits and the layout it ingests are one
schema read two ways. Pane leaves carry the same id/ref as the flat
`panes` array and participate in --id-format (refs/uuids/both). The field
is `null` when unavailable, so existing consumers are unaffected.
- New ControlSystemTreeLayoutNode (keeps the socket package free of any
Bonsplit dependency; the app maps ExternalTreeNode into it at capture)
- Capture treeSnapshot() at the existing serializer call site
- Serialize in systemTreeWorkspacePayload; the CLI relays it unchanged
(treeApplyMarkers passes unknown keys through)
- tests/test_cli_tree_layout.py: single-leaf + nested H-over-V round-trip
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-06 08:27:38 -04:00
2241 changed files with 214372 additions and 22988 deletions
# Main-controlled broker. This workflow runs only from refs/heads/main and
# hydrates only main, so no candidate branch can edit the guards that run
# before begin-testbox exposes its auth token, and no candidate build script
# executes inside this token-bearing job. A candidate revision reaches the
# Testbox later, through `blacksmith testbox run`, which synchronizes a
# maintainer's local worktree onto the already-warm VM.
on:
workflow_dispatch:
inputs:
testbox_id:
description:"Testbox session ID supplied by blacksmith testbox warmup"
required:true
type:string
permissions:{}
concurrency:
# A Testbox is a mutable shared workspace. Serialize every request for the
# same ID so two dispatches cannot corrupt one cache.
group:cmux-tui-testbox-${{ inputs.testbox_id }}
cancel-in-progress:false
jobs:
cmux-tui-rust:
name:cmux-tui Rust setup
runs-on:blacksmith-32vcpu-ubuntu-2404
environment:
# Configure this environment with required reviewers, no secrets, and a
# deployment branch rule of exactly `main`. Approval is evaluated before
# the first step, so it precedes begin-testbox.
name:blacksmith-testbox-trusted
permissions:
contents:read
# Hydration plus three sequential 20-minute bounded remote builds happen
# after setup. Keep the GitHub job alive long enough for all stages and
# cleanup; the Testbox itself still has its separate idle timeout.
timeout-minutes:120
steps:
# Every check here is main-controlled. `blacksmith testbox warmup`
# resolves both this file and the hydrated source from the same --ref, so
# refusing any ref other than refs/heads/main is what keeps a candidate
# branch outside the trust boundary.
- name:Validate broker ref
env:
DISPATCH_REF:${{ github.ref }}
EVENT_NAME:${{ github.event_name }}
REPOSITORY:${{ github.repository }}
TESTBOX_ID:${{ inputs.testbox_id }}
shell:bash
run:|
set -euo pipefail
[[ "$REPOSITORY" == "manaflow-ai/cmux" ]] || {
echo "::error::this Testbox lane is only valid for manaflow-ai/cmux" >&2
exit 1
}
[[ "$EVENT_NAME" == "workflow_dispatch" ]] || {
echo "::error::Testbox setup must be dispatched manually, never from a PR event" >&2
exit 1
}
[[ "$DISPATCH_REF" == "refs/heads/main" ]] || {
echo "::error::this broker lane runs only from refs/heads/main, got $DISPATCH_REF; warm up with --ref main and sync the candidate through blacksmith testbox run" >&2
@@ -17,6 +17,10 @@ A tag gives the app its own name, bundle ID, socket, and derived data path, so i
Other variants: `reloadp.sh` (Release), `reloads.sh` (Release as isolated "cmux STAGING"), `reload2.sh --tag <tag>` (both).
## Shared Mac fleet capacity
Every healthy slot in the canonical Mac fleet is general-purpose. Builds, iOS archives, tests, profiling, simulator and UI verification, and any other resource-intensive workload may use any available slot. Do not wait for an AWS-only builder or infer capacity from a workload label. Use the shared lease state and slot-isolated paths supplied by the fleet tooling.
Compile-only check, no launch:
```bash
@@ -44,15 +48,33 @@ The helper refuses to run without `CMUX_TAG`, targets `/tmp/cmux-debug-<tag>.soc
## iOS builds open on the iPhone by default
Any work verified by opening the iOS app installs BOTH an isolated-simulator build AND the same build on the user's iPhone. Never stop at simulator-only. Use `ios/scripts/reload-cloud.sh --tag <tag>` (or `ios/scripts/reload.sh --tag <tag>`); with a default iPhone configured (`CMUX_IPHONE_DEVICE_ID` or `~/.config/cmux/iphone-device-id`) the device leg is automatic, and `--device-id <id>` still overrides (`xcrun devicectl list devices`). Auto sign-in and auto-pair apply as usual; launch the app so it is immediately open on the phone. The simulator leg uses the tag's own isolated device `cmux-dev-<slug>`, created on demand; do not target a shared or user-visible simulator.
Any work verified by opening the iOS app installs BOTH an isolated-simulator build AND the same build on the user's iPhone. Never stop at simulator-only. Use `ios/scripts/reload-cloud.sh --tag <tag>` (or `ios/scripts/reload.sh --tag <tag>`); with a default iPhone configured (`CMUX_IPHONE_DEVICE_ID` or `~/.config/cmux/iphone-device-id`) the device leg is automatic, and `--device-id <id>` still overrides (`xcrun devicectl list devices`). Physical iPhone builds always select the `personal` auth profile. Agent-driven Simulator verification always selects `agent`. Both named profiles live in `~/.secrets/cmuxterm-dev.env`; neither may fall back to the other. The simulator leg uses the tag's own isolated device `cmux-dev-<slug>`, created on demand; do not target a shared or user-visible simulator.
**Every phone install MUST be authenticated before handoff. Installed-but-signed-out is a failed install.** A tagged bundle id can retain an older account, so every authenticated launch clears that tagged session, signs both surfaces into the selected profile, verifies the exact tagged Mac account through `auth status`, then mints the pairing ticket. The iPhone auth gate passes only after the same-account host accepts the phone RPC and emits `mobile.rpc.ready`. `scripts/verify-iphone-auth.sh --tag <tag> [--device-id <id>]` repeats the Mac-account check, relaunches the phone without credentials, and passes only when persisted phone state reconnects. Never install with raw `devicectl device install app`, and never pass `--no-sign-in`/`--no-attach`/`--no-setup` for a dogfood build. The scripts refuse those device paths unless a human sets `CMUX_ALLOW_UNAUTHENTICATED_INSTALL=1`. If setup fails, report the gate reason and exact retry command.
Every phone build requires the same-tag Mac dev build (the iOS app is unusable without its Mac). The reload scripts build the Mac tag first when it is missing and refuse to ship a phone-only build if that fails; do not bypass this with `CMUX_IOS_SKIP_MAC_BUILD_CHECK` in normal work.
If the iPhone is unreachable at build time, the reload still completes: the signed build is parked in the offline install queue (`scripts/iphone-install-queue.sh`, persistent under `~/Library/Application Support/cmux-dev/iphone-install-queue`), and a LaunchAgent auto-installs and launches it within seconds of the phone being plugged back in or reappearing on the network, then sends a `cmux notify` with the installed tags. The LaunchAgent is a one-time per-Mac setup: `scripts/install-iphone-queue-agent.sh install`; it runs a stable copy of the queue script, so re-run the installer after changing that script. In the handoff, report the queued state (`scripts/iphone-install-queue.sh list`) instead of treating an unreachable phone as a failure; `drain` retries manually,`clear` abandons a queued build.
If the iPhone is unreachable at build time, the signed build is parked in `scripts/iphone-install-queue.sh`. Each entry stores the chosen profile, normalized account, and credentials-file path. Drain revalidates that snapshot before device mutation and uses installed stable copies of the launcher and auth helpers, so an old or pruned feature worktree cannot change policy. Install or refresh that control plane with `scripts/install-iphone-queue-agent.sh install`. Report `scripts/iphone-install-queue.sh list` in the handoff; `drain` retries delivery and`clear` abandons a queued build.
## All fleet slots are general-purpose
Agent verification, macOS/iOS builds, archives, tests, profiling, and any other work too resource-intensive for the local Mac use the same Mac fleet. A slot is not a "build slot" or a "verify slot". From the cmuxterm-hq checkout that owns this worktree, every workload leases the canonical `~/.config/macfleet/hosts.json` inventory and shared `maclease` state.
Before waiting for a builder, run `scripts/macfleet-doctor.sh report --probe` from that hq checkout. If it reports `needs-sync`, run `scripts/macfleet-doctor.sh sync --apply`; it backs up the canonical manifest and merges legacy `hosts-verify.json` entries by SSH endpoint. Refresh the hq checkout before diagnosing capacity. Do not infer capacity from a stale checkout, one pool tag, or a remembered host list.
Agent verification runs on the fleet, not on the local Mac. `scripts/verify-remote.sh` leases a general-purpose slot, pushes the tagged build to the leased Mac, drives it there (per-lease uniquely named simulator for iOS; console launch with debug-socket and computer-use evidence for macOS), and fetches screenshots, recordings, and logs back into the hq `artifacts/verify-remote/` directory:
Boot a local simulator only when all-purpose `capacity` reports no free slot, and keep at most 3 local sims booted. Scripted XCUITests go through the hosted `test-e2e.yml` lane when appropriate. The physical-iPhone signing/install leg stays local via the install queue; its archive build may use any healthy fleet slot. Verify leases carry a description and TTL, so a crashed agent frees its slot automatically; see `skills/infra/macfleet/references/verify-remote.md` in cmuxterm-hq for the shared-pool contract and host onboarding.
## iOS dev auth
`ios/scripts/reload.sh` and `scripts/mobile-dev-launch.sh` auto-sign-in from `~/.secrets/cmuxterm-dev.env`. If the phone lands on the login screen or the helper reports missing credentials, do not ask the user to authenticate every build. Tell them to run `scripts/setup-team-dev.sh` once; it verifies their Stack login and writes the file chmod 600. Manual fallback: create it with `CMUX_DOGFOOD_STACK_EMAIL=...` and `CMUX_DOGFOOD_STACK_PASSWORD=...`.
`~/.secrets/cmuxterm-dev.env` is the only mobile dev credential file. `CMUX_DOGFOOD_STACK_*` is the `personal` profile for physical iPhone dogfood. `CMUX_UITEST_STACK_*` is the `agent` profile for isolated Simulators. Run `scripts/setup-team-dev.sh` once to verify and merge the personal pair without deleting the agent pair. Use `scripts/mobile-dev-launch.sh --check-auth-contract --auth-profile personal` or `--auth-profile agent` for a mutation-free preflight. Never substitute one profile when the requested profile is incomplete.
## Regression test commits
@@ -109,3 +131,7 @@ Detailed contributor rules live in `skills/`. Use the task-specific skill before
-`cmux-shared-behavior`: shared action paths for multi-entrypoint behavior and optimistic updates.
-`cmux-ghostty`: Ghostty submodule and GhosttyKit workflow.
-`cmux-release`: release, version bump, changelog, pretag guard, release assets.
- Blacksmith Testbox (remote Linux builds for cmux-tui): warm your own box before any cmux-tui Rust or Zig
build, and never compile cmux-tui on the Mac. The skill lives in cmuxterm-hq at
`skills/infra/blacksmith-testbox/SKILL.md`; the workflows, `scripts/blacksmith-*.sh`, and the
defaultValue:"cmux could not create the hooks directory: a file exists at %@. Remove or rename the conflicting file, then run `cmux hooks setup` again."
defaultValue:"cmux could not create the hooks directory: a file exists at %@; remove or rename the conflicting file and re-run `cmux hooks setup`"
defaultValue:"cmux could not create the hooks directory: a file exists at %@. Remove or rename the conflicting file, then run `cmux hooks setup` again."
letstatus=String(localized:"cli.ssh.autoReconnect.status",defaultValue:"[cmux] ssh exited with status %s; reconnecting (attempt %s/%s).",bundle:bundle)
letstopHint=String(localized:"cli.ssh.autoReconnect.stopHint",defaultValue:"[cmux] close this pane or press Ctrl-C to stop reconnecting.",bundle:bundle)
letstatus=String(localized:"cli.ssh.manualReconnectPrompt.status",defaultValue:"[cmux] ssh exited with status %s.")
letdetail=String(localized:"cli.ssh.manualReconnectPrompt.detail",defaultValue:"[cmux] the remote VM may have been paused, destroyed, or lost network.")
letprompt=String(localized:"cli.ssh.manualReconnectPrompt.prompt",defaultValue:"[cmux] press Enter to close this pane. Press r then Enter to reconnect.")
letstatus=String(localized:"cli.ssh.manualReconnectPrompt.status",defaultValue:"[cmux] ssh exited with status %s.",bundle:bundle)
letdetail=String(localized:"cli.ssh.manualReconnectPrompt.detail",defaultValue:"[cmux] the SSH connection ended; the remote session may still be running.",bundle:bundle)
letprompt=String(localized:"cli.ssh.manualReconnectPrompt.prompt",defaultValue:"[cmux] press Enter to close this pane. Press r then Enter to reconnect.",bundle:bundle)
letstatus=String(localized:"cli.ssh.manualReconnectPrompt.status",defaultValue:"[cmux] ssh exited with status %s.",bundle:bundle)
letdetail=String(localized:"cli.ssh.manualReconnectPrompt.detail",defaultValue:"[cmux] the SSH connection ended; the remote session may still be running.",bundle:bundle)
letprompt=String(localized:"cli.ssh.terminalExitPrompt.prompt",defaultValue:"[cmux] press Enter to close this pane.",bundle:bundle)
" if [ \"$cmux_ssh_status\" -eq 0 ]; then break; fi",
" cmux_ssh_reset_terminal_modes",
" case \"$cmux_ssh_status\" in \(retryableStatusPattern)) ;; *) break ;; esac",
]
ifretryPTYAttachStatus{
@@ -436,6 +449,7 @@ extension CMUXCLI {
scriptLines.append(retryLimitCondition)
scriptLines+=[
" cmux_ssh_retry=$((cmux_ssh_retry + 1))",
"\(backoffBuilder.terminalInputModeResetLine)",
" cmux_ssh_note '\\n\\033[33m[cmux] ssh exited with status %s; reconnecting (attempt %s/%s).\\033[0m\\n\\033[2m[cmux] close this pane or press Ctrl-C to stop reconnecting.\\033[0m\\n' \"$cmux_ssh_status\"\"$cmux_ssh_retry\"\"$cmux_ssh_reconnect_limit\"",
]
scriptLines+=backoffBuilder.waitLines
@@ -448,8 +462,18 @@ extension CMUXCLI {
"trap - EXIT HUP INT TERM",
"cmux_ssh_session_end",
"if [ \"$cmux_ssh_status\" -ne 0 ]; then",
"printf '\\n\\033[31m[cmux] ssh exited with status %s.\\033[0m\\n\\033[2m[cmux] the remote VM may have been paused, destroyed, or lost network.\\033[0m\\n\\033[2m[cmux] press Enter to close this pane.\\033[0m\\n' \"$cmux_ssh_status\" >&2 || true",
terminalText.text=String(localized:"terminal.sample",defaultValue:"Last login: Fri Aug 7 19:45:12 on ttys006\n\n~/cmux git:(feat/keyboard-pinning-lab)\n❯")
Invariant: during interrupted rapid keyboard show and hide cycles, the Shortcut bar and Composer bar remain one rigid dock. The dock bottom stays coincident with the keyboard top, with no transient gap, overlap, lag, or snap-back.
Scenario: iPhone 17 Pro Max simulator on iOS 26.5, dark appearance, keyboard initially shown, 20 first-responder reversals at 135 ms intervals, then a final focused state. This interval is shorter than a normal keyboard transition and forces animation interruption.
Result: the live layout diagnostic remained `PIN GAP 0.0 pt`. The final run was sampled at 15 fps, including steady, partial-hide, hidden, partial-show, reversed, and final frames. The invariant held in every sampled frame.
Durable evidence is stored at `cmux-assets/feat-keyboard-pinning-lab/rapid-toggle-final/` in the cmuxterm-hq checkout. The directory contains the raw recording, annotated frames, contact sheet, and manifest with the success criterion.
This standalone iOS app isolates the Workspace Detail keyboard geometry from cmux state and networking.
The Composer and Shortcut bars live in one `ComposerDockView`. Its bottom edge is constrained directly to `UIKeyboardLayoutGuide.topAnchor`. UIKit therefore owns the keyboard frame, the dock frame, and interrupted animation timing in one constraint graph.
The circular-arrow header button reverses first-responder state every 135 ms to stress interrupted keyboard transitions. The header reports the live constraint gap. Green `PIN GAP 0.0 pt` means the dock and keyboard guide are coincident in the current layout pass.
Tapping the terminal canvas focuses the same Composer text field, so terminal tap, direct Composer tap, the keyboard button, and the stress control all exercise one keyboard ownership path.
Generate the Xcode project with `xcodegen generate`, then build the `KeyboardPinningLab` scheme.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.