link: cmux link seed push and seed clone, a Cloud project seed with the copy as fallback
Piece 4 of the Cloud project seed (leo-workspace design/cloud.md, "Getting the project onto the machine"), the CLI half. Leo, on the start screen's line:
Your project folder is copied up.
we'll want to deal with the edge cases for this too
This adds cmux link seed push and cmux link seed clone, which put a project on a Cloud machine the way the design says: a git folder with a GitHub remote is seeded (the machine clones the remote, this Mac sends one bundle of what the remote lacks), and everything else takes the streamed copy. It builds on #30464 (the Mac-side plan), #30382 (the machine's git-seed-v1), #30427 (the copy fallback) and #30280 (the move pump's --sync). start-chat copy and the app switch to it in the next PR.
CLI
cmux link seed push --socket S LOCAL REMOTE [--since-last]
cmux link seed clone --socket S --repo OWNER/NAME [--ref REF] REMOTE
cmux link agent move ... --sync seed --sync-socket S --sync-local L --sync-remote R
-
push reads
identify. A machine withoutfs-v1answersmachine_outdated. Otherwise it plans (seed::plan), uploads the bundle throughfs-v1toREMOTE/../.cmux-seed-<name>-<id>.bundle(inside the fs roots), callsgit.seed.start, and readsgit.seed.statusonce every 500 ms until the job ends or 45 minutes pass. That wait is a bounded loop inside the CLI process, not an agent polling loop. The upload is deleted afterwards either way. -
Fallback to the copy (
sync_cli::push, a full copy) happens when:- the plan answers a
Fallback(not_git, not_top, no_commit, no_remote, ambiguous_remote, not_github, no_base, unmerged, submodule_local_work, git_failed); - the machine is another build without
git-seed-v1(machine_outdated); - the machine answers
seed.needs_github,seed.base_missingorseed.clone_failed(orseed.unavailable).
seed.base_missingfirst re-plans once from the remote default branch's tip (plan_with_basewithPlanned::bases' last). If the failed seed made the folder, the folder is removed before the copy. The answer then saysmode:"copy",fallback_codeandfallback_why. Forneeds_githubit also carriesinstall_url, so the app can offer the install while the chat goes on. - the plan answers a
-
--since-lastnever falls back to a full copy:- After a seed, it sends a delta bundle (
plan_since). Nothing changed:unchanged:trueand no upload at all. - After a copy (mode
copy), it re-pushes the copy to the same REMOTE, which the copy's manifest makes incremental. - No record, or a record from another machine:
seed.no_previous. The machine'sseed.since_mismatchandseed.machine_changedcome back as errors.
- After a seed, it sends a delta bundle (
-
Record:
<state>/link-seed/<sha256(local NUL remote)>.json(0600, folder 0700), beside sync_cli's<state>/link-syncbase record. It holds{mode, machine (identify's session_id), url, head, index, work}. -
clone: the clone-only
git.seed.start. It answers{ok, mode:"clone", head, head_ref}, or the error code plusinstall_url. -
agent move
--sync seed: once the chat is frozen, the pump callsseed_cli::push_since_last. An error gives the chat back, as--sync pushdoes, withseed_cli::failure_code's code.
Output
stderr progress JSON lines (throttled to one per 250 ms, first and last always):
{"progress":{"phase":"upload","bytes_done","bytes_total"}}while the bundle uploads;{"progress":{"phase":"clone|fetch|bundle|checkout|submodules|lfs","bytes_done","objects","objects_total"}}from the machine's job;- the copy's own
{"progress":{"files_done","files_total","bytes_done","bytes_total"}}for a copy.
stdout, one line:
{"ok":true,"mode":"seed"|"delta"|"copy","unchanged":bool,"bundle_bytes":N,"head":SHA|null,"work":SHA|null,
"skipped_files":[{"path","kind","why"}], "copy":{sync summary}?, "fallback_code"?, "fallback_why"?, "install_url"?}
{"ok":false,"error","error_code","install_url"?}
Test seam
git-seed-v1 got a debug-build-only seam, CMUX_TEST_GIT_SEED_GITHUB=<dir>. It makes the daemon's seeds clone https://github.com/O/N.git from file://<dir>/O/N.git with no credential. GitSeedService::with_test_github does the same for in-process tests. Release builds never read the variable, and nothing on the wire reaches it.
Tests (Linux, cmux-ci test)
tests/link_seed.rs: a realFsServiceandGitSeedServicebehind a socket, with afile://bare repo as GitHub. It covers:- a full seed (unpushed commit, staged and untracked files,
.envskipped by name, ignored folder left out, no bundle left behind), then a delta, then an unchanged delta that makes nofs.write; --since-lasterrors:seed.no_previous(and nothing copied),seed.machine_changed,seed.since_mismatch;- fallback to the copy for a non-git folder (
not_git, then a copy-mode--since-lastre-push that writes only the new file), for a GitLab remote (not_github) and for a repo the machine cannot clone (seed.clone_failed); - a machine without
git-seed-v1(machine_outdated); seed clone, ok andseed.clone_failed.
- a full seed (unpushed commit, staged and untracked files,
tests/cloud_move_journey.rs(two real daemons): the journey now seeds the dirty folder withcmux link seed push(modeseed, git status identical on the VM). It then moves the chat mid-turn with--sync seed, and the turn's last edit (src/turn.txt, written after the seed) arrives through the delta bundle.
Risk: the change is additive. start-chat copy and the app still call link sync push until the next PR. The only edit to existing behaviour is the agent move's --sync parse (push and pull unchanged) and two small sync_cli helpers (upload, is_refusal).
After review: the bundle is written in a private 0700 folder (git writes it world-readable under umask 022); a failed big upload's .cmux-sync-part is deleted too; seed clone asks for LFS files; a machine that does not answer identify is an error.
Before a fallback copy's own lines, stderr carries one {"progress":{"phase":"copy","fallback_code","install_url"?}}, so the card can say why while the copy runs.
Follow-ups: #30644 (--since-last checks the machine's folder before answering unchanged); a full re-seed into a folder that already holds a seed fetches into it instead of starting clean; submodules with local work still fall back to the copy (plan's submodule_local_work).
Agent session: agent session
š¤ Generated with Claude Code