fix(bin): let spawns proceed on projects with no origin remote - #2887
fix(bin): let spawns proceed on projects with no origin remote#2887matixacik-cell wants to merge 6 commits into
Conversation
fm-spawn's pooled-worktree base refresh unconditionally fetched origin and refused the spawn when that failed, so every fresh task worktree on a project that has no origin remote at all was refused with "could not fetch origin". Freshening asks whether a worktree's base is stale against its upstream. A repository with no origin configured has no upstream, so that question is meaningless rather than failed. Read git's local remote list first, which makes no network call, and skip the refresh with a loud notice when origin is not configured. A configured origin that cannot be reached, an unresolved default branch, a dirty worktree, and a stale base all still refuse exactly as before, so a network outage is never reclassified as "no remote". The check lives inside freshen_spawn_worktree_base so the freshening contract, including when it does not apply, stays with its one owner.
Confidence Score: 4/5The PR is not yet safe to merge because a no-origin project with a custom default branch becomes unspawnable as soon as it has another local branch. The new resolver recognizes a custom local default only when it is the repository’s sole branch, while the shared resolver handles only origin/HEAD, main, or master; consequently, valid multi-branch local-only projects are still rejected. Files Needing Attention: bin/fm-spawn.sh; tests/fm-spawn-pool-base-freshen.test.sh Reviews (3): Last reviewed commit: "fix(bin): stop a stranded primary's feat..." | Re-trigger Greptile |
…n spawn path With no origin remote, origin/HEAD can never exist, so the shared resolver's main/master fallback was the only local reading and a local-only project whose default branch has another name was refused as unverifiable. The no-origin path now falls back to the branch the repository's own HEAD names, read from the shared git dir with no network call, and a colocated test pins that a no-origin trunk-default project spawns with the skip notice.
…l default The no-origin fallback read the shared git dir's HEAD, which names whatever branch the primary checkout happens to have checked out. A primary stranded on a feature branch therefore blessed that feature tip as the local default, and a detached pool sitting on that committed feature work passed the leftover check and started a new task on top of it. The fallback now resolves a custom-named default only when the repository has exactly one local branch, the sole case where default is unambiguous without an upstream, and several branches with no main or master refuse loudly as an unverifiable base. A colocated test pins the stranded-primary scenario: the spawn refuses, names no skipped refresh, and leaves the feature commit untouched.
| fi | ||
| heads=$(git -C "$worktree" for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null) || return 1 | ||
| [ -n "$heads" ] || return 1 | ||
| case $heads in |
There was a problem hiding this comment.
Multiple branches block custom defaults
When a no-origin repository uses a custom default such as trunk and retains another local branch, this newline check rejects the branch list instead of resolving the actual default, causing an otherwise valid local-only spawn to exit with “could not determine the local default branch.”
|
Greptile findings disposition: the unresolved-custom-default finding is fixed by 0cb4b0e (a no-origin repository resolves main/master first, then its sole local branch), and the feature-branch-as-default finding is fixed by 357d6a9 (the primary checkout's HEAD is never consulted). The third finding (multiple local branches with a custom default refuse the spawn) is deliberate: with no upstream and several branches, the default is genuinely ambiguous, and guessing could bless a feature tip and defeat the committed-leftover refusal, so the spawn refuses loudly instead. That behaviour is pinned by test_stranded_primary_feature_branch_is_not_read_as_the_default in tests/fm-spawn-pool-base-freshen.test.sh. CI workflows have not run because this is a fork PR awaiting maintainer approval; bin/fm-lint.sh (pinned ShellCheck 0.11.0, actionlint 1.7.12) and the full spawn test file pass locally on this head. |
Intent
Fix bin/fm-spawn.sh's freshen_spawn_worktree_base so a spawn onto a project with no origin remote configured proceeds instead of being refused with 'could not fetch origin'. Projects that live only on this machine (registered local-only, no remote by design) must be spawnable into a fresh pooled worktree. A repository with no origin has no upstream to be stale against, so the base refresh is skipped with one clear notice, never silently. The distinction is drawn on whether a remote is CONFIGURED (git's local remote list, no network call), not on whether a command failed, so a configured origin that cannot be reached, an unresolvable default branch, and a stale base all still refuse loudly, and a network outage on an ordinary project is never reclassified as no-remote. The skip path still refuses a pooled worktree carrying uncommitted work, and also refuses one whose HEAD sits on committed leftovers off the local default branch tip, refusing rather than resetting because with no remote those commits may be the only copy of that work. Colocated tests in tests/fm-spawn-pool-base-freshen.test.sh cover both directions: a no-origin project spawns with the notice, a configured-but-unreachable origin still refuses, plus dirty-worktree and committed-leftover refusals that preserve the work. Constraints: repo style (one sentence per line, plain dashes, shellcheck-clean bin scripts, colocated tests, no agent co-author), do not weaken the isolation assertion in validate_spawn_worktree, do not touch anything under projects/, no unrelated behaviour changes or refactors.
What Changed
freshen_spawn_worktree_baseinbin/fm-spawn.shnow reads git's configured remote list (no network call) and, when no origin remote exists, skips the base refresh with a loud notice instead of failing with "could not fetch origin" — so local-only projects can spawn into pooled worktrees. A configured-but-unreachable origin still falls through to the strict path and refuses.spawn_worktree_is_cleanhelper also used by the refresh path), and refuses one whose HEAD sits on committed leftovers off the local default branch tip — naming the worktree, commit, and expected tip rather than resetting, since with no remote those commits may be the only copy of that work.tests/fm-spawn-pool-base-freshen.test.shcovering the no-origin spawn notice, the configured-but-unreachable refusal, and the dirty-worktree and committed-leftover refusals;docs/architecture.mddocuments the skip path and its refusals.Risk Assessment
✅ Low: The change is a well-bounded carve-out inside one function, satisfies every required intent constraint (configuration-based distinction, loud notice, dirty and committed-leftover refusals that preserve work, unreachable-origin still refusing), leaves the strict refresh path byte-for-byte in behavior, and ships four colocated regression tests covering both directions of the new boundary.
Testing
Ran the colocated freshen test suite (10/10 pass) plus four sibling fm-spawn suites, then demonstrated the fix end-to-end with a manual before/after spawn on a no-origin project: the base commit refuses with "could not fetch origin" while the target prints the skip notice and spawns successfully, and all refusal paths (unreachable origin, dirty pool, committed leftovers) were verified to still fire and preserve the work.
Evidence: Before/after spawn transcript on a no-origin project (base 197afbb vs target 06a70ff)
===== BEFORE fix (base 197afbb): spawn on a project with no origin remote ===== error: could not fetch origin for pooled worktree '.../fm-noorigin-manual.HPcGPZ/pool'; refusing to launch from a potentially stale base exit=1 ===== AFTER fix (target 06a70ff): same project, same spawn ===== notice: pooled worktree '.../fm-noorigin-manual.HPcGPZ/pool' has no origin remote configured; skipping the base refresh because there is no upstream it can be stale against spawned pool-no-origin-manual harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-pool-no-origin-manual worktree=.../fm-noorigin-manual.HPcGPZ/pool exit=0Evidence: Full freshen test run with observed spawn/refusal messages (FM_TEST_EVIDENCE=1)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
docs/architecture.md:168- docs/architecture.md describes the no-origin skip path's dirty-worktree refusal ("does not lower the clean-worktree bar") but omits the committed-leftover refusal added in commit 06a70ff, so the doc's summary of the skip path's refusals is incomplete relative to the header comment and tests. Adding one sentence noting that a HEAD off the local default branch tip also refuses would keep the doc consistent.bin/fm-spawn.sh:1772- Thespawn_worktree_is_clean+case $?idiom in freshen_spawn_worktree_base (bin/fm-spawn.sh:1772-1782 and :1820-1830) is only errexit-safe because the sole caller invokes the function asfreshen_spawn_worktree_base "$WT" || exit 1(line 2334), which suppressesset -einside the function. A future caller invoking it as a plain statement under the file'sset -euwould exit at the helper's non-zero return before the diagnostic message prints. No action needed today; noting the invariant.✅ **Test** - passed
✅ No issues found.
FM_TEST_EVIDENCE=1 bash tests/fm-spawn-pool-base-freshen.test.sh— all 10 tests pass on the target, including the four new ones: no-origin spawn with notice, configured-but-unreachable origin still refuses, dirty no-origin pool refuses preserving the file, committed-leftover pool refuses naming worktree/commit/tip/branch without resettingRan the same test file against agit archiveextraction of base commit 197afbb — the no-origin test fails there (not ok - spawn should launch on a project that has no origin remote: expected exit 0, got 1), proving the tests pin the regressionManual end-to-end before/after: built a local-only repo with a detached pooled worktree and invoked base vs targetbin/fm-spawn.shdirectly — base exits 1 withcould not fetch origin, target exits 0 with the skip notice followed byspawned pool-no-origin-manual ...Regression sweep of sibling suites:tests/fm-spawn-batch.test.sh,tests/fm-spawn-dispatch-profile.test.sh,tests/fm-spawn-worktree-settle.test.sh,tests/fm-trace-context-spawn.test.sh— all passConstraint checks:git diff 197afbb..06a70ffshows validate_spawn_worktree unmodified and no files under projects/;git log --format='%B'shows no co-author trailers✅ **Document** - passed
✅ No issues found.
⏭️ **Lint** - skipped
🔧 Fix: lint clean after installing pinned linters; no code changes
1 warning still open:
✅ **Push** - passed
✅ No issues found.