Skip to content

fix(fm-teardown): open own-PR vetoes the content-in-default landed check#988

Open
jokim1 wants to merge 1 commit into
kunchenguid:mainfrom
jokim1:fix/teardown-landed-false-positive
Open

fix(fm-teardown): open own-PR vetoes the content-in-default landed check#988
jokim1 wants to merge 1 commit into
kunchenguid:mainfrom
jokim1:fix/teardown-landed-false-positive

Conversation

@jokim1

@jokim1 jokim1 commented Jul 24, 2026

Copy link
Copy Markdown

Summary

bin/fm-teardown.sh's landed-work safety (work_is_landed -> content_in_default) can delete a ship task's state/<id>.meta, .status, .turn-ended, and PR-poll/check sidecars while that task's own PR is still open (green but unmerged).

content_in_default() (bin/fm-teardown.sh:385) only checks whether the branch's content is already in the up-to-date default branch, via git merge-tree --write-tree tree-equality. That is true both when the task's own PR squash-merged (intended) and when a sibling PR landed the same/overlapping content first (false positive). Because work_is_landed was pr_is_merged || content_in_default, an open PR did not stop the fallback, so a normal (no --force) teardown proceeded and reaped live work.

Impact (observed)

Three tasks on 2026-07-22 in one home lost .meta + .status after reaching done, with no intended teardown, all shared-surface slices:

Recovery required hand-rewriting .meta and re-running fm-pr-check before the still-open PRs could be merged. The watcher's .seen-*/.hb-surfaced-* markers survived with the same mtimes as the removed files, ruling out an age-based external reaper or git clean; the exact removed set matches teardown's own rm list. The mechanism was reproduced end to end against the real bin/fm-teardown.sh (open, unmerged PR + sibling content in default -> teardown removed .meta/.status/.turn-ended, exit 0, no REFUSED).

Fix

An open PR is affirmative proof the task's own work has not landed, so content_in_default (a fallback for the unknown/absent PR-state case) must not override it.

In work_is_landed(): when a pr= is recorded and GitHub reports it OPEN, refuse without consulting content_in_default. The content fallback is unchanged for tasks with no recorded PR, a gh lookup error, or a merged PR (squash-merge-then-delete still works). The refusal now tells the operator the PR is still open.

Test coverage

Extends tests/fm-teardown.test.sh with the sibling-content scenarios:

  • recorded pr= reported OPEN + content already in default via a sibling -> REFUSE (new open-PR veto).
  • recorded pr= reported MERGED + content in default -> ALLOW (own PR genuinely landed; veto must not over-refuse).
  • the existing no-PR + content-in-default case (test_content_in_default_fallback_allows) is unchanged.

All teardown cases pass; bin/fm-lint.sh is clean (shellcheck 0.11.0, the pinned version).

Fixes #986

work_is_landed() fell back to content_in_default() whenever the task's own
PR was not merged. That fallback only asks "is this branch's content already
in the default branch?" via tree-equality, which is true both when the task's
own PR squash-merged (intended) and when a SIBLING PR carried the same content
while this task's own PR is still open (false positive). A normal teardown then
deleted the task's state records (.meta/.status/.turn-ended/PR-poll sidecars)
while its PR was live - reaping three real tasks on 2026-07-22.

An open PR is affirmative proof the task's own work has not landed. When a pr=
is recorded and GitHub reports it OPEN, work_is_landed now refuses without
consulting content_in_default. The content fallback is unchanged for tasks with
no recorded PR, a gh error, or a merged PR (squash-then-delete still works). The
refusal now tells the operator the PR is still open.

Extends tests/fm-teardown.test.sh with the sibling-content scenarios: recorded
OPEN PR + content in default -> REFUSE; recorded MERGED PR + content in default
-> ALLOW. The existing no-PR + content-in-default case is unchanged.

Refs kunchenguid#986
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.

fm-teardown: content_in_default fallback reaps an unmerged ship task's state when a sibling PR's content is already in the default branch

1 participant