Skip to content

codegen: Swift emitter for CmuxNextDaemon request types (sdk-schema.json), with --check; Layout wire tests - #31805

Merged
cmux-bot[bot] merged 9 commits into
feat-cmux-nextfrom
rust-migration/swift-codegen
Nov 2, 2026
Merged

cmux-bot[bot] merged 9 commits into
feat-cmux-nextfrom
rust-migration/swift-codegen

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

The app's daemon request types (Packages/macOS/CmuxNext/Sources/CmuxNextDaemon/Requests/*, with Tree/*, Events/*, Conversations/* and CloudConversations/*: about 14,000 lines) are hand-written copies of cmux-tui/spec/sdk-schema.json. They copy command names, field names, which fields are optional, enum values and capabilities. The seven SDKs generate theirs from that spec, but Swift had no emitter (layer-ownership.md V-30 and L6, cx-3bi.7). This PR adds the Swift emitter, its tests and its --check, and it pins the JSON of the first family to convert. It converts no family yet: the generated files must be made at build time, not committed (see "Next: A4b").

What changed

  • cmux-tui/bindings/codegen/swift_app/ renders CmuxNextDaemon request types from the SDK schema IR (codegen/ir.py), the same validated input the seven SDK emitters read.
    • The schema owns the wire: the command name, requiredCapability, each field's wire key, required/optional/nullable, enum values and result shapes.
    • swift_app/bindings.json holds only what the schema can't say about Swift: the type name, the typed id an Id field carries (PaneID, SplitID), the Swift type a schema object binds to, init parameter order, how the app reads a result, and doc comments.
    • A shape the bindings don't cover is refused with a BindingError. These are: an Id field with no id type, an unbound object or enum, a union, a non-empty result with no response binding, and a required field gated by a capability.
    • An optional field gated by a capability gets a doc line that names it. Family keys and type names must be plain Swift identifiers, because they become output paths.
  • Wire rules:
    • every field gets an explicit coding key equal to its wire name;
    • required fields are always encoded;
    • optional fields are Swift Optionals, nil by default, encoded only when set;
    • float32 is Double in the app.
  • Output: one *.generated.swift per request or enum in Requests/<Family>/, with a // GENERATED by cmux-tui/bindings/codegen/swift_app ... header. At most three top-level types per file.
  • codegen/generate.py runs the step after the SDKs and the page router, and passes --schema through. Its existing --check and --write cover the Swift step. bindings.json lists no family, so today both have nothing to write. No CI job or step is added.
  • LayoutRequestWireTests pins the whole JSON line of every Requests/Layout request except new-row and move-column (MoveColumnRequestTests already pins that one), decodes the undo-layout and zoom-pane replies, and checks each capability gate, on today's hand-written types. The A4b conversion must keep it green.

Why sdk-schema.json and not resource-operations-v2.json

The backlog named resource-operations-v2.json, but none of the 231 command names the CmuxNextDaemon types send is a v2 operation. 197 are sdk-schema.json commands (protocol 12); the other 34 are in no spec yet. The v2 operation names live elsewhere: about 650 string literals, mostly in Connection/*StateClient*.swift. They are the last step of the plan below, in a later PR (lead decision).

Evidence

Commits: ba2a9718a05 emitter tests (red), c8e6c207b83 emitter, 8578dab0e09 Layout wire tests, 7f688b978c4 Layout conversion, 230c2b46f03 its revert, e65dc2d2b61 review-finding tests (red), ba2ce273b23 their fix.

  • Emitter tests, PYTHONPATH=cmux-tui/bindings python3 -m unittest discover -s cmux-tui/bindings/codegen/tests:
    • red at ba2a9718a05 (ModuleNotFoundError);
    • green at c8e6c207b83;
    • red again at e65dc2d2b61 (10 failures, for review 1's findings), green at ba2ce273b23 (97 tests);
    • review 2 found eight refusals with no test; they are added (105 tests), and review 3's fix keeps the output_root test inside its own temp folder. Removing each of the seven emitter or writer checks in a scratch copy turns its test red; the eighth (a request that is not an object) is refused first by the schema validator.
  • python3 cmux-tui/bindings/codegen/generate.py --check at the head: every SDK, the page router and swift-app checked.
  • python3 scripts/ci/generated_files_guard.py check at the head: exit 0, no committed generated file added.
  • python3 scripts/cmux-next/prepush-gate.py: no new violations.
  • Compile (cmux-ci compile-check --app --package CmuxNext --recipe-version 2):
    • 8578dab0e09: job ff898b210e15a7121d18a6e4 passed.
    • ba2ce273b23: job 9079741b4fca48d7450229cf passed (CmuxNext, app and cmux-tui receipts ok). Later heads change only Python files under cmux-tui/bindings/codegen.
    • The head's Packages/ tree is byte-identical to 8578dab0e09 (git diff 8578dab0e09 HEAD -- Packages is empty).
  • Mac Swift suites on 8578dab0e09, through proof-step.py: job 506ace4f2c4837aa8f08c9e9 ran LayoutRequestWireTests (11 tests), MoveColumnRequestTests (2), StandInReferenceTests with StandInLineTests (3) and PinnedDockMoveRequestTests (2). 18 passed, 0 failed.
  • The reverted conversion (7f688b978c4, the generated Layout family) was compiled (job 8b30900f27e10c9d5cc9dd24) and ran the same 18 tests green (job 32b7615ef9569947503e22d5). That shows the emitter's output sends the same JSON as the hand-written types.

Observed / Not run

  • Observed: the Python tests and checks above on this VM, and the fleet compile and Mac suite jobs above (I read their logs: each suite ran, 0 failures).
  • Not run:
    • A Mac suite run at the head itself. Its Swift tree is identical to 8578dab0e09, which ran them.
    • A live daemon round trip.
    • A tagged build: no user-visible change.
  • ./scripts/lint-ios-package-conventions.sh --namespace-fix does not cover CmuxNext (iOS line only).
  • Review findings left for A4b, because they matter only once Swift output exists:
    • scripts/cmux-next/base-autofix.sh:130 lists only changed files under cmux-tui/bindings;
    • scripts/ci/next_batch.py SDK_GENERATED has no Swift pattern.

Next: A4b, generate the Swift at build time (proposal)

scripts/ci/generated_files_guard.py refuses a PR that adds a committed generated file (Leo, 2026-10-21). A first version of this PR committed the generated Layout family and was blocked for that. The family conversions wait for build-time generation. That touches the build path (risk:high) and needs an owner for the hq recipe.

Why the generation must run inside SwiftPM. Every place that compiles CmuxNext runs SwiftPM:

  • the Xcode cmux-next target, through the package product;
  • swift build in the fleet recipe cmux-next-swift-check (Linux);
  • swift test in scripts/ci/package-test-lane.sh (Mac lane).

Page strings are made in an Xcode script phase (build-agent-pane-web.sh), but SwiftPM builds the package before any app-target phase, and swift build runs no phase at all. A pre-build step in each script would leave every other swift build red with missing types.

Design: a SwiftPM build tool plugin.

  • Packages/macOS/CmuxNext/Plugins/DaemonWireTypes/plugin.swift: a BuildToolPlugin on the CmuxNextDaemon target.
    • It returns one .prebuildCommand that runs python3 cmux-tui/bindings/codegen/swift_app/generate.py --write --out <pluginWorkDirectory>.
    • SwiftPM compiles everything in the work directory into the target.
  • A prebuild command is used, not a build command, because the set of output files depends on bindings.json, and a prebuild command may produce outputs SwiftPM does not know in advance.
  • The plugin is the only writer of generated Swift. SwiftPM compiles every .swift file under a target's folder and ignores .gitignore, so a generated file left in Sources/ beside the plugin's copy fails with "invalid redeclaration". A4b therefore removes output_root from bindings.json, and generate.py gets --out DIR (required for --write). Its in-tree --write and --check go away, together with the hook in codegen/generate.py; --check becomes "the bindings render against the schema", which the codegen unit tests already run. generate.py --write also refuses any --out inside Packages/**/Sources. Then no agent, batch workflow or base autofix can write a second copy. This also settles review 1's findings on base-autofix.sh and next_batch.py: they have no Swift output left to list.
  • Requests/**/*.generated.swift goes into .gitignore. Each family then moves in its own PR: its hand-written files go, its binding is added, and LayoutRequestWireTests (or the family's own wire tests) stays green.
  • Package.swift: .plugin(name: "DaemonWireTypes", capability: .buildTool()) and plugins: ["DaemonWireTypes"] on CmuxNextDaemon.

Builder changes:

  • Xcode: the cmux-next build passes -skipPackagePluginValidation (scripts/reload.sh, the release and nightly scripts), or trusts the plugin once per checkout. A local Xcode GUI asks once.
  • Fleet: cmux-next-swift-check (hq, build-fleet/recipes) and package-test-lane.sh should need nothing for SwiftPM itself. Not proven: what SwiftPM's macOS plugin sandbox allows a prebuild command to read. It limits writes to the plugin work directory and blocks the network; I have not checked whether it also limits reads, and the inputs (cmux-tui/spec/sdk-schema.json, the codegen) are outside the package. The spike answers this. If reads outside the package are blocked, the plugin declares the inputs as files in the package (a symlinked Plugins/DaemonWireTypes/inputs), or the recipes pass --disable-sandbox (swift) and -skipPackagePluginValidation (xcodebuild).
  • First A4b step: a spike on one fleet Mac and the Linux pool (swift build, swift test, xcodebuild of cmux-next) with an empty family list. It shows the plugin runs and the sandbox lets it read the schema, before any family moves.

When Python is missing on a builder.

  • Every fleet Mac has Xcode, whose command line tools include /usr/bin/python3. The Linux pool image has python3.
  • The plugin looks for /usr/bin/python3, then /usr/bin/env python3. If both fail, it stops the build with an error that names the missing tool and the fix (xcode-select --install). It never falls back to stale or committed output.
  • If the fleet ever needs a build with no Python, the emitter can move into a Rust cmux-tui binary, shipped as an artifactbundle binary target the plugin runs. That is the same pinned-artifact path the app-ffi xcframework uses. This is not needed now.

Conversion order after A4b (hand-written lines per family, and how many of its commands sdk-schema.json has; each PR is one family plus at most one emitter feature):

# Family Lines In sdk-schema Emitter feature it needs
1 Requests/Layout 177 8 of 8 (new-row with Creation) none (shown by 7f688b978c4)
2 Requests/Terminals, Projection, History 287 6 of 6 a field group (origin + mutation_id as MutationIdentity); selection enums
3 Requests/Screens, TabGroups, TabMove, Bookmarks, Workspace 1724 50 of 50 expected_generation/expected_revision
4 Requests/Profiles, Session, TerminalIO, Tabs 2370 70 of 71 tri-state optionals (absent, null, value)
5 Requests/Creation and new-row 474 7 of 7 the shared SpawnOptions group
6 Conversations, CloudConversations 3580 44 of 44 tagged unions, literal types
7 Requests/Agents (schema part) ~600 of 1199 9 of 17 none beyond 1-6
8 Events 677 45 of 46 event names event payloads; the DaemonEvent decoder
9 Tree (snapshots) 1885 snapshot types decoding with the schema's defaults (the hand-written decoders are lenient on purpose)
10 In no spec yet: Requests/Apps, Files, Machines, Resource, 8 Agents commands, client-hello, loopback-open, browser-runtime-* ~1700 0 of 34 add them to sdk-schema.json first (Rust owns them)
11 v2 operation names (L6): Connection/*StateClient*.swift and others ~650 literals resource-operations-v2.json op-name constants

Changelog

none

The tests name the emitter's contract: one generated file per request and
enum under its family folder with an ownership header, the command name and
capability from the schema, typed Swift ids and bound types from a bindings
file, required fields always encoded and optional ones only when set, a
--check that reports changed, missing and stale owned files, and refusals
for every schema shape the bindings do not cover.

Red here: codegen.swift_app does not exist yet (ModuleNotFoundError).
codegen/swift_app renders the app's daemon request types from the SDK
schema (cmux-tui/spec/sdk-schema.json), the spec the Requests/* types
mirror by hand today. The schema owns the wire: command names,
capabilities, field names, presence, nullability, enum values and result
shapes. swift_app/bindings.json owns only what the schema cannot say about
Swift: the type name, the typed id an Id field carries, the Swift type a
schema object is bound to, init parameter order, how the app reads a
result, and docs. A shape the bindings do not cover is refused.

Output is one *.generated.swift per request or enum in its Requests/<Family>
folder, with a GENERATED header (the inventory counts these as generated;
the god-file limit of three top-level types per file holds). --check
reports changed, missing and stale owned files; --write updates them and
deletes stale ones; files of other generators are left alone.

codegen/generate.py runs this step after the SDKs and the page router, so
its existing --check (cmux-tui-sdks.yml, cmux-tui-spec.yml) and --write
(base autofix, next-batch) cover it. No CI job or step is added.

The bindings list no family yet; the next commits convert Requests/Layout.

Green here: PYTHONPATH=cmux-tui/bindings python3 -m unittest discover -s
cmux-tui/bindings/codegen/tests (91 tests, 27 of them new, red at ba2a971
with ModuleNotFoundError), and generate.py --check (checked swift-app).
LayoutRequestWireTests encodes every Requests/Layout request (except
new-row, which carries the shared spawn options and moves with the
Creation family) through WireCoding and compares the whole JSON line,
decodes the undo-layout and zoom-pane replies, and checks each request's
capability gate. The next commit replaces the hand-written types with
ones generated from cmux-tui/spec/sdk-schema.json; these tests must stay
green across it, which shows the JSON on the wire did not change.

The gate check accepts a gate from DaemonCapability.required: the schema
names viewport-column-resize-v1 for set-viewport-pane-width and
layout-undo-v1 for undo-layout, which the hand-written types left out.
Every daemon the app connects to serves both (DaemonCompatibility refuses
one that lacks a required capability), so that gate changes nothing.

Expected green on this commit (hand-written types); run on the fleet.
….json

The Layout request types now come from cmux-tui/spec/sdk-schema.json
through codegen/swift_app (bindings.json lists the family): MoveColumn,
SetColumnDock, SetColumnWidth (set-viewport-pane-width), SetRowHeights,
SetSplitRatio, SwapPane, UndoLayout and ZoomPane requests, and the
ColumnMoveDirection and PaneDirection enums. Ten *.generated.swift files
replace six hand-written files and part of a seventh (177 lines).

What the schema cannot say stays hand-written, as Swift-side API only:
SwapTarget and SwapPaneRequest.init(pane:target:), the DockSnapshot init
of SetColumnDockRequest, and UndoLayoutRequest.Response (the schema's
untagged union of two reply shapes, read as one struct). new-row stays
hand-written in NewRowRequest.swift: it carries the shared SpawnOptions
and moves with the Creation family.

The JSON on the wire is the same: LayoutRequestWireTests (previous commit,
green on the hand-written types) must stay green. Two requests gain the
schema's capability gate, set-viewport-pane-width (viewport-column-resize-v1)
and undo-layout (layout-undo-v1); both are in DaemonCapability.required,
which every connected daemon serves, so nothing changes at run time.
set-column-dock gains the schema's optional `permanent` field, nil and
not encoded. SetRowHeightsRequest.fit is now Bool? (nil omits it); its
only caller passes it every time.
@lawrencecchen lawrencecchen added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

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

Review bace1ee: 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=pr31805 -f ref=rust-migration/swift-codegen -f platform=macos -f backend_mode=local

web and size checks: green

Shipped in:

@blacksmith-sh

blacksmith-sh Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Found 6 test failures on Blacksmith runners:

Failures

Test View Logs
main.DeveloperIdAppTests.test_download_prints_the_active_profile_only/
test_download_prints_the_active_profile_only
View Logs
stubs: /System/Library/Frameworks/Security.framework/Versions/A/Security has no stub/
Security has no stub
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_CFRetain']/
CoreFoundation, missing 1: ['_CFRetain']
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_log2']/
CoreFoundation, missing 1: ['_log2']
View Logs
test_every_compile_job_names_its_warm_sources (main.ThisTree.test_every_compile_job
_names_its_warm_sources)/
test_every_compile_job_names_its_warm_sources (main.ThisTree.test_every_compile_job
_names_its_warm_sources)
View Logs
test_the_repository_highlights_validate (main.DigestTests.test_the_repository_highl
ights_validate)/
test_the_repository_highlights_validate (main.DigestTests.test_the_repository_highl
ights_validate)
View Logs

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

@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent): BLOCK at 7f688b9

Blocking

  1. The PR adds 10 new committed generated files. The repository refuses them.

Low (these matter only if committed output stays)

  1. cmux-tui/bindings/codegen/swift_app/emit.py:308: the output path is f"{family}/{name}.generated.swift". The family key and the request name from bindings.json are not checked. output_root is checked (swift_app/generate.py:47-54), but a name such as ../../X still writes outside it. name is also never checked as a valid Swift identifier.
  2. scripts/cmux-next/base-autofix.sh:130: after --write, the script lists only changed files under cmux-tui/bindings. Regenerated Swift files are not reported and are not filtered against base-autofix-frozen.txt. The workflow's git add -A still commits them. The PR body says base autofix covers the Swift files, so this gap is not stated.
  3. scripts/ci/next_batch.py:163: SDK_GENERATED has no pattern for the Swift outputs. A next-batch conflict in a generated Swift file is not resolved automatically the way SDK outputs are (take one side, then regenerate).
  4. cmux-tui/bindings/codegen/swift_app/emit.py:259-292: the emitter ignores field-level capability in the schema. Examples: set-column-dock.pinned needs pinned-dock-v1, role needs dock-column-role-v1, and transaction on set-split-ratio and set-viewport-pane-width needs layout-undo-v1. The wire does not change, but the module docstring says every shape the bindings do not cover is refused, and that is not true here.
  5. cmux-tui/bindings/codegen/generate.py:202-203: the Swift step always reads the default schema, even when --schema is given. The page-router step already had the same behavior.

Checked and correct

  • Wire equality: for all 8 requests, the JSON keys, which fields are always sent and which only when set, the types, the response keys, command and the capability values are the same as in the deleted hand-written code. The new gates (viewport-column-resize-v1, layout-undo-v1) are in the .required tier. SetRowHeightsRequest.fit is now Bool?, and its only caller always passes a Bool.
  • Swift API: the init overloads are not ambiguous.
  • Swift validity: the keywords and coding keys are correct, and the Response in an extension satisfies the associated type.
  • File limits: no file has more than 3 top-level types or 400 lines. The PR adds no namespace enums and no CI jobs or steps.
  • Generator checks: the 91 emitter tests pass, and generate.py --check exits 0.
  • Other reds: tooling-tests and acpmux tests are also red on other open PRs, so they are not caused by this PR.

…k-schema.json"

This reverts commit 7f688b9.

scripts/ci/generated_files_guard.py refuses a pull request that adds a
committed generated file (Leo, 2026-10-21), and the converted family added
ten Requests/Layout/*.generated.swift files. Lead decision: this PR keeps
the emitter, its tests and --check, and the family conversion waits for
build-time generation (backlog A4b). The hand-written Layout types are back
unchanged; LayoutRequestWireTests stays and pins their JSON for that move.
… honors --schema (red)

Review findings on #31805 (comment 6569578652), each as a test:
- a family key or Swift type name from bindings.json became a path part
  unchecked, so `../../X` wrote outside output_root and `Not-Valid` gave
  invalid Swift;
- a field the schema gates by capability was emitted with no word of it,
  while the module said every uncovered shape is refused;
- codegen/generate.py ran the Swift step on the default schema even with
  --schema.

Red here: 10 failures in test_swift_app.py (the new tests); the
exit-code test passes already and stays as coverage.
…honors --schema

- A family key must be one folder name and a Swift type name a plain,
  non-keyword identifier; both become output paths, so `../../X` no longer
  writes outside output_root.
- An optional field the schema gates by a capability gets a doc line
  naming the capability and protocol; a required gated field is refused.
  The module docstring now lists exactly what is refused.
- codegen/generate.py passes --schema through to the Swift step.
- The repository-bindings test no longer passes over zero files: it checks
  output_root exists and the file count, and a second test renders the
  Layout binding (backlog A4b) against today's schema.

Green here: PYTHONPATH=cmux-tui/bindings python3 -m unittest discover -s
cmux-tui/bindings/codegen/tests (97 tests; red at e65dc2d with 10
failures), generate.py --check (checked swift-app).
@lawrencecchen lawrencecchen changed the title codegen: Swift emitter for CmuxNextDaemon request types; Requests/Layout generated from sdk-schema.json codegen: Swift emitter for CmuxNextDaemon request types (sdk-schema.json), with --check; Layout wire tests Nov 2, 2026
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent 2): FIX at ba2ce27

The generator code is correct, and the Swift test is valid. The items below are a test gap, two errors in the PR body and one gap in the A4b proposal. None of them changes what the code does today, because bindings.json lists no family.

Findings

  1. Medium (repository test rule): 8 refusal and safety paths have no test that fails without them. I removed each check in a copy of the tree and ran test_swift_app.py. All 33 tests still passed each time:
    • swift_app/emit.py:293-296: refuse a required field gated by a capability. This is the fix for finding 5 of the previous review. The red commit e65dc2d2b61 tests only the doc line, not this refusal.
    • swift_app/generate.py:49-50: refuse an output_root that is absolute or contains ... This is the only path-safety check on the output root.
    • swift_app/generate.py:83-84: refuse to overwrite a file at a generated path that does not have the header. test_a_generated_file_of_another_generator_is_not_owned tests only that such a file is not deleted.
    • emit.py:310-311 (two fields with the same Swift name), emit.py:323-324 (two bindings make the same type), emit.py:352-353 (unknown protocol), emit.py:213-215 (types binds a type the schema does not have), emit.py:349-350 (a request that is not an object).
    • I ran these refusals directly, and each one works. Add one test for each, at least for the first three. They are the refusals that the PR body and the module docstring list as guarantees.
  2. Low (PR body): an unfilled placeholder. The Evidence section says "head: job 9079741b4fca48d7450229cf, HEAD_COMPILE". That job is now done: CmuxNext, app and cmux-tui all ok: true. Write "passed" in its place.
  3. Low (PR body): the body says LayoutRequestWireTests "pins the whole JSON line of every Requests/Layout request except new-row". It does not contain move-column. The move-column line is pinned by the existing MoveColumnRequestTests (MoveColumnTests.swift), which job 506ace4f also ran. Name that suite in the sentence.
  4. Medium (A4b proposal, not code): generated files written in the source tree and by the plugin would be declared twice. The proposal keeps generate.py --write writing to output_root (Sources/CmuxNextDaemon/Requests). It only adds --out for the plugin and puts the in-tree files in .gitignore. But SwiftPM compiles every .swift file in the target folder and does not read .gitignore. After a family moves, a local codegen/generate.py --write would leave in-tree copies that SwiftPM compiles together with the plugin's output, and the build fails with "invalid redeclaration". cmux-next-batch.yml:334, base-autofix.sh:129, next_batch.py:1106 and agents all run that command. The plan must make the plugin the only writer: --write must not write into Sources, or output_root must move out of the target.
  5. Low (A4b proposal, unproven claim): the proposal says the SwiftPM macOS plugin sandbox lets a prebuild command "read only the package directory". As I remember SwiftPM's sandbox profile, it allows file-read* everywhere and limits only writes and network. I did not check this here, so the spike must confirm it. If I am correct, reading cmux-tui/spec works without --disable-sandbox. The command can then not write into the source tree, for example __pycache__ beside the codegen (Python ignores that failure; PYTHONDONTWRITEBYTECODE=1 removes the attempt). The rest of the proposal correctly says it is a proposal and that the spike will decide. That part is honest.

Previous review (BLOCK at 7f688b9)

  • 1 (committed generated files): fixed by the revert 230c2b46f03. generated_files_guard.py check --base cb5c64bc2eed --head HEAD exits 0 and shows only notes about the base.
  • 2 (path safety of family keys and type names): fixed at emit.py:205-206 and emit.py:321-322. Tests exist, and they were red at e65dc2d2b61.
  • 3 (base-autofix.sh:130) and 4 (next_batch.py SDK_GENERATED): not fixed. The body says they are left for A4b. That is acceptable while the generator writes no file.
  • 5 (field capability ignored): fixed. The doc line is tested. The refusal for a required field is not tested (finding 1).
  • 6 (--schema not passed through): fixed for the Swift step (generate.py:202) and tested. The page-router step does not read sdk-schema.json, so it is not affected.

What I checked and found correct

  • I ran PYTHONPATH=cmux-tui/bindings python3 -m unittest discover -s cmux-tui/bindings/codegen/tests: 97 tests, OK. At e65dc2d2b61 the same command gives 10 failures, as the body says.
  • I ran python3 cmux-tui/bindings/codegen/generate.py --check, also with python3 -I from another folder (as base-autofix.sh runs it): exit 0, and the last line is checked swift-app.
  • Exit codes: a failing Swift step gives 1 (tested). A schema that is missing gives a clear message and exit 1. available_languages reads only codegen/emit_*.py, so swift_app/emit.py does not become an SDK language.
  • --check and --write: changed, missing and stale owned files are reported or fixed. Only files whose first line is HEADER_MARK are deleted. Tree/ProtectedFolderEntry.generated.swift (another generator) is outside output_root and has a different header. The output is deterministic: every map is sorted and sets are used only to test membership.
  • Unions: a ref to a tagged_union (apply-layout.layout → DeclarativeLayout) or to an object that is not bound is refused, and inline unions and literals stop at the last raise in swift_type. The repository's Layout binding renders 10 files against today's schema.
  • LayoutRequestWireTests.swift: I read it against Requests/Layout/*, DaemonRequest.swift (Encodable, Sendable, Response: Decodable & Sendable) and DaemonCapability.required/optional ([String]). The calls match. git diff 8578dab0e09 HEAD -- Packages is empty. Job 506ace4f (aws-m4pro-1, package-test-lane.sh suite, exit 0) shows Test run with 11 tests in 1 suite passed for LayoutRequestWireTests, and 18 tests passed in total. The head compile 9079741b passed. The test compares whole decoded lines, and its gate checks cover the conversion that comes later.
  • Repository rules: the diff changes no file in .github/ or in scripts/ci. The new tests run in the existing unittest discover steps (cmux-tui-sdks.yml:161, cmux-tui-spec.yml:111). The Swift step runs inside the existing generate.py --check steps, and today it has nothing to check. There is no new job, step or gate.
  • Red checks on the head (acpmux tests: path filter for schemas/settings/source/agent-retry.json; release-notary: test_digest.py highlight; tooling-tests) are in files that this PR does not change. They are base findings.

Review 2 on #31805 (comment 6569885939) found eight refusals that no test
made red when removed. Each has a test now: a required field gated by a
capability, two fields with one Swift name, two bindings making one type,
an unknown protocol, a bound type the schema lacks, a request that is not
an object (the schema validator refuses it first), an output_root outside
the repository, and a hand-written file at a generated path (never
replaced, in --check and --write).

Checked by mutation in a scratch copy: removing each of the seven emitter
or writer checks turns its test red. 105 tests pass at this commit.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent 3): FIX at ba61fa6

The repair delta is one file: cmux-tui/bindings/codegen/tests/test_swift_app.py (+56). All four items of review 2 are resolved. One new test has a hermeticity defect, and that defect makes the suite red on this VM at the head now.

Finding

  1. Medium: test_an_output_root_outside_the_repository_is_refused asserts on a shared path outside its own temp folder, and a red run leaves that path behind. test_swift_app.py:386-396.
    • setUp uses the temp folder itself as the repository root (self.root = Path(self.temp.name)). The roots ../Requests and Sources/../../Requests therefore point to $TMPDIR/Requests, which is shared by every process. The test then checks self.assertFalse((self.root.parent / "Requests").exists()).
    • When the output_root check is removed (the mutation that the body describes), --write writes the Layout fixture into $TMPDIR/Requests/Layout/. tearDown removes only the temp folder, so the files stay. After that, every later run on that machine is red, also with the code restored.
    • Evidence: on this VM, /work/tmp/Requests/Layout/ holds 5 *.generated.swift fixture files (SwapPaneRequest, ZoomPaneRequest, SetSplitRatioRequest, SetRowHeightsRequest, PaneDirection), created 2026-11-02 05:35:39 UTC, 34 s before the head commit. The requested command PYTHONPATH=cmux-tui/bindings python3 -m unittest discover -s cmux-tui/bindings/codegen/tests at the head gives Ran 105 tests ... FAILED (failures=4): the four subtests of this test, AssertionError: True is not false. With TMPDIR set to an empty folder, the same command gives 105 tests OK. My own mutation run (in a scratch copy with its own TMPDIR) left tmp/Requests behind the same way.
    • The test also fails for any unrelated $TMPDIR/Requests folder. Fresh CI runners do not have one, so CI stays green, but agent VMs share /work/tmp.
    • Fix: put the repository one level inside the temp folder (for example self.root = Path(self.temp.name) / "repo", then mkdir), and assert on Path(self.temp.name) / "Requests". An escape then stays inside the temp folder, and tearDown removes it. Remove the leftover /work/tmp/Requests on this VM.

Review 2 items

  • 1. Tests for eight refusals: resolved. I removed each check in a scratch copy (/work/tmp, deleted after) and ran test_swift_app.py. Each new test went red without its check:
    • emit.py:293 required field gated by a capability: test_a_required_field_gated_by_a_capability_is_refused red.
    • generate.py:49 absolute or .. output root: test_an_output_root_outside_the_repository_is_refused red (3 subtests; the "" subtest is held by generate.py:46, which I did not remove).
    • generate.py:83 file at a generated path without the header: test_a_hand_written_file_at_a_generated_path_is_never_replaced red in check and in write.
    • emit.py:310 two fields with one Swift name, emit.py:323 two bindings with one type, emit.py:352 unknown protocol, emit.py:214 types binds a missing type: each named test red.
    • emit.py:349 request that is not an object: the suite stays green, as the body says. codegen.validate refuses that schema first, and the test accepts ValidationError. So the emitter check is not reachable from a validated schema, and no test pins it. The body says this, so I do not count it as a defect.
  • 2. HEAD_COMPILE placeholder: resolved. The body now says job 9079741b4fca48d7450229cf passed, with the CmuxNext, app and cmux-tui receipts. Later heads change only test_swift_app.py.
  • 3. LayoutRequestWireTests claim: resolved. The body now says "except new-row and move-column (MoveColumnRequestTests already pins that one)". I read the test: it pins set-split-ratio, set-viewport-pane-width, undo-layout, set-row-heights, set-column-dock, swap-pane and zoom-pane. MoveColumnTests.swift:8-14 pins the whole move-column line. RowRequests.swift:7 (new-row) is the only other Layout command.
  • 4a. Plugin is the only writer: resolved. The A4b section now removes output_root, makes --out required for --write, refuses an --out inside Packages/**/Sources, and removes the in-tree --check hook. That removes the redeclaration risk from cmux-next-batch.yml, base-autofix.sh and next_batch.py.
  • 4b. Plugin-sandbox claim: resolved. The body now says "Not proven" for reads outside the package, names the spike as the test, and gives two fallbacks. (The __pycache__ write note from review 2 is not in the body. That is a detail for the spike, not a defect.)

Checks I ran at the head

  • nice -n 10 python3 cmux-tui/bindings/codegen/generate.py --check: exit 0, last line checked swift-app.
  • python3 scripts/ci/generated_files_guard.py check --base $(git merge-base HEAD origin/feat-cmux-next) --head HEAD: exit 0, only note: lines about base files.
  • git diff 8578dab0e0924aea8f37f88016beb9eb50471106 HEAD -- Packages: empty. So the compile ff898b210e15a7121d18a6e4 and the Mac suites 506ace4f2c4837aa8f08c9e9 still apply to the Swift tree.
  • The delta changes no file in .github/ or scripts/ci, and it adds no CI job, step or gate.
  • I did not compile or run Swift.

Review 3 on #31805 (comment 6570012552): the test used the temp folder as
the repository, so `../Requests` was $TMPDIR/Requests, a folder every
process shares. With the check removed (the mutation run), --write left
fixture files there, and every later run on the VM failed (4 failures).
The repository now sits one level down, so an escape lands in the test's
own temp folder and tearDown removes it.

Checked: the mutation run with a private TMPDIR turns all seven new tests
red and leaves nothing behind; 105 tests pass. The leftover
/work/tmp/Requests from the first mutation run is removed.
@lawrencecchen

Copy link
Copy Markdown
Contributor Author

Review (independent subagent 3): SHIP at bace1ee

My finding at ba61fa650ab is fixed. The delta from that head has one change, in cmux-tui/bindings/codegen/tests/test_swift_app.py:349-354. CheckAndWriteTests.setUp now puts the repository at <temp>/repo. As a result, ../Requests and Sources/../../Requests resolve to <temp>/Requests, and the existing assertion self.root.parent / "Requests" checks that path. Both stay inside the temp folder of the test, and tearDown removes them. I found no new defect.

What I ran at the head

  • PYTHONPATH=cmux-tui/bindings nice -n 10 python3 -m unittest discover -s cmux-tui/bindings/codegen/tests: 105 tests OK. Then the same command with a private, empty TMPDIR: 105 tests OK, and the TMPDIR was empty after the run.
  • Mutation of swift_app/generate.py:49 (if relative.is_absolute() or ".." in relative.parts: changed to if False:), in a scratch copy under /work/tmp with a private TMPDIR. CheckAndWriteTests went red: 3 failures and 1 error, all in test_an_output_root_outside_the_repository_is_refused.
    • ../Requests: BindingError not raised.
    • Sources/../../Requests: the escape check fails.
    • /abs/Requests: the write fails with PermissionError (this VM does not run as root).
    • "": fails only because an earlier subtest left files in the same temp folder. Without the mutation, generate.py:46 refuses an empty root.
    • After the run, the private TMPDIR was empty, and /work/tmp/Requests and /abs did not exist. I deleted the scratch copy.
  • The leftover /work/tmp/Requests from the earlier run is gone.
  • nice -n 10 python3 cmux-tui/bindings/codegen/generate.py --check: exit 0, checked swift-app.
  • python3 scripts/ci/generated_files_guard.py check --base $(git merge-base HEAD origin/feat-cmux-next) --head HEAD: exit 0, only note: lines about base files.
  • git diff 8578dab0e0924aea8f37f88016beb9eb50471106 HEAD -- Packages is empty. So the compile ff898b210e15a7121d18a6e4 and the Mac suites 506ace4f2c4837aa8f08c9e9 still apply to the Swift tree.

Note (not a defect)

  • If the generate.py:49 check is removed and the test runs as root, the /abs/Requests subtest would write to /abs on the host, because an absolute root does not depend on the temp folder. This can happen only in a mutation run, because the unmodified code refuses the root before any write.

The four items of review 2 stay resolved (see my comment at ba61fa650ab). I did not compile or run Swift.

@cmux-bot
cmux-bot Bot merged commit 4acd981 into feat-cmux-next Nov 2, 2026
216 of 233 checks passed
@cmux-bot
cmux-bot Bot deleted the rust-migration/swift-codegen branch November 2, 2026 06:10

This branch was previously deployed

3 inactive deployments
artifacts — bace1eeb Deployed Nov 2, 2026 by lawrencecchen via publish tree-addressed cmux-tui #99568
sentry-dsyms — bace1eeb Deployed Nov 2, 2026 by lawrencecchen via upload cmux-tui dSYMs to Sentry #99568
ci-routing-agent — bace1eeb Deployed Nov 2, 2026 by lawrencecchen via cmux-tui tree preflight #99568
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