Skip to content

fix(bin): let spawns proceed on projects with no origin remote - #2887

Open
matixacik-cell wants to merge 6 commits into
kunchenguid:mainfrom
matixacik-cell:fm/spawn-no-remote-fix-v2
Open

fix(bin): let spawns proceed on projects with no origin remote#2887
matixacik-cell wants to merge 6 commits into
kunchenguid:mainfrom
matixacik-cell:fm/spawn-no-remote-fix-v2

Conversation

@matixacik-cell

Copy link
Copy Markdown

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_base in bin/fm-spawn.sh now 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.
  • The no-origin skip path keeps its own safety bar: it refuses a pooled worktree with uncommitted work (via a new shared spawn_worktree_is_clean helper 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.
  • Added four tests to tests/fm-spawn-pool-base-freshen.test.sh covering the no-origin spawn notice, the configured-but-unreachable refusal, and the dirty-worktree and committed-leftover refusals; docs/architecture.md documents 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=0

===== BEFORE fix (base 197afbb): spawn on a project with no origin remote =====
and the repository exists.
error: could not fetch origin for pooled worktree '/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//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 '/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//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=/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-noorigin-manual.HPcGPZ/pool
exit=0
Evidence: Full freshen test run with observed spawn/refusal messages (FM_TEST_EVIDENCE=1)
# observed spawn: spawned pool-current-base-r1 harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-pool-current-base-r1 worktree=/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/current-base/pool
# observed base: HEAD=f1aa7c6d11082e33987f99d3ddb4ab6143d3b935 origin/main=f1aa7c6d11082e33987f99d3ddb4ab6143d3b935 advanced-main=must survive a newly spawned branch
ok - a stale pooled worktree refreshes to current origin/main before a crew branch is created
ok - a stale pooled worktree resolves and refreshes a non-main default branch
# observed direct-pr spawn: spawned pool-direct-pr-r3 harness=codex kind=ship mode=direct-PR yolo=off window=firstmate:fm-pool-direct-pr-r3 worktree=/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/direct-pr/pool
# observed scout spawn: spawned pool-scout-r3 harness=codex kind=scout window=firstmate:fm-pool-scout-r3 worktree=/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/scout/pool
ok - direct-PR ships and scouts both refresh stale pooled worktrees before launch
# observed dirty refusal: error: pooled worktree '/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/dirty-refusal/pool' is not clean; refusing to discard uncommitted work while refreshing its base; preserved=keep this local work
ok - a dirty pooled worktree is refused without discarding its local work
# observed unresolved-default refusal: error: could not resolve origin's current default branch for pooled worktree '/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/unresolved-default/pool'; refusing to launch from a potentially stale base
ok - an unresolved remote default branch refuses the pooled worktree
# observed unreachable-origin refusal: error: could not fetch origin for pooled worktree '/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/unreachable-origin/pool'; refusing to launch from a potentially stale base
ok - an unreachable origin refuses a potentially stale pooled worktree
# observed no-origin spawn: spawned pool-no-origin-r6 harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-pool-no-origin-r6 worktree=/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/no-origin/pool
# observed no-origin notice: notice: pooled worktree '/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/no-origin/pool' has no origin remote configured; skipping the base refresh because there is no upstream it can be stale against
ok - a project with no origin remote spawns and says the base refresh was skipped
# observed no-origin dirty refusal: error: pooled worktree '/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/no-origin-dirty/pool' has uncommitted work; refusing to start a new task on top of it; preserved=keep this local-only work
ok - a dirty pooled worktree on a project with no origin remote is still refused
# observed committed-leftover refusal: error: pooled worktree '/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/no-origin-committed/pool' is sitting on commit '0cb3069272a61746430d5e255dd911724a33722e', not the tip of its local default branch 'main' ('02c8f2f216bd539289eb9381e1663f8d6195ea78'); refusing to start a new task on top of commits that are not on 'main'. Inspect that worktree and decide what to do with those commits before it is reused
# observed preserved commit: HEAD=0cb3069272a61746430d5e255dd911724a33722e tip=02c8f2f216bd539289eb9381e1663f8d6195ea78
ok - a pooled worktree holding a previous task's committed work is refused, not reset
# observed unreachable-not-missing refusal: error: could not fetch origin for pooled worktree '/var/folders/sx/97t21t2d30xdp4yq61zj5mkm0000gn/T//fm-spawn-pool-base-freshen.zMUHhv/unreachable-not-missing/pool'; refusing to launch from a potentially stale base
ok - a configured origin that cannot be reached still refuses instead of being read as no remote
# all fm-spawn-pool-base-freshen tests passed

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ℹ️ 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 - The spawn_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 as freshen_spawn_worktree_base "$WT" || exit 1 (line 2334), which suppresses set -e inside the function. A future caller invoking it as a plain statement under the file's set -eu would 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 resetting
  • Ran the same test file against a git archive extraction 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 regression
  • Manual end-to-end before/after: built a local-only repo with a detached pooled worktree and invoked base vs target bin/fm-spawn.sh directly — base exits 1 with could not fetch origin, target exits 0 with the skip notice followed by spawned 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 pass
  • Constraint checks: git diff 197afbb..06a70ff shows 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
  • ⚠️ linter found issues (exit code 1)

🔧 Fix: lint clean after installing pinned linters; no code changes
1 warning still open:

  • ⚠️ linter found issues (exit code 1)
✅ **Push** - passed

✅ No issues found.

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.
@greptile-apps

greptile-apps Bot commented Aug 23, 2026

Copy link
Copy Markdown

Confidence Score: 4/5

The 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

Comment thread bin/fm-spawn.sh Outdated
…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.
Comment thread bin/fm-spawn.sh Outdated
…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.
Comment thread bin/fm-spawn.sh
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

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 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.”

@matixacik-cell

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants