Skip to content

feat(bin): add configurable base/merge-target branch for ship tasks#213

Open
abdullah-kasim wants to merge 3 commits into
kunchenguid:mainfrom
abdullah-kasim:fm/base-branch-w3
Open

feat(bin): add configurable base/merge-target branch for ship tasks#213
abdullah-kasim wants to merge 3 commits into
kunchenguid:mainfrom
abdullah-kasim:fm/base-branch-w3

Conversation

@abdullah-kasim

Copy link
Copy Markdown

Intent

Add a configurable base/merge-target branch for ship tasks so a captain working on a long-lived feature branch can have crewmate work based on - and landed into - that branch instead of the repo's default branch. fm-spawn.sh gains a --base flag (single and batch dispatch) that records base= in state/.meta; the flag is deliberately REFUSED (not ignored) for --scout and --secondmate, since a scout worktree is scratch and a secondmate home has no task branch - this refusal choice was left to the implementer and is documented in the script header and AGENTS.md. base= in meta is the single source of truth all consumers read. fm-brief.sh gains --base: the scaffolded ship Setup tells the crewmate to verify the base branch exists in the worktree (fetching from origin if needed) and branch from it with git checkout -b fm/ ; local-only landing/rebase wording and the direct-PR open-a-PR line also target the base branch. fm-review-diff.sh diffs against origin/ (after fetch) for remote-backed projects or the local base branch otherwise. fm-merge-local.sh fast-forwards the recorded base branch with the same safety refusals adapted (checkout on the base branch and clean, ff-only, divergence refused with rebase advice, plus a clear error for a missing base branch). fm-teardown.sh treats a recorded base= as the landing target via a landing_branch() helper. Everything is backward-compatible: a meta without base= behaves byte-for-byte as today, and fm-brief output without --base was verified byte-identical to the pre-change scaffold for all three delivery modes. A prior run's review gate found three actionable findings which the user decided on; fix them in this run when they resurface, resolved as follows: (1) teardown must verify the recorded base branch exists (mirroring fm-merge-local's refs/heads check) before the local-only landing check and refuse clearly when it does not, so a missing base can never silently pass unlanded work; (2) for the no-mistakes delivery mode the user explicitly chose to DOCUMENT the limitation that --base does not redirect the pipeline-opened PR's landing target (the no-mistakes pipeline owns PR creation against its configured push target) rather than refuse --base or thread the base into the pipeline - the user's primary flow is local-only mode where --base works end to end; (3) fm-review-diff must guard the origin fetch of the base branch and emit an explicit actionable error when the base branch does not exist on origin. Docs: header comments of the touched scripts plus minimal surgical AGENTS.md wording in sections 1, 6, and 7 (CLAUDE.md is a symlink, only AGENTS.md edited). Tests: new tests/fm-base-branch.test.sh with 17 cases following existing conventions. Out of scope per the task: no per-project standing base= field in data/projects.md, no scout/secondmate changes beyond the refusal, no changes to PR-based remote-reachability logic beyond the base generalization. The full test suite (34 files) and CI's shellcheck invocation pass locally.

What Changed

  • bin/fm-spawn.sh gains a --base <branch> flag (single and batch dispatch) that records base= in the task's meta as the single source of truth; the flag is refused for --scout and --secondmate. bin/fm-brief.sh --base scaffolds the ship Setup to verify and branch from that base, and points local-only/direct-PR landing wording at it.
  • bin/fm-review-diff.sh, bin/fm-merge-local.sh, and bin/fm-teardown.sh now honor a recorded base= as the review/merge/landing target, with explicit refusals when the base branch is missing locally or on origin (teardown mirrors fm-merge-local's refs/heads existence check; review-diff guards its origin fetch with an actionable error). Tasks without base= behave exactly as before, and fm-brief.sh output without --base was verified byte-identical to the pre-change scaffold for all three delivery modes.
  • Adds tests/fm-base-branch.test.sh (19 cases covering meta recording, refusals, brief rewriting, review-diff bases, merge fast-forward and refusal paths, and teardown landing checks), and documents the flag — including the no-mistakes-mode limitation that --base does not redirect the pipeline-opened PR's target — in the script headers, AGENTS.md, and docs/.

Risk Assessment

✅ Low: All three previously-decided review findings are correctly fixed and fail closed (missing-base teardown refusal, guarded review-diff fetch, documented no-mistakes limitation), the feature is backward compatible with base= absent, and 17 new tests cover every refusal and retarget path; only two cosmetic/informational observations remain.

Testing

Baseline full suite plus a rerun (34 files, 502 assertions) pass; the new 19-case fm-base-branch test file covers the whole feature matrix including the three review-gate fixes; a manual CLI end-to-end demo showed the captain's primary local-only --base flow working (brief/spawn/review/merge/teardown all targeting the feature branch, main untouched, premature teardown refused); and no-flag brief output was confirmed byte-identical to the pre-change scaffold in all three delivery modes. No issues found; this is a CLI/scripting change with no rendered UI surface, so transcripts are the appropriate visual evidence.

Evidence: End-to-end local-only --base lifecycle transcript (brief → spawn → review → refused teardown → merge into feature branch → teardown)

============ SETUP: local-only project 'myapp' with long-lived branch feature/train ============
registry: - myapp [local-only] - demo app (added 2026-07-03)
project checkout: feature/train

============ 1. fm-brief.sh add-metrics-q7 myapp --base feature/train ============
scaffolded: /tmp/fm-base-demo.KnARi3/home/data/add-metrics-q7/brief.md (ship, mode=local-only; replace {TASK})

============ 1a. generated brief: Setup section (base-branch instructions) ============

============ 1b. generated brief: landing wording targets feature/train ============
13:1. First action: create your branch from the base branch `feature/train`. Verify the base branch exists in the worktree (`git rev-parse --verify feature/train`); if it is missing and the project has an origin remote, fetch it first (`git fetch origin feature/train:feature/train`). Then create your branch: `git checkout -b fm/add-metrics-q7 feature/train`
16:1. Never push to any remote and never open a PR. Work only on your `fm/add-metrics-q7` branch; firstmate handles the merge into local `feature/train`.
38:Keep your branch a clean fast-forward onto the base branch - if `feature/train` has advanced, rebase onto it so the eventual merge stays a fast-forward.
40:Firstmate then reviews your branch diff, the captain approves, and firstmate merges it into local `feature/train`.

============ 2. fm-spawn.sh add-metrics-q7 <project> --base feature/train (fake tmux/treehouse) ============
spawned add-metrics-q7 harness=claude kind=ship mode=local-only yolo=off window=:fm-add-metrics-q7 worktree=/tmp/fm-base-demo.KnARi3/wt

============ 2a. recorded state/add-metrics-q7.meta (base= is the single source of truth) ============
window=:fm-add-metrics-q7
worktree=/tmp/fm-base-demo.KnARi3/wt
project=/tmp/fm-base-demo.KnARi3/myapp
harness=claude
kind=ship
mode=local-only
yolo=off
base=feature/train
tasktmp=/tmp/fm-add-metrics-q7
model=default
effort=default

============ 3. refusal: fm-spawn.sh scout-probe <project> --scout --base feature/train ============
error: --base applies only to ship tasks; refusing for --scout
exit code: 1 (refused)

============ 4. crewmate follows the brief: branch fm/add-metrics-q7 from feature/train, commit ============
2becb36 add metrics
e9e23d9 feature-branch groundwork
52a764d initial

============ 5. fm-review-diff.sh add-metrics-q7 --stat (diffs against recorded base, not main) ============
diff base: feature/train
 metrics.txt | 1 +
 1 file changed, 1 insertion(+)

============ 6. premature fm-teardown.sh add-metrics-q7 -> must REFUSE (not merged into feature/train) ============
REFUSED: local-only worktree /tmp/fm-base-demo.KnARi3/wt has work not yet merged into feature/train and not on any remote.
commits not yet on feature/train:
2becb36 add metrics
Merge the branch into local feature/train first (bin/fm-merge-local.sh after the captain approves), or push to a fork/remote, or get the captain's explicit OK to discard, then --force.
exit code: 1 (refused)

============ 7. captain approves: fm-merge-local.sh add-metrics-q7 ============
merged fm/add-metrics-q7 into local feature/train (e9e23d9 -> 2becb36) in /tmp/fm-base-demo.KnARi3/myapp

============ 7a. resulting branch state: feature/train advanced, main untouched ============
feature/train:
2becb36 add metrics
e9e23d9 feature-branch groundwork
52a764d initial
main:
52a764d initial
main unchanged: yes

============ 8. fm-teardown.sh add-metrics-q7 now succeeds (work landed in feature/train) ============
teardown add-metrics-q7 complete (window :fm-add-metrics-q7, worktree /tmp/fm-base-demo.KnARi3/wt)
Backlog: add-metrics-q7 just finished. Run tasks-axi done add-metrics-q7 --note "local main", then run tasks-axi ready for dependency-cleared candidates, check date gates, and dispatch only work whose blockers are gone and date is due.
exit code: 0 (teardown allowed)

============ DEMO COMPLETE ============
Evidence: New fm-base-branch test file output (19/19 ok)
ok - fm-spawn --base records base= in the task's meta
ok - fm-spawn without --base keeps meta byte-compatible (no base= line)
ok - batch dispatch threads a shared --base into every pair's meta
ok - fm-spawn refuses --base for --scout and --secondmate before any side effect
ok - fm-brief --base starts the ship branch from the base branch and lands into it
ok - fm-brief without --base keeps the default-branch scaffold wording
ok - fm-brief refuses --base for --scout and --secondmate
ok - fm-review-diff diffs against the recorded local base branch
ok - fm-review-diff fetches and diffs against origin/<base> for remote-backed projects
ok - fm-review-diff errors explicitly when the recorded base branch is missing from origin
ok - fm-review-diff without base= keeps the default-branch comparison
ok - fm-merge-local fast-forwards the recorded base branch and leaves the default branch alone
ok - fm-merge-local refuses when the project checkout is not on the base branch
ok - fm-merge-local refuses divergence against the base branch with rebase advice
ok - fm-merge-local refuses a recorded base branch missing from the project
ok - teardown accepts local-only work merged into the recorded base branch
ok - teardown still refuses base-branch work merged only into the default branch
ok - teardown refuses a recorded base branch missing from the project instead of passing unlanded work
ok - teardown's content-landed fallback targets origin/<base>, not origin/<default>, when base= is recorded
Evidence: Brief backward-compatibility check: no --base output byte-identical to pre-change scaffold for all three modes

mode=no-mistakes: brief without --base is BYTE-IDENTICAL to pre-change (951f5cf) scaffold mode=direct-PR: brief without --base is BYTE-IDENTICAL to pre-change (951f5cf) scaffold mode=local-only: brief without --base is BYTE-IDENTICAL to pre-change (951f5cf) scaffold

mode=no-mistakes: brief without --base is BYTE-IDENTICAL to pre-change (951f5cf) scaffold
mode=direct-PR: brief without --base is BYTE-IDENTICAL to pre-change (951f5cf) scaffold
mode=local-only: brief without --base is BYTE-IDENTICAL to pre-change (951f5cf) scaffold
Evidence: direct-PR base targeting and documented no-mistakes limitation
=== direct-PR brief with --base: PR-opening line targets the base branch ===
13:1. First action: create your branch from the base branch `feature/train`. Verify the base branch exists in the worktree (`git rev-parse --verify feature/train`); if it is missing and the project has an origin remote, fetch it first (`git fetch origin feature/train:feature/train`). Then create your branch: `git checkout -b fm/dpr-t1 feature/train`
38:When it is implemented and committed, push your branch and open a PR with `gh-axi` targeting base branch `feature/train`, not the default branch, then append `done: PR {url}` to the status file and stop.

=== documented no-mistakes --base limitation (fm-spawn.sh header) ===
20:#   no-mistakes-mode limitation: --base governs the branch starting point, the
21:#   review diff, and teardown's landed-work checks, but the no-mistakes pipeline
22-#   owns PR creation and still opens the PR against its configured push target
23-#   (typically the default branch) - the PR's landing target is NOT redirected.
24-#   The end-to-end --base flows are local-only and direct-PR.

=== AGENTS.md wording on --base ===
36:   A ship task whose meta records `base=` (dispatched with `--base <branch>`) treats that branch, not the default branch, as its landing target throughout these checks.
494:bin/fm-spawn.sh <id> projects/<repo> --base <branch>  # ship task branched from and landing into <branch> instead of the default branch; records base= in meta
506:An explicit `--base <branch>` (ship tasks only; refused for `--scout` and `--secondmate`) is recorded as `base=` in meta, the single source of truth that makes `fm-brief.sh --base`, `fm-review-diff.sh`, `fm-merge-local.sh`, and teardown's landed-work checks target that branch instead of the default branch; absent flag means no `base=` line and default-branch behavior.
543:Pooled clones keep their local default refs frozen at clone time and can lag `origin`; the helper always compares against the authoritative base (the task's recorded base branch when `base=` is set, else the default branch).
Evidence: Reproducible e2e demo script
#!/usr/bin/env bash
# End-to-end demo: ship task based on and landing into a long-lived feature
# branch (--base) in a local-only project, driving the real bin/ scripts.
set -u
ROOT=$(pwd)   # firstmate repo root (run from repo root)
D=$(mktemp -d /tmp/fm-base-demo.XXXXXX)
trap 'rm -rf "$D"' EXIT
export GIT_AUTHOR_NAME=demo GIT_AUTHOR_EMAIL=demo@example.invalid
export GIT_COMMITTER_NAME=demo GIT_COMMITTER_EMAIL=demo@example.invalid

HOME_DIR="$D/home"; PROJ="$D/myapp"; WT="$D/wt"
mkdir -p "$HOME_DIR/data" "$HOME_DIR/state" "$HOME_DIR/config" "$HOME_DIR/projects"
printf '%s\n' claude > "$HOME_DIR/config/crew-harness"
touch "$HOME_DIR/state/.last-watcher-beat"

banner() { printf '\n============ %s ============\n' "$*"; }

banner "SETUP: local-only project 'myapp' with long-lived branch feature/train"
git init -q -b main "$PROJ"
printf 'v1\n' > "$PROJ/README.md"; git -C "$PROJ" add README.md; git -C "$PROJ" commit -qm "initial"
git -C "$PROJ" checkout -qb feature/train
printf 'train work\n' > "$PROJ/train.txt"; git -C "$PROJ" add train.txt; git -C "$PROJ" commit -qm "feature-branch groundwork"
printf -- '- myapp [local-only] - demo app (added 2026-07-03)\n' > "$HOME_DIR/data/projects.md"
echo "registry: $(cat "$HOME_DIR/data/projects.md")"
echo "project checkout: $(git -C "$PROJ" branch --show-current)"

ID=add-metrics-q7

banner "1. fm-brief.sh $ID myapp --base feature/train"
FM_ROOT_OVERRIDE='' FM_HOME="$HOME_DIR" FM_STATE_OVERRIDE="$HOME_DIR/state" FM_DATA_OVERRIDE="$HOME_DIR/data" \
  "$ROOT/bin/fm-brief.sh" "$ID" myapp --base feature/train
banner "1a. generated brief: Setup section (base-branch instructions)"
sed -n '/## Setup/,/^## /p' "$HOME_DIR/data/$ID/brief.md" | sed '$d'
banner "1b. generated brief: landing wording targets feature/train"
grep -n 'feature/train' "$HOME_DIR/data/$ID/brief.md"

banner "2. fm-spawn.sh $ID <project> --base feature/train (fake tmux/treehouse)"
FAKEBIN="$D/fakebin"; mkdir -p "$FAKEBIN"
cat > "$FAKEBIN/tmux" <<'SH'
#!/usr/bin/env bash
set -u
case "$*" in *"#{pane_current_path}"*) printf '%s\n' "${FM_FAKE_PANE_PATH:-}"; exit 0 ;; esac
exit 0
SH
printf '#!/usr/bin/env bash\nexit 0\n' > "$FAKEBIN/treehouse"
cat > "$FAKEBIN/gh-axi" <<'SH'
#!/usr/bin/env bash
case "${1:-} ${2:-}" in
  "pr list") printf '%s\n' "count: 0 (showing first 0)" "pull_requests[]: []"; exit 0 ;;
  "pr view") echo "error: pull request not found" >&2; exit 1 ;;
esac
exit 0
SH
cp "$FAKEBIN/gh-axi" "$FAKEBIN/gh"; chmod +x "$FAKEBIN"/*
git -C "$PROJ" worktree add -q --detach "$WT" feature/train
FM_ROOT_OVERRIDE='' FM_HOME="$HOME_DIR" FM_STATE_OVERRIDE="$HOME_DIR/state" FM_DATA_OVERRIDE="$HOME_DIR/data" \
  FM_PROJECTS_OVERRIDE="$HOME_DIR/projects" FM_CONFIG_OVERRIDE="$HOME_DIR/config" \
  FM_SPAWN_NO_GUARD=1 FM_FAKE_PANE_PATH="$WT" TMUX="fake,1,0" FM_BACKEND=tmux \
  PATH="$FAKEBIN:$PATH" "$ROOT/bin/fm-spawn.sh" "$ID" "$PROJ" --base feature/train
banner "2a. recorded state/$ID.meta (base= is the single source of truth)"
cat "$HOME_DIR/state/$ID.meta"

banner "3. refusal: fm-spawn.sh scout-probe <project> --scout --base feature/train"
FM_ROOT_OVERRIDE='' FM_HOME="$HOME_DIR" FM_STATE_OVERRIDE="$HOME_DIR/state" FM_DATA_OVERRIDE="$HOME_DIR/data" \
  FM_PROJECTS_OVERRIDE="$HOME_DIR/projects" FM_CONFIG_OVERRIDE="$HOME_DIR/config" \
  FM_SPAWN_NO_GUARD=1 FM_FAKE_PANE_PATH="$WT" TMUX="fake,1,0" FM_BACKEND=tmux \
  PATH="$FAKEBIN:$PATH" "$ROOT/bin/fm-spawn.sh" scout-probe "$PROJ" --scout --base feature/train
echo "exit code: $? (refused)"

banner "4. crewmate follows the brief: branch fm/$ID from feature/train, commit"
git -C "$WT" checkout -qb "fm/$ID" feature/train
printf 'metrics\n' > "$WT/metrics.txt"; git -C "$WT" add metrics.txt; git -C "$WT" commit -qm "add metrics"
git -C "$WT" log --oneline -3

banner "5. fm-review-diff.sh $ID --stat (diffs against recorded base, not main)"
FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$HOME_DIR/state" "$ROOT/bin/fm-review-diff.sh" "$ID" --stat

banner "6. premature fm-teardown.sh $ID -> must REFUSE (not merged into feature/train)"
FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$HOME_DIR/state" FM_CONFIG_OVERRIDE="$HOME_DIR/config" \
  PATH="$FAKEBIN:$PATH" "$ROOT/bin/fm-teardown.sh" "$ID"
echo "exit code: $? (refused)"

banner "7. captain approves: fm-merge-local.sh $ID"
MAIN_BEFORE=$(git -C "$PROJ" rev-parse main)
FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$HOME_DIR/state" "$ROOT/bin/fm-merge-local.sh" "$ID"
banner "7a. resulting branch state: feature/train advanced, main untouched"
echo "feature/train:"; git -C "$PROJ" log --oneline feature/train
echo "main:"; git -C "$PROJ" log --oneline main
[ "$(git -C "$PROJ" rev-parse main)" = "$MAIN_BEFORE" ] && echo "main unchanged: yes"

banner "8. fm-teardown.sh $ID now succeeds (work landed in feature/train)"
FM_ROOT_OVERRIDE="$ROOT" FM_STATE_OVERRIDE="$HOME_DIR/state" FM_CONFIG_OVERRIDE="$HOME_DIR/config" \
  PATH="$FAKEBIN:$PATH" "$ROOT/bin/fm-teardown.sh" "$ID" && echo "exit code: 0 (teardown allowed)"
[ ! -d "$WT" ] && echo "worktree removed: yes"

banner "DEMO COMPLETE"

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 infos
  • 🚨 bin/fm-teardown.sh:108 - landing_branch() returns the recorded base= branch without verifying it exists (unlike fm-merge-local.sh's show-ref check), and the local-only landing check at line 633 swallows git's 'bad revision' error (git log HEAD --not &#34;$DEFAULT&#34; -- 2&gt;/dev/null | head -5 || true), leaving unmerged empty. Reproduced: a clean local-only worktree with unmerged commits whose recorded base branch is missing passes the safety check, and teardown deletes the branch and force-returns the worktree — unlanded work destroyed, violating hard rule docs: align tmux and harness guidance #3. This is prior review finding (1), which the user resolved as: verify the recorded base branch exists (mirroring fm-merge-local's refs/heads check) before the local-only landing check and refuse clearly. The fix is absent at this commit; add it plus a teardown test for the missing-base refusal.
  • ⚠️ bin/fm-review-diff.sh:86 - The origin fetch of the review base is unguarded: when the task's recorded base branch does not exist on origin, git fetch origin &#34;+refs/heads/$TARGET:refs/remotes/origin/$TARGET&#34; fails and set -eu kills the script with git's raw fatal message instead of an explicit actionable error naming the missing base branch. This is prior review finding (3), which the user resolved as: guard the fetch and emit an explicit actionable error when the base branch does not exist on origin. It fails closed (no wrong diff is produced), but the decided fix is absent at this commit.
  • ⚠️ bin/fm-spawn.sh:12 - Prior review finding (2) was resolved as DOCUMENT the no-mistakes limitation — that --base does not redirect the pipeline-opened PR's landing target, because the no-mistakes pipeline owns PR creation against its configured push target — but no such documentation exists anywhere at this commit: not in the fm-spawn.sh --base header block, not in fm-brief.sh's no-mistakes definition-of-done (which happily emits a branch-from-base Setup step for no-mistakes projects), and not in AGENTS.md. Add the decided limitation note to the fm-spawn.sh header (and ideally the no-mistakes brief wording) so a --base dispatch on a no-mistakes project doesn't silently produce a PR targeting the wrong branch with no warning on record.

🔧 Fix: refuse missing base branch in teardown/review-diff, document no-mistakes limitation
2 infos still open:

  • ℹ️ bin/fm-teardown.sh:273 - The local-only backlog reminder hardcodes tasks-axi done &lt;id&gt; --note &#34;local main&#34; even when the task's meta records base=, so the Done record misstates the landing target (says "local main" though the work merged into the recorded base branch). Suggest noting local &lt;landing branch&gt; using landing_branch() / BASE_BRANCH.
  • ℹ️ bin/fm-review-diff.sh:94 - For a remote-backed project the review base is origin/<base> after fetch. If the captain's local base branch is ahead of origin (unpushed commits) and the crewmate branched from the local ref, origin/&lt;base&gt;...fm/&lt;id&gt; attributes those unpushed base commits to the crewmate's diff. This matches the deliberate origin-authoritative design mirrored from the default-branch flow; recording it as a known tradeoff only.
✅ **Test** - passed

✅ No issues found.

  • command -v tmux >/dev/null || { echo "tmux is required for e2e tests" >&2; exit 1; }; tmux -V; rc=0; for t in tests/*.test.sh; do echo "== $t =="; bash "$t" || rc=1; done; exit "$rc"
  • bash tests/fm-base-branch.test.sh (19/19 pass: spawn --base meta recording single+batch, scout/secondmate refusals, brief rewriting, review-diff local/origin base + missing-origin-base error, merge-local ff + wrong-checkout/divergence/missing-base refusals, teardown base-landing + missing-base refusal + content-fallback against origin/<base>)
  • full suite baseline command re-run: for t in tests/*.test.sh; do bash &#34;$t&#34;; done — 34 files, 502 ok, 0 failures, exit 0
  • manual e2e demo driving real bin/ scripts on a local-only project with long-lived feature/train branch: fm-brief.sh --base, fm-spawn.sh --base (meta shows base=feature/train), fm-spawn.sh --scout --base refusal, fm-review-diff.sh (diff base: feature/train, base-only commits excluded), premature fm-teardown.sh REFUSED naming feature/train, fm-merge-local.sh fast-forwarded feature/train with main unchanged, final fm-teardown.sh succeeded and removed the worktree
  • backward-compat check: brief generated without --base by HEAD vs 951f5cf fm-brief.sh is byte-identical for no-mistakes, direct-PR, and local-only modes
  • docs check: direct-PR brief with --base targets the base branch in its open-a-PR line; no-mistakes --base limitation documented in bin/fm-spawn.sh header and AGENTS.md sections 1/7
⚠️ **Document** - 1 info
  • ℹ️ CONTRIBUTING.md:64 - Pre-existing gap unrelated to this change: tests/fm-gotmp.test.sh (present at the base commit) is missing from CONTRIBUTING.md's enumerated test list; left unfixed because it is not part of the base-branch change being documented.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Copilot AI review requested due to automatic review settings July 3, 2026 03:53

Copilot AI 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.

Pull request overview

Captain, this PR adds a configurable base/merge-target branch for ship tasks so work can be branched from—and (for local-only/direct-PR flows) landed into—a long-lived feature branch instead of always using the repo default branch. The change is implemented by recording base=<branch> in state/<id>.meta and having the brief/review/merge/teardown helpers consistently honor it.

Changes:

  • Add fm-spawn.sh --base <branch> (ship only) to record base= in task meta, and fm-brief.sh --base to scaffold branch-from-base and base-targeted landing instructions (with explicit refusals for scout/secondmate).
  • Update fm-review-diff.sh, fm-merge-local.sh, and fm-teardown.sh to use the recorded base= as the review/merge/landing target, including guarded error paths for missing base branches.
  • Add a comprehensive behavior test suite for base-branch flows and document the feature + the no-mistakes limitation.

Reviewed changes

Copilot reviewed 5 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
bin/fm-spawn.sh Adds --base <branch> for ship tasks, records base= in meta, and refuses --base for scout/secondmate.
bin/fm-brief.sh Adds --base to scaffold branching from the base branch and retarget local-only/direct-PR wording; documents no-mistakes limitation.
bin/fm-review-diff.sh Uses recorded base= as diff base (origin-backed fetch when applicable) and errors explicitly when missing on origin.
bin/fm-merge-local.sh Fast-forwards the recorded base branch (or default) with the same safety refusals (clean checkout, ff-only, divergence refusal).
bin/fm-teardown.sh Treats recorded base= as landing target for local-only and content-landed checks; adds a fail-closed missing-base refusal for local-only.
tests/fm-base-branch.test.sh Adds behavior tests covering meta recording, refusals, brief rewriting, review/merge/teardown base targeting, and missing-base failure modes.
AGENTS.md Documents base= semantics, how it affects local-only landing, and updates usage examples and lifecycle notes.
docs/architecture.md Documents base= as the landing target and clarifies the no-mistakes PR-target limitation.
docs/scripts.md Updates script catalog entries to mention base-branch support in spawn/brief/merge/review/teardown.
CONTRIBUTING.md Adds the new base-branch test file to the enumerated test list.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

kunchenguid and others added 3 commits July 4, 2026 11:07
fm-spawn --base <branch> records base= in the task's meta (ship only;
refused for --scout/--secondmate; threaded through batch dispatch).
fm-brief --base scaffolds the ship Setup to branch from the base branch
and points local-only/direct-PR landing text at it. fm-review-diff,
fm-merge-local, and fm-teardown's landed-work checks read base= as the
single source of truth for the review base and landing target. A meta
without base= behaves byte-for-byte as before.
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.

3 participants