fix(bin): spawn crewmates for projects with no origin remote - #2693
fix(bin): spawn crewmates for projects with no origin remote#2693mikkel-kaj wants to merge 4 commits into
Conversation
A registered local-only project may have no git remote, but fm-spawn's pooled-worktree freshen unconditionally fetched origin, so every ship and scout spawn for such a project refused to launch. When no origin remote is configured, skip the fetch and reset the clean pooled worktree to the primary checkout's local default branch tip - the shared object store makes that tip the freshest base. Origin-backed behavior is unchanged: an unreachable origin, unresolved remote default, or dirty worktree still refuses the spawn, and a remoteless repo whose local default branch cannot be resolved refuses loudly too.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8e3a9808c9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| echo "error: could not fetch '$target' for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2 | ||
| return 1 | ||
| fi | ||
| else |
There was a problem hiding this comment.
Restrict remoteless ship spawns to local-only mode
Captain, when origin is absent, this branch accepts every ship delivery mode, including direct-PR and no-mistakes (the new test even exercises no-mistakes). Those workers launch successfully but cannot push or open the PR their selected path requires, whereas the previous fetch failure stopped them before work began; permit this path only for scouts or MODE=local-only, and refuse other ship modes.
AGENTS.md reference: AGENTS.md:L315-L317
Useful? React with 👍 / 👎.
| ref=$(git --git-dir="$common" symbolic-ref --quiet --short HEAD 2>/dev/null) || return 1 | ||
| [ -n "$ref" ] || return 1 | ||
| git -C "$worktree" show-ref --verify --quiet "refs/heads/$ref" || return 1 |
There was a problem hiding this comment.
Reject custom local defaults until landing can resolve them
For a remoteless repository whose only default branch is something like trunk, this fallback makes spawning succeed by reading the primary checkout's HEAD, but both bin/fm-review-diff.sh and bin/fm-merge-local.sh resolve only origin/HEAD, main, or master. The resulting local-only task therefore cannot be reviewed or landed through the required guarded helper; either teach those helpers the same custom-default resolution or refuse this spawn instead.
AGENTS.md reference: AGENTS.md:L327-L327
Useful? React with 👍 / 👎.
|
Speaking as Kun's firstmate: I reviewed the full diff (spawn freshen gate, remoteless local-default resolution, colocated tests) and approved fork CI. Waiting on green checks including no-mistakes. |
Intent
Fix a bug in firstmate's bin/fm-spawn.sh: crewmate/scout spawns are impossible for a registered local-only project that has NO git remote, even though the rest of firstmate treats local-only-no-remote as a first-class supported shape (fm-teardown and fm-fleet-sync both handle remoteless repos deliberately). Reproduced live 2026-08-20: a spawn for a remoteless project acquired a treehouse pooled worktree, then failed in freshen_spawn_worktree_base, which unconditionally runs git fetch origin, git remote set-head origin --auto, and resets to origin/; with no origin configured, the spawn fails for every ship AND scout task of such a project. Expected behavior: when the task worktree's repository has NO origin remote configured, skip the origin fetch/set-head/remote-default resolution entirely; still require a clean worktree (unchanged refusal on dirty state); reset the clean pooled worktree to the tip of the repository's LOCAL default branch, preserving the never-launch-from-a-stale-base intent - the shared object store means the local branch tip IS the freshest base; fail loudly and refuse the spawn if the local default branch cannot be resolved. Captain decision (2026-08-20, review-gate escalation): resolve the remoteless local default via the fleet convention first - default_branch (local main/master) - and fall back to the primary checkout's HEAD symref ONLY when neither main nor master resolves; this deliberately keeps an unconventional default (e.g. trunk) spawnable via the fallback while preventing a primary parked on a wip feature branch from propagating that branch into the task base (pinned by test); downstream trunk refusals in fm-review-diff/fm-merge-local/fm-teardown are pre-existing and deliberately out of this task's scope (spawn-only scope intended). When origin IS configured, behavior must be byte-for-byte unchanged: an unreachable origin, unresolved remote default branch, or dirty worktree still refuses the spawn; do NOT weaken remote-backed guarantees; 'no origin configured' (git remote get-url origin fails because origin is absent, not because the network is down) is the only new branch. Acceptance criteria: (1) a scout or ship spawn for a remoteless local-only project proceeds past the freshen step and launches, proven with the repro shape in a test; (2) remote-backed projects keep exact current behavior, including all existing refusals; (3) colocated regression tests in tests/ cover remoteless refresh-to-local-tip, remoteless dirty refusal, origin-configured-but-unreachable refusal, the off-default wip-primary case, and the unresolvable-local-default refusal, following the existing fm-spawn test patterns; (4) bin/fm-spawn.sh --help freshen description updated to state the remoteless behavior; (5) shellcheck-clean per repo conventions and the full relevant test files pass. Keep the fix minimal and boring: one clearly-owned branch in freshen_spawn_worktree_base, no new flags, no config knob. This changes firstmate's shared tracked material, so follow .agents/skills/firstmate-coding-guidelines/SKILL.md (one-owner rule, colocated tests, bin/fm-lint.sh as the lint owner, plain dash, no agent co-author). Delivery: push the branch to the configured fork (mikkel-kaj/firstmate) and open the PR against upstream kunchenguid/firstmate main - the captain has no push rights on upstream.
What Changed
freshen_spawn_worktree_baseinbin/fm-spawn.shnow branches on whetheroriginis configured: with an origin it keeps the existing fetch /remote set-head --auto/ reset-to-origin/<default>path byte-for-byte, and without one it skips the fetch entirely and resets the clean pooled worktree to the local default branch tip (the shared object store makes that tip the freshest base). The clean-worktree requirement and all existing refusals - unreachable origin, unresolved remote default, dirty worktree - are unchanged.spawn_local_default_branch, which resolves the remoteless local default via the shareddefault_branchconvention (localmain/master) and falls back to the primary checkout's HEAD symref only when neither exists, so a primary parked on a feature branch cannot propagate that branch into the task base; an unresolvable local default refuses the spawn with a distinct error.tests/fm-spawn-pool-base-freshen.test.shwith a remoteless fixture and four cases (refresh to local tip formainandtrunkdefaults, off-default wip primary, dirty-pool refusal, unresolvable local default refusal), and updated the--helpfreshen description plusdocs/architecture.mdto state the remote-backed and remoteless behaviors separately.Risk Assessment
✅ Low: The change adds one clearly-owned branch behind an origin-absence probe that leaves every remote-backed command sequence and refusal untouched, reuses the repo's existing default_branch and --git-common-dir conventions, and is pinned by four behavioral tests that drive the real spawn path; the only notes are a stale test header and an explicitly accepted trunk-repo fallback tradeoff.
Testing
Ran the colocated suite
tests/fm-spawn-pool-base-freshen.test.sh(all 10 cases green, including the 4 new remoteless ones) plus the adjacentfm-spawn-worktree-settlesuite, then proved the user intent at the product level: a hand-built local-only project with no git remote and a stale treehouse-style pooled worktree fails the spawn on the base commit withcould not fetch origin ... refusing to launch from a potentially stale base, and on the target commit both a scout and a ship spawn launch with the pool reset exactly to the local default branch tip (newest local commit present in the task worktree). Also confirmed red-before-green (new tests fail against the pre-fix script) and that remote-backed behavior is untouched by running the base commit's own version of the test file against the fixed script, where all six origin-backed refusals and refreshes still pass. CLI transcripts of the before/after runs, the test output, the regression proof, and the updated--helpfreshen section are saved as evidence; no linters were run per the phase rules, and the worktree is clean with all temp fixtures removed.Evidence: Remoteless spawn CLI transcript: before vs after the fix (scout + ship)
Source: Remoteless spawn CLI transcript: before vs after the fix (scout + ship)
=== BEFORE THE FIX (base commit 1cb900c) === $ git -C project remote -v # local-only project, no remote configured $ git -C project log --oneline -1 main 42d7e56 advance-local-main $ git -C pool log --oneline -1 HEAD # pooled worktree still on the stale base bb0f96b initial $ fm-spawn.sh ship-remoteless-demo-a1 <project> --scout error: could not fetch origin for pooled worktree '/tmp/repro-before/pool'; refusing to launch from a potentially stale base exit status: 1 pool HEAD (bb0f96b) != local main tip (42d7e56) -> NOT refreshed === AFTER THE FIX (target commit c8cca8f) === $ fm-spawn.sh ship-remoteless-demo-a1 <project> --scout spawned ship-remoteless-demo-a1 harness=codex kind=scout window=firstmate:fm-ship-remoteless-demo-a1 worktree=/tmp/repro-after/pool exit status: 0 pool HEAD == local main tip (23fae49) -> launched from the freshest local base $ ls pool/newest.txt /tmp/repro-after/pool/newest.txt === AFTER THE FIX - SHIP spawn === $ fm-spawn.sh ship-remoteless-demo-a1 <project> --mode no-mistakes --yolo off spawned ship-remoteless-demo-a1 harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-ship-remoteless-demo-a1 worktree=/tmp/repro-after-ship/pool exit status: 0 pool HEAD == local main tip (9ba2404) -> launched from the freshest local baseEvidence: Regression proof: new tests red pre-fix, remote-backed tests green against fixed script
Source: Regression proof: new tests red pre-fix, remote-backed tests green against fixed script
New remoteless tests vs PRE-FIX script (1cb900c): ok - a stale pooled worktree refreshes to current origin/main before a crew branch is created ok - an unreachable origin refuses a potentially stale pooled worktree not ok - a remoteless local-only project should still spawn (default=main): expected exit 0, got 1 exit=1 BASE-COMMIT test file (6 origin-backed cases) vs FIXED script (c8cca8f): # all fm-spawn-pool-base-freshen tests passed exit=0Evidence: Targeted suite output with observed spawn/base evidence lines
Source: Targeted suite output with observed spawn/base evidence lines
# observed remoteless main spawn: spawned pool-remoteless-main-r6 harness=codex kind=scout ... # observed remoteless base: HEAD=8b10958 local main=8b10958 # observed remoteless trunk spawn: spawned pool-remoteless-trunk-r6 ... # observed off-default remoteless base: HEAD=6dc0ab7 main=6dc0ab7 wip=62a2cd6 ok - a remoteless pooled worktree refreshes to the local default branch tip before launch ok - a remoteless primary parked on a feature branch still bases the task on the local default branch ok - a dirty remoteless pooled worktree is refused without discarding its local work ok - a remoteless pool with no resolvable local default branch refuses the spawn # all fm-spawn-pool-base-freshen tests passedEvidence: fm-spawn.sh --help freshen section (rendered user-facing help)
Source: fm-spawn.sh --help freshen section (rendered user-facing help)
Before a fresh ship or scout worker starts, its clean task worktree fetches origin, resolves the current remote default branch, and resets to its tip. A repository with no origin remote configured (a supported local-only project shape) skips the fetch and instead resets the clean worktree to the tip of the local default branch - local main or master, else the branch the primary checkout is on - which the shared object store keeps as the freshest base. An unreachable origin, an unresolved remote or local default branch, or a non-clean worktree refuses the spawn rather than risking a PR based on stale history.Evidence: Manual repro script used for the end-to-end demonstration
Source: Manual repro script used for the end-to-end demonstration
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
tests/fm-spawn-pool-base-freshen.test.sh:4- The test file's own header still describes coverage as only "starts the worker from the fetched origin/main tip or stops when origin is unreachable", but the file now also owns four remoteless cases (local-default refresh, off-default primary, dirty refusal, unresolvable local default). docs/architecture.md:161 names this file as the owner of the portable regression coverage for the base-freshness boundary, so its header is the contract a reader consults; leaving it origin-only understates what the suite pins. Extend lines 4-8 to name the remoteless local-default-tip path.bin/fm-spawn.sh:1751- The wip-propagation guard only holds for repos whose trunk is main or master. Concrete trace: remoteless repo with default branch 'trunk' and no main/master, primary checked out on a feature branch 'wip'. default_branch returns 1, so spawn_local_default_branch falls back to the common dir's HEAD symref and yields 'wip'; the pooled worktree is then reset to the wip tip and the task bases on the feature branch - the exact outcome test_remoteless_off_default_primary_bases_on_local_default forbids for a main-based repo. This is the direct and stated consequence of the captain's decision ("fall back to the primary checkout's HEAD symref ONLY when neither main nor master resolves"), which trades this gap for keeping unconventional-trunk repos spawnable, so it is noted for visibility rather than as a defect to fix in this change.✅ **Test** - passed
✅ No issues found.
FM_TEST_EVIDENCE=1 bash tests/fm-spawn-pool-base-freshen.test.sh- all 10 cases pass (6 pre-existing origin-backed + 4 new remoteless: local-tip refresh for main and trunk, off-default wip primary, dirty refusal, unresolvable local default refusal)Manual end-to-end repro/home/mikke/.no-mistakes/evidence/01M0GAHMZQMNCCMED1Z483GG36/repro-remoteless-spawn.shdriving the realbin/fm-spawn.shagainst a no-remote project with a stale pooled git worktree - pre-fix (1cb900c) fails withcould not fetch origin ...exit 1; post-fix (c8cca8f)--scoutspawn succeeds and pool HEAD == local main tipSame manual repro withSPAWN_ARGS='--mode no-mistakes --yolo off'- ship spawn also launches and bases on the local main tipRegression proof: new test file run against a pre-fix checkout (git archive 1cb900c) -not ok - a remoteless local-only project should still spawn (default=main): expected exit 0, got 1No-regression proof: base-commit version oftests/fm-spawn-pool-base-freshen.test.shrun against the fixed script - all 6 origin-backed cases (unreachable origin, unresolved remote default, dirty refusal, direct-PR/scout refresh, non-main default) still passbash bin/fm-spawn.sh --help- freshen section renders the new remoteless behavior descriptionbash tests/fm-spawn-worktree-settle.test.sh- adjacent spawn-path check, passes✅ **Document** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.