Skip to content

fix(bin): spawn crewmates for projects with no origin remote - #2693

Open
mikkel-kaj wants to merge 4 commits into
kunchenguid:mainfrom
mikkel-kaj:fm/fix-spawn-noremote
Open

fix(bin): spawn crewmates for projects with no origin remote#2693
mikkel-kaj wants to merge 4 commits into
kunchenguid:mainfrom
mikkel-kaj:fm/fix-spawn-noremote

Conversation

@mikkel-kaj

Copy link
Copy Markdown

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_base in bin/fm-spawn.sh now branches on whether origin is 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.
  • Added spawn_local_default_branch, which resolves the remoteless local default via the shared default_branch convention (local main/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.
  • Extended tests/fm-spawn-pool-base-freshen.test.sh with a remoteless fixture and four cases (refresh to local tip for main and trunk defaults, off-default wip primary, dirty-pool refusal, unresolvable local default refusal), and updated the --help freshen description plus docs/architecture.md to 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 adjacent fm-spawn-worktree-settle suite, 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 with could 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 --help freshen 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 base

Remoteless (local-only, no git remote) spawn - real bin/fm-spawn.sh driven end to end
with a real pooled git worktree and a fake tmux/treehouse so no terminal is created.
Same fixture shape in both runs: local main advanced after the pool worktree was handed out.

=== 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
Please make sure you have the correct access rights
and the repository exists.
error: could not fetch origin for pooled worktree '/tmp/repro-before/pool'; refusing to launch from a potentially stale base
exit status: 1

$ git -C pool log --oneline -1 HEAD    # base the scout actually launches from
bb0f96b initial
pool HEAD (bb0f96be203e99e58f024d48dcf18c0a88b7d9ef) != local main tip (42d7e5618814eb638a19c89c8b423374c55bdca1)  -> NOT refreshed
$ ls pool/newest.txt
ls: cannot access '/tmp/repro-before/pool/newest.txt': No such file or directory

=== AFTER THE FIX (target commit c8cca8f) ===
$ git -C project remote -v      # local-only project, no remote configured
$ git -C project log --oneline -1 main
23fae49 advance-local-main
$ git -C pool log --oneline -1 HEAD    # pooled worktree still on the stale base
500a8fa initial

$ 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

$ git -C pool log --oneline -1 HEAD    # base the scout actually launches from
23fae49 advance-local-main
pool HEAD == local main tip (23fae49dda737f3fbfb1c87afb676297cc1419c8)  -> launched from the freshest local base
$ ls pool/newest.txt
/tmp/repro-after/pool/newest.txt

=== AFTER THE FIX - SHIP spawn (target commit c8cca8f) ===
$ git -C project remote -v      # local-only project, no remote configured
$ git -C project log --oneline -1 main
9ba2404 advance-local-main
$ git -C pool log --oneline -1 HEAD    # pooled worktree still on the stale base
dad2e91 initial

$ fm-spawn.sh ship-remoteless-demo-a1 <project> --mode no-mistakes --yolo off
warning: /tmp/repro-after-ship/home/data/ship-remoteless-demo-a1/brief.md records no delivery contract line (scaffolded before ship briefs recorded one); launching on the explicit --mode no-mistakes - confirm its definition of done matches
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

$ git -C pool log --oneline -1 HEAD    # base the scout actually launches from
9ba2404 advance-local-main
pool HEAD == local main tip (9ba2404cacde4e122f0097c4a237f74c3aa544b1)  -> launched from the freshest local base
$ ls pool/newest.txt
/tmp/repro-after-ship/pool/newest.txt
Evidence: 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=0

Regression proof: the four new remoteless tests run against the PRE-FIX script (1cb900c).
Pre-existing remote-backed tests still pass there; the first remoteless test goes red.

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
not ok - a remoteless local-only project should still spawn (default=main): expected exit 0, got 1
exit=1

Remote-backed behavior unchanged: the BASE-COMMIT version of the test file
(six pre-existing origin-backed cases) run against the FIXED script (c8cca8f).

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
# all fm-spawn-pool-base-freshen tests passed
exit=0
Evidence: 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 passed

# 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.yNozdl/current-base/pool
# observed base: HEAD=e95ba98e79cad5bd243a2ae5cbb32cd630d39a44 origin/main=e95ba98e79cad5bd243a2ae5cbb32cd630d39a44 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.yNozdl/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.yNozdl/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.yNozdl/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.yNozdl/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.yNozdl/unreachable-origin/pool'; refusing to launch from a potentially stale base
ok - an unreachable origin refuses a potentially stale pooled worktree
# observed remoteless main spawn: spawned pool-remoteless-main-r6 harness=codex kind=scout window=firstmate:fm-pool-remoteless-main-r6 worktree=/tmp/fm-spawn-pool-base-freshen.yNozdl/remoteless-main/pool
# observed remoteless base: HEAD=8b10958efa3dbd063b666a4f5c73df5d602949ca local main=8b10958efa3dbd063b666a4f5c73df5d602949ca
# observed remoteless trunk spawn: spawned pool-remoteless-trunk-r6 harness=codex kind=scout window=firstmate:fm-pool-remoteless-trunk-r6 worktree=/tmp/fm-spawn-pool-base-freshen.yNozdl/remoteless-trunk/pool
# observed remoteless base: HEAD=28f35badad0887dc0f55b7e57d9d94cf4373b826 local trunk=28f35badad0887dc0f55b7e57d9d94cf4373b826
ok - a remoteless pooled worktree refreshes to the local default branch tip before launch
# observed off-default remoteless base: HEAD=6dc0ab711347e8418ab1dff9a423a02c54f20d35 main=6dc0ab711347e8418ab1dff9a423a02c54f20d35 wip=62a2cd6c44fe08430c0ca0c9a8cb77f2654040cf
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 passed
Evidence: 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.

  see AGENTS.md task lifecycle); --secondmate records kind=secondmate and launches in a
  provisioned firstmate home; the default is kind=ship.
  Before a secondmate launch, the home is locally fast-forwarded to the primary
  default-branch commit when safe; skipped syncs warn and launch unchanged.
  Ship/scout spawns refuse to launch unless the resolved task path is a real
  git worktree root distinct from the primary project checkout.
  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.
Batch dispatch: pass one or more `id=repo` pairs instead of a single <id> <project>, e.g.
Evidence: Manual repro script used for the end-to-end demonstration

Source: Manual repro script used for the end-to-end demonstration

#!/usr/bin/env bash
# Manual end-user repro: spawn a scout for a REGISTERED LOCAL-ONLY project that
# has no git remote at all, using the real bin/fm-spawn.sh, a real treehouse-style
# pooled worktree, and a fake tmux/treehouse so no terminal is actually created.
set -u
SPAWN=$1          # path to the fm-spawn.sh under test
ROOTDIR=$2        # scratch dir for this run
LABEL=$3

rm -rf "$ROOTDIR"; mkdir -p "$ROOTDIR"
home="$ROOTDIR/home"; project="$ROOTDIR/project"; pool="$ROOTDIR/pool"; fake="$ROOTDIR/fakebin"
id='ship-remoteless-demo-a1'

mkdir -p "$fake"
cat > "$fake/tmux" <<'SH'
#!/usr/bin/env bash
set -u
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' > "$fake/treehouse"
chmod +x "$fake/tmux" "$fake/treehouse"

mkdir -p "$home/data/$id" "$home/projects" "$home/state" "$home/config"
printf 'codex\n' > "$home/config/crew-harness"
printf 'brief for %s\n' "$id" > "$home/data/$id/brief.md"
touch "$home/state/.last-watcher-beat"

# local-only project: git init, NO remote ever added
git init --quiet -b main "$project"
printf 'base\n' > "$project/README.md"
git -C "$project" add README.md
git -C "$project" -c user.name=Captain -c user.email=c@example.invalid commit -qm initial
base=$(git -C "$project" rev-parse HEAD)
git -C "$project" worktree add --quiet --detach "$pool" "$base"   # treehouse-style pooled worktree
# local main advances after the pool worktree was handed out (the stale-base shape)
printf 'newest local work\n' > "$project/newest.txt"
git -C "$project" add newest.txt
git -C "$project" -c user.name=Captain -c user.email=c@example.invalid commit -qm advance-local-main
tip=$(git -C "$project" rev-parse refs/heads/main)

echo "=== $LABEL ==="
echo "\$ git -C project remote -v      # local-only project, no remote configured"
git -C "$project" remote -v
echo "\$ git -C project log --oneline -1 main"
git -C "$project" log --oneline -1 main
echo "\$ git -C pool log --oneline -1 HEAD    # pooled worktree still on the stale base"
git -C "$pool" log --oneline -1 HEAD
echo
echo "\$ fm-spawn.sh $id <project> ${SPAWN_ARGS:---scout}"
FM_ROOT_OVERRIDE='' FM_HOME="$home" \
  FM_STATE_OVERRIDE="$home/state" FM_DATA_OVERRIDE="$home/data" \
  FM_PROJECTS_OVERRIDE="$home/projects" FM_CONFIG_OVERRIDE="$home/config" \
  FM_GATE_REFUSE_BYPASS=1 FM_SPAWN_NO_GUARD=1 TMUX="fake,1,0" FM_FAKE_PANE_PATH="$pool" \
  PATH="$fake:$PATH" \
  "$SPAWN" "$id" "$project" ${SPAWN_ARGS:---scout} 2>&1 | tail -n 3
rc=${PIPESTATUS[0]}
echo "exit status: $rc"
echo
echo "\$ git -C pool log --oneline -1 HEAD    # base the scout actually launches from"
git -C "$pool" log --oneline -1 HEAD
if [ "$(git -C "$pool" rev-parse HEAD)" = "$tip" ]; then
  echo "pool HEAD == local main tip ($tip)  -> launched from the freshest local base"
else
  echo "pool HEAD ($(git -C "$pool" rev-parse HEAD)) != local main tip ($tip)  -> NOT refreshed"
fi
echo "\$ ls pool/newest.txt"
ls "$pool/newest.txt" 2>&1
echo

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • ℹ️ 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.sh driving the real bin/fm-spawn.sh against a no-remote project with a stale pooled git worktree - pre-fix (1cb900c) fails with could not fetch origin ... exit 1; post-fix (c8cca8f) --scout spawn succeeds and pool HEAD == local main tip
  • Same manual repro with SPAWN_ARGS=&#39;--mode no-mistakes --yolo off&#39; - ship spawn also launches and bases on the local main tip
  • Regression 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 1
  • No-regression proof: base-commit version of tests/fm-spawn-pool-base-freshen.test.sh run against the fixed script - all 6 origin-backed cases (unreachable origin, unresolved remote default, dirty refusal, direct-PR/scout refresh, non-main default) still pass
  • bash bin/fm-spawn.sh --help - freshen section renders the new remoteless behavior description
  • bash tests/fm-spawn-worktree-settle.test.sh - adjacent spawn-path check, passes
✅ **Document** - passed

✅ No issues found.

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

✅ No issues found.

Mikkel Andersen added 4 commits August 20, 2026 20:54
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.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 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".

Comment thread bin/fm-spawn.sh
echo "error: could not fetch '$target' for pooled worktree '$worktree'; refusing to launch from a potentially stale base" >&2
return 1
fi
else

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 Badge 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 👍 / 👎.

Comment thread bin/fm-spawn.sh
Comment on lines +1753 to +1755
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

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 Badge 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 👍 / 👎.

@kunchenguid

Copy link
Copy Markdown
Owner

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.

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