Skip to content

link: plan a git-seed-v1 seed of a GitHub working copy - #30464

Merged
teamleaderleo merged 1 commit into
feat-cmux-nextfrom
seed-plan
Nov 1, 2026
Merged

teamleaderleo merged 1 commit into
feat-cmux-nextfrom
seed-plan

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

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:

Your project folder is copied up.
we'll want to deal with the edge cases for this too

Design: leo-workspace design/cloud.md, "Getting the project onto the machine". Today cmux link start-chat copy uploads 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> and plan_with_base(folder, out_bundle, base):

  • Bundle refs: 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), and refs/cmux-seed/stash when 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.
  • Nothing in the working copy changes: the commits are built through a copy of the index. Every object goes to a scratch object store that borrows the repository's store through alternates, and the bundle is made from a scratch bare repository. No ref, object, index or file in the person's repository is written. A test compares git status --ignored, every ref and the stash list before and after.
  • Remote: the HEAD branch's upstream remote, else 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 to https://github.com/O/N.git. A login in the URL is dropped, and it is redacted from messages.
  • Base: the merge-base with the upstream, else with the remote's default branch (refs/remotes/R/HEAD, main, master). bases lists every candidate, ending with the default branch's tip, so the caller can re-plan through plan_with_base when the machine answers seed.base_missing.
  • Secrets: untracked .env*, SSH private keys (id_rsa*, id_ed25519*, ...; .pub goes), *.pem, *.p12, *.pfx, *.key, .netrc, and .npmrc/.pypirc files that hold a token stay on the Mac. They come back in skipped with a reason in plain words. Untracked nested git repositories (not submodules) are skipped and named too. Tracked files are never dropped.
  • Edge cases: big untracked files go in the bundle. Detached HEAD gives 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). lfs is true when a .gitattributes names filter=lfs. Submodules are listed with their checked-out commits.
  • Every git call runs with 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=1 and stdin null. Caller GIT_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 that git bundle resent every unchanged file. A 2 MB untracked file went again in full until this was added. It sets params.base and params.since to previous.work (since is 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 answers unchanged: true with no bundle written. Previous commits this repository no longer has (after a gc) give no_previous. So that a later delta can build on them, the seed's objects go into the repository's object store unreferenced, as git stash create does. 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/head when 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

  • A submodule with unpushed commits or dirty changes falls back (submodule_local_work) for now. The design seeds it with one bundle per submodule. Nested submodules are not walked.
  • LFS: list LFS objects only this Mac has, so they can be uploaded.
  • The CLI has no --since flag yet; the handoff lane calls plan_since in-process.
  • An in-progress merge with conflicts falls back (unmerged).

Tests

These use real fixture repositories in temp dirs, with a bare file:// repository standing in for GitHub through a url.*.insteadOf rewrite. In the key test, a clone of the remote at the base plays the machine: it verifies the bundle, fetches refs/cmux-seed/*, checks out WORK and compares every file with the original working copy, less ignored files and secrets. The INDEX diff must equal git 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 through cmux-ci test -- cargo test -p cmux-tui link::seed. cargo clippy -p cmux-tui --all-targets -- -D warnings is 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


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with 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 via cmux link seed plan, not yet wired into start-chat.

What the plan does

  • Builds refs/cmux-seed/{head,index,work,stash} with ^BASE as prerequisite, using a scratch object store with alternates and a copied index; the person's index, refs, and files never change.
  • Picks the base from the upstream merge-base, then the remote default branch candidates; plan_with_base retries after a seed.base_missing reply.
  • plan_since produces delta seeds that send only changes since an earlier seed, with ^previous.work ^previous.head prerequisites and the previous trees excluded.
  • Leaves out untracked .env*, SSH keys, *.pem/*.p12/*.pfx/*.key, .netrc, registry files with tokens, and nested git repositories; reports them in skipped.
  • Falls back to the streamed copy for cases like not_git, no_remote, not_github, no_base, unmerged, submodule_local_work, with plain-words reasons.

Contract note

  • Git omits a seed ref when its commit is already behind a prerequisite; the params always name every commit, so the machine reads shas from params, not from the bundle.
  • 21 integration tests cover the plan, delta seeds, fallbacks, sparse/LFS/shallow, and the unchanged-repo guarantee.

Written for commit 10db91d. Summary will update on new commits.

View guided diff Turn on auto-fix

`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
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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('/')?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>

Comment on lines +66 to +68
"--local" => local = Some(PathBuf::from(value)),
"--bundle" => bundle = Some(PathBuf::from(value)),
"--base" => base = Some(value.clone()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
"--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;
}
}

Comment on lines +25 to +28
if matches!(args.first().map(String::as_str), None | Some("-h" | "--help" | "help")) {
println!("{USAGE}");
return if args.is_empty() { 64 } else { 0 };
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
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") {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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>
Suggested change
if name.starts_with(".env") {
if name.to_ascii_lowercase().starts_with(".env") {

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

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

  1. [P1] [follow-up] The seed writes blobs, trees, and commits into the user’s object database. git add, write-tree, and commit-tree only replace the index; they never redirect GIT_OBJECT_DIRECTORY to the scratch store (plan.rs:463, plan.rs:573, plan.rs:588). Every run mutates .git/objects, potentially retaining large files and skipped secret blobs, contrary to the stated no-object-mutation guarantee.

  2. [P1] [follow-up] Delta planning can report unchanged while the stash changed. The check compares only previous.work to the new work commit (plan.rs:505); stash changes are explicitly ignored (plan.rs:207). The machine then receives no bundle and keeps a stale or missing stash.

Follow-ups:

  • Redirect seed object writes to the scratch store while preserving objects needed for later deltas.
  • Include stash identity in delta unchanged detection.

@teamleaderleo
teamleaderleo merged commit 1b7b01c into feat-cmux-next Nov 1, 2026
81 of 97 checks passed
@teamleaderleo
teamleaderleo deleted the seed-plan branch November 1, 2026 07:58
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 10db91d: 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=pr30464 -f ref=seed-plan -f platform=macos -f backend_mode=local

Shipped in:

@lawrencecchen

Copy link
Copy Markdown
Contributor

This PR added a new panic-ratchet hit on feat-cmux-next. #30439 added summary["turn"] in acpmux hub/views.rs and self.running[at] in subagents.rs, which made acpmux indexing_slicing 689 > 686. #30464 added &text[prefix.len()..] in link/seed/github.rs, which made cmux-tui string_slice 83 > 82. Job 1764d0cc925c9f12951dc4c3 shows both. #30478 fixes them forward with get/get_mut/split_at_checked and does not change behavior. Before you land, run cmux-ci run --class light --script scripts/ci/cmux-tui-rust-check.sh --arg panic --os linux --ref SHA.

lawrencecchen added a commit that referenced this pull request Nov 1, 2026
…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.
@blacksmith-sh

blacksmith-sh Bot commented Nov 1, 2026

Copy link
Copy Markdown

Found 9 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp6zfjqnrl/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmprokdk6sw/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
test_no_suite_runs_page_javascript_on_a_built_bundle (main.BundleInputs.test_no_sui
te_runs_page_javascript_on_a_built_bundle)/
test_no_suite_runs_page_javascript_on_a_built_bundle (main.BundleInputs.test_no_sui
te_runs_page_javascript_on_a_built_bundle)
View Logs
the committed export failed: check-daemon-capabilities-fresh: plans/cmux-next/
daemon-capabilities.json is stale:
View Logs
the committed export failed: check-daemon-capabilities-fresh: plans/cmux-next/
daemon-capabilities.json is stale:
View Logs

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

lawrencecchen added a commit that referenced this pull request Nov 1, 2026
…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.

This branch was previously deployed

3 inactive deployments
artifacts — 10db91d0 Deployed Nov 1, 2026 by teamleaderleo via add the Windows daemon to the tree #94723
sentry-dsyms — 10db91d0 Deployed Nov 1, 2026 by teamleaderleo via upload cmux-tui dSYMs to Sentry #94723
ci-routing-agent — 10db91d0 Deployed Nov 1, 2026 by teamleaderleo via cmux-tui tree preflight #94723
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants