Repository navigation
Conversation
…ks on a later branch A direct-PR worker that stacks several pull requests in one copy moves on to the next part on a new local branch before pushing it. Merging the earlier, pushed and green pull request then failed the named-head gate, because the gate tested the copy's HEAD, which is the later unpushed commit. On the merge path, fm-pr-check now also accepts when the pull request's own head branch in the copy is the forge's head or is reachable outside the copy. An unpushed commit on that branch is still refused, and arming, the ship done gate, GitLab, Gerrit and local-only are unchanged.
|
…ng now preserves tracking refs, and publication refusals occur before readiness registration. All four relevant test suites and lint pass. CI and Require no-mistakes reported action_required with zero jobs; those workflow blocks are external to the code
# Conflicts: # bin/fm-dod-lib.sh
|
Speaking as Kun's firstmate: reviewed against main What I checked: What still needs fixing (Greptile P1 on
The no-mistakes review also left one info open: CI: approved fork runs Contract-class: new-default. The goal (a pushed, green PR shouldn't be blocked by later unrelated work) is reasonable. But the tip replaces the specified merge-time gate. Main gates the worker copy's HEAD, and this gates the PR's head branch with a new ancestry rule. It also drops the merge-time "PR ready" line to the parent channel. That's a change to a security-relevant default merge path, not a repair of a broken specified one, so even when ready it goes to the captain rather than auto-merging. VISION.md, rule by rule
Next step: push a fix for the P1. Once a fresh no-mistakes raise and green CI are on the new head, this becomes a captain-decision hold, not an auto-merge. |
…GitLab fork branch-name collision), as you asked last round. That fix was never in HEAD 2d6a5f0, so I added it now. The problem: when a GitLab merge request came from a fork, the merge check compared the MR against any local branch in the copy that happened to have the same name as the fork's source branch. That branch is unrelated, so the check could refuse a mergeable MR. Changes in bin/fm-pr-merge.sh: - `gitlab_verify_mergeable` now also reads whether the MR comes from a fork, meaning its source and target project ids differ. It stores that as `FM_PR_MERGE_FORK`. The expected field count goes from 9 to 10. - `require_pr_branch_published` skips the same-named local-branch comparison for a fork MR. It falls through to the existing HEAD gate (`fm_dod_accept_ship_done`) instead. - I updated the header comment to say this. Same-project MRs, GitHub and unreadable-branch refusals behave as before. A payload missing both project ids is treated as same-project, so it gets the stricter check it had before. Test changes in tests/fm-pr-merge.test.sh: - The GitLab MR fixture (`write_mr_json`) now includes `source_project_id` and `target_project_id` (both 1 by default), with a `source_project=` override. - New test `test_gitlab_fork_ignores_a_same_named_local_branch`, which sets up an unrelated local `fm/task-x1` branch in a direct-PR copy: - **Fork case** (`source_project_id` 2): the merge now goes through, bound to `--sha` of the verified head. This case fails without the fix and passes with it. - **Same-project case**: still refused, with "named head <sha> could not be verified in pull request head <head>". Verification: - `tests/fm-pr-merge.test.sh`: 109 ok, rc=0 - `tests/fm-pr-check-security.test.sh`: rc=0 - `bin/fm-lint.sh` and shellcheck on both changed files: clean - `bin/fm-test-run.sh --check-coverage`: ok No code change for CI and Require no-mistakes (ci-1 and ci-3): per your decision they're waiting on the upstream maintainer's fork-workflow approval and aren't caused by this code. I left the changes uncommitted for the pipeline to record
…fuse") with the option (b) you chose: GitLab fork MRs no longer get a special case. In the stacked case, a direct-PR fork MR whose part-1 is pushed and green no longer gets refused because the worker's HEAD is on an unpushed part-2. bin/fm-pr-merge.sh is back to exactly what it was at 2d6a5f0: - removed FM_PR_MERGE_FORK - removed the `fork=` field (the field count is back from 10 to 9) - removed the extra fork condition in require_pr_branch_published - removed the fork clause in the header comment Fork MRs now get the same source-branch check as same-project MRs. tests/fm-pr-merge.test.sh: - removed test_gitlab_fork_ignores_a_same_named_local_branch and its call - kept the source_project_id/target_project_id fields in the write_mr_json fixture, so a test can describe a fork MR - added a `fork` case to test_gitlab_direct_pr_merge_checks_the_source_branch: direct-PR mode, a fork MR (source_project_id 2) whose part-1 is pushed and green, HEAD on an unpushed part-2. It must merge, and the merge must be bound with --sha to part-1's verified head. Verification: - The new fork case fails against the previous script ("gitlab-stacked-fork: a pushed part-1 was refused for a later unpushed branch: expected exit 0, got 1") and passes with the fix. - tests/fm-pr-merge.test.sh: 108 ok, rc=0 (one fewer than before because a test was removed). It prints one harmless "No such file" message from a check that the parent-replies file is absent; this change doesn't touch that code. - tests/fm-pr-check-security.test.sh: rc=0. - bin/fm-lint.sh: clean. `shellcheck -x` on both files: clean. - bin/fm-test-run.sh --check-coverage: ok. As you decided, a fork MR is now refused if the copy happens to have an unrelated local branch with the same name as the fork's source branch. That only blocks a merge (it never lets a bad one through), and there is no code for it. No code change for CI (ci-1) or Require no-mistakes (ci-3): as you decided, they're waiting on the upstream maintainer's approval for the fork workflow and this code doesn't cause them. The changes are left uncommitted for the pipeline to record
…nge, building on the work the timed-out agent left in the run worktree. The CI and Require no-mistakes checks need no code change, as you decided. **The fix, in bin/fm-pr-merge.sh** - `gitlab_verify_mergeable` now also records the MR's source project id when it differs from the target project's (`FM_PR_MERGE_FORK_PROJECT`, new `fork_project=` field; the field count goes from 9 to 10). It reads this from the same MR view it already uses. - New `local_branch_is_pr_source <wt> <branch>`: - Same-project MR (the ids match): it returns 0 straight away. - Fork MR: it returns 0 only when the local branch's upstream remote URL exactly matches the fork's `web_url`, `http_url_to_repo` or `ssh_url_to_repo`. It reads those with `glab api projects/<id>` on the MR's own host. - `require_pr_branch_published` now uses the source-branch ancestry check only when that helper returns 0. Otherwise it falls back to the HEAD gate (`fm_dod_accept_ship_done`). - GitHub is unchanged. I updated the header comment to describe the fork scoping. **Tests, in tests/fm-pr-merge.test.sh** - The fake `glab` answers `api projects/2` with the fork's URLs. - `test_gitlab_direct_pr_merge_checks_the_source_branch` now builds its fork cases with part-1 tracking the fork remote: - `fork`: HEAD is on an unpushed part-2, and the merge goes through, bound with `--sha` to part-1. - `fork-unpushed-fix`: still refused. - Both fork cases assert the fork lookup went to the MR's host. - New `test_gitlab_fork_ignores_an_unrelated_same_named_branch` covers an unrelated local `fm/task-x1` that tracks origin: - Fork MR with HEAD pushed: merges. - Fork MR with HEAD unpushed: refused by the HEAD gate ("named head … is unreachable outside the worker copy"). - Same-project MR: still refused ("named head <sha> could not be verified in pull request head <head>"), as before. - docs/secondmate-parent-channel.md is untouched, as you asked. **Verification** - tests/fm-pr-merge.test.sh: 109 ok, rc=0. - tests/fm-pr-check-security.test.sh: rc=0. - bin/fm-lint.sh: clean. `shellcheck -x` on both changed files: clean. - Regression check against the previous script (HEAD d7d9b9b): - With the previous `bin/fm-pr-merge.sh`, the new namesake test fails: "gitlab-namesake-fork-pushed-head: a fork merge request was refused for an unrelated same-named branch: expected exit 0, got 1". - The updated stacked-fork case also fails there, on its fork-lookup assertion. I left the changes uncommitted so the pipeline records them, as in earlier rounds
…"Fork lookup bypasses branch check") as you instructed. CI and Require no-mistakes need maintainer workflow approval, not a code change, so I left them alone. **What was wrong:** for a GitLab merge request from a fork, if the `glab api projects/<id>` lookup failed, the script treated the local branch with the same name as absent. It then checked the worker copy's HEAD instead. So a pushed HEAD could let the merge through even when a fix on the MR's branch was never pushed. The URL check was also an exact string match, so the same project written as `https://…/`, `ssh://git@…` or `git@host:path` didn't count as a match. **Fix, in bin/fm-pr-merge.sh:** - `local_branch_is_pr_source` now returns one of three results: 0 = this is the MR's source branch, 1 = it is not, 2 = the fork could not be read. - It reads the fork first. If glab fails, the reply isn't valid JSON, or it contains none of `web_url` / `http_url_to_repo` / `ssh_url_to_repo`, it returns 2. - Only after a successful read does it return 1, including for a branch with no upstream. - `require_pr_branch_published` refuses on 2 with "source project <id> could not be read to tell whether local branch <branch> is the merge request's source branch". It never falls back to the HEAD check on an unknown. On 1 it uses the HEAD check (`fm_dod_accept_ship_done`) as before. On 0 it checks the MR branch is contained in the verified head, as before. - New `project_url_key` reduces a URL to the lowercase host plus the path. It drops the user part, a trailing slash and `.git`, and it handles `https://`, `ssh://git@host/path` and `git@host:path`. Both the local remote URL and the fork's URLs go through it before comparing. - I updated the header comment. GitHub and same-project MRs are unchanged. **Tests, in tests/fm-pr-merge.test.sh:** - The fake `glab` can now be made to fail the `api projects/2` call. - `test_gitlab_direct_pr_merge_checks_the_source_branch` has three new stacked-fork cases. part-1 tracks the fork through a different URL form in each, and HEAD is on an unpushed part-2. All three merge, bound with `--sha` to part-1: - `fork-slash`: `https://GitLab.Example/fork-owner/project/` (trailing slash, mixed-case host) - `fork-ssh`: `ssh://git@…/project.git` - `fork-scp`: `git@host:path` without `.git` - `test_gitlab_fork_ignores_an_unrelated_same_named_branch` has three new cases, all with HEAD pushed: - `fork-no-upstream`: the lookup succeeds and the branch has no upstream, so the HEAD check applies and the merge goes through. - `fork-lookup-fails` (glab exits 1) and `fork-lookup-empty` (a 404 reply with no URLs): both are refused with the new error, and no merge is attempted. - The existing cases are unchanged: the plain `fork` case, the unpushed fixes, the unpushed HEAD, and same-project. **Verification:** - tests/fm-pr-merge.test.sh: 109 ok, rc=0. It still prints one harmless "No such file" line for parent-replies.status, from code this change doesn't touch. - tests/fm-pr-check-security.test.sh: rc=0. - bin/fm-lint.sh: clean. `shellcheck -x` on both changed files: clean. - Against the HEAD (0ca990f) script, the new tests fail: - "gitlab-stacked-fork-slash: a pushed part-1 was refused for a later unpushed branch: expected exit 0, got 1" - "gitlab-namesake-fork-lookup-fails: an unreadable fork fell back to the copy's pushed HEAD: expected exit 1, got 0" - `project_url_key` gives the same result for all six URL forms under macOS /bin/bash 3.2. I left the changes uncommitted so the pipeline records them, as in earlier rounds
|
@kunchenguid thanks for the review. Fork P1 is addressed, new head is
CI and Require no-mistakes are waiting on workflow approval again for the new head. |
|
Speaking as Kun's firstmate: re-reviewed head What changed since my last look (
There are no workflow edits and no new network calls beyond the read-only CI: approved fork runs Contract-class: new-default (unchanged). The tip replaces the specified merge-time publication gate. Main checks the worker copy's HEAD, and this checks the PR's own head branch with an ancestry rule. It also stops the merge-time re-record from sending the parent "PR ready" line. That makes it a captain call, not an auto-merge. VISION.md, rule by rule
Next step: once CI and Require no-mistakes are green on |
|
@kunchenguid thanks for the re-review. CI on The other failure, |
…lel 2", following your round-6 instructions. "Behavior portable serial 4" failed on something else that this PR doesn't touch. **The failure in parallel 2:** tests/fm-captain-hold-lifecycle.test.sh failed with "the PR merge entrypoint refused a home carrying no backlog". Its fixture records kind=ship, mode=ship and a worktree that doesn't exist. `require_pr_branch_published` in bin/fm-pr-merge.sh only skipped no-mistakes mode. For every other mode it ran `cd "$wt" && gh pr view`, which fails when the worktree is gone, so it refused with "pull request head branch could not be verified". Main never checks this case, because `fm_dod_should_gate_ship_done` does not gate that mode. I reproduced the failure by running just that test locally. **Fix in bin/fm-pr-merge.sh (`require_pr_branch_published`):** 1. It now returns 0 when `fm_dod_should_gate_ship_done` would not gate the task's kind, mode and `done: PR <url>` line. This sits next to the existing no-mistakes early return. 2. GitHub's headRefName is read by URL (`gh pr view "$URL" --json headRefName`) without going into the worktree. It is still required and validated. 3. The local-branch ancestry check only runs when the worktree is an existing directory with that branch. Otherwise it goes through `fm_dod_accept_ship_done` as main does, so a direct-PR task with a missing worktree is still refused with "worktree missing". The same rule applies to GitLab. The guard also stops `git -C ""` from quietly running in the current directory when the recorded worktree is empty. 4. I updated the header comment to match. **Tests:** - tests/fm-captain-hold-lifecycle.test.sh: the fake gh now answers `--json headRefName`. With fix 1 the mode=ship fixture returns before it ever asks, so this answer isn't exercised today. - tests/fm-pr-merge.test.sh: new `test_missing_worktree_merge_follows_the_mode`. A no-mistakes merge with the recorded worktree gone still merges, bound with `--match-head-commit`. A direct-PR one is refused with "named head cannot be verified: worktree missing" and no merge is attempted. - With the previous script the direct-PR case fails ("the refusal did not name the missing worktree"). The no-mistakes case passed before too, since that early return was already there. The captain-hold test is the one that reproduces the CI failure. **Verification:** - tests/fm-captain-hold-lifecycle.test.sh: 61 ok, rc=0, including the case that failed in CI. - tests/fm-pr-merge.test.sh: 110 ok, rc=0. It still prints three "No such file" lines from `assert_no_grep` on parent-replies.status, which the merge-time re-record no longer writes after the earlier R5-1 change. These are harmless, and the assertions pass. - tests/fm-pr-check-security.test.sh: rc=0. - bin/fm-lint.sh, `shellcheck -x` on the changed files, and `bin/fm-test-run.sh --check-coverage`: clean. **Not changed:** - Serial 4 failed in tests/fm-remote-reply.test.sh ("the NUL-bearing line was not captured"). That's a timed wait in a test that doesn't use fm-pr-merge, fm-pr-check or fm-dod-lib, so I left it alone as you said. - Require no-mistakes needs nothing. I left the changes uncommitted so the pipeline records them, as in earlier rounds
|
Speaking as Kun's firstmate: re-reviewed new head What changed since my last stamp ( CI: approved fork runs Contract-class: new-default (unchanged). The tip replaces the specified merge-time publication gate. Main checks the worker copy's HEAD, and this checks the PR's own head branch with an ancestry rule. It also stops the merge-time re-record from sending the parent "PR ready" line. That makes it a captain call, not an auto-merge. VISION.md, rule by rule
Next step: once CI and Require no-mistakes are green on |
|
@kunchenguid CI is green on |
|
Speaking as Kun's firstmate: thanks @anconina, and thanks for the nudge. You're right: CI and both Require no-mistakes runs are green on One thing has changed since then. Main moved on, and GitHub now reports this branch as conflicting with main ( Contract-class: new-default (unchanged). The tip replaces the specified merge-time publication gate: main checks the worker copy's HEAD, and this checks the PR's own head branch with an ancestry rule. It also stops the merge-time re-record from sending the parent "PR ready" line. Because it's a captain decision and not an auto-merge, I won't rebase it on your behalf. That part stays with you. VISION.md, rule by rule (unchanged from my 10-07 review; no code moved since): one captain, one interface: aligns (fewer bogus refusals, no "PR ready" before a refused merge). Authority: aligns (merge authority unchanged; unpublished commits on the PR branch, or a direct-PR task whose copy is gone, still refuse). Scripts own the mechanics: aligns. A restart is a non-event: aligns. Delegation with a spine: aligns (publication still proven before anything lands; live validation go 10 of 14). The fleet outlives any vendor: aligns. Scope: aligns. What would help (on you), whenever you have time: merge or rebase onto current main, resolve Next step: waiting on you for the rebase. Once it's green and mergeable, it goes to Kun as a merge-or-leave decision, and from then on it isn't waiting on you. |
|
@kunchenguid thanks for the heads-up about the conflict. The branch is up to date now. New head:
CI and Require no-mistakes are waiting on fork workflow approval for the new head. |
|
Speaking as Kun's firstmate: thanks @anconina, this is just what was needed. I checked the merge commit Contract-class: new-default (unchanged). The merge-time publication gate now checks the PR's own head branch instead of the worker copy's HEAD. That replaces a specified default path, so it goes to Kun and won't be auto-merged. VISION.md per rule (unchanged; this push changes no logic): one captain, one interface — aligns. Authority — aligns (merge authority is unchanged, and unpublished commits still refuse). Scripts own the mechanics — aligns. A restart is a non-event — aligns. Delegation with a spine — aligns. Fleet outlives any vendor — aligns (GitHub and GitLab). Scope — aligns. Next step: waiting on CI. Nothing more is needed from you right now. Once CI and no-mistakes are green and the PR is mergeable, it goes to Kun as a merge-or-leave decision. |
|
CI is back on Looking at the log for that job, the four tests that finished all exited 0. I can't re-run the job from the fork. Could you re-run "Behavior portable serial 2" on run 38073758071 before merging, so it has a real verdict on this head? |
|
Speaking as Kun's firstmate: thanks @anconina, and thanks for reading the log. You're right: the four tests that finished all exited 0, Head Next step: waiting on CI only. Nothing more is needed from you. Once serial 2 is green and the PR is still mergeable, it goes to Kun as a merge-or-leave decision. Triage won't auto-merge it. |
|
Speaking as Kun's firstmate: thanks @anconina for merging main and working through the Forgejo interaction. What changed since my 2026-10-10 stamp ( CI: the fork runs were Contract-class (mine, unchanged): new-default. The merge-time gate checks the PR's own head branch instead of the worker copy's HEAD. VISION.md per rule (unchanged): all rules align; delegation and vendor neutrality are, if anything, strengthened by the explicit Forgejo handling. Next: waiting on CI only. Nothing is needed from you. Once it's green and still mergeable, it goes to Kun as a merge-or-leave decision. Triage won't auto-merge it. |
I kept hitting this while building stacked PRs in one task copy. Part 1 was pushed and green, then I moved on to part 2 on a new local branch without pushing it. Merging the part-1 PR refused with "named head is unreachable outside the worker copy" because the gate was checking my current HEAD. I had to push the unfinished next part to unblock the earlier merge, about eight times in one day.
To reproduce, push branch A and open a PR, wait for its checks to pass, then create branch B and commit without pushing. Run
bin/fm-pr-merge.shfor A. The old gate refuses because B exists only in the copy.For GitHub and GitLab direct-PR tasks, the merge now checks the PR's own head branch against the head just verified on the forge. Its local tip must equal that head or be an ancestor of it. A missing head is fetched by SHA without switching branches or moving refs. Later work on another branch no longer blocks the merge; unpushed commits on the PR's own branch still do, even if another remote branch contains them. A missing local PR branch falls back to the existing HEAD gate. GitLab fork MRs only use a local branch when its upstream remote matches the source fork; an unreadable source project refuses the merge.
In no-mistakes mode, GitHub and GitLab merges bind to the pipeline-verified forge head; a later commit in the copy is left out of the merge and stays protected there. The pipeline is the publisher in that mode, so the copy can have an older branch after pipeline validation. I added behavior tests for both forges with a later unpushed commit on the same branch, alongside the direct-PR refusal cases.
Forgejo arrived with the upstream merge and deliberately keeps its old registration-time HEAD gate. Without the carve-out, it would lose its only publication check. Extending the stacked-PR behavior to Forgejo would be a separate follow-up.
The merge-time re-record still records
pr=and arms the merge poll before live verification, but it no longer sends a premature "PR ready" line to the parent channel. Ordinary PR registration, the ship done gate, Gerrit, local-only landing, and merge authority keep their existing behavior.One capacity side effect remains: recording
pr=frees a declared project-capacity place. A direct-PR merge attempt that has not registered its PR can now record it before refusing an unpublished PR-branch commit, freeing that place while work remains in the copy. Existing red-check, conflict and draft refusals already leave the record that way.I merged current upstream main with an ordinary merge, without rebasing or force-pushing. The conflict was in the
fm-pr-merge.shheader; I kept the existing publication contract and upstream's Forgejo notes, then preserved Forgejo's old gate explicitly. The merge, PR-registration security, project-capacity and captain-hold lifecycle suites all passed, as did pinned ShellCheck/actionlint and the documentation audience check. The regression cases cover pushed stacked branches, unpublished PR-branch commits, missing or rebased heads, GitLab fork identity, and the suppressed parent ready line. The pipeline also drove 33 runs of the real scripts against a local stand-in forge backed by a real bare Git repository, including before-and-after checks against upstream main. I have not performed a real end-to-end merge against a hosted forge.Pipeline
Updates from git push no-mistakes
... (3 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)
🔧 Fix applied.
3 issues (1 warning, 2 infos) still open:
bin/fm-pr-check.sh:169- The refusal this change sets out to remove is still reachable on Forgejo. Intent: "when the PR being merged has its head on the remote and its checks are green, a later unpushed commit on a different branch in the copy must not block that merge." The merge resolution keeps Forgejo on the registration-time HEAD gate (|| { [ "$PROVIDER" != github ] && [ "$PROVIDER" != gitlab ]; }), and the Forgejo merge case (bin/fm-pr-merge.sh:1856) never calls require_pr_branch_published. Sequence: Forgejo task in direct-PR mode (or no-mistakes, since Forgejo records no pr_head), part-1 pushed and green, copy HEAD on an unpushed part-2 -> fm-pr-merge.sh -> merge-time fm-pr-check.sh -> fm_dod_accept_ship_done gates the copy's HEAD -> "named head <sha> is unreachable outside the worker copy". tests/fm-pr-merge.test.sh:2452 pins that gate. The carve-out itself is correct: without it Forgejo would lose its only named-head check. So this is a scope decision, and the remedy rather than the defect needs authorization: either accept GitHub/GitLab-only for this PR (the header and docs already state it), or extend the publication check to Forgejo (read.head.refand fork identity in forgejo_verify_mergeable, call require_pr_branch_published before the tea merge, drop the provider carve-out).bin/fm-dod-lib.sh:709- Tradeoff of the strict containment check: a direct-PR branch whose history was rewritten on the forge (GitLab's Rebase button, GitHub's "Update with rebase") is refused at merge. The copy's branch tip is no longer an ancestor of the verified head, so require_pr_branch_published reports "named head <tip> could not be verified in pull request head <head>" until the worker resets its local branch to the rebased head. Upstream accepted this sequence only while the copy's remote-tracking refs were stale. A forge-side merge-commit update still passes, because the missing head is fetched by SHA and the tip stays an ancestor. Strict containment is the documented and tested design, so no action is requested.tests/fm-pr-merge.test.sh:2986- The comment in test_failed_merge_reports_nothing still says "The registration's ready line is a fact of its own; only a merge line would misreport the unlanded merge." That is no longer true: since this branch's earlier commit f8b343b, the merge-time re-record in bin/fm-pr-check.sh:266 sends no PR-ready line, so in this test the parent channel file is never written at all. The same commit updated the two sibling comments (tests/fm-pr-merge.test.sh:2938 and :3007) to say so but left this one behind; it was not introduced by this run's fix round. The assertion under it is still correct. Reword the comment to match the siblings, for example: the merge-time re-record sends no ready line, and a merge line would misreport the unlanded merge. No behaviour change is involved.🔧 Fix applied.
2 issues (1 warning, 1 info) still open:
bin/fm-pr-check.sh:169- The refusal this change sets out to remove is still reachable on Forgejo. Intent: "when the PR being merged has its head on the remote and its checks are green, a later unpushed commit on a different branch in the copy must not block that merge." The merge resolution keeps Forgejo on the registration-time HEAD gate (|| { [ "$PROVIDER" != github ] && [ "$PROVIDER" != gitlab ]; }), and the Forgejo merge case (bin/fm-pr-merge.sh:1856) never calls require_pr_branch_published. Sequence: Forgejo task in direct-PR mode (or no-mistakes, since Forgejo records no pr_head), part-1 pushed and green, copy HEAD on an unpushed part-2 -> fm-pr-merge.sh -> merge-time fm-pr-check.sh -> fm_dod_accept_ship_done gates the copy's HEAD -> "named head <sha> is unreachable outside the worker copy". tests/fm-pr-merge.test.sh:2452 pins that gate. The carve-out itself is correct: without it Forgejo would lose its only named-head check. So this is a scope decision, and the remedy rather than the defect needs authorization: either accept GitHub/GitLab-only for this PR (the header and docs already state it), or extend the publication check to Forgejo (read.head.refand fork identity in forgejo_verify_mergeable, call require_pr_branch_published before the tea merge, drop the provider carve-out).bin/fm-dod-lib.sh:709- Tradeoff of the strict containment check: a direct-PR branch whose history was rewritten on the forge (GitLab's Rebase button, GitHub's "Update with rebase") is refused at merge. The copy's branch tip is no longer an ancestor of the verified head, so require_pr_branch_published reports "named head <tip> could not be verified in pull request head <head>" until the worker resets its local branch to the rebased head. Upstream accepted this sequence only while the copy's remote-tracking refs were stale. A forge-side merge-commit update still passes, because the missing head is fetched by SHA and the tip stays an ancestor. Strict containment is the documented and tested design, so no action is requested.✅ **Test** - passed
✅ No issues found.
--match-head-commit 2e04b9d...(the head pushed from the second clone), origin main gained part-1.txt and review.txt, and 'worker copy branches,…bash bin/fm-test-run.sh --json <evidence>/targeted-suites.json tests/fm-pr-merge.test.sh tests/fm-pr-check-security.test.sh tests/fm-project-capacity.test.sh tests/fm-captain-hold-lifecycle.test.sh- total=4 failed=0 skipped_gate=0bash drive-stacked-merge.sh <worktree> 'this change' <scenario>for 16 scenarios: github-stacked, github-own-branch-unpushed, github-own-branch-amended, github-stacked-own-branch-unpushed, github-fix-only-on-another-remote-branch, github-forge-head-ahead, github-branch-absent-unpushed-head, github-no-mistakes-unpushed, gitlab-stacked, gitlab-own-branch-unpushed, gitlab-fix-only-on-another-remote-branch, gitlab-no-mistakes-unpushed, gitlab-fork-stacked, gitlab-fork-own-branch-unpushed, gitlab-fork-unreadable-stacked, forgejo-stackedThe same driver against the base commit's scripts (git archive 20dcbf91 bin) for github-stacked, gitlab-stacked, github-own-branch-unpushed, github-fix-only-on-another-remote-branch, github-no-mistakes-unpushed, gitlab-no-mistakes-unpushedFM_LIVE_REGISTER_FIRST=1runs of github-stacked and gitlab-stacked on both the change and the base: register part 1 withbin/fm-pr-check.sh, move on to an unpushed part 2, thenbin/fm-pr-merge.shFM_LIVE_CHECK=FAILURErun of github-stacked: a red check still refuses the stacked mergeFM_LIVE_MATE=1runs in a secondmate lab home: a refused merge, and a register-then-merge, with state/parent-replies.status inspected afterwardsFM_LIVE_ENTRY=fm-pr-check.shruns: ordinary registration with an unpushed HEAD on the change (GitHub, GitLab, own-branch fix) and on the basegh pr view https://github.com/kunchenguid/firstmate/pull/6497 --json headRefName -q .headRefNamewith the real gh against github.com (read-only) returnedfm/fm-merge-stacked-headRead-onlycurlof gitlab.com's public merge request and project records confirmed the fields the change reads: sha, source_branch, source_project_id, target_project_id, web_url, http_url_to_repo, ssh_url_to_repo✅ **Document** - passed
✅ No issues found.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.