Skip to content

feat(spawn): add a per-task --base branch for ship and scout spawns - #8

Merged
talktejas merged 4 commits into
mainfrom
fm/spawn-per-task-base-r2
Sep 20, 2026
Merged

talktejas merged 4 commits into
mainfrom
fm/spawn-per-task-base-r2

Conversation

@talktejas

Copy link
Copy Markdown
Owner

Intent

This branch replaces PR 3 #3, which cannot be revalidated: its head was replaced outside the pipeline, so no-mistakes refuses to reconcile that branch's private mirror record. Same content, one clean commit from current main.

The captain approved merging both firstmate pull requests, but neither can merge as it stands:

  • PR 3 feat(spawn): add a per-task --base branch for ship and scout spawns #3 (feat(spawn): add a per-task --base branch for ship and scout spawns) conflicts with main, and its only failing check is "PR must be raised via no-mistakes".
  • PR 4 fix(spawn): claim a task's pool worktree so it is never reassigned under a live task #4 (fix(spawn): claim a task's pool worktree so it is never reassigned under a live task) conflicts with main, and fails "PR must be raised via no-mistakes", "Behavior portable serial 2" and "Stock macOS Bash snapshot compatibility".
    Both fixes matter daily: without the first, every worker starts from the project's default branch and the captain's integration-branch rule depends on hand-patched instructions; without the second, spawns fail with "pooled worktree not clean" because a live worker's copy is handed to a new task.

This branch is PR 3. Its own ask, which the approval above is an approval of: the captain ruled on 2026-09-17 that a multi-part effort must accumulate on one integration branch and merge into the active branch only once, checked as a whole. He gave that ruling after a night in which fifteen separate changes were merged into a product's develop branch one at a time, none of them ever compiled, and he found the 35 compile errors himself by failing to sign in to his own demo. Honouring that ruling with today's tooling means editing a shared project-wide base-branch record at the start of an effort and remembering to restore it at the end, and a forgotten restore bases the next unrelated fix on a dead integration branch with nothing reporting it. Close that gap by giving a spawn an explicit per-task base branch that governs that one task only, is never written back to the shared record, refuses rather than silently falling back when the named branch does not resolve, and is recorded with the task so a relaunch keeps the branch the task was created against.

Captain's ruling on conflicts, given while this rebase was under way: upstream (kunchenguid/firstmate) code wins wherever upstream has something; where it has nothing, resolve on your own. Upstream has NOT landed either fix - the per-task base branch is still open issue kunchenguid#3411 and the pool-reassignment bug is open issues kunchenguid#1441 and kunchenguid#1573, with several competing unmerged PRs (kunchenguid#4828, kunchenguid#4071, kunchenguid#4909, kunchenguid#4468, kunchenguid#3896). So: when a conflict hunk is upstream's own change to shared code, take upstream's version and re-apply our behaviour on top of it; only our two features are ours to keep. Do not adopt an unmerged upstream PR.

What Changed

  • bin/fm-spawn.sh accepts --base <branch> on ship and scout spawns: it overrides the project's registry/default base for that spawn alone, is never written back to shared state, refuses the spawn (naming the branch) when it does not resolve instead of falling back, and is rejected alongside --relaunch or --secondmate. A spawn that carries no --mode of its own reads the project's registered posture to decide between origin/<branch> and the local branch.
  • The resolved base is recorded as base= in the task's meta and carried forward by --relaunch; bin/fm-merge-local.sh now fast-forwards that recorded branch and bin/fm-teardown.sh measures merged work against it, both falling back to the project's standing base when no base was recorded or the recorded branch has disappeared.
  • Documented the per-task base in docs/architecture.md, docs/scripts.md, and the project-management skill, and extended the spawn, teardown, and control-relaunch shell tests to cover flag parsing, resolution precedence, refusal cases, meta recording, and the landing/cleanup paths.

Risk Assessment

✅ Low: The per-task base is opt-in and recorded only for an explicitly dispatched spawn, every new resolution path fails closed with a refusal rather than a silent fallback, the landing remains gated on an exact checkout match plus --ff-only so a wrong base can only refuse and never mis-merge, tasks dispatched without the flag are unchanged, and each new path has a regression test that fails against the pre-fix code.

Testing

I stood up sandbox firstmate homes and real git projects and drove the actual CLIs rather than only running unit assertions. The headline journey is the one the captain's ruling is about: an effort accumulating on integration/x, a third part dispatched against it with --base, landed with fm-merge-local.sh, and cleaned up — integration/x advanced, the project's standing main never moved, and the shared registry was never rewritten. The adversarial passes confirmed the flag fails closed (unresolvable branch, empty value, relaunch override, secondmate) and that a refused dispatch leaves no task record and no moved worktree. The two behaviour-preservation cases the last review round required both hold live: a no-flag task follows the project's current declaration even after it changes mid-flight, and a vanished recorded base falls back instead of refusing forever. The repo's three affected test files also pass in full. Evidence is CLI transcripts — this change has no UI surface, so no screenshots apply.

  • Live validation: ✅ go - 10 of 10 scenarios driven live against the product
Scenario Result Live Evidence
A spawn dispatched with --base starts the worker on that branch and records it, without touching the shared registry ✅ pass live journey-explicit-base-lifecycle.txt step 3: worktree HEAD == integration/x, task record shows base=integration/x, projects.md still reads base=main
A local-only task created against an integration base lands onto that base, not the project's standing branch ✅ pass live journey-explicit-base-lifecycle.txt step 6: fm-merge-local.sh fast-forwarded integration/x to the task head while main stayed at its prior commit
Cleanup reads such a task's work as landed instead of refusing against the wrong branch ✅ pass live journey-explicit-base-lifecycle.txt step 7: fm-teardown.sh exited 0 with no REFUSED line (an earlier pre-landing run refused naming integration/x, proving it measures against the recorded base)
Adversarial: a --base naming a branch the project cannot resolve refuses the spawn instead of silently falling back ✅ pass live journey-explicit-base-lifecycle.txt step 1: "error: explicit base 'integration/typo' is not a commit ... refusing to launch rather than falling back to another base", exit 1, no task record, pool work…
Adversarial: --base with no value is refused ✅ pass live journey-explicit-base-lifecycle.txt step 2: "error: --base requires a value", exit 1
Adversarial: a relaunch cannot be handed a different base; the task keeps the branch it was created against ✅ pass live journey-explicit-base-lifecycle.txt step 4 refusal, plus tests/fm-control-relaunch.test.sh asserting base= survives a replacement launch
Adversarial: a secondmate, which has no task worktree, refuses --base ✅ pass live bash tests/fm-spawn-base-branch.test.sh case "--secondmate refuses --base" (drives the real fm-spawn.sh)
Regression: a task dispatched without --base behaves as before, landing on the project's current declaration even after it changes mid-flight ✅ pass live journey-no-base-regression.txt: no base= in the task record; project adopts release/2026-10 after dispatch; fm-merge-local.sh lands there with no hand-editing, main untouched, cleanup accepts
Boundary: a recorded base branch that has since been deleted falls back to the standing base rather than stranding the work ✅ pass live journey-vanished-recorded-base.txt (teardown) and tests/fm-spawn-base-branch.test.sh case "a landing whose recorded base has vanished falls back to the project's standing base" (fm-merge-local.sh)
A scout, which is refused --mode, resolves an explicit base against the local branch on a local-only project ✅ pass live bash tests/fm-spawn-base-branch.test.sh cases "a scout also starts from an explicit --base" and "a scout on a local-only project starts from the local base branch", both driving the real fm-spawn.sh…
Evidence: Effort lifecycle transcript: dispatch against integration/x, land there, main untouched, cleanup accepts

Source: Effort lifecycle transcript: dispatch against integration/x, land there, main untouched, cleanup accepts


=== 1. adversarial: dispatch against a branch the project does not have
  | warning: /tmp/fmjourney/run/home/data/mc-part-three/launch-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
  | error: explicit base 'integration/typo' is not a commit for pooled worktree '/tmp/fmjourney/run/cold-slot'; refusing to launch rather than falling back to another base
  exit=1
  PASS  refused, naming the branch, instead of falling back to main
  PASS  no task record was written for the refused dispatch
  PASS  the pool worktree was left on main, no worker launched

=== 2. adversarial: --base with no value
  | error: --base requires a value
  PASS  empty --base refused (exit 1)

=== 3. dispatch part three against the effort's integration branch
  | warning: /tmp/fmjourney/run/home/data/mc-part-three/launch-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
  | spawned mc-part-three harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-mc-part-three worktree=/tmp/fmjourney/run/cold-slot
  exit=0
  PASS  spawn succeeded
  PASS  worker starts on integration/x (carrying parts 1 and 2), not on main
  worktree log:
    eadf084 effort part 2
    0f1fcfd effort part 1
    3cdfbee seed
  task record:
    worktree=/tmp/fmjourney/run/cold-slot
    project=/tmp/fmjourney/run/projects/multicurrency
    kind=ship
    mode=local-only
    base=integration/x
  PASS  the dispatched base is recorded with the task
  PASS  the shared project registry was not touched (still base=main)
  registry now: - multicurrency [local-only base=main] - lands locally (added 2026-09-17)

=== 4. adversarial: relaunch cannot be handed a different base
  | error: --relaunch reuses the task's recorded base branch; --base cannot override it
  PASS  relaunch refuses --base; the task keeps the branch it was created against

=== 5. the worker does part three
  worker branch: fm/mc-part-three (from integration/x)
    efa7700 effort part 3
    eadf084 effort part 2
    0f1fcfd effort part 1

=== 6. land it
  | merged fm/mc-part-three into local integration/x (eadf084 -> efa7700) in /tmp/fmjourney/run/projects/multicurrency
  exit=0
  PASS  landing succeeded
  PASS  integration/x received part three
  PASS  main is untouched: the effort was NOT fast-forwarded into the standing branch
  main:          3cdfbee seed
  integration/x: efa7700 effort part 3

=== 7. clean up the task worktree
  | teardown mc-part-three complete (window firstmate:fm-mc-part-three, worktree /tmp/fmjourney/run/cold-slot)
  | Backlog: mc-part-three just finished (this home keeps no markdown backlog at /tmp/fmjourney/run/home/data/backlog.md). Update /tmp/fmjourney/run/home/data/backlog.md - move mc-part-three to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due.
  exit=0
  PASS  cleanup sees the work as landed on the task's own base

JOURNEY RESULT: all checks passed
Evidence: Regression transcript: a task with no --base follows the project's current declaration after it changes mid-flight

Source: Regression transcript: a task with no --base follows the project's current declaration after it changes mid-flight


=== 1. dispatch with NO --base
  | warning: /tmp/fmjourney/run2/home/data/ledger-fix/launch-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
  | spawned ledger-fix harness=codex kind=ship mode=local-only yolo=off window=firstmate:fm-ledger-fix worktree=/tmp/fmjourney/run2/cold-slot
  PASS  spawn succeeded
  task record:
    project=/tmp/fmjourney/run2/projects/ledger
    kind=ship
    mode=local-only
  PASS  no base= recorded: the task owns no base and keeps following the project

=== 2. the worker commits, then the project changes its declared branch under it
  project now declares: release/2026-10

=== 3. land it
  | merged fm/ledger-fix into local release/2026-10 (c2ff9fc -> 5218d3f) in /tmp/fmjourney/run2/projects/ledger
  PASS  landing succeeded with no hand-editing of the task record
  PASS  landed on the project's CURRENT declaration (release/2026-10)
  PASS  main untouched

=== 4. clean up
  | teardown ledger-fix complete (window firstmate:fm-ledger-fix, worktree /tmp/fmjourney/run2/cold-slot)
  | Backlog: ledger-fix just finished (this home keeps no markdown backlog at /tmp/fmjourney/run2/home/data/backlog.md). Update /tmp/fmjourney/run2/home/data/backlog.md - move ledger-fix to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due.
  PASS  cleanup accepted the landed work

JOURNEY RESULT: all checks passed
Evidence: Boundary transcript: cleanup falls back when the task's recorded base branch has been deleted

Source: Boundary transcript: cleanup falls back when the task's recorded base branch has been deleted

  recorded base: integration/done (deleted: yes)
  | teardown stale-base complete (window firstmate:fm-stale-base, worktree /tmp/fmjourney/run3/wt)
  | Backlog: stale-base just finished (this home keeps no markdown backlog at /tmp/fmjourney/run3/home/data/backlog.md). Update /tmp/fmjourney/run3/home/data/backlog.md - move stale-base to Done, keep Done to the 10 most recent, then re-scan Queued and dispatch only work whose blockers are gone and date is due.
  exit=0
  PASS  cleanup fell back to the standing base and removed the task
Evidence: tests/fm-spawn-base-branch.test.sh output

Source: tests/fm-spawn-base-branch.test.sh output

ok - base= parses alongside the delivery mode without changing any existing line's mode
ok - a cold slot for a develop-based project starts the worker on develop
ok - a project with no base record still starts from the repo default branch
ok - a project whose default branch is the right base passes the assertion
ok - an unregistered project falls back to the default branch and spawns as today
ok - a base= record naming a branch the repo does not have is refused
ok - a local-only project starts the worker from the local default branch
ok - a scout also starts from the project's recorded base branch
ok - an explicit --base wins over the project's registry base record
ok - an explicit --base is never written back to the shared registry
ok - the registry base still applies when --base is absent
ok - an explicit base the project cannot resolve refuses instead of falling back
ok - a scout also starts from an explicit --base
ok - a scout on a local-only project starts from the local base branch
ok - --relaunch refuses --base and keeps the task's own recorded base
ok - --secondmate refuses --base
ok - a local-only task lands on the base it was created against, not the standing one
ok - a task dispatched without --base lands on the project's current declaration
ok - a landing whose recorded base has vanished falls back to the project's standing base
ok - a declaration landed only on the development branch is still read by a clone on the default branch
ok - the repository's own declaration wins over a home-private registry record
ok - the private registry remains the fallback for a project that has not adopted the file
ok - a malformed declaration is rejected rather than handed to git
ok - the resolver reads a declaration from an unchecked-out branch under stock /bin/bash
# all fm-spawn-base-branch tests passed
Evidence: Landing refuses to move the standing branch (from the lifecycle transcript)
| merged fm/mc-part-three into local integration/x (eadf084 -> efa7700) in .../projects/multicurrency
PASS integration/x received part three
PASS main is untouched: the effort was NOT fast-forwarded into the standing branch
main: 3cdfbee seed
integration/x: efa7700 effort part 3

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-spawn.sh:2937 - The per-task base stops at spawn: base= is written to state/<id>.meta but no consumer reads it. bin/fm-merge-local.sh:98 and bin/fm-teardown.sh:1743 still resolve the project's standing base via bin/fm-project-base.sh. Concrete sequence: a local-only project registered base=main, spawn task A with --base integration/x; the worker branches fm/A off integration/x and commits. At landing, fm-merge-local.sh computes DEFAULT=main, requires the project checkout to be on main, finds main is an ancestor of fm/A, and fast-forwards main over the whole integration branch - the single task silently lands every accumulated commit of the effort into main, which is exactly the "merge into the active branch only once, checked as a whole" rule the flag exists to protect. Teardown has the mirror problem: git log HEAD --not main reports commits that are already merged into integration/x as unmerged and refuses cleanup. The remedy (teaching merge-local and teardown to read the task's recorded base) extends the change beyond its stated spawn-only scope, so it needs authorization rather than being an auto-fix; scoping the flag to non-local-only projects, or documenting the local-only landing gap, are the alternatives.
  • ⚠️ bin/fm-spawn.sh:2911 - Simplification / unrequired component: the new posture fallback (fm-project-mode.sh --raw when $MODE is empty) changes base resolution for spawns that pass no --base at all. Before this commit a scout on a local-only project with a registry base= resolved against origin/&lt;base&gt;; now it resolves against the local branch. That is a separate behavior change from the intent's per-task --base (nothing in the intent requires re-deciding the origin-vs-local choice for registry-based scouts), and it also adds a registry read plus a subprocess on every spawn. The narrower form that satisfies the intent is to consult the registered posture only when $BASE_ARG is set - i.e. [ -n &#34;$BASE_ARG&#34; ] &amp;&amp; [ -z &#34;$MODE&#34; ] - leaving the existing registry path byte-identical; if the scout/local-only resolution is genuinely a bug worth fixing, it belongs in its own change with its own regression test rather than riding along here.

🔧 Fix applied.
2 warnings still open:

  • ⚠️ bin/fm-spawn.sh:2890 - --base is fed to git without the branch-name validation bin/fm-project-base.sh:45 applies to the repo-file tier ('A branch name reaching git from a repository file is a trust boundary'). On the local-only / no-origin path target=$default and the only check is rev-parse --verify &#34;$target^{commit}&#34; (line 2923), which accepts any revision. Concrete sequence: a local-only project with origin configured, fm-spawn.sh t1 proj --mode local-only --yolo off --base origin/develop (or a tag, or HEAD~3). posture=local-only so target="origin/develop"; rev-parse resolves it, reset --hard succeeds, the assertion at 2932 passes, and base=origin/develop is written to state/t1.meta. The spawn reports success. At landing bin/fm-merge-local.sh:106 looks up refs/heads/origin/develop, does not find it, and refuses with "was created against base branch 'origin/develop', which no longer exists" - a branch that never existed - leaving the task unlandable without hand-editing meta; bin/fm-teardown.sh:1750 likewise cannot resolve it and silently measures against the standing base instead. The intent requires the flag to name a branch and to refuse when the named branch does not resolve; a tag or remote-tracking ref satisfies neither. Remedy: validate BASE_ARG the way fm-project-base.sh's emit_if_valid does (charset plus git check-ref-format --branch) and resolve the local-only target as refs/heads/<branch> rather than a bare revision, so a non-branch refuses at spawn instead of producing an unlandable task. The shared-clone path is safe only incidentally, because its fetch refspec +refs/heads/$default:... rejects the same input.
  • ⚠️ bin/fm-spawn.sh:2939 - The fix round records base= for every ship/scout spawn, not only for one dispatched with --base: SPAWN_RESOLVED_BASE=$default is set whatever base_source was (explicit, recorded, or default). Because bin/fm-merge-local.sh:104 and bin/fm-teardown.sh:1747 now prefer that record, every local-only task freezes its landing target at spawn time, and merge-local additionally HARD-REFUSES (lines 105-109) when the recorded branch is gone, where teardown falls back. Concrete regression for a task that never used the flag: a local-only project with an origin configured (so freshen_spawn_worktree_base does not return early at 2868) spawns task t1 while .firstmate-base declares develop, recording base=develop; the project then lands its adoption of main and deletes develop. Before this change bin/fm-merge-local.sh resolved the current declaration (main) and the fast-forward succeeded; now it reads base=develop, cannot verify refs/heads/develop, and refuses permanently, so an in-flight task can only be landed by hand-editing state/<id>.meta. The authorized remedy was 'read the task's own recorded base= and fall back to the project's standing base only when the task has none'; recording a base for tasks that were dispatched without one, plus the new refusal branch that has no counterpart in teardown, exceeds that. The narrower form that satisfies the intent is to set SPAWN_RESOLVED_BASE only when base_source=explicit, which leaves every non---base task's landing and cleanup byte-identical to before. It is the extra machinery, not a repair to it, that needs the author's decision.

🔧 Fix applied.
1 info still open:

  • ℹ️ bin/fm-spawn.sh:2849 - The intent states this branch is "Same content, one clean commit from current main". HEAD now carries three commits above the base: the author's feature commit 7335c69 plus two pipeline-authored fix commits (c808f59, 9b8522f). Nothing is functionally wrong - the fix commits are this run's own review fixes - but if the "one clean commit" shape is part of what makes PR 3 replaceable, the branch needs squashing before it is pushed. That is the author's call, not something review should do.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 10 of 10 scenarios driven live against the product
Scenario Result Live Evidence
A spawn dispatched with --base starts the worker on that branch and records it, without touching the shared registry ✅ pass live journey-explicit-base-lifecycle.txt step 3: worktree HEAD == integration/x, task record shows base=integration/x, projects.md still reads base=main
A local-only task created against an integration base lands onto that base, not the project's standing branch ✅ pass live journey-explicit-base-lifecycle.txt step 6: fm-merge-local.sh fast-forwarded integration/x to the task head while main stayed at its prior commit
Cleanup reads such a task's work as landed instead of refusing against the wrong branch ✅ pass live journey-explicit-base-lifecycle.txt step 7: fm-teardown.sh exited 0 with no REFUSED line (an earlier pre-landing run refused naming integration/x, proving it measures against the recorded base)
Adversarial: a --base naming a branch the project cannot resolve refuses the spawn instead of silently falling back ✅ pass live journey-explicit-base-lifecycle.txt step 1: "error: explicit base 'integration/typo' is not a commit ... refusing to launch rather than falling back to another base", exit 1, no task record, pool work…
Adversarial: --base with no value is refused ✅ pass live journey-explicit-base-lifecycle.txt step 2: "error: --base requires a value", exit 1
Adversarial: a relaunch cannot be handed a different base; the task keeps the branch it was created against ✅ pass live journey-explicit-base-lifecycle.txt step 4 refusal, plus tests/fm-control-relaunch.test.sh asserting base= survives a replacement launch
Adversarial: a secondmate, which has no task worktree, refuses --base ✅ pass live bash tests/fm-spawn-base-branch.test.sh case "--secondmate refuses --base" (drives the real fm-spawn.sh)
Regression: a task dispatched without --base behaves as before, landing on the project's current declaration even after it changes mid-flight ✅ pass live journey-no-base-regression.txt: no base= in the task record; project adopts release/2026-10 after dispatch; fm-merge-local.sh lands there with no hand-editing, main untouched, cleanup accepts
Boundary: a recorded base branch that has since been deleted falls back to the standing base rather than stranding the work ✅ pass live journey-vanished-recorded-base.txt (teardown) and tests/fm-spawn-base-branch.test.sh case "a landing whose recorded base has vanished falls back to the project's standing base" (fm-merge-local.sh)
A scout, which is refused --mode, resolves an explicit base against the local branch on a local-only project ✅ pass live bash tests/fm-spawn-base-branch.test.sh cases "a scout also starts from an explicit --base" and "a scout on a local-only project starts from the local base branch", both driving the real fm-spawn.sh…
  • bash tests/fm-spawn-base-branch.test.sh (24 cases incl. the new explicit-base, scout, relaunch, secondmate, and landing cases)
  • bash tests/fm-teardown.test.sh (full file, incl. new test_local_only_merged_to_task_base_allows)
  • bash tests/fm-control-relaunch.test.sh (incl. base= surviving relaunch)
  • Live lifecycle driver: fm-spawn.sh &lt;id&gt; &lt;proj&gt; --mode local-only --yolo off --base integration/x → worker commit → fm-merge-local.sh &lt;id&gt; → fm-teardown.sh &lt;id&gt;
  • Live adversarial driver: fm-spawn.sh ... --base integration/typo, --base with no value, fm-spawn.sh &lt;id&gt; --relaunch --base main
  • Live regression driver: spawn with no --base, project adopts release/2026-10 via .firstmate-base mid-flight, then fm-merge-local.sh + fm-teardown.sh
  • Live boundary driver: task meta carrying base=integration/done after that branch was deleted, then fm-teardown.sh
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Supersedes #3, which cannot be revalidated: that branch's head was replaced
outside the pipeline, and no-mistakes then refuses to reconcile its private
mirror record for the branch. Same content, clean branch from main.

A task worktree's base branch comes from the project's own declaration, so
honouring the captain's integration-branch ruling means editing shared state at
the start of an effort and remembering to restore it at the end - and a
forgotten restore bases the next unrelated fix on a dead integration branch
with nothing reporting it.

bin/fm-spawn.sh now accepts --base <branch>, which governs one spawn only: it
takes precedence over bin/fm-project-base.sh's resolution and the repository
default when a pooled worktree is freshened, writes nothing back, refuses the
spawn naming the source rather than falling back when the branch does not
resolve, and is refused alongside --relaunch and --secondmate. The resolved
base is recorded as base= in the task's metadata so a relaunch keeps the branch
the task was created against.
@talktejas
talktejas merged commit a077d3d into main Sep 20, 2026
36 of 37 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.

1 participant