Skip to content

fix(bin): launch remote-less projects from their local default branch - #1

Merged
LeonidShamis merged 5 commits into
mainfrom
fm/firstmate-spawn-no-origin
Sep 5, 2026
Merged

LeonidShamis merged 5 commits into
mainfrom
fm/firstmate-spawn-no-origin

Conversation

@LeonidShamis

@LeonidShamis LeonidShamis commented Sep 5, 2026 •

Copy link
Copy Markdown
Owner

Intent

Fix: bin/fm-spawn.sh refuses to launch a crewmate or scout for a local-only project whose git repository has no remote.

Observed on a freshly git init-ed project registered [local-only] in data/projects.md (firstmate's project-management skill explicitly allows 'a local-only project may have no remote'):

fatal: 'origin' does not appear to be a git repository
error: could not fetch origin for pooled worktree ''; refusing to launch from a potentially stale base

Cause: freshen_spawn_worktree_base() unconditionally ran git fetch origin, git remote set-head origin --auto, resolved origin/, and reset --hard'd the pooled worktree to it. The treehouse pool worktree is a linked worktree of the project checkout under projects/ (shared common git dir), so for a remote-less repo the correct base is the checkout's own default branch, not a remote-tracking ref.

Required behavior:

  1. A repository with NO REMOTES AT ALL (git remote prints nothing) freshens against the local default branch: resolve it, require the worktree to be clean exactly as today, reset --hard to refs/heads/, and verify HEAD matches that commit. Keep every existing refusal (dirty worktree, stale submodule pin, unresolvable default branch, mismatch after reset) with wording that names the local branch rather than origin.
  2. When any remote exists, behavior must be byte-for-byte unchanged, including all current error messages. A repository with remotes but none named origin keeps today's refusal.
  3. Do not weaken the 'refusing to launch from a potentially stale base' guarantee: a remote-less repo whose local default branch cannot be resolved must still refuse.
  4. Update the fm-spawn.sh header comment where it describes the pooled-base freshen (fm-spawn.sh's usage() derives --help from that header block, so the header edit covers both).
  5. Regression tests extend tests/fm-spawn-pool-base-freshen.test.sh, which drives the real spawn path with a fake terminal.

Explicitly excluded: do NOT add a self-pointing 'origin' remote as the fix; that is the temporary operator workaround this task replaces.

Two review decisions were escalated to the repository owner during an earlier validation run and accepted; both are already implemented and must not be reverted:

  • The local-base path is gated on the repository having NO remotes at all, not merely no remote named origin, so any remote-backed repository still refuses rather than launching from a base that could be stale against its real remote.
  • local_default_branch() resolves the default branch through the same default_branch() helper every other sync path in bin/fm-ff-lib.sh uses (one owner), then the configured init.defaultBranch when that local branch exists. The branch the project checkout happens to have out is NEVER taken on its own: firstmate's own tangle guard exists because that checkout can be stranded on a feature branch, and primary_head_commit() deliberately reads the default-branch ref for the same reason. A name nothing recognises as the default is unresolvable and refuses.

The final commit on this branch fixes a test-hermeticity finding raised by the previous run's review: local_default_branch() reads init.defaultBranch, which falls through to the host's global and system git config, and tests/lib.sh does not isolate it. test_unresolved_local_default_refuses_pool used a 'trunk' fixture and expected the base to be unresolvable, so it only refused because no host config named 'trunk'. Every spawn in that test file now runs with GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM pointed at /dev/null; each case already states the configuration it needs in the project repository itself. Verified both directions under a global init.defaultBranch of 'trunk': the case fails before the change and the whole file passes after it.

This repository is firstmate itself, so its own coding guidelines apply: one sentence per line in tracked Markdown, plain dashes never em dashes, bin/*.sh must pass bin/fm-lint.sh (pinned shellcheck 0.11.0 + actionlint 1.7.12), tests colocated in tests/ named .test.sh following tests/lib.sh conventions, extend the existing test file rather than adding a new one, tests must exercise behavior through the executable and never assert on implementation source bytes, each script header is the single owner of that script's contract, keep the change minimal and focused on this defect with no refactoring of surrounding code, and never add an agent name as a commit co-author.

Context for the lint step: the previous run's lint gate failed only because shellcheck and actionlint were absent from the pipeline daemon's PATH, not because of any code issue. Both pinned binaries (shellcheck 0.11.0, actionlint 1.7.12) are now installed on that PATH, and bin/fm-lint.sh plus bin/fm-doc-audience-check.sh and the full tests/fm-spawn-pool-base-freshen.test.sh suite were run green on this exact head before starting this run.

What Changed

  • bin/fm-spawn.sh now checks whether the pooled worktree's repository has any remotes before freshening its base: with no remotes at all it skips the origin fetch and resets to refs/heads/<default>, resolved by a new local_default_branch() helper that uses the shared default_branch() lookup and then falls back to the configured init.defaultBranch when that local branch exists; the currently checked-out branch is never taken on its own, an unresolvable local default refuses with a message naming the local branch, and any repository with remotes keeps the existing origin-based path and error messages unchanged.
  • The fm-spawn.sh header block (which also feeds --help) and docs/architecture.md describe the remote-less base rule, including the refusal when no default branch resolves.
  • tests/fm-spawn-pool-base-freshen.test.sh adds a local-only fixture and seven cases covering the remote-less refresh, dirty-worktree refusal, unresolvable default, default-over-feature-branch selection, configured-default fallback, unrecognised checkout branch refusal, and non-origin remotes still refusing; every spawn in the file now runs with GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM pointed at /dev/null so host git config cannot influence results.

Risk Assessment

✅ Low: The change is a narrow, well-gated branch in one function that reuses the shared default-branch resolver, leaves the remote-backed path and all its messages untouched, honours every recorded owner decision, and is covered by executable regression tests that reproduce the reported failure and the stranded-checkout refusals.

Testing

Ran the targeted tests/fm-spawn-pool-base-freshen.test.sh suite on the target head (all 18 cases pass), reproduced the original origin-fetch refusal by running the same new tests and a manual fm-spawn.sh transcript against the base commit, confirmed the target spawns a remote-less local-only project from the advanced local default tip, and proved the final hermeticity commit by showing the previous head fails and the target passes under a host global init.defaultBranch of trunk; --help reflects the new header contract and the remote-backed path is unchanged.

Evidence: Manual before/after CLI transcript: remote-less local-only spawn on base vs target

Source: Manual before/after CLI transcript: remote-less local-only spawn on base vs target

=== BEFORE (base c03bfbe) $ git -C project remote # prints nothing: no remote at all $ fm-spawn.sh local-only-before project --mode local-only --yolo off fatal: 'origin' does not appear to be a git repository error: could not fetch origin for pooled worktree '.../BEFORE/pool'; refusing to launch from a potentially stale base exit=1 $ git -C pool rev-parse HEAD -> b3d3284 (still the stale initial commit) === AFTER (target ff8d919) $ fm-spawn.sh local-only-after project --mode local-only --yolo off spawned local-only-after harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-local-only-after worktree=.../AFTER/pool exit=0 $ git -C pool rev-parse HEAD -> b4ea0b3 (== refs/heads/main tip, advance-main) $ ls pool -> README.md newer.txt

=== BEFORE: /tmp/fm-base/bin/fm-spawn.sh
$ git -C project remote            # (prints nothing: no remote at all)
$ git -C project log --oneline main
8d30737 advance-main
b3d3284 initial
$ git -C pool rev-parse HEAD        # pool slot before spawn (stale base)
b3d32840b0a69f4d86a4156f7bbccb364e22f86f
$ fm-spawn.sh local-only-before project --mode local-only --yolo off
warning: /tmp/fm-manual-remote-less.uRe8xG/BEFORE/home/data/local-only-before/brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode local-only - confirm its definition of done matches
notice: local-only-before ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
fatal: 'origin' does not appear to be a git repository
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
error: could not fetch origin for pooled worktree '/tmp/fm-manual-remote-less.uRe8xG/BEFORE/pool'; refusing to launch from a potentially stale base
exit=1
$ git -C pool rev-parse HEAD        # pool slot after spawn
b3d32840b0a69f4d86a4156f7bbccb364e22f86f
$ git -C pool branch --show-current
$ ls pool
README.md

=== AFTER: <target-tree>/bin/fm-spawn.sh
$ git -C project remote            # (prints nothing: no remote at all)
$ git -C project log --oneline main
b4ea0b3 advance-main
a43e94b initial
$ git -C pool rev-parse HEAD        # pool slot before spawn (stale base)
a43e94b0f02379b08ff3090f7cf6a51dd44bcfd4
$ fm-spawn.sh local-only-after project --mode local-only --yolo off
warning: /tmp/fm-manual-remote-less.uRe8xG/AFTER/home/data/local-only-after/brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode local-only - confirm its definition of done matches
notice: local-only-after ships mode=local-only while the standing posture for project is no-mistakes - less rigor than the captain's standing posture; proceed only on a current explicit captain instruction or an intake judgment you can state
spawned local-only-after harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-local-only-after worktree=/tmp/fm-manual-remote-less.uRe8xG/AFTER/pool
exit=0
$ git -C pool rev-parse HEAD        # pool slot after spawn
b4ea0b384a8e308478c1e5c5e99036e4d5979907
$ git -C pool branch --show-current
$ ls pool
README.md
newer.txt
Evidence: Script that produces the manual transcript

Source: Script that produces the manual transcript

#!/usr/bin/env bash
# Manual reproduction of the reported defect: a freshly `git init`-ed project
# registered local-only, with NO remote, spawning a crewmate through the real
# bin/fm-spawn.sh with a fake tmux/treehouse. Runs the same fixture against the
# base commit tree (BEFORE) and the target tree (AFTER) and prints the transcript.
set -u
BEFORE_ROOT=${1:?base tree}; AFTER_ROOT=${2:?target tree}
WORK=$(mktemp -d /tmp/fm-manual-remote-less.XXXXXX)
trap 'rm -rf "$WORK"' EXIT

make_fixture() {  # <dir> <id>
  local d=$1 id=$2
  mkdir -p "$d/home/data/$id" "$d/home/projects" "$d/home/state" "$d/home/config" "$d/fake"
  printf 'codex\n' > "$d/home/config/crew-harness"
  printf 'brief for %s\n' "$id" > "$d/home/data/$id/brief.md"
  touch "$d/home/state/.last-watcher-beat"
  cat > "$d/fake/tmux" <<'SH'
#!/usr/bin/env bash
case "$*" in *"#{pane_current_path}"*) printf '%s\n' "$FM_FAKE_PANE_PATH"; exit 0;; esac
case "${1:-}" in display-message) printf 'firstmate\n'; exit 0;; esac
exit 0
SH
  printf '#!/usr/bin/env bash\nexit 0\n' > "$d/fake/treehouse"
  chmod +x "$d/fake/tmux" "$d/fake/treehouse"
  git init --quiet -b main "$d/project"
  printf 'base\n' > "$d/project/README.md"
  git -C "$d/project" add README.md
  git -C "$d/project" -c user.name=t -c user.email=t@example.invalid commit -qm initial
  git -C "$d/project" worktree add --quiet --detach "$d/pool" HEAD
  printf 'advanced after pool allocation\n' > "$d/project/newer.txt"
  git -C "$d/project" add newer.txt
  git -C "$d/project" -c user.name=t -c user.email=t@example.invalid commit -qm advance-main
}

run_case() {  # <label> <root> <id>
  local label=$1 root=$2 id=$3 d="$WORK/$1" out status
  make_fixture "$d" "$id"
  echo "=== $label: $root/bin/fm-spawn.sh"
  echo "\$ git -C project remote            # (prints nothing: no remote at all)"
  git -C "$d/project" remote
  echo "\$ git -C project log --oneline main"
  git -C "$d/project" log --oneline main
  echo "\$ git -C pool rev-parse HEAD        # pool slot before spawn (stale base)"
  git -C "$d/pool" rev-parse HEAD
  echo "\$ fm-spawn.sh $id project --mode local-only --yolo off"
  out=$(FM_GATE_REFUSE_BYPASS=1 GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
    FM_ROOT_OVERRIDE='' FM_HOME="$d/home" \
    FM_STATE_OVERRIDE="$d/home/state" FM_DATA_OVERRIDE="$d/home/data" \
    FM_PROJECTS_OVERRIDE="$d/home/projects" FM_CONFIG_OVERRIDE="$d/home/config" \
    FM_SPAWN_NO_GUARD=1 TMUX="fake,1,0" FM_FAKE_PANE_PATH="$d/pool" \
    PATH="$d/fake:$PATH" \
    "$root/bin/fm-spawn.sh" "$id" "$d/project" --mode local-only --yolo off 2>&1)
  status=$?
  printf '%s\n' "$out"
  echo "exit=$status"
  echo "\$ git -C pool rev-parse HEAD        # pool slot after spawn"
  git -C "$d/pool" rev-parse HEAD
  echo "\$ git -C pool branch --show-current"
  git -C "$d/pool" branch --show-current
  echo "\$ ls pool"
  ls "$d/pool"
  echo
}

run_case BEFORE "$BEFORE_ROOT" local-only-before
run_case AFTER "$AFTER_ROOT" local-only-after
Evidence: Targeted suite on target head with observed spawn/refusal lines

Source: Targeted suite on target head with observed spawn/refusal lines

# observed spawn: spawned pool-current-base-r1 harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-pool-current-base-r1 worktree=/tmp/fm-spawn-pool-base-freshen.264wC6/current-base/pool
# observed base: HEAD=a9bd22784ddb6d7459637cf39d482079067cb305 origin/main=a9bd22784ddb6d7459637cf39d482079067cb305 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=/tmp/fm-spawn-pool-base-freshen.264wC6/direct-pr/pool
# observed scout spawn: spawned pool-scout-r3 harness=codex kind=scout window=firstmate:fm-pool-scout-r3 worktree=/tmp/fm-spawn-pool-base-freshen.264wC6/scout/pool
ok - direct-PR ships and scouts both refresh stale pooled worktrees before launch
# observed dirty refusal: error: pooled worktree '/tmp/fm-spawn-pool-base-freshen.264wC6/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 '/tmp/fm-spawn-pool-base-freshen.264wC6/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 '/tmp/fm-spawn-pool-base-freshen.264wC6/unreachable-origin/pool'; refusing to launch from a potentially stale base
ok - an unreachable origin refuses a potentially stale pooled worktree
# observed remote-less spawn: spawned pool-local-only-base-r12 harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-pool-local-only-base-r12 worktree=/tmp/fm-spawn-pool-base-freshen.264wC6/local-only-base/pool
# observed base: HEAD=4cb19592c186b7ba48bb02bdd2122e6db0fd39e9 refs/heads/trunk=4cb19592c186b7ba48bb02bdd2122e6db0fd39e9
ok - a remote-less project refreshes its pooled worktree to the local default branch tip
ok - a dirty pooled worktree in a remote-less project is still refused without discarding its work
# observed unresolvable-local-default refusal: error: could not determine the local default branch for pooled worktree '/tmp/fm-spawn-pool-base-freshen.264wC6/local-only-unresolved/pool', which has no remote; refusing to launch from a potentially stale base
ok - an unresolvable local default branch refuses a remote-less pooled worktree
# observed feature-checkout spawn: spawned pool-local-only-feature-checkout-r15 harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-pool-local-only-feature-checkout-r15 worktree=/tmp/fm-spawn-pool-base-freshen.264wC6/local-only-feature-checkout/pool
# observed base: HEAD=5b353daa3c16ea36ec3e64fb858e30954d92ba90 refs/heads/main=5b353daa3c16ea36ec3e64fb858e30954d92ba90 feature=4a9cbd660e5cfffb61d0204d26ec488f76ff337f
ok - a remote-less project launches from the default branch even when the checkout is on a feature branch
# observed non-origin-remote refusal: error: could not fetch origin for pooled worktree '/tmp/fm-spawn-pool-base-freshen.264wC6/upstream-only/pool'; refusing to launch from a potentially stale base
ok - a repository whose only remote is not origin is still refused without fetching
# observed unrecognised-checkout-branch refusal: error: could not determine the local default branch for pooled worktree '/tmp/fm-spawn-pool-base-freshen.264wC6/local-only-unrecognised-head/pool', which has no remote; refusing to launch from a potentially stale base
ok - a remote-less checkout on a branch nothing recognises as the default is refused, not promoted
# observed configured-default spawn: spawned pool-local-only-configured-default-r18 harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-pool-local-only-configured-default-r18 worktree=/tmp/fm-spawn-pool-base-freshen.264wC6/local-only-configured-default/pool
# observed base: HEAD=7db68be4d686c1fc2a04657f4c27dc22e0ad0c3f refs/heads/trunk=7db68be4d686c1fc2a04657f4c27dc22e0ad0c3f feature=06dcdd4963065750af7dfe30f1be85b5a1a725ed
ok - a remote-less project launches from its configured default branch, not the checked-out feature branch
# observed stale-pin refusal: error: submodule 'ui' is checked out at d0a7d6f79282b02f2af3e0d13c3de546a2b7f8bf, but this base records 3558123dd3538596473d34c75113d67cbb2a5ac3
ok - two consecutive spawns across a moved submodule pin end in a refusal naming both pins and no remedy
ok - an unpushed submodule commit keeps the uncommitted-work refusal and survives it
ok - work inside a submodule is still refused as uncommitted work, not called stale
ok - a stale pin carrying real work is refused conservatively, never called stale
ok - a stale pin beside other dirt yields the conservative refusal alone, with no stale-pin line
# all fm-spawn-pool-base-freshen tests passed
Evidence: Regression reproduction: new tests against base commit fail at the remote-less case

Source: Regression reproduction: new tests against base commit fail at the remote-less case

## new test file run against base commit c03bfbe (before fix)
# observed spawn: spawned pool-current-base-r1 harness=codex kind=ship mode=no-mistakes yolo=off window=firstmate:fm-pool-current-base-r1 worktree=/tmp/fm-spawn-pool-base-freshen.upKjfw/current-base/pool
# observed base: HEAD=9809b70cf79f04fdf1b0b13d0e887137a3e91c10 origin/main=9809b70cf79f04fdf1b0b13d0e887137a3e91c10 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=/tmp/fm-spawn-pool-base-freshen.upKjfw/direct-pr/pool
# observed scout spawn: spawned pool-scout-r3 harness=codex kind=scout window=firstmate:fm-pool-scout-r3 worktree=/tmp/fm-spawn-pool-base-freshen.upKjfw/scout/pool
ok - direct-PR ships and scouts both refresh stale pooled worktrees before launch
# observed dirty refusal: error: pooled worktree '/tmp/fm-spawn-pool-base-freshen.upKjfw/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 '/tmp/fm-spawn-pool-base-freshen.upKjfw/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 '/tmp/fm-spawn-pool-base-freshen.upKjfw/unreachable-origin/pool'; refusing to launch from a potentially stale base
ok - an unreachable origin refuses a potentially stale pooled worktree
not ok - spawn should refresh a stale pooled worktree in a repository with no origin: expected exit 0, got 1
exit=1
Evidence: Hermeticity: previous head fails under global init.defaultBranch=trunk

Source: Hermeticity: previous head fails under global init.defaultBranch=trunk

## previous head 4744cd3 test file, host global init.defaultBranch=trunk (expect r14 to fail)
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
ok - direct-PR ships and scouts both refresh stale pooled worktrees before launch
ok - a dirty pooled worktree is refused without discarding its local work
ok - an unresolved remote default branch refuses the pooled worktree
ok - an unreachable origin refuses a potentially stale pooled worktree
ok - a remote-less project refreshes its pooled worktree to the local default branch tip
ok - a dirty pooled worktree in a remote-less project is still refused without discarding its work
not ok - spawn succeeded despite an unresolvable local default branch
exit=1
Evidence: Hermeticity: target head passes under global init.defaultBranch=trunk

Source: Hermeticity: target head passes under global init.defaultBranch=trunk

## target head ff8d919 test file, host global init.defaultBranch=trunk (expect all pass)
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
ok - direct-PR ships and scouts both refresh stale pooled worktrees before launch
ok - a dirty pooled worktree is refused without discarding its local work
ok - an unresolved remote default branch refuses the pooled worktree
ok - an unreachable origin refuses a potentially stale pooled worktree
ok - a remote-less project refreshes its pooled worktree to the local default branch tip
ok - a dirty pooled worktree in a remote-less project is still refused without discarding its work
ok - an unresolvable local default branch refuses a remote-less pooled worktree
ok - a remote-less project launches from the default branch even when the checkout is on a feature branch
ok - a repository whose only remote is not origin is still refused without fetching
ok - a remote-less checkout on a branch nothing recognises as the default is refused, not promoted
ok - a remote-less project launches from its configured default branch, not the checked-out feature branch
ok - two consecutive spawns across a moved submodule pin end in a refusal naming both pins and no remedy
ok - an unpushed submodule commit keeps the uncommitted-work refusal and survives it
ok - work inside a submodule is still refused as uncommitted work, not called stale
ok - a stale pin carrying real work is refused conservatively, never called stale
ok - a stale pin beside other dirt yields the conservative refusal alone, with no stale-pin line
# all fm-spawn-pool-base-freshen tests passed
exit=0

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-spawn-pool-base-freshen.test.sh with FM_TEST_EVIDENCE=1 on target head ff8d919: all 18 cases pass, including the 7 new remote-less, non-origin-remote, feature-checkout, and configured-default cases
  • Regression reproduction: same new test file run against a git archive of base commit c03bfbe fails at test_remote_less_pool_refreshes_to_local_default_tip (exit 1) while all pre-existing origin cases still pass there
  • Manual end-user transcript (manual-remote-less-spawn.sh): fresh git init project with no remote, pool slot allocated then local main advanced, fm-spawn.sh &lt;id&gt; &lt;project&gt; --mode local-only --yolo off driven with fake tmux/treehouse; base tree prints the reported fatal: &#39;origin&#39; does not appear to be a git repository / could not fetch origin ... refusing to launch from a potentially stale base and leaves the pool stale, target tree prints spawned ... mode=local-only and the pool HEAD equals the advanced local main tip with the new file present
  • Hermeticity check: previous head 4744cd3 test file run with GIT_CONFIG_GLOBAL naming init.defaultBranch = trunk fails at test_unresolved_local_default_refuses_pool; target head test file under the same global config passes all 18 cases
  • bin/fm-spawn.sh --help output contains the updated pooled-base freshen paragraph describing the no-remote rule, the init.defaultBranch fallback, the never-take-checkout-HEAD rule, and the non-origin-remote refusal
  • Reviewed freshen_spawn_worktree_base() remote branch against base: the remote-backed code path and all its error strings are textually identical to before, and the local path's refusals name refs/heads/&lt;default&gt; or 'the local default branch' rather than origin
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

LeonidBallers and others added 5 commits September 5, 2026 16:32
A local-only project is allowed to have no remote, but the pooled-worktree
base refresh unconditionally fetched origin, so every spawn into a freshly
`git init`-ed project refused with "could not fetch origin ... refusing to
launch from a potentially stale base".

When the slot's repository has no origin remote, resolve the base from the
shared checkout instead: the common git dir's HEAD names the branch that
checkout is on, falling back to the local main or master heads rather than
assuming a name, and the reset targets refs/heads/<default>. Every refusal
still stands - dirty worktree, stale submodule pin, unresolvable default
branch, and mismatch after reset - now naming the local branch. Behavior with
an origin remote, including all of its messages, is unchanged.
local_default_branch() reads init.defaultBranch, which falls through to the
host's global and system git config. test_unresolved_local_default_refuses_pool
uses a 'trunk' fixture and expects the base to be unresolvable, so it only
refused because no host config happened to name 'trunk'; on a machine whose
global init.defaultBranch is 'trunk' the spawn resolved a base and the case
failed for an environmental reason.

Run every spawn in this file with GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM
pointed at /dev/null. Each case already states the configuration it needs in
the project repository itself, so nothing depends on the host any more.
Verified both directions with a global init.defaultBranch of 'trunk': the case
fails before this change and the whole file passes after it.
@LeonidShamis LeonidShamis reopened this Sep 5, 2026
@LeonidShamis
LeonidShamis merged commit d2ccd7b into main Sep 5, 2026
25 of 27 checks passed
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