Skip to content

fix(repo-fleet-hygiene): report merged evidence on a protected branch - #2689

Merged
kyle-sexton merged 4 commits into
mainfrom
fix/f2-merged-protected-branch
Aug 15, 2026
Merged

fix(repo-fleet-hygiene): report merged evidence on a protected branch#2689
kyle-sexton merged 4 commits into
mainfrom
fix/f2-merged-protected-branch

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Closes #2687

Summary

A branch carrying exact-OID merged-PR evidence that is also protected fell through both arms of the pr_match block and emitted nothing. The merged-worktree arm requires a non-main worktree; the merged-local-branch arm requires protected=false. A branch attached to the main worktree, or the canonical checkout's current branch, satisfies neither — so the strongest merge evidence the collector holds was computed and then discarded.

The tell that this was an oversight rather than a protection rule: the weaker merged-pr-tip-drift immediately below carries no protection guard at all and does emit. Silence on the strong path read as "nothing merged" rather than "merged, but protected".

Fix

Adds an else arm emitting LOW merged-protected-branch, naming the PR number, the matching headRefOid, and which protection applies.

The protection rule is unchanged. The new kind is deliberately absent from branch_action_kind() — which matches only merged-local-branch — so it is reported and can never become a cleanup candidate, inflate the rollup candidate count, or enter the action plan.

Documented in reference/confidence-model.md; the repo already gates on every emitted kind appearing in that tier table, and that gate caught the omission during development.

Verification

Replaying the exact conditions the collector evaluates, across all five reachable input shapes:

BEFORE:
  unattached, unprotected            HIGH merged-local-branch
  attached to NON-main worktree      HIGH merged-worktree
  attached to MAIN worktree          <NOTHING EMITTED>
  current branch, unattached         <NOTHING EMITTED>
  no merged PR                       <NOTHING EMITTED>

AFTER:
  unattached, unprotected            HIGH merged-local-branch
  attached to NON-main worktree      HIGH merged-worktree
  attached to MAIN worktree          LOW merged-protected-branch
  current branch, unattached         LOW merged-protected-branch
  no merged PR                       <NOTHING EMITTED>

Two cases go from silent to reported; the other three are byte-identical, so no existing classification changes.

Also run: bash -n clean, shellcheck -x clean, markdownlint-cli2 0 errors on the reference doc.

What I could not verify, stated plainly

The full audit-fleet.test.sh suite does not complete on Windows under Git Bash. It exceeded 500s here with no buffered output; two independent earlier attempts hit the same wall at a 9-minute budget. So the decision-table evidence above comes from isolating the arm, not from a suite run.

I drafted a fixture assertion and then reverted it, because I could not run the suite to confirm it, and an unvalidated fixture edit to a shared harness risks breaking CI for everyone else. The shape a follow-up needs: a branch attached to the main worktree (index 0 of the worktree porcelain list) that is not the default branch, plus a matching row in the PR fixture table. Note the default branch cannot be used — it is excluded from merge-evidence collection upstream, which I discovered by trying it first.

That suite's Windows runtime is worth its own issue; it is now the second finding this session that could not be closed by running the tests that exist.

No version bump or CHANGELOG entry — a consolidating bump is landing separately, and parallel PRs staging the same version collide on check-changelog-parity.sh --check-order.

Related

Supersedes the mechanism described in #2600, which was closed by #2629 against a moved-identity stop that does not exist. That fix is real but addresses a different path; this defect survived it, which is why the symptom recurred after the issue was closed.

N/A

@cursor

cursor Bot commented Aug 15, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 4s —— View job


Security review — no issues found

Scope: plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.sh (+21/-1) and plugins/repo-fleet-hygiene/skills/audit/reference/confidence-model.md (+1/-0).

  • Gather context (PR diff, changed files)
  • Run skip gate check
  • Review changed files for security-relevant surface
  • Report findings

The change adds a single else arm in analyze_repo() (audit-fleet.sh#L2231-L2253) that emits a LOW merged-protected-branch finding when exact-OID merged-PR evidence exists for a protected branch, plus a matching doc row.

Checked for the things this lane cares about:

  • Injection / trust-boundary: the new values ($branch, $pr_num, $pr_oid, $pr_url, $protection_reason) flow only into bash array fields (F_CONF, F_KIND, etc.) via emit_finding, then out through print_field/printf. Nothing here is eval'd, passed to bash -c, or interpolated into a shell/SQL/template command — same sink pattern as the existing merged-local-branch / merged-worktree arms, so no new injection surface.
  • Authorization/logic: branch_action_kind() (audit-fleet.sh#L1510-L1512) matches only the literal string merged-local-branch, confirming the new merged-protected-branch kind is excluded by construction — it can't be picked up as a cleanup candidate, inflate the rollup, or enter the fleet action plan. The protection check itself (default_branch / current_branch / is_main) is unchanged, only the reporting behavior on that branch is new.
  • GitHub Actions surface: no workflow files touched.
  • Supply chain: no dependency/action-pin changes.

This is an informational-only reporting fix with no security-relevant behavior change. No findings to report.

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

Last security-reviewed head: eb8271ef857889f75d97b6f2905c495347c69005. On the next push, the relevance gate compares only the commits since this SHA; delete this comment to force a full re-review.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9313504fcf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.sh Outdated
Comment thread plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.sh Outdated
@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 4m 30s —— View job


Code review complete

Scope: plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.sh and plugins/repo-fleet-hygiene/skills/audit/reference/confidence-model.md (the two files changed in this PR).

What the fix does: Adds an else arm to the pr_match handling in analyze_repo() (audit-fleet.sh) so a protected branch carrying exact-OID merged-PR evidence emits LOW merged-protected-branch instead of being silently dropped. Verified merged-protected-branch is correctly excluded from both branch_action_kind() (audit-fleet.sh:1510-1512) and worktree_action_kind(), so it can't inflate candidate counts or reach the action plan — matches the PR's claim. The emit_finding call signature matches usage. The tier-table drift test in audit-fleet.test.sh:1480-1509 does mechanically enforce that every emitted kind has a documented row, which is a real, existing gate (confirmed by reading it), consistent with what the PR description says caught the original omission.

Finding posted inline (1, Important): The if [[ "$branch" == "$default_branch" ]] ... protection_reason="default branch" check added at audit-fleet.sh:2243-2244 is dead code. pr_match is only ever populated when branch != $default_branch (the pre-existing guard at line 2211, combined with the unconditional reset at line 2210), so this else arm — and thus this specific default-branch check — can never be reached when branch == default_branch. This lines up with the PR's own decision table, which enumerates five reachable input shapes and doesn't include a "default branch" row, and with the PR body's own note that "the default branch cannot be used — it is excluded from merge-evidence collection upstream." Functionally harmless (no wrong output), but it's misleading dead code, and the parallel claim in confidence-model.md:28 ("the branch is the default branch, ...") documents a disposition the collector can't actually produce — flagged with a lighter note there too, since it isn't caught by the emitted/documented kind-set equality test (that test only checks kinds appear in the table, not that every clause of a kind's evidence description is reachable).

Verified independently:

  • branch_action_kind() / worktree_action_kind() correctly exclude the new kind — read directly.
  • The tier-table parity test exists and does what the PR says — read directly.
  • The dead-code path above — traced by reading the guard/reset logic across lines 2206-2254.

Not independently verified (author-claimed only): bash -n, shellcheck -x, markdownlint-cli2, and the "five reachable input shapes" replay in the PR body — this session's sandbox required approval for direct script execution (bash -n, shellcheck, gh pr view) that wasn't available in this lane, so these are stated as author-claimed and unverified rather than confirmed. Static review of the guard logic didn't turn up anything that would contradict the claimed before/after table.

Everything else in the diff (variable declaration, emit_finding call shape, the two new elif branches for current_branch/is_main, the doc row's confidence/disposition columns) reads correctly.

Comment thread plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.sh Outdated
Comment thread plugins/repo-fleet-hygiene/skills/audit/reference/confidence-model.md Outdated
Comment thread plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.sh Outdated
@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

Claude has reviewed this PR 3 times. The lane skips further automatic reviews after 5; deleting this comment resets the count.

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 5m 2s —— View job


Code review complete

Scope: files changed since the last review pass (commit 9313504) — this synchronize event added 7da876d, which is test-only (audit-fleet.test.sh, +27/-1). audit-fleet.sh and confidence-model.md are unchanged since the prior Claude review.

  • Gather context (PR diff, prior review comments)
  • Review new commit 7da876d (test coverage added since last review)
  • Check whether prior dead-code finding (default-branch check) was addressed
  • Post findings

New finding posted inline (1, Important) on audit-fleet.test.sh:238: the new "F2" fixture, meant to exercise is_main==true (a branch attached to the main worktree) for the merged-protected-branch arm, instead inserts a second worktree porcelain record for the same canonical-a path — so the tested branch lands at array index 1, not index 0. is_main is only ever set true for index 0 (audit-fleet.sh#L2199-L2203), so this branch actually resolves to attached=true, is_main=false and falls into the pre-existing HIGH merged-worktree arm, not the new LOW merged-protected-branch arm the fixture claims to cover. The two new assert_contains checks (audit-fleet.test.sh#L744-L747) are unscoped (plain string match against the whole run output, not assert_kind_targets), and since no other fixture in the suite reaches merged-protected-branch either (every canonical here mocks current_branch == default_branch == main, so the "current branch, unattached" row is unreachable everywhere), the suite likely fails on this addition rather than validating it. Full trace and a suggested fix are in the inline comment.

Caveat: this sandbox's allowed-tools for this lane don't include running the test script (bash audit-fleet.test.sh required approval that isn't available here), so this is a static trace through audit-fleet.sh's array-index and guard logic, not an executed confirmation. Worth an actual run to verify before merge, given the author's own note that the full suite doesn't complete for them on Windows either.

Prior findings from earlier review passes remain unaddressed (no code in audit-fleet.sh/confidence-model.md changed since 9313504):

  • The unreachable default_branch check inside the new else arm (audit-fleet.sh#L2243-L2244) — pr_match can never be populated when branch == default_branch (guard at line 2211), so this branch of the elif chain is dead code, flagged by two prior Claude passes and Codex.
  • The matching "the branch is the default branch" phrase in confidence-model.md:28 documents that same unreachable case.

Neither is a correctness bug (dead code, no wrong output), so I'm not re-posting duplicate inline comments on unchanged lines — just noting they're still open.
· branch fix/f2-merged-protected-branch

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 43s —— View job


Security review — no issues found

Scope: commits since the last-reviewed head 9313504 → current head 7da876d (the test: commit only touches plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.test.sh, +27/-1).

  • Gather context (diff since last-reviewed SHA)
  • Run skip gate check
  • Review changed files for security-relevant surface
  • Report findings

Skip gate: triggered

The only change since the last security review is test-fixture data and assertions in audit-fleet.test.sh:

  • New hardcoded worktree-porcelain and PR-lookup fixture lines (feature/main-attached, PR 47, fixed OIDs/dates) exercising the merged-protected-branch arm added in the prior commit.
  • A new assert_contains pair and a grep -A6 -F ... | grep -Fq regression check confirming the finding doesn't route into Run /repo-hygiene:clean git.

All values are static, author-authored test literals — no new user input, no injection sink, no execution of tainted data, no workflow/permissions/dependency changes. This has no security-relevant surface, so per the skip gate this lane stops here with nothing further to report.

(For reference, the 9313504 commit itself — the actual merged-protected-branch logic — was already reviewed with no findings; see the earlier review comment on this PR.)

Comment thread plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.test.sh
A branch carrying an exact-OID merged PR that is ALSO protected fell
through both arms of the pr_match block and emitted nothing. The
merged-worktree arm requires a non-main worktree; the merged-local-branch
arm requires protected=false. A branch attached to the main worktree, or
the canonical checkout's current branch, satisfies neither, so the
strongest merge evidence the collector holds was computed and discarded.

The tell that this was an oversight rather than a protection rule: the
weaker merged-pr-tip-drift below carries no protection guard at all and
does emit. Silence on the strong path therefore read as "nothing merged"
rather than "merged, but protected".

Adds a LOW merged-protected-branch finding naming which protection
applies. The protection rule itself is unchanged -- the kind is
deliberately absent from branch_action_kind(), so it is reported and never
becomes a cleanup candidate or an action-plan entry.

Note the default branch cannot reach this arm: it is excluded from merge
evidence collection upstream. The reachable cases are a main-worktree
attached branch and the current branch.
The fleet-finding-test-coverage gate requires every emitted finding kind to
carry a Finding: assertion in the collector's test file, and correctly
red-lined the new kind.

Fixture is a branch attached to the MAIN worktree that is not the default
branch: merged-worktree needs a non-main worktree and merged-local-branch
needs protected=false, so it reaches the new arm and nothing else. The
default branch cannot stand in — merge evidence collection excludes it
upstream.

Asserts the finding is emitted, that it names the protection reason, and
that it never routes to branch cleanup.
@kyle-sexton
kyle-sexton force-pushed the fix/f2-merged-protected-branch branch from 7da876d to eb8271e Compare August 15, 2026 06:46
@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 2m 51s —— View job


Code review complete

Scope: all three changed files at head eb8271e (audit-fleet.sh, audit-fleet.test.sh, confidence-model.md). This is the CODE-REVIEW lane (/review:code-review); security is out of scope (a claude-security-review lane already covered this PR with no findings).

  • Gather context (PR diff vs origin/main, prior review threads on this PR)
  • Review changed files for correctness/maintainability findings
  • Post findings

Finding posted inline (1, Important) on audit-fleet.test.sh:238: the new "F2" fixture still doesn't exercise the is_main path the PR's fix depends on — this is the identical defect flagged on the prior commit's review round, reproduced rather than fixed in this one. It appends a second worktree porcelain record for canonical-a instead of replacing the first, so feature/main-attached lands at array index 1. is_main is only ever true at index 0 (audit-fleet.sh#L2200-L2203), so the branch is attached=true, is_main=false, protected=false and falls into the pre-existing HIGH merged-worktree arm, not the new LOW merged-protected-branch arm. Since no other fixture in the suite reaches merged-protected-branch, the two new assert_contains checks at audit-fleet.test.sh#L744-L747 have nothing to match and the suite looks set to fail on this addition rather than validate the fix — the opposite of what this commit is meant to accomplish. Traced statically (root-cause + full evidence chain in the inline comment); this sandbox's Bash lacks approval to actually execute audit-fleet.test.sh, so please run it to confirm before merge.

Not re-flagged (already open from 3 prior review rounds, unchanged in this commit, non-blocking): the unreachable default_branch check inside the new else arm (audit-fleet.sh#L2243-L2244) and the matching phrase in confidence-model.md#L28 are still dead/unreachable — pr_match can never be populated when branch == default_branch (guard at line 2211). Dead code, no wrong output, so not re-posting a 4th duplicate inline comment on unchanged lines.

Everything else in the diff — the protection_reason variable declaration, the emit_finding call shape for merged-protected-branch, the current_branch/is_main elif branches (both reachable and correct, unlike the default_branch one), and the new confidence-model table row's confidence/disposition columns — reads correctly.
· branch fix/f2-merged-protected-branch

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude finished @kyle-sexton's task in 1m 42s —— View job


Security review — no issues found

Scope: plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.sh, plugins/repo-fleet-hygiene/skills/audit/reference/confidence-model.md, plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.test.sh (all 3 files changed in this PR, head eb8271e).

  • Gather context (PR diff since last-reviewed head)
  • Run skip gate check
  • Review changed files for security-relevant surface
  • Report findings

Skip gate: triggered

This synchronize event's new commits (eb8271e and its now-superseded predecessor ee9b48d) are a rebase onto updated main (picking up the unrelated c38fe71 cloud-bootstrap split), not new PR content. Diffing this PR's three changed files directly against the last-reviewed head (7da876d) confirms they are byte-identical:

git diff 7da876d..eb8271e -- audit-fleet.sh confidence-model.md audit-fleet.test.sh
(no output)

Per the skip gate, a head whose reviewable surface is unchanged from an already-reviewed head gets no further security pass. The prior review of this same content (comment, run 31869142485) found no security-relevant surface: the merged-protected-branch arm only routes existing string/OID values through emit_findingprint_field/printf (no eval, no shell/SQL/template interpolation), branch_action_kind() still excludes the new kind by construction, and the test-fixture-only commit adds static, author-authored literals with no new trust boundary. Nothing in this synchronize event changes that assessment.

(Unrelated to security: the non-security review threads on this PR have open findings — dead default_branch code in the new else arm, and a test-fixture bug in the "F2" case that may not exercise the path it claims to — both still present in this identical content. Those are logic/test-correctness issues, out of scope for this security lane.)

Comment thread plugins/repo-fleet-hygiene/skills/audit/scripts/audit-fleet.test.sh
CI ran the suite on Linux and showed the fixture never reached the new
arm. is_main is `wt_index -eq 0` — the FIRST worktree record — so putting
feature/main-attached at index 1 gave attached=true with is_main=false,
which routes to merged-worktree instead. The branch is now the main
worktree's own record.

Three shapes fail to reach the arm and only the fourth works: the default
branch (excluded from merge-evidence collection upstream), a non-first
worktree record (routes to merged-worktree), an unattached non-current
branch (routes to merged-local-branch), and the main worktree checked out
on a non-default branch. Recorded in the fixture comment.

Adds the version bump and CHANGELOG entry that check-changelog-parity
requires whenever shipped plugin files change. The earlier no-bump
approach was correct only while several PRs were staged in parallel and
would have collided on --check-order; this is the sole open PR touching
the plugin, so the bump is both required and safe.
@github-actions github-actions Bot deleted a comment from claude Bot Aug 15, 2026
@github-actions github-actions Bot deleted a comment from claude Bot Aug 15, 2026
@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

Warning

Automated review did not complete — this is an infrastructure failure, not a review verdict.

Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."

Re-run the job, or workflow_dispatch this workflow with the PR number, to retry the review. A new push re-triggers this lane only if the caller's pull_request triggers include synchronize (the canonical caller omits it).
An automatic retry may already have run — it is skipped when a partial review could duplicate comments, or when the failure class needs an operator (auth).

@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

Warning

Automated security review did not complete — this is an infrastructure failure, not a review verdict.

Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."

The check is green on purpose, and it is not evidence. It certifies that a security pass ran, and this one did not complete — but the cause is outside this PR's control, so merging is deliberately left unblocked rather than locking every merge for the length of the outage. Nothing was reviewed at this head. Where this check is required, it is satisfied without that evidence; a human should review security-sensitive changes here before merging.

Re-run the job, or workflow_dispatch this workflow with the PR number, to retry the review (ci-workflows#227). A new push also retries it only if the caller's pull_request triggers include synchronize (the canonical security caller keeps it). An automatic retry may already have run — it is skipped when a partial review could duplicate comments, or when the failure class needs an operator.

Re-running does NOT help for every class:

  • rate-limit that persists across re-runs, or auth — the credential or usage budget needs an operator; retrying will not clear it.
  • a run that exhausted its turn budget ("subtype":"error_max_turns" above) will exhaust it again. As the PR author, split the change into smaller PRs; raising --max-turns is a change to the caller workflow, not something you can set on this PR.

Two P2/Important review findings, both correct.

Confidence: the evidence here is the same successful MERGED PR with an
exact headRefOid match that gives merged-local-branch and merged-worktree
HIGH. The confidence model separates evidence strength from disposition,
so downgrading to LOW conflated the two. Raised to HIGH; the protection
stays in the disposition, and the kind remains absent from
branch_action_kind() so it is still never a cleanup candidate.

Dead code: the default_branch protection-reason arm can never execute.
pr_match is populated only under [[ "$branch" != "$default_branch" ]] and
is reset each iteration, so a default branch never enters the enclosing
block at all. I noted that reachability in the PR body and then left the
unreachable branch in the code. Removed, with a comment recording why the
case is impossible rather than merely absent.

Reference table and CHANGELOG updated to match both.
@github-actions github-actions Bot deleted a comment from claude Bot Aug 15, 2026
@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 1s —— View job


I'll analyze this and get back to you.

@github-actions github-actions Bot deleted a comment from claude Bot Aug 15, 2026
@claude

claude Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@kyle-sexton
kyle-sexton merged commit f196432 into main Aug 15, 2026
40 checks passed
@kyle-sexton
kyle-sexton deleted the fix/f2-merged-protected-branch branch August 15, 2026 08:01
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.

repo-fleet-hygiene: exact-OID merged evidence on a protected branch is computed and discarded

1 participant