Skip to content

ci: portable jobs take fleet Linux, then a free mini, then Blacksmith - #29130

Merged
teamleaderleo merged 7 commits into
feat-cmux-nextfrom
ci/linux-jobs-fall-back-to-minis
Oct 31, 2026
Merged

teamleaderleo merged 7 commits into
feat-cmux-nextfrom
ci/linux-jobs-fall-back-to-minis

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Leo, 2026-10-31: "i assume blacksmith prob cut us off cuz we were using like $10k a day so", "y'know, we'll have to be more self-reliant and size the stuff better", and "i mean the linux things cant our minis just run them" (rulebook: leo-workspace#1867).

What changes

The jobs below need neither Linux nor Blacksmith. Their runner is now picked in this order:

  1. fleet Linux: vars.LINUX_RUNNER, when it names one of our own Linux runners (not a Blacksmith label or a GitHub-hosted image);
  2. a free owned mini: the side label (vars.CI_SIDE_LANE_RUNNER), or the same label on a wider class. The mini must be idle at placement time, after subtracting the jobs the janitor snapshot already shows queued on it. A mini takes nothing while any nightly or release job is queued on its pool family (reserved_queued), so nightly jobs still come first;
  3. their existing Blacksmith label, with its CI_CLOUD_OVERFLOW_SAVED failover. It stays the last || term.
Job Workflow Order
cmux-tui build aarch64-apple-darwin, build x86_64-apple-darwin cmux-tui-artifacts.yml → cmux-tui-build-package.yml free mini first, built natively on the Mac path (the path the Linux build's macOS fallback already uses), then the Linux cross build on Blacksmith 32 vCPU. Both targets or neither.
cmux-next checks cmux-next.yml fleet Linux → free mini → Blacksmith 16 vCPU
cmux-next CI guard group cmux-next-guards.yml fleet Linux → free mini → Blacksmith 32 vCPU
nightly-guards / workflow-guard-tests / release-notary cmux-next-guards.yml → ci-guards.yml (new optional input guard_tests_runner) fleet Linux → free mini → vars.LINUX_RUNNER / Blacksmith 4 vCPU
tooling-tests cmux-next-guards.yml fleet Linux → free mini → Blacksmith 4 vCPU (16 vCPU under the overflow switch)
  • scripts/ci/portable_job_placement.py: the decision. It reuses side_lane_placement.side_route(), pr_runner_pool.live_owned_free() and the janitor snapshot. Anything uncertain places nothing on a mini: runners it can't read, no snapshot or a stale one, a person's re-run, attempt 3 or later, no side label, or the AWS pool. Each job then keeps its Blacksmith route.
  • A portable-placement job in cmux-next.yml and in cmux-next-guards.yml runs the base branch's copy of the script, the way macos-placement does, using the routing App's runner read with cmux-bot as the fallback. Every step fails open. The placed jobs read its outputs only in the attempt that placed them (placed_attempt), so a re-run of failed jobs never reuses a stale mini.
  • In cmux-tui-artifacts, tree-preflight makes the same call, all or none, and passes darwin_mac_runner to the build. darwin_builder and the tree key stay linux, so the 32 vCPU cross-compile shadow does not start. A re-run takes macos_retry_runner, as the Mac path always has.
  • On a mini, a job runs scripts/ci/reset-stale-submodules.sh right after checkout, because a reused workspace keeps the previous job's submodule checkouts. The guard group's --jobs "$(nproc)" becomes getconf _NPROCESSORS_ONLN, since macOS has no nproc.
  • Routing-table guard (scripts/ci/runner_routing_table.py, .github/runners.json): new class portable-checks ("needs": "any"). A job in that class fails the check unless its runs-on takes portable placement's fleet Linux output, then its mini output, and ends on a Blacksmith Linux label. mini_runner counts as a macOS route, so these jobs are listed.
  • tests/test_ci_portable_job_placement.py (registered in tests/test-execution.toml, run in the ci guard group) covers the order, the nightly hold, queued jobs taking idle runners first, the std spill, all-or-none, every uncertain case, the output format and the wiring in each workflow. The existing runs-on tests are updated: test_feat_ci_guard_group.py, test_cmux_next_checks_workflow.py (checks and the placement job now count as able to run on a mini and get the submodule reset), and test_ci_linux_single_size.py (counts lowered: ci-guards 6→5, cmux-next-guards 10→9).

Performance

These are Blacksmith vCPU-minutes per day that move off Blacksmith whenever a mini is free (or all of them, once vars.LINUX_RUNNER names fleet Linux). The coordinator measured the "before" column on 2026-10-31. The "after" column is the most that can move. How much actually moves depends on how often minis are idle, and each placement job's summary records it.

metric before (Blacksmith vCPU-min/day) after (Blacksmith vCPU-min/day, minis free) delta harness or run link
cmux-tui build aarch64-apple-darwin 349k 0 (mini) −349k coordinator's 2026-10-31 Blacksmith usage read
cmux-tui build x86_64-apple-darwin 339k 0 (mini) −339k same
cmux-next checks 264k 0 (fleet Linux / mini) −264k same
cmux-next CI guard group 221k 0 (fleet Linux / mini) −221k same
release-notary guards, tooling-tests not measured (4 vCPU, under 1 min each) 0 small coordinator's 2026-10-31 Blacksmith usage read
total ≥1,173k 0 at full mini availability up to −1,173k/day coordinator's 2026-10-31 Blacksmith usage read

Placement adds one 2 vCPU job of about 15 s per run, on fleet Linux when LINUX_RUNNER names it.

Risk

risk:normal, CI routing only. Two risks:

  • A check that behaves differently on macOS. The guard group runs head and base on the same machine, so a red that comes from the machine shows up as a base red. checks, tooling-tests and release-notary have no base comparison: a macOS-only red there would turn a PR red only when it lands on a mini. The probe run (below) runs them on a mini before this lands.
  • Two placements racing for the same idle runner. The loser queues on the side label behind one job. The idle count already subtracts the jobs the snapshot shows queued there.

No timer, observer or loop added: placement is one bounded read (60 s deadline) per run.

Not touched: nightly workflows (none of these jobs is nightly; test_ci_nightly_off_blacksmith.py passes), signing, and cmux#29033 (the react-apps shards).

Observed / Not run

  • Passed locally: tests/test_ci_portable_job_placement.py (18), test_feat_ci_guard_group.py, test_cmux_next_checks_workflow.py (165), test_ci_linux_single_size.py, test_ci_fork_runner_routing.py, test_supplychain_security.py, test_ci_side_lane_placement.py, test_ci_nightly_off_blacksmith.py, test_ci_test_execution_registry.py, test_ci_tui_routes_avoid_nightly_label.py, test_ci_reusable_workflow_permissions.py, test_ci_linux_guard_routing.py.
  • Red on the head with the same failures as base 7fc75a9: test_runner_routing_table.py (stable-next.yml and nightly.yml entries), test_ci_blacksmith_linux_failover.py (app-platform.yml, cmux-next-chromium-currency.yml), test_ci_runs_on_fallback.py (nightly.yml), test_ci_signing_keychain_is_deleted_before_bundle_code.py (stable-next.yml). runner_routing_table.py check reports the same 5 problems as base, and none for the new jobs.
  • On macOS (run by hand, the way a mini would run them): checks and tooling-tests steps gave 81 pass and 5 fail. Two were macOS differences, fixed here: the web lint pre-push test read nice (BSD nice prints usage), and package_deps_heal.offender() passed \s, \w and \b to git log -G. macOS's regex has none of those, so the heal never found the offending commit there; it now uses POSIX classes. The other three ("No new SwiftUI", "Scrollbars thin", "Package conventions lint") are continue-on-error warnings that report findings in base content.
  • Not run: the jobs on a real mini, in this PR's own run. That run uses the base branch's placement, which doesn't exist yet, so it routes the old way. A probe commit forced them onto the side label for one run (then reverted). They waited over 50 minutes with no free mini, because every side runner was busy, and the next push cancelled them. So there is no on-mini result yet. That wait is the case this PR avoids: placement sends a job to a mini only when that mini is idle now.

Routing classifiers

On a mini, a job that portable placement places still runs only its own scripts, none of the app's Mac tiers. scripts/ci/cmux_next_route.py (PORTABLE_RUNNERS) and scripts/ci/detect_ci_change_areas.py (the guard_tests_runner term) therefore read these runners as Linux. Without that, an edit to checks would run every Mac tier, and a change to any guard test the release-notary job names would count as reaching the app. That made test_ci_nightly_push_throttle.py and test_cmux_next_route.py red on this branch; both now pass, and test_ci_change_areas.py fails per test exactly as on base. Main's owned-pool rescue must also watch cmux-next-guards.yml (test_ci_owned_pool_sources.py). That is cmux#29343, into main; this PR lands after it.

Fleet Linux

RUNNER_POOLS.linux and LINUX_RUNNER still resolve to Blacksmith 32 vCPU. No self-hosted Linux Actions runner is registered: the fleet dashboard's runner list is all glaeda Mac labels. So no job can take fleet Linux yet. This PR reads vars.LINUX_RUNNER as step 1 whenever it names a non-Blacksmith label. Once a GCP pool runner label exists and the variable names it, these jobs take it with no further change. Registering those runners is a runner-trust change and is not in this PR.

Changelog

none

🤖 Generated with Claude Code

Agent session: agent session

The cmux-next checks, the cmux-next CI guard group, the release-notary
guards and the tooling tests need neither Linux nor Blacksmith, and the
cmux-tui macOS targets are Mac builds. They now take fleet Linux
(vars.LINUX_RUNNER when it names our own runner), else a mini idle now
with no nightly job waiting on its pool, else their Blacksmith label
(scripts/ci/portable_job_placement.py). The cmux-tui macOS targets take
the free mini first and build natively there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review 4caaeb3: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr29130 -f ref=ci/linux-jobs-fall-back-to-minis -f platform=macos -f backend_mode=local

web and size checks: green

Shipped in:

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

…n 3.9 from uv

Review at 6f925b3: setup-python has no macOS arm64 3.9, and run_ci_guards.py
ran the new submodule reset inside every parallel guard group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64

@teamleaderleo teamleaderleo left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

SW-15 independent exact-head review — FIX at b47a6add70c5b843b049c69c73ffd1f84bb97edf (owner @teamleaderleo; branch ci/linux-jobs-fall-back-to-minis; base feat-cmux-next). Read the current portable-placement code, all three consuming workflows and the actual tests. Two P1 route-selection gaps undermine the PR's promised fleet Linux → idle owned mini → Blacksmith choice.

[P1] Unconditional vars.CI_SIDE_LANE_RUNNER wins before the new placement output. .github/workflows/cmux-next.yml:1578, cmux-next-guards.yml:230 and :628 all choose ... || vars.CI_SIDE_LANE_RUNNER || needs.portable-placement.outputs...linux_jobs ... || ...mini_jobs ... || blacksmith. GitHub Actions short-circuits ||: whenever CI_SIDE_LANE_RUNNER is configured, its nonempty string wins regardless of which job portable-placement selected, whether its snapshot is stale, whether the mini is occupied/reserved for a nightly, or whether this is an ineligible rerun. The guard group additionally has an unguarded vars.LINUX_RUNNER after the selected outputs. The order test test_ci_portable_job_placement.py:166-184 only checks relative substring indices for linux/mini/Blacksmith and never checks earlier || terms, so it passes while routing bypasses the planner. Fix: remove the unconditional legacy side labels from these eligible jobs; use only attempt-matched linux_jobs/mini_jobs outputs before the cloud route, with a separate intentional emergency override if required. Regression: render the expression (or inspect parsed OR terms) with configured CI_SIDE_LANE_RUNNER and planner returning empty; assert Blacksmith wins, not the side label. With planned fleet Linux assert Linux wins before a side label.

[P1] The Python planner assigns an unverified/offline/busy Linux runner without checking availability. scripts/ci/portable_job_placement.py:81-86,128-147 makes fleet_linux a string-prefix filter, then puts every non-MAC_FIRST job into linux_jobs merely because FLEET_LINUX has a label. runners and the janitor snapshot are consulted only for the remaining mini jobs. In fact, test_ci_portable_job_placement.py:69-73 asserts all jobs route to gcp-linux-32 while the runner fixture contains five Macs and zero Linux hosts. If that configured Linux runner is offline, already busy or all of its slots queued, all those jobs can wait on it indefinitely; the spare mini / Blacksmith overflow are never considered. Fix: validate that the Linux label is an allowlisted owned runner and at least one matching runner is online/idle after already queued work. Allocate at most the available capacity; put remainder through mini and cloud. Treat unreadable runner/snapshot as an unverified Linux choice (fail over), and add explicit offline/missing/busy, partial-capacity, and real idle-Linux tests.

Both defects are visible in current source and tests. I did not trigger CI jobs or claim a live failure. The existing tests exercise the decision function and check for output strings while missing the complete workflow expression and Linux capacity reality. Light fix: gate the workflow's actual runs-on order and verify runner capacity. Heavy rewrite of scheduling/leases is unnecessary here; leave the owner's branch and nightly reservations intact, and rerun workflow/routing tests at its next head before a merge.

Comment thread .github/workflows/cmux-next.yml Outdated
runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.CI_CLOUD_OVERFLOW_SAVED && 'blacksmith-32vcpu-ubuntu-2404' || 'blacksmith-16vcpu-ubuntu-2404' }}
# Fleet Linux, then a free mini (portable-placement), then Blacksmith 16 vCPU, which runs the script
# tests 32 at once (scripts/ci/run-checks-concurrently.sh). The checks are Python and bash; none needs Linux.
runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || vars.CI_SIDE_LANE_RUNNER || needs.portable-placement.outputs.placed_attempt == format('{0}', github.run_attempt) && contains(needs.portable-placement.outputs.linux_jobs, ' checks ') && needs.portable-placement.outputs.linux_runner || needs.portable-placement.outputs.placed_attempt == format('{0}', github.run_attempt) && contains(needs.portable-placement.outputs.mini_jobs, ' checks ') && needs.portable-placement.outputs.mini_runner || vars.CI_CLOUD_OVERFLOW_SAVED && 'blacksmith-32vcpu-ubuntu-2404' || 'blacksmith-16vcpu-ubuntu-2404' }}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[P1] Placement bypass. A truthy vars.CI_SIDE_LANE_RUNNER is evaluated before any needs.portable-placement output and wins every || chain, so this job takes that mini label even when placement found no free mini, rejected a stale snapshot, or chose fleet Linux. The same bug is in cmux-next-guards.yml:230,628. Remove the unguarded legacy side-route terms; add an expression-order regression that asserts empty placement → Blacksmith with this variable set.

jobs = tuple(dict.fromkeys((env.get("JOBS") or "").split()))
mac_first = set((env.get("MAC_FIRST") or "").split())
linux = fleet_linux(env.get("FLEET_LINUX", ""))
linux_jobs = tuple(job for job in jobs if linux and job not in mac_first)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[P1] Missing/offline/busy fleet Linux always wins. linux_jobs is filled from FLEET_LINUX string alone; the runner list and pool snapshot are considered only for minis. The current test_fleet_linux_comes_before_the_minis passes five Mac runners and zero Linux runners yet asserts all jobs go to Linux. Verify owned Linux runner online/idle capacity and subtract queue before assigning jobs, then carry the rest to mini/cloud; add offline/absent/busy and partial-capacity cases.

self.assertIn("release-notary take Blacksmith", placed.why)

def test_fleet_linux_comes_before_the_minis(self):
placed = placement.decide(env(FLEET_LINUX="gcp-linux-32"), idle(5), snapshot(), NOW)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This test uses idle(5) — five Mac runners, zero Linux — but asserts all three jobs route to gcp-linux-32. This fixture currently accepts the broken promise: a configured Linux label is treated as a functioning free runner. Include an actual available Linux fixture for positive priority, then assert fallback when the Linux runner is absent, offline, busy or already queued.

teamleaderleo and others added 2 commits October 31, 2026 02:56
…nothing

tooling-tests may run on a mini now; macOS nice with no argument prints its
usage, so the fakes logged nice= and three tests failed there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
@blacksmith-sh

blacksmith-sh Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Found 17 test failures on Blacksmith runners:

Failures

Test View Logs
1 runner guard check(s) failed:/1 runner guard check(s) failed: View Logs
1 runner guard check(s) failed:/1 runner guard check(s) failed: View Logs
test_big_red (main.FleetRuleLints.test_big_red)/
test_big_red (main.FleetRuleLints.test_big_red)
View Logs
test_every_blacksmith_linux_label_fails_over (main.BlacksmithLinuxFailoverTest.test
_every_blacksmith_linux_label_fails_over)/
test_every_blacksmith_linux_label_fails_over (main.BlacksmithLinuxFailoverTest.test
_every_blacksmith_linux_label_fails_over)
View Logs
test_every_listed_hook_is_one_glaeda_released_with_the_check (main.SigningHooksInSt
epWithGlaedaTests.test_every_listed_hook_is_one_glaeda_released_with_the_check)/
test_every_listed_hook_is_one_glaeda_released_with_the_check (main.SigningHooksInSt
epWithGlaedaTests.test_every_listed_hook_is_one_glaeda_released_with_the_check)
View Logs
test_every_owned_workflow_is_watched (main.OwnedPoolSources.test_every_owned_workfl
ow_is_watched) (tree='/home/runner/_work/cmux/cmux')
View Logs
test_every_upload_is_short_lived_and_small_or_allowlisted (main.WorkflowTests.test_
every_upload_is_short_lived_and_small_or_allowlisted)/
test_every_upload_is_short_lived_and_small_or_allowlisted (main.WorkflowTests.test_
every_upload_is_short_lived_and_small_or_allowlisted)
View Logs
test_no_retry_attempt_of_any_workflow_takes_an_owned_label (main.Wiring.test_no_ret
ry_attempt_of_any_workflow_takes_an_owned_label) (workflow='cmux-next-guards.yml')/
test_no_retry_attempt_of_any_workflow_takes_an_owned_label (main.Wiring.test_no_ret
ry_attempt_of_any_workflow_takes_an_owned_label) (workflow='cmux-next-guards.yml')
View Logs
test_no_retry_attempt_of_any_workflow_takes_an_owned_label (main.Wiring.test_no_ret
ry_attempt_of_any_workflow_takes_an_owned_label) (workflow='nightly.yml', job='generate
-nightly-deltas', attempt='3')/
test_no_retry_attempt_of_any_workflow_takes_an_owned_label (main.Wiring.test_no_ret
ry_attempt_of_any_workflow_takes_an_owned_label) (workflow='nightly.yml', job='generate
-nightly-deltas', attempt='3')
View Logs
test_no_retry_attempt_of_any_workflow_takes_an_owned_label (main.Wiring.test_no_ret
ry_attempt_of_any_workflow_takes_an_owned_label) (workflow='nightly.yml', job='sign-nig
htly-identity-legacy', attempt='3')/
test_no_retry_attempt_of_any_workflow_takes_an_owned_label (main.Wiring.test_no_ret
ry_attempt_of_any_workflow_takes_an_owned_label) (workflow='nightly.yml', job='sign-nig
htly-identity-legacy', attempt='3')
View Logs

...and 7 more test failures. View all on Blacksmith

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…he side cap

A job that portable placement may put on a mini (checks, the release-notary
guard tests) runs the same scripts there, none of the app's Mac tiers. So
cmux_next_route and detect_ci_change_areas read its runner as Linux: an edit
to checks runs no Mac tier, and a guard test it names stays off the macOS and
Release lanes (test_ci_nightly_push_throttle reddened without this).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-d750cf10922c785efa64
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at 4caaeb3

Reviewer: codex gpt-6-astra high, fast tier.

Follow-ups: 3. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward.

SHIP at 4caaeb3

Visual verdict: no images; Leo tries the build

  1. [P2] [follow-up] portable_job_placement.py:158: Pool selection happens before queue subtraction and nightly reservations. Reproduced with three idle light runners, three queued light jobs, and three idle std runners: everything goes to Blacksmith. Evaluate effective availability across compatible pools before choosing one.

  2. [P2] [follow-up] cmux-tui-artifacts.yml:302: TUI placement precedes tree-owner-wait, which can run for minutes before handing ownership to this run. The build then consumes the old mini label without rechecking availability. Those runners may now be busy or reserved for nightly work, putting publication back into the queue this change aims to avoid. Refresh placement after ownership resolves.

  3. [P2] [follow-up] cmux-next-guards.yml:230: Moving ci-guard-group onto macOS drops existing test coverage. run_ci_guards.skipped_here() skips “Validate CMUX workload profile contract” on macOS; I confirmed this against the generated plan. Keep that test on a Linux job so mini availability does not determine whether the contract is tested.

Relevant local workflow, placement, fork-isolation, and supply-chain checks passed. Three guard-test cases require Git history absent from this snapshot.

Follow-ups:

  • Choose compatible pools using queue-adjusted availability and nightly reservations.
  • Refresh TUI placement after the publisher ownership wait.
  • Preserve Linux workload-profile test coverage.

@teamleaderleo teamleaderleo removed the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@teamleaderleo
teamleaderleo merged commit aa55699 into feat-cmux-next Oct 31, 2026
217 of 229 checks passed
@teamleaderleo
teamleaderleo deleted the ci/linux-jobs-fall-back-to-minis branch October 31, 2026 13:00
teamleaderleo added a commit that referenced this pull request Nov 2, 2026
* ci guards: depth-limited fetches only in shallow checkouts (base red)

scripts/measure/fresh-home-launch.sh and workers/cmux-vm/scripts/test-mesh-label-compat.ts passed
--depth to a fetch in the caller's checkout, which turns a full clone shallow. Both now pass it only
when git rev-parse --is-shallow-repository says the checkout is already shallow.

Regressed by #30495 (fresh-home-launch.sh) and #30501 (test-mesh-label-compat.ts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: map the transcript's TurnRows.tsx in place of the deleted MessageGroupRow.tsx (base red)

#29800 removed MessageGroupRow.tsx (every sent message is its own card); TurnRows.tsx now draws
each sent message row in order (SentMessageLine.tsx is already mapped), so chat-transcript maps it.

Regressed by #29800 (deleted the file #28698 had mapped).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: scan cmux-next-ios/verify/ for errno-after-assertion (base red)

cmux-next-ios/verify/imsg-replies/GestureServerUITests.swift is an XCTest source outside every
TEST_ROOTS entry. Scan cmux-next-ios/verify/ (it has no errno use, so nothing else changes).

Regressed by 201101a (added the verify/imsg-replies rig; landed by a safe-push train, no PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: list the four manual-lane workflow readers in UNWIRED (base red)

tests/test_ci_blacksmith_linux_sizes.py, test_ci_overflow_pick_reads_pool.py,
test_nightly_next_control_rescue.py and test_ci_cloud_probe_rescue.py read .github/workflows/
but are lane = "manual" in tests/test-execution.toml (no new CI tests; wiring them as linux-guard
reddened tooling-tests on every PR). They join UNWIRED with the same reason as the other
manual-lane entries, and each says when to run it by hand.

Regressed by #30360, #30878, #30657 and #30796 (each added its test without an UNWIRED entry).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: fleet rule lints pass again (base red)

"ci: Validate fleet rule lints" failed on feat-cmux-next in three rules.

big-red: the personal machine's name left product code and tests. Doc
comments say "one lane VM" / "<host>"; the webview tests use my-vm (its
machine hue still differs from gpu-box's).
  Regressed by #28105 (peer_handshake_tests.rs, list_reply.rs,
  list_reply_tests.rs), #29495 (host_update_tests.rs), #29126
  (chatOpenPacedList.test.ts), #31108 (project-chooser-machine-tag).

pkill: the iOS verify rigs and the tool-evals remote daemon stop only
what they started. imsg-replies records the conversation-sim, the
recorders (simctl itself, not the xcrun shim) and the gesture servers in
pidfiles and signals those PIDs after checking the command line still
matches; pq_browser_events records its two site servers' PIDs; the
tool-evals remote stop kills the setsid group whose PID the launcher
recorded beside the socket.
  Regressed by 201101a (imsg-replies, first in #22328), #25915
  (pq_browser_events.py), #29080 (tool-evals/session.mjs).

Allowlisted as safe, with reasons: scripts/ci/macos-stubs/Security.tbd
(a linker stub listing symbol names, no keychain read; #27603) and
tests/tool-evals/cases/hard.ndjson (an eval case forbidding pkill;
#28738).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: determinize python tests flagged by test-determinism (base red)

Regressed by the commits introducing each assertion (see report).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: nightly Xcode selection, unused anchors, daemon retention key (base red)

- Validate macOS jobs select a pinned Xcode: the nine flagged nightly.yml
  jobs already run select-ci-xcode.sh through the `- *select-xcode` step
  alias; the guard did not expand step aliases. It now resolves `- *name`
  to the `- &name` step, with self-tests for both an alias to the selector
  and an alias to another step. Regressed by #30471 (steps turned into
  aliases to keep nightly.yml under its size limit).
- Lint every workflow: drop the five sign-nightly-identity-legacy-* anchors
  left after the Blacksmith twin that aliased them was removed
  (a7b6a61, "legacy sign and deltas run on owned Macs only"; the twin
  came from #27357). No behaviour change.
- upload-artifact retention: the 14-day cmux-nightly-remote-daemon upload
  moved from build-nightly-app to build-nightly-remote-daemon in #25935
  with its retention unchanged; move the allowlist key with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: media tours and visual-gate state match their validators (base red)

Drop the unknown step key "record" (re-added after #26888 removed it by #27933 and f283a2e), the unimplemented top-level frameInterval in home-chief-shimmer (c022070), the focusLeft right after closePane in palette-chat-toggles (#25405), split pane-polish (31 steps) into pane-polish and pane-polish-themes, and give the app-store-consent-deploy state a synthetic host that is not an email shape (4a05792).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: allowlist remaining test-determinism findings with reasons (base red)

Swift/web/tool-evals findings cannot be determinized without Mac or webview runs; each entry names its reason and adding PR.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: Linux pool for the push kicks; record pool readers as unrescued (base red)

- cmux-next-base-tier-kick, cmux-next-dev-kick and ios-internal-nightly-trigger waited on
  one fixed Blacksmith Linux size (!startsWith(LINUX_RUNNER) || overflow 8 || 2 vCPU). They
  now read fromJSON(vars.RUNNER_POOLS).linux after the fork branches. Their pinning tests
  (base tier, DEV kick, iOS internal builder) assert the pool read instead. Regressed by #30837.
- owned_pool_rescue.py: feat-cmux-next's UNRESCUED_WORKFLOW_PATHS records the secretless
  Linux pool readers the owned-pool sources test found unlisted (fast gates, gallery health,
  PR checks, cloud backend check, web validation, and the three kicks above), with the existing
  _POOL_READ_WHY. Regressed by #29844 and #31138.
- test_ci_linux_single_size.py: drop cmux-next-fast-gates.yml from FIXED_PAIR (moved by #31138);
  test_ci_overflow_pick_reads_pool.py: drop the two kicks from PICKS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: signing-runner and retry-attempt guards follow the owned-Mac signing rule (base red)

Signing keychain guard (rule 7): accept owned-pool runner variables only where
safety is proven in the job itself:
- 7a: a job in glaeda's exclusive signing class whose first step is the
  verbatim claim gate may fall back to CI_NIGHTLY_NEXT_BUILD_RUNNER /
  CI_SIDE_LANE_RUNNER (nightly.yml sign-nightly-identity-legacy and
  generate-nightly-deltas, owned-only by design, Lawrence a7b6a61;
  deltas' Sparkle key added in #29261). test_ci_signing_owned_mac.py holds the
  claim script, always() keychain delete and twin/picker rules for them.
- 7b: stable-next.yml sign (#27898) reads every signing secret only under
  env.SIGN == 'developer-id', whose condition picks a literal Blacksmith label
  before any variable; its owned dry-run holds no key. Proven structurally.

Retry guard: cmux-next.yml swift-test/daemon-test/generated-files' side-lane
fallback (#29814) is now gated at run_attempt <= 3 for the bot, so the bot's
attempt 4 takes blacksmith-6vcpu-macos-26 like every other cmux-next fleet
fallback (#22565). The three owned-only nightly jobs are exempt while they keep
exactly their owned route. The textual check now forbids naming attempts after
2 (== 3+), as its comment says, instead of also == 2 (#29847 nightly.yml,
#29130 cmux-next-guards.yml name the bot's attempt 2, which may stay owned).

Regressed by #29814, #27898, #29261, #29847, #29130 and a7b6a61.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: owned Mac pools through the picker/vars pattern; follow nightly step scripts for sentry-cli (base red)

check_no_self_hosted_fleet_runners
The guard told every workflow to "use a cloud label so required jobs never
land on a mini". That is outdated policy: owned fleet Macs now come first for
macOS jobs (Linux, then owned Macs through the picker, Blacksmith last),
release signing may run on owned Macs, display tests gate nothing, and the
NEXT nightly runs on the fleet with the nightly-only minis dedicated to it.
The guard kept up only through exact per-workflow selector strings, which
drifted with every routing change and turned the guard red on base:
- cmux-next.yml swift-test/daemon-test/generated-files: the glaeda-side
  deferred fallback (regressed by #29814) and the attempt-1 gui selector
  (#28605, #27853)
- cmux-next-base-autofix.yml / cmux-next-pr-autofix.yml: the gui selector
  gained a re-run predicate (#27853)
- nightly.yml sign/prepare jobs: owned pool as the final fleet fallback
  (#29154)

Fix the guard instead of moving those jobs to Blacksmith: an owned POOL label
(glaeda-[<lane>-]<class>-xcode-<version>, served by many minis, never one
host) is accepted as a quoted literal in a ${{ }} runs-on that routes through
the declared picker/vars pattern (reads needs.<job>.outputs.* or vars.*).
In a workflow a pull request can trigger, the fork pull request branch
(fork heads to Blacksmith) must still come first on the line, so fork code
never reaches an owned Mac (runner trust unchanged). Still refused: bare
self-hosted/macOS/ARM64, single-host labels (cmux-macos, mac-mini, slot-N,
tart-*, ...), macos-26 outside the fork branch, and an owned label named
directly with no picker or variable route. The exact gui selector strings are
replaced by this rule; self-tests cover each accept and refuse case.

check_sentry_cli_install_portability
#31013 moved nightly.yml's "Upload dSYMs to Sentry" body into
scripts/ci/nightly-steps/app-upload-dsyms.sh (size budget), so the guard no
longer saw the ./scripts/upload-sentry-dsyms.sh call. The guard now reads
the step's step script too. Regressed by #31013.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: change-area tests follow the RUNNER_POOLS and devEngines routing; helper walk skips non-running namers (base red)

- release-admission and remote-daemon-admission read fromJSON(vars.RUNNER_POOLS).linux by
  design since #30878; the two tests now assert that instead of vars.LINUX_RUNNER.
- fast-checks.yml's setup-bun reads the bun pin from webviews/package.json devEngines at run
  time (#29757); the setup-bun test accepts a steps.<id>.outputs.version whose step reads
  devEngines, and still requires the literal pin everywhere else.
- indexnow.yml pinned bun 1.3.14 when #30697 backported it; it now installs devEngines' 1.4.4.
- ci_helper_areas() failed open on files that name a helper without running it, so
  owned_pool_rescue.py, owned_build_state.py and e2e_warm_derived_data.py stopped routing by
  where they run. Regressed by #28528 (test_cmux_next_route.py names owned_pool_rescue.py as a
  routing input and itself), #28513 (docs/ci/cmux-next-tiers.md names that test), #30657
  (nightly_next_control_rescue.py's only test is manual-lane), #28568 (.github/warm-sources.json
  prose) and #30467 (base_red_excuse.py's guard-test table and its test's fixtures). The walk
  now ignores a test naming itself, docs, a Python test naming it only in a docstring, the
  listed path tables and warm-sources.json, and treats a test the registry declares in an inert
  lane that nothing imports as a dead end. An unregistered test nothing names still fails open.
- cache_restore_receipt.py now routes precisely (macOS + Release, via ci-macos.yml's
  cache-restore action) instead of failing open; its assertion says so.

python3 tests/test_ci_change_areas.py: 4 of 234 failed before, 0 after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: review fixes for #31366 (daemon pgid, fork-first owned pools, stale pidfiles)

- tests/tool-evals/session.mjs: the remote launcher recorded $! of the backgrounded && chain
  (the wrapping subshell), so the stop signalled a group that did not exist. setsid now starts
  a shell that writes its own PID and execs the daemon, so the recorded PID is the daemon's
  process group. Checked locally: the recorded PID equals the daemon's PGID and SID, and
  kill -TERM -- -PID stops it.
- tests/test_ci_self_hosted_guard.sh (security): the owned-pool exemption in pull request
  workflows only required the fork branch somewhere before the owned literal, so
  `needs.x.outputs.runner || <fork branch> || 'glaeda-...'` passed. The fork branch must now be
  the first selector of the expression (after only the hosted repository_owner clause). New
  negative self-tests for reordered expressions; red with the old match, green with the new.
- cmux-next-ios/verify/parity/pq_browser_events.py: stop_site() now stops only servers this run
  started (serve() reports each pidfile it wrote), kills a PID only while its command line is
  still that server's, and removes the pidfile after. Checked with a local stub harness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

* ci guards: universal nightly helper runs-on match works under mawk (base red)

The helper runs-on expected string passed to awk -v carried `\&\&`, which gawk reads as `&&`
(with a warning) but mawk, the default awk on Ubuntu runners, keeps as `\&\&`, so the whole
expression never matched and "nightly must build and hand off the macOS 26 helper" failed.
Plain `&&` means the same in both. Regressed by #30824.

bash tests/test_nightly_universal_build.sh: FAIL before, PASS after (mawk 1.3.4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-fc739fd2f4e557ae7189

---------

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

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 4caaeb37 Deployed Oct 31, 2026 by teamleaderleo via cmux-tui tree preflight #89726
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:normal Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant