Repository navigation
link: plan a git-seed-v1 seed of a GitHub working copy - #30464
Conversation
`link::seed::plan` decides whether a folder can be seeded from its GitHub remote and writes one bundle of what the remote lacks: unpushed commits, an INDEX commit of the staged tree and a WORK commit of the working tree (untracked files that are not ignored, less likely secrets), plus the stash. `plan_with_base` re-plans from another base candidate and `plan_since` builds a delta after an earlier seed. Nothing in the working copy changes. `cmux link seed plan --local FOLDER --bundle OUT` prints the plan as one JSON line. Not wired into start-chat yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-3bab30503f0b9a7059e5
|
Owned by an agent session · session |
There was a problem hiding this comment.
14 issues found across 7 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="cmux-tui/crates/cmux-tui/src/link/seed/github.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui/src/link/seed/github.rs:30">
P2: The https authority ends only at the first `/`, so `?` or `#` before the host can hide the real host. `https://evil.example?@github.com/acme/widget.git` is accepted as GitHub, while the transport would contact evil.example. End the authority at the first `/`, `?` or `#`.</violation>
<violation number="2" location="cmux-tui/crates/cmux-tui/src/link/seed/github.rs:32">
P2: An HTTPS remote with explicit default port `:443` is rejected because `host` includes the port. Strip default `:443` before comparing with `github.com`.
(Based on your team's feedback about normalizing default HTTPS ports and ignoring SSH transport ports.)</violation>
<violation number="3" location="cmux-tui/crates/cmux-tui/src/link/seed/github.rs:36">
P2: The SSH prefix requires `/` immediately after `github.com`, so explicit-port GitHub remotes fall back instead of being canonicalized. Parse the SSH authority and ignore its transport port when extracting the repository.
(Based on your team's feedback about normalizing default HTTPS ports and ignoring SSH transport ports.)</violation>
</file>
<file name="cmux-tui/crates/cmux-tui/src/link/seed.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui/src/link/seed.rs:13">
P1: The seed can include a secret under a non-UTF-8 path: lossy path conversion makes the later `git rm` miss the file, while `--ignore-unmatch` suppresses the failure. Preserve Git paths losslessly through secret filtering and pathspec removal.</violation>
<violation number="2" location="cmux-tui/crates/cmux-tui/src/link/seed.rs:13">
P2: An unchanged delta plan leaves a previous file at `out_bundle` while reporting no bundle, so a caller that reuses the pathname can send stale data. Reject an existing output for unchanged plans or otherwise make the no-bundle result unambiguous to the caller.</violation>
<violation number="3" location="cmux-tui/crates/cmux-tui/src/link/seed.rs:25">
P3: With no arguments, this prints the usage text to stdout and exits 64, while every other bad-usage path in this file writes it to stderr. Print usage to stderr for the empty case so stdout stays JSON-only, matching `access_cli::run`.</violation>
<violation number="4" location="cmux-tui/crates/cmux-tui/src/link/seed.rs:66">
P2: `parse` silently accepts duplicate `--local`, `--bundle`, and `--base` flags, so a repeated option changes the selected value instead of reporting bad usage. Reject a flag when its option has already been set.</violation>
</file>
<file name="cmux-tui/crates/cmux-tui/src/link/seed/plan.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui/src/link/seed/plan.rs:463">
P2: `git add -A` captures an existing `out_bundle` when the output path is inside the working copy. Re-running with `--bundle ./seed.bundle` then sends the previous bundle as project content; reject or exclude output paths inside the repository before staging.</violation>
<violation number="2" location="cmux-tui/crates/cmux-tui/src/link/seed/plan.rs:507">
P1: `unchanged` ignores stash changes, so creating or updating a stash with the same HEAD, index, and work tree returns no bundle and leaves the machine without the current stash object. Include the previous stash in the delta state and compare it, including stash deletion, before declaring the seed unchanged.</violation>
<violation number="3" location="cmux-tui/crates/cmux-tui/src/link/seed/plan.rs:688">
P2: A read failure of the sparse-checkout file is silently turned into empty patterns via `unwrap_or_default()`, and the plan still reports sparse checkout as on. Propagate the error as `GitFailed` instead.</violation>
</file>
<file name="cmux-tui/crates/cmux-tui/src/link/seed/secrets.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui/src/link/seed/secrets.rs:2">
P2: The new doc line is wrong for tracked files with local edits. The remote holds only the committed version, but `git add -A` ships working-tree and staged edits to tracked files, so a tracked `.env` or `.npmrc` with a newly added token reaches the bundle. Either exclude secret-named tracked paths whose content differs from HEAD, or state that tracked edits ship as-is.</violation>
<violation number="2" location="cmux-tui/crates/cmux-tui/src/link/seed/secrets.rs:10">
P3: The `.env` check compares the raw name while the key checks use the lowercased name. A file such as `.ENV` is not excluded, although macOS's default case-insensitive filesystem treats it as the same env file. Compare lowercased names throughout.</violation>
<violation number="3" location="cmux-tui/crates/cmux-tui/src/link/seed/secrets.rs:22">
P1: `reason` misses credential files and secret directories such as `.git-credentials`, `.aws/credentials`, and `.ssh/work_key`, so untracked secrets can be included in `refs/cmux-seed/work` and sent to the machine. Align this detector with the existing `access_cli::is_secret` policy or share one implementation.</violation>
</file>
<file name="cmux-tui/crates/cmux-tui/src/link/seed/git.rs">
<violation number="1" location="cmux-tui/crates/cmux-tui/src/link/seed/git.rs:32">
P1: `base()` leaves Git clean/process filters active, but the seed planner runs `git add -A` through this helper. A repository’s attributes and config can therefore execute arbitrary commands or block planning while the seed is built. Confine or blank filter drivers before indexing the working tree, or use a raw-byte indexing path.</violation>
</file>
Reply with feedback, questions, or to request a fix.
View guided diff | Turn on auto-fix | Re-trigger cubic
| if [".pem", ".p12", ".pfx", ".key"].iter().any(|ext| lower.ends_with(ext)) { | ||
| return Some("key or certificate file"); | ||
| } | ||
| if name == ".netrc" { |
There was a problem hiding this comment.
P1: reason misses credential files and secret directories such as .git-credentials, .aws/credentials, and .ssh/work_key, so untracked secrets can be included in refs/cmux-seed/work and sent to the machine. Align this detector with the existing access_cli::is_secret policy or share one implementation.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed/secrets.rs, line 22:
<comment>`reason` misses credential files and secret directories such as `.git-credentials`, `.aws/credentials`, and `.ssh/work_key`, so untracked secrets can be included in `refs/cmux-seed/work` and sent to the machine. Align this detector with the existing `access_cli::is_secret` policy or share one implementation.</comment>
<file context>
@@ -0,0 +1,78 @@
+ if [".pem", ".p12", ".pfx", ".key"].iter().any(|ext| lower.ends_with(ext)) {
+ return Some("key or certificate file");
+ }
+ if name == ".netrc" {
+ return Some("credentials file (.netrc)");
+ }
</file context>
| "i18n.commitEncoding=UTF-8", | ||
| ]); | ||
| command | ||
| .env("GIT_CONFIG_NOSYSTEM", "1") |
There was a problem hiding this comment.
P1: base() leaves Git clean/process filters active, but the seed planner runs git add -A through this helper. A repository’s attributes and config can therefore execute arbitrary commands or block planning while the seed is built. Confine or blank filter drivers before indexing the working tree, or use a raw-byte indexing path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed/git.rs, line 32:
<comment>`base()` leaves Git clean/process filters active, but the seed planner runs `git add -A` through this helper. A repository’s attributes and config can therefore execute arbitrary commands or block planning while the seed is built. Confine or blank filter drivers before indexing the working tree, or use a raw-byte indexing path.</comment>
<file context>
@@ -0,0 +1,88 @@
+ "i18n.commitEncoding=UTF-8",
+ ]);
+ command
+ .env("GIT_CONFIG_NOSYSTEM", "1")
+ .env("GIT_TERMINAL_PROMPT", "0")
+ // `status` and friends never refresh the person's index.
</file context>
| // work sha means all three are as the previous seed left them. | ||
| let (base, unchanged) = match since { | ||
| Since::Base(base) => ((*base).to_string(), false), | ||
| Since::Previous(previous) => (previous.work.clone(), previous.work == work_commit), |
There was a problem hiding this comment.
P1: unchanged ignores stash changes, so creating or updating a stash with the same HEAD, index, and work tree returns no bundle and leaves the machine without the current stash object. Include the previous stash in the delta state and compare it, including stash deletion, before declaring the seed unchanged.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed/plan.rs, line 507:
<comment>`unchanged` ignores stash changes, so creating or updating a stash with the same HEAD, index, and work tree returns no bundle and leaves the machine without the current stash object. Include the previous stash in the delta state and compare it, including stash deletion, before declaring the seed unchanged.</comment>
<file context>
@@ -0,0 +1,713 @@
+ // work sha means all three are as the previous seed left them.
+ let (base, unchanged) = match since {
+ Since::Base(base) => ((*base).to_string(), false),
+ Since::Previous(previous) => (previous.work.clone(), previous.work == work_commit),
+ };
+ let mut bundle_bytes = 0;
</file context>
|
|
||
| mod git; | ||
| mod github; | ||
| pub(crate) mod plan; |
There was a problem hiding this comment.
P1: The seed can include a secret under a non-UTF-8 path: lossy path conversion makes the later git rm miss the file, while --ignore-unmatch suppresses the failure. Preserve Git paths losslessly through secret filtering and pathspec removal.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed.rs, line 13:
<comment>The seed can include a secret under a non-UTF-8 path: lossy path conversion makes the later `git rm` miss the file, while `--ignore-unmatch` suppresses the failure. Preserve Git paths losslessly through secret filtering and pathspec removal.</comment>
<file context>
@@ -0,0 +1,95 @@
+
+mod git;
+mod github;
+pub(crate) mod plan;
+mod secrets;
+#[cfg(test)]
</file context>
| let url = url.trim(); | ||
| let path = if let Some(rest) = strip_prefix_ignore_case(url, "https://") { | ||
| // A login (`x-access-token:...@`) never goes further. | ||
| let (authority, path) = rest.split_once('/')?; |
There was a problem hiding this comment.
P2: The https authority ends only at the first /, so ? or # before the host can hide the real host. https://evil.example?@github.com/acme/widget.git is accepted as GitHub, while the transport would contact evil.example. End the authority at the first /, ? or #.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed/github.rs, line 30:
<comment>The https authority ends only at the first `/`, so `?` or `#` before the host can hide the real host. `https://evil.example?@github.com/acme/widget.git` is accepted as GitHub, while the transport would contact evil.example. End the authority at the first `/`, `?` or `#`.</comment>
<file context>
@@ -0,0 +1,138 @@
+ let url = url.trim();
+ let path = if let Some(rest) = strip_prefix_ignore_case(url, "https://") {
+ // A login (`x-access-token:...@`) never goes further.
+ let (authority, path) = rest.split_once('/')?;
+ let host = authority.rsplit_once('@').map_or(authority, |(_, host)| host);
+ if !host.eq_ignore_ascii_case("github.com") {
</file context>
| let (authority, path) = rest.split_once('/')?; | |
| let end = rest.find(|c| matches!(c, '/' | '?' | '#')).unwrap_or(rest.len()); | |
| let (authority, path) = (&rest[..end], rest[end..].strip_prefix('/')?); |
| // A login (`x-access-token:...@`) never goes further. | ||
| let (authority, path) = rest.split_once('/')?; | ||
| let host = authority.rsplit_once('@').map_or(authority, |(_, host)| host); | ||
| if !host.eq_ignore_ascii_case("github.com") { |
There was a problem hiding this comment.
P2: An HTTPS remote with explicit default port :443 is rejected because host includes the port. Strip default :443 before comparing with github.com.
(Based on your team's feedback about normalizing default HTTPS ports and ignoring SSH transport ports.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed/github.rs, line 32:
<comment>An HTTPS remote with explicit default port `:443` is rejected because `host` includes the port. Strip default `:443` before comparing with `github.com`.
(Based on your team's feedback about normalizing default HTTPS ports and ignoring SSH transport ports.) </comment>
<file context>
@@ -0,0 +1,138 @@
+ // A login (`x-access-token:...@`) never goes further.
+ let (authority, path) = rest.split_once('/')?;
+ let host = authority.rsplit_once('@').map_or(authority, |(_, host)| host);
+ if !host.eq_ignore_ascii_case("github.com") {
+ return None;
+ }
</file context>
| if !host.eq_ignore_ascii_case("github.com") { | |
| if !host.strip_suffix(":443").unwrap_or(host).eq_ignore_ascii_case("github.com") { |
|
|
||
| mod git; | ||
| mod github; | ||
| pub(crate) mod plan; |
There was a problem hiding this comment.
P2: An unchanged delta plan leaves a previous file at out_bundle while reporting no bundle, so a caller that reuses the pathname can send stale data. Reject an existing output for unchanged plans or otherwise make the no-bundle result unambiguous to the caller.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed.rs, line 13:
<comment>An unchanged delta plan leaves a previous file at `out_bundle` while reporting no bundle, so a caller that reuses the pathname can send stale data. Reject an existing output for unchanged plans or otherwise make the no-bundle result unambiguous to the caller.</comment>
<file context>
@@ -0,0 +1,95 @@
+
+mod git;
+mod github;
+pub(crate) mod plan;
+mod secrets;
+#[cfg(test)]
</file context>
| "--local" => local = Some(PathBuf::from(value)), | ||
| "--bundle" => bundle = Some(PathBuf::from(value)), | ||
| "--base" => base = Some(value.clone()), |
There was a problem hiding this comment.
P2: parse silently accepts duplicate --local, --bundle, and --base flags, so a repeated option changes the selected value instead of reporting bad usage. Reject a flag when its option has already been set.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed.rs, line 66:
<comment>`parse` silently accepts duplicate `--local`, `--bundle`, and `--base` flags, so a repeated option changes the selected value instead of reporting bad usage. Reject a flag when its option has already been set.</comment>
<file context>
@@ -0,0 +1,95 @@
+ while let Some(flag) = rest.next() {
+ let value = rest.next()?;
+ match flag.as_str() {
+ "--local" => local = Some(PathBuf::from(value)),
+ "--bundle" => bundle = Some(PathBuf::from(value)),
+ "--base" => base = Some(value.clone()),
</file context>
| "--local" => local = Some(PathBuf::from(value)), | |
| "--bundle" => bundle = Some(PathBuf::from(value)), | |
| "--base" => base = Some(value.clone()), | |
| "--local" => { | |
| if local.replace(PathBuf::from(value)).is_some() { | |
| return None; | |
| } | |
| } | |
| "--bundle" => { | |
| if bundle.replace(PathBuf::from(value)).is_some() { | |
| return None; | |
| } | |
| } | |
| "--base" => { | |
| if base.replace(value.clone()).is_some() { | |
| return None; | |
| } | |
| } |
| if matches!(args.first().map(String::as_str), None | Some("-h" | "--help" | "help")) { | ||
| println!("{USAGE}"); | ||
| return if args.is_empty() { 64 } else { 0 }; | ||
| } |
There was a problem hiding this comment.
P3: With no arguments, this prints the usage text to stdout and exits 64, while every other bad-usage path in this file writes it to stderr. Print usage to stderr for the empty case so stdout stays JSON-only, matching access_cli::run.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed.rs, line 25:
<comment>With no arguments, this prints the usage text to stdout and exits 64, while every other bad-usage path in this file writes it to stderr. Print usage to stderr for the empty case so stdout stays JSON-only, matching `access_cli::run`.</comment>
<file context>
@@ -0,0 +1,95 @@
+const USAGE: &str = "usage: cmux link seed plan --local FOLDER --bundle OUT [--base SHA]";
+
+pub(super) fn run(args: &[String]) -> i32 {
+ if matches!(args.first().map(String::as_str), None | Some("-h" | "--help" | "help")) {
+ println!("{USAGE}");
+ return if args.is_empty() { 64 } else { 0 };
</file context>
| if matches!(args.first().map(String::as_str), None | Some("-h" | "--help" | "help")) { | |
| println!("{USAGE}"); | |
| return if args.is_empty() { 64 } else { 0 }; | |
| } | |
| if matches!(args.first().map(String::as_str), Some("-h" | "--help" | "help")) { | |
| println!("{USAGE}"); | |
| return 0; | |
| } | |
| if args.is_empty() { | |
| eprintln!("{USAGE}"); | |
| return 64; | |
| } |
| /// or None when it goes. | ||
| pub(super) fn reason(top: &Path, relative: &str) -> Option<&'static str> { | ||
| let name = relative.rsplit('/').next().unwrap_or(relative); | ||
| if name.starts_with(".env") { |
There was a problem hiding this comment.
P3: The .env check compares the raw name while the key checks use the lowercased name. A file such as .ENV is not excluded, although macOS's default case-insensitive filesystem treats it as the same env file. Compare lowercased names throughout.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At cmux-tui/crates/cmux-tui/src/link/seed/secrets.rs, line 10:
<comment>The `.env` check compares the raw name while the key checks use the lowercased name. A file such as `.ENV` is not excluded, although macOS's default case-insensitive filesystem treats it as the same env file. Compare lowercased names throughout.</comment>
<file context>
@@ -0,0 +1,78 @@
+/// or None when it goes.
+pub(super) fn reason(top: &Path, relative: &str) -> Option<&'static str> {
+ let name = relative.rsplit('/').next().unwrap_or(relative);
+ if name.starts_with(".env") {
+ return Some(".env file: it may hold secrets, so it is shared only on purpose");
+ }
</file context>
| if name.starts_with(".env") { | |
| if name.to_ascii_lowercase().starts_with(".env") { |
|
Review (codex, router-review): SHIP at 10db91d Reviewer: codex gpt-6-astra high, fast tier. Follow-ups: 2. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward. SHIP at 10db91d Visual verdict: no images; Leo tries the build
Follow-ups:
|
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Shipped in:
|
|
This PR added a new panic-ratchet hit on feat-cmux-next. #30439 added |
…x, cmux-host, cmux-tui, cmux-tui-core) (#30478) * base red: panic ratchet, #30361's new JSON index reads use get The Rust panic ratchet is red on feat-cmux-next since 72dc85d (#30361, Cloud repository discovery), tip watcher job 63b147f0938fdb56741e7c83 at 4cd800a: clippy::indexing_slicing over the baseline in four crates (acpmux 694 > 686, cmux-host 1 > 0, cmux-tui 836 > 831, cmux-tui-core 880 > 853). The other commits in 4b15acd..4cd800a add no index or slice in production code. This rewrites every serde_json index read #30361 added in production code (cloud_repo_mcp.rs, github_repos.rs, cli/cloud_repo.rs, cloud_repos/ engine.rs, local.rs, runtime.rs, store.rs) as get / pointer reads, the one IndexMut as get_mut, and the `command[..2]` slice in cli.rs as `command.get(..2)`. Behavior is the same: a missing key read through get compares and converts the same as the Null that indexing returned (`v["k"] == "x"` is `as_str() == Some("x")`, `v["ok"] != true` is `as_bool() != Some(true)`, `v["ref"] != json!(r)` keeps Null for a missing key). The baseline is not raised. * base red: panic ratchet, #30439's and #30464's new index and string slice use checked forms After the merge of feat-cmux-next 1dfc4dd, the panic ratchet at 9d4a172 (job 1764d0cc925c9f12951dc4c3) is still red on two rows that later base commits added: - acpmux indexing_slicing 689 > 686: #30439 (229803e) added `summary["turn"].is_object()` in hub/views.rs and two `self.running[at]` in subagents.rs. - cmux-tui string_slice 83 > 82: #30464 (1b7b01c) added `&text[prefix.len()..]` in link/seed/github.rs. Now `summary.get("turn").is_some_and(Value::is_object)`, `self.running.get_mut(at)` (`at` is a position in the same Vec, found just above with no change between, so the slot is always there), and `text.split_at_checked(prefix.len())` (None exactly where the old `text.get(..prefix.len())` was None). No behavior change.
|
Found 9 test failures on Blacksmith runners: Failures
|
…lice use checked forms After the merge of feat-cmux-next 1dfc4dd, the panic ratchet at 9d4a172 (job 1764d0cc925c9f12951dc4c3) is still red on two rows that later base commits added: - acpmux indexing_slicing 689 > 686: #30439 (229803e) added `summary["turn"].is_object()` in hub/views.rs and two `self.running[at]` in subagents.rs. - cmux-tui string_slice 83 > 82: #30464 (1b7b01c) added `&text[prefix.len()..]` in link/seed/github.rs. Now `summary.get("turn").is_some_and(Value::is_object)`, `self.running.get_mut(at)` (`at` is a position in the same Vec, found just above with no change between, so the slot is always there), and `text.split_at_checked(prefix.len())` (None exactly where the old `text.get(..prefix.len())` was None). No behavior change.
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Piece 2 of the Cloud project seed: the Mac side that decides whether a folder can be seeded from its GitHub remote, and builds the one git bundle of what the remote lacks.
Leo:
Design: leo-workspace
design/cloud.md, "Getting the project onto the machine". Todaycmux link start-chat copyuploads every file plus all of.git. With this, a git folder with a GitHub remote sends only HEAD's unpushed commits, the staged tree, the working tree and the stash. The machine side (git-seed-v1) is built in parallel, and wiring start-chat to it is a later piece. Nothing calls this yet except the new CLI entry.What it does
link::seed::plan::plan(folder, out_bundle) -> Result<Planned, Fallback>andplan_with_base(folder, out_bundle, base):refs/cmux-seed/head,refs/cmux-seed/index(staged tree, parent HEAD),refs/cmux-seed/work(working tree with untracked files that are not ignored, less secrets; parent the index commit), andrefs/cmux-seed/stashwhen there is a stash. The one prerequisite is^BASE. The index and work commits use a fixed author, committer and date, so the same tree gives the same sha.git status --ignored, every ref and the stash list before and after.origin, else the only remote. GitHub URLs (https://github.com/O/N(.git),git@github.com:O/N(.git),ssh://git@github.com/O/N(.git)) are normalized tohttps://github.com/O/N.git. A login in the URL is dropped, and it is redacted from messages.refs/remotes/R/HEAD,main,master).baseslists every candidate, ending with the default branch's tip, so the caller can re-plan throughplan_with_basewhen the machine answersseed.base_missing..env*, SSH private keys (id_rsa*,id_ed25519*, ...;.pubgoes),*.pem,*.p12,*.pfx,*.key,.netrc, and.npmrc/.pypircfiles that hold a token stay on the Mac. They come back inskippedwith a reason in plain words. Untracked nested git repositories (not submodules) are skipped and named too. Tracked files are never dropped.head_ref: null. Linked worktrees, alternates and shallow clones work while the base is local. Sparse checkouts send their patterns, and untracked files outside the patterns are still carried (add --sparse).lfsis true when a.gitattributesnamesfilter=lfs. Submodules are listed with their checked-out commits.core.fsmonitor=false,safe.bareRepository=explicit,core.hooksPath=/dev/null, no auto gc, no signing,GIT_CONFIG_NOSYSTEM=1,GIT_OPTIONAL_LOCKS=0,GIT_NO_LAZY_FETCH=1and stdin null. CallerGIT_DIR/GIT_INDEX_FILE/object-store variables are dropped.Delta seed (for a mid-turn live handoff: seed, move begin, a second seed of only what changed, finish):
plan_since(folder, out_bundle, &PreviousSeed{head, index, work})builds the same refs, with prerequisites^previous.work ^previous.head. Their trees (and the previous index's) are excluded too: the new commits are siblings of the previous ones, so without thatgit bundleresent every unchanged file. A 2 MB untracked file went again in full until this was added. It setsparams.baseandparams.sincetoprevious.work(sinceis null on a full seed). When the new work sha equals the previous one, nothing changed: the work commit's parents chain to the index commit and HEAD, and the fixed identity and date make the sha exact. It then answersunchanged: truewith no bundle written. Previous commits this repository no longer has (after a gc) giveno_previous. So that a later delta can build on them, the seed's objects go into the repository's object store unreferenced, asgit stash createdoes. No ref, index or file changes.Fallback codes (the folder takes the streamed copy):
not_git,not_top,no_commit,no_remote,ambiguous_remote,not_github,no_base,unmerged,submodule_local_work,no_previous,git_failed.CLI:
cmux link seed plan --local FOLDER --bundle OUT [--base SHA]prints one JSON line:{"ok":true,"params":{...},"skipped":[...],"submodules":[...],"bases":[...],"bundle_bytes":N,"unchanged":false}and exits 0, or prints{"ok":false,"code":...,"why":...}and exits 1.One contract note for the machine side: git leaves a seed ref out of the bundle when its commit is already behind a prerequisite. That covers
refs/cmux-seed/headwhen HEAD is the base (nothing unpushed), and in a delta seed an unchanged HEAD or index. The params always name every commit, so the machine reads shas from the params, not refs from the bundle.Follow-ups
submodule_local_work) for now. The design seeds it with one bundle per submodule. Nested submodules are not walked.--sinceflag yet; the handoff lane callsplan_sincein-process.unmerged).Tests
These use real fixture repositories in temp dirs, with a bare
file://repository standing in for GitHub through aurl.*.insteadOfrewrite. In the key test, a clone of the remote at the base plays the machine: it verifies the bundle, fetchesrefs/cmux-seed/*, checks out WORK and compares every file with the original working copy, less ignored files and secrets. The INDEX diff must equalgit diff --cached. A delta test runs a full seed, an unchanged re-plan, then an edit plus a new untracked file. The delta bundle is under 1/20 of the full one and applies on top of the first. Other tests cover the same tree giving the same sha, detached HEAD, stash, linked worktree, sparse, LFS, shallow, a missing base, default-tip candidates, submodule local work, unmerged, and each fallback code. All 21 pass on the fleet throughcmux-ci test -- cargo test -p cmux-tui link::seed.cargo clippy -p cmux-tui --all-targets -- -D warningsis clean.Size: about 1,600 lines, of which about 560 are tests. It is one module (plan, its git wrapper, the URL parser and the secret rules), and its pieces only make sense together, so it is not split.
Agent session: agent session
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Adds the Mac-side plan for
git-seed-v1(design/cloud.md) that decides whether a GitHub working copy can be seeded and builds a single bundle of what the remote lacks — unpushed commits, staged tree, working tree (minus ignored files and likely secrets), and the stash — without modifying the repository. Exposed viacmux link seed plan, not yet wired intostart-chat.What the plan does
refs/cmux-seed/{head,index,work,stash}with^BASEas prerequisite, using a scratch object store with alternates and a copied index; the person's index, refs, and files never change.plan_with_baseretries after aseed.base_missingreply.plan_sinceproduces delta seeds that send only changes since an earlier seed, with^previous.work ^previous.headprerequisites and the previous trees excluded..env*, SSH keys,*.pem/*.p12/*.pfx/*.key,.netrc, registry files with tokens, and nested git repositories; reports them inskipped.not_git,no_remote,not_github,no_base,unmerged,submodule_local_work, with plain-words reasons.Contract note
Written for commit 10db91d. Summary will update on new commits.