ci(cmux-next): base autofix lets a running repair finish and re-applies it on a moved base

Merged
TE
18d ago
+234-17

On 2026-10-19 feat-cmux-next's settings exports went stale: defaults_guard.py check-fresh failed at the tip (e10a2d6a318: sources hash f725a91d, export stamp beb7c1ea), so defaults-snapshot went red on every PR. The base autofix workflow regenerates those exports on a Mac after each push, but it never landed them:

  • Both its Mac "action exports" job and its "autofix" job used cancel-in-progress: true, so each push cancelled the run in progress. 9 runs in a row ended cancelled.
  • When a run did reach its push, it refused because feat-cmux-next had moved.

What changes:

  • A running regeneration and repair finish. Both jobs use cancel-in-progress: false; a newer push replaces only a pending run.
  • The repair is re-applied on a moved base, but only when that's provably right. It is one commit on the run's head. If the push is rejected because feat-cmux-next moved, the job compares two sets of autofix areas: those whose inputs the commits since its head touched (autofix-areas.sh), and those that build the files the repair wrote. Each output maps to every area that builds it: the defaults snapshot to both settings and actions, the shortcuts to shortcuts and actions. An output no area is known to build never re-applies.
    • No overlap: the repair's inputs are unchanged, so its files are exactly right at the tip. It cherry-picks and pushes again, up to 5 times.
    • Overlap (a revert of a new setting, say): the old repair could be wrong at the tip, so the run stops and the newer push's run regenerates it. This also stops a late re-run from landing old output.
    • Conflict: the run still fails and stays out of the next run's "since".
    • Rejected without the base moving: the run says so instead of "kept moving".
  • CI runs the new test: cmux-next.yml's "Companion workflows" step and its path filters now include it.

Changelog

none

Validation

  • Red, then green: python3 -m unittest tests.test_cmux_next_base_autofix_finishes. It covers both concurrency settings, and runs the real push loop, extracted from the workflow, against local repositories:
    • a repair lands on a base that moved elsewhere;
    • a repair whose settings inputs moved meanwhile is left to the newer run (that test failed on the previous version, which pushed the stale export);
    • a push rejected without the base moving reports it;
    • the defaults snapshot waits when the action exports moved;
    • a shortcuts-only repair lands past an unrelated change;
    • an unmapped output is never re-applied;
    • a conflict fails without pushing;
    • a retry succeeds when the branch moves twice.
      Commits 1, 3 and 5 are the red tests; 2, 4 and 6 are the fixes. 10 of 10 pass.
  • python3 scripts/verify-local.py --affected origin/feat-cmux-next: 9 of 10 pass. localization fails identically on the base.
  • Three review rounds:
    • A subagent review found that the first version could land wrong content after an intervening change to the same area, or from a late re-run. The area guard closes both.
    • coord-2 then found that the guard mapped outputs through an input-only map, so the defaults snapshot and the shortcuts file mapped to no area. The output map closes that.
  • Not verifiable before merge: a real run. After merge, the next push run should log "Pushed … (attempt N)", "Nothing to repair", or "the newer run repairs it", and check-fresh should pass at the tip.

The graph and ratchets areas cover all of Packages/, so those repairs will often wait for the newer run rather than be re-applied. That's the safe side.

It changes workflow files, so a person lands it.

🤖 Generated with Claude Code