fix(spawn): let a local-only task launch in a project with no origin - #11
Merged
Conversation
freshen_spawn_worktree_base fetched origin unconditionally, so every fresh ship or scout spawn in a project that intentionally has no remote refused with "could not fetch origin ...; refusing to launch from a potentially stale base". A configured origin was the masking condition: every project registered before this one had one, so the unconditional fetch always succeeded. A repository with no origin at all is a different fact from one whose origin is unreachable - nothing can go stale against a remote that does not exist. Decide that once, at the shared boundary, before the first git call that assumes origin: a local-only ship task resets to the repository's own current default branch, while every push mode and every scout still refuses, so a repository with no origin can never back a pushed branch or PR. An existing but unreachable origin, an unresolved remote default branch, a dirty worktree, and every other refusal are untouched, and the clean-tree check, reset, and verification stay shared by both paths.
…in architecture invariant
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Fix Firstmate spawning for a registered local-only project that intentionally has no origin remote.
Reported end-user problem, already observed through the real path: after a repository moved into a secondmate home and was registered local-only with no remote, every product worker spawn reached freshen_spawn_worktree_base in bin/fm-spawn.sh, ran 'git fetch --quiet origin' unconditionally, and refused with "could not fetch origin ...; refusing to launch from a potentially stale base". Planning-repository workers still spawned because that repository has an origin.
Required behavior the captain specified:
Diagnosis performed before coding, per the repo's diagnostic-reasoning procedure. Reproduced through the real bin/fm-spawn.sh with a fixture project that has no remote at all. Initiating trigger: freshen_spawn_worktree_base fetches origin unconditionally on every fresh ship or scout spawn. Masking condition: a configured origin - every previously registered project had one, so the unconditional fetch always succeeded, which is why the origin-backed planning repository still worked. Visible symptom: 'fatal: origin does not appear to be a git repository' followed by the stale-base refusal and exit 1. Earliest divergence from the proven origin-backed path is the existence of the remote, not its reachability. History: the function arrived whole in commit fc5f164, whose purpose was protecting a PR from a stale base - with no origin there is no PR and nothing to be stale against. Smallest counterfactual: the same fixture under --mode local-only now spawns and lands on the local default-branch tip. Disconfirming check deliberately retained as a test: an existing-but-unreachable origin under --mode local-only must still refuse, proving the change is not 'tolerate offline'.
Deliberate implementation decisions a reviewer reading only the diff would not know:
Validation already run locally before this pipeline: bin/fm-lint.sh green, bin/fm-doc-audience-check.sh green, and the spawn/brief group (fm-spawn-pool-base-freshen, fm-spawn-batch, fm-spawn-dispatch-profile, fm-spawn-worktree-settle, fm-brief, fm-task-delivery, fm-trace-context-spawn) green with 0 failures.
What Changed
freshen_spawn_worktree_baseinbin/fm-spawn.shnow checks for a configuredoriginbefore its first git call: with an origin present the existing fetch,remote set-head, default-branch resolution and refusals are unchanged; with no origin at all a--mode local-onlyship task bases on the repository's own local default branch (refs/heads/<default>) instead.main/masterrefuses with an explicit unknown-base error. The shared clean-tree check, reset and post-reset HEAD verification run for both paths.tests/fm-spawn-pool-base-freshen.test.shgains cases driving the real spawn executable: no-origin local-only success from an advanced local default branch, no-origin refusal for no-mistakes, direct-PR and scout with HEAD proven unmoved, and local-only still refused by an existing-but-unreachable origin.docs/architecture.mdrecords the no-origin local-only exception to the base-freshness invariant.Risk Assessment
✅ Low: The change is one guard at the single shared spawn boundary, leaves every existing refusal path intact, is covered by behavioral tests on the real executable, and the pipeline's doc fix correctly patches the one authoritative invariant sentence in place.
Testing
Ran the colocated base-freshen regression suite (9 tests green), proved the 3 new cases are a real regression by re-running them against the base-commit script (the no-origin local-only case fails there with exit 1), and captured a before/after CLI transcript from the real
bin/fm-spawn.shshowing the reportedfatal: 'origin' does not appear to be a git repository+ stale-base refusal turning into a successful spawn that starts from the verified local default-branch tip, while push modes, scouts, and an unreachable origin still refuse with the pooled worktree HEAD unmoved; adjacent spawn/delivery tests stayed green and the worktree was left clean. This is a shell CLI change with no rendered surface, so the product-level evidence is CLI transcripts rather than screenshots.Evidence: Before/after CLI transcript: no-origin local-only spawn
Source: Before/after CLI transcript: no-origin local-only spawn
=== BEFORE (base commit fc6a4e1) === $ fm-spawn.sh demo-local-only-task <project> --mode local-only --yolo off fatal: 'origin' does not appear to be a git repository fatal: Could not read from remote repository. error: could not fetch origin for pooled worktree '<work>/pool'; refusing to launch from a potentially stale base exit=1 $ git -C pool rev-parse HEAD 2f34d82c... (project main = 46a19dd7... -> stale) $ ls pool/newer.txt No such file or directory === AFTER (target commit 7a827f4) === $ fm-spawn.sh demo-local-only-task <project> --mode local-only --yolo off spawned demo-local-only-task harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-demo-local-only-task worktree=<work>/pool exit=0 $ git -C pool log --oneline -1 68e2be8 advance-main $ git -C project rev-parse main 68e2be8156e1396c2fe18b74955e22802b387b59 $ git -C pool rev-parse HEAD 68e2be8156e1396c2fe18b74955e22802b387b59 $ ls pool/newer.txt <work>/pool/newer.txtEvidence: Observed spawn lines and exact refusal messages from the real executable
Source: Observed spawn lines and exact refusal messages from the real executable
# observed no-origin local-only spawn: spawned pool-no-origin-local-only-r6 harness=codex kind=ship mode=local-only yolo=off ... # observed no-mistakes no-origin refusal: error: pooled worktree '<tmp>/no-origin-refusal-1/pool' has no 'origin' remote; only a local-only ship task may launch from a repository with no origin, and this spawn is mode=no-mistakes; give the project an origin or ship the task --mode local-only # observed direct-PR no-origin refusal: ... this spawn is mode=direct-PR; ... # observed scout no-origin refusal: ... this spawn is a scout; ... # observed local-only unreachable-origin refusal: error: could not fetch origin for pooled worktree '<tmp>/local-only-unreachable-origin/pool'; refusing to launch from a potentially stale base # all fm-spawn-pool-base-freshen tests passedEvidence: Reproduction script used for the manual end-to-end run
Source: Reproduction script used for the manual end-to-end run
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
docs/architecture.md:166- Intent requires: "Update authoritative operator/maintainer documentation only if current behavior or a stable invariant changes." A stable invariant did change, and docs/architecture.md:166 still states the old one: "no worker starts until its clean task worktree matches the fetched tip of origin's resolved default branch, and any unsafe or unverifiable base stops the spawn." After bin/fm-spawn.sh:1913-1924, a --mode local-only spawn in a repository with no origin starts from refs/heads/<local default> and matches no origin tip. The author's stated assumption that "no other maintained prose surface documents it" is contradicted by this line, which the same doc pairs with "Its header owns the exact refusal mechanics" - i.e. architecture.md deliberately owns the invariant, the header owns the mechanics. Needs a one-clause exception added at architecture.md:166.bin/fm-spawn.sh:1913- A scout in a no-origin project still refuses (MODE empty for a fresh scout spawn, so the guard rejects with "this spawn is a scout"). This is intentional per the stated intent and is the pre-existing safe behavior, and the message is accurate: secondmate spawns skip freshen entirely (line 2439) and relaunches skip it too, so an empty MODE at this point can only be a scout. Noting it as a known remaining limitation, not a defect.🔧 Fix: docs: state local-only no-origin base exception in architecture invariant
1 info still open:
bin/fm-spawn.sh:1919- On the no-origin path the base comes from default_branch(), which without origin/HEAD can only guess 'main' then 'master' (bin/fm-ff-lib.sh:37-51). A no-origin repo whose real default is e.g. 'develop' but that still carries a leftover local 'main' would silently reset the pool to 'main' rather than refusing. This is the pre-existing repo-wide heuristic (fm-merge-local.sh and fm-review-diff.sh state the same 'expected origin/HEAD, main, or master' contract and would merge into the same branch), so the change stays internally consistent rather than introducing a new divergence, and the author explicitly documented the no-main/no-master refusal as the chosen ceiling. Noting the ceiling, not requesting a change.✅ **Test** - passed
✅ No issues found.
bash tests/fm-spawn-pool-base-freshen.test.sh(9 tests, all pass) - the 3 new cases cover no-origin local-only success, no-origin refusal for no-mistakes/direct-PR/scout, and local-only vs unreachable originRegression proof: temporarily restoredgit show fc6a4e1:bin/fm-spawn.shand re-ran the same test file -not ok - a local-only ship task should launch in a project with no origin: expected exit 0, got 1; restored the fixed script and the worktree back to cleanManual end-user reproduction script driving the real spawn executable against a fixture project with no remote, run against both the base and target script (/tmp/no-mistakes-evidence/01M0WB4NPF71THS4XVD2FMSGPC/reproduce-no-origin-spawn.sh)Post-spawn base verification in the manual run:git -C pool rev-parse HEADequalsgit -C project rev-parse main, andpool/newer.txt(the commit made after worktree allocation) is presentAdjacent shared-spawn-path tests:bash tests/fm-spawn-batch.test.sh,bash tests/fm-spawn-worktree-settle.test.sh,bash tests/fm-spawn-dispatch-profile.test.sh,bash tests/fm-task-delivery.test.sh- all exit 0FM_TEST_EVIDENCE=1 bash tests/fm-spawn-pool-base-freshen.test.shto capture the observed spawn lines and exact refusal messages✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.