Skip to content

fix(ci): key the queue-flake anchor lookup on the file identity, not on finding + open - #14740

Merged
baozhoutao merged 2 commits into
mainfrom
claude/issue-14682-queue-flake-anchor-lookup
Sep 3, 2026
Merged

fix(ci): key the queue-flake anchor lookup on the file identity, not on finding + open#14740
baozhoutao merged 2 commits into
mainfrom
claude/issue-14682-queue-flake-anchor-lookup

Conversation

@claude

@claude claude Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #14682

The mechanism, as found on origin/main (4d0d9445a)

.github/workflows/merge-queue-triage.yml:624-627 looked the anchor up with

const res = await github.rest.issues.listForRepo({
  owner, repo, state: 'open', labels: ANCHOR_LABEL,   // ANCHOR_LABEL = 'finding' (:552)
  sort: 'created', direction: 'desc', per_page: 100, page: p,
});

and matched on the body marker queue-signature-anchor:KEY or the exact title
Queue-flake anchor: KEY (:632-633). The counting was never involved and is untouched here.

finding is a triage-state label: first-touch grading takes it off by definition. So the
moment an anchor is graded the lookup stops seeing it and the next ejection files a fresh one.
Measured against the live issues today, through the REST API:

issue state state_reason labels body marker
#14648 open null tests, priority:p1, pm:dispatched, domain:cli present
#14679 closed duplicate finding present

#14648 — the surviving anchor, the one the standing disposition names — is open and carries
the marker, and was invisible to the shipped lookup for one reason only: finding is gone. That
is the whole defect, and it is what triage 5516748133 suspected: closing the duplicates removes
the only anchors the lookup could still see, so correct triage was feeding the loop.

The fix

The anchor's identity is now the file key plus a machine-owned label the triage protocol
never touches, queue-flake-anchor, applied on create and adopted onto pre-existing anchors.
The lookup is a pass list, so what it selects is one readable literal:

const ANCHOR_QUERIES = [
  { state: 'all', labels: ANCHOR_IDENTITY_LABEL, firstMatchWins: false, adopts: false },
  { state: 'open', firstMatchWins: true, adopts: true },
];

Pass 1 is the identity query: any state, any triage grade, bounded because only anchors carry
the label, and it is the pass that owns the scanComplete argument (an unestablished absence still
refuses to create, unchanged). Pass 2 is transitional adoption — a bounded scan of OPEN issues that
finds anchors filed before the identity label existed (a graded one carries neither label), and puts
the identity label on them with issues.addLabels so pass 1 owns them from the next ejection on. A
later pass is skipped only once an open anchor is in hand: a half-migrated key can have a
labelled closed duplicate and an unlabelled open survivor at the same time.

Resolution order

  1. an OPEN anchor for the key (any labels) — refresh it; the oldest open one wins, because
    that is the issue the duplicates were closed against.
  2. the lookup could not be completed — announce and create nothing (unchanged; a duplicate
    anchor is worse than a late one).
  3. no open anchor and the newest closed one has state_reason: duplicate — create nothing,
    and name the closed anchor in the victim's comment. Filing here is exactly what feeds the loop:
    the new anchor would be closed as a duplicate in turn.
  4. no open anchor and the newest closed one was closed on its merits — the flake was answered
    once and the file is ejecting PRs again, so a new anchor is legitimate, and its body links the
    issue it regressed from
    (the link survives refreshes: the lookup now runs before the body is
    built, so the note is rebuilt every time rather than only at creation).
  5. nothing at all — create, now labelled finding + queue-flake-anchor.

Deviation from the dispatched order, with the measurement behind it. The order as dispatched
had branch 3 "follow to the canonical the duplicate points at, else treat as a regression". GitHub's
issue payload does not carry a duplicate's target: the read of #14679 above returns
state_reason: "duplicate" and no duplicate_of (the only related key on the payload is
sub_issues_summary). Where the canonical shares the key it is already found as an OPEN anchor by
branch 1 — that is the incident's own shape — and where it does not, it cannot be followed from
this channel at all. So branch 3 stops and says where the conversation was sent, rather than
guessing at a new home for it; the workflow names and decides nothing, which is its declared
boundary. Falling through to "create" instead would restart the exact loop this card is about.

Known transitional gap, stated rather than hidden: adoption is open-only. A pre-identity anchor
that is already closed is not adopted, because a bounded scan of this repo's ~14k closed issues
does not exist. Nothing regresses from that — it is the status quo — and it ages out as anchors
filed by this version carry the label from birth.

Pins added (scripts/check-merge-queue-triage-outcome.mjs)

Scenarios, driving the shipped bytes extracted from the YAML under doubles:

  • A14 — GRADED anchor: an OPEN anchor whose finding was taken off is REFRESHED, not
    duplicated. This is Queue-flake anchor: test/run-dev-unbuilt-workspace.e2e.test.ts #14648 as the API returned it, and it is the card's stated acceptance.
    Also asserts the adoption addLabels call.
  • A15 — the newest anchor is closed as a duplicate and an older one is open: the OLDER OPEN
    one is refreshed. The shape the incident actually reached.
  • A16 — every anchor for the key is closed as a duplicate: nothing is filed, the run is
    annotated, and the victim's comment names the closed anchor.
  • A17 — the only anchor was closed on its merits: a regression anchor IS filed, it links the
    closed one, and it carries the identity label.

The listForRepo double now honours state and labels. It did not before, and that is
load-bearing: a double that ignores the filters lets a lookup keyed on open + finding satisfy a
scenario about a graded anchor, so every one of these pins would have been unfalsifiable. A2's
rerun world now carries the created anchor's labels and state for the same reason.

Mutations added to --self-test (each asserts its anchor is present in the shipped source first):

  • M17 — the lookup reverts to selecting open + finding. Expects A14, A15, A16, A17 RED;
    keepGreen A1, A2 — A1 has no prior anchor and A2's anchor still carries finding, so both
    are found either way, which is what makes the four reds a reading of the graded/closed cases
    rather than of a lookup that stopped finding anything.
  • M18 — the closed-as-duplicate branch collapses into create. Expects A16 RED, keepGreen A14, A17.
  • M19 — a regression anchor stops naming the anchor it regressed from. Expects A17 RED,
    keepGreen A1, A14, A16.

The discriminating mutation, measured

Run over the committed tree, in memory (the substitution is applied to the extracted source, never
to the working tree, so there is no restore leg to get wrong):

anchor present in extracted source: yes
CLEAN  failures: 0
M17    failures by scenario: A14, A15, A16, A17
  [A14] NO second anchor is filed for a key that already has an open one, got 1: ["Queue-flake anchor: src/dev-plugin-security-enforcement-warning.test.ts"]
  [A14] the graded anchor #14648 is the one refreshed, got []
  [A15] the surviving OPEN anchor #14648 is refreshed -- not the closed duplicate, got []
  [A16] no anchor is filed into a closed-as-duplicate chain, got 1
  [A17] the new anchor LINKS the closed one, got: ...

A1 and A2 do not appear: the reds come from the graded and closed cases specifically.

Verification

Gate union derived in this worktree and run on the final commit b897b09da
(node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, 33 commands;
re-derived after merging origin/main and byte-identical to the pre-merge derivation), plus this
script's own --self-test, which the derived list does not name. Every exit code captured by
redirect-then-read, never through a pipe.

33 of 34 green, 1 NOT MEASURED. No reds.

node scripts/check-merge-queue-triage-outcome.mjs
  check-merge-queue-triage-outcome: OK (90 assertions over 24 scenarios, driving the
  33918-char script extracted from .github/workflows/merge-queue-triage.yml against real
  captured logs).

node scripts/check-merge-queue-triage-outcome.mjs --self-test
  check-merge-queue-triage-outcome --self-test: 126 assertions, 23 mutations of the shipped
  script each driven to red.

node scripts/check-self-test-wired.mjs
  check-self-test-wired: every one of the 164 script(s) CI runs that ship a `--self-test` has
  that self-test run by CI.

pnpm check:workflow-status-functions
  check-workflow-status-functions: OK (scanned 30 workflow file(s), 53 job(s), 26 job-level
  if: expression(s); 10 read needs.*.outputs.*, all naming a status function).

pnpm check:pm-dispatch-gates
  dispatch-gates self-test: 1240 cases pass.

node scripts/check-self-test-workflow-commands.mjs
  check-self-test-workflow-commands: no self-test CI runs prints a line the Actions runner
  would parse as a workflow command.

node scripts/check-nul-bytes.mjs
  check-nul-bytes: OK (scanned 8042 text file(s); no raw ASCII control bytes).
  plus a direct sweep of the two edited files for control bytes -- no hits.

NOT MEASURED (not a red): node scripts/check-test-completeness.mjs exits 3 with
PREREQUISITE NOT MET -- it grades a saved turbo run test log and the derived family names it
with no argument, so there is nothing local to hand it. Its own text says this branch is
unreachable in CI, which tees the log and passes the path. Recorded as NOT MEASURED, per that
gate's instruction, rather than as a pass or a failure.

The remaining 26 commands of the union all exited 0. The always-runs CI tail (workflows with no
path filter) is not part of this local reading and is CI's to run.

Governed-surface verdict, re-run on the final file list:

node scripts/pm/check-governed-merges.mjs --test .github/workflows/merge-queue-triage.yml scripts/check-merge-queue-triage-outcome.mjs
governed-surface predicate: 0 of 2 path(s) hit the register (5 surfaces, repo-agnostic).
  NOT governed -- ordinary queue landing applies to a PR with exactly this file list.

skip-changeset: the diff publishes nothing from any package — one workflow and the gate script
that tests it.

Out of scope and untouched, per the card's own boundary: the ejection counting and aggregation
(correct 5/5), the anchor body template beyond the regression line the resolution order needs, and
the live anchor issues themselves — #14648 and its duplicates are triage's.

git diff --stat dbf115284 HEAD -- .github/workflows/merge-queue-triage.yml scripts/check-merge-queue-triage-outcome.mjs
 .github/workflows/merge-queue-triage.yml     | 153 +++++++++++++++----
 scripts/check-merge-queue-triage-outcome.mjs | 214 +++++++++++++++++++++++++--
 2 files changed, 330 insertions(+), 37 deletions(-)

🤖 Generated with Claude Code

https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV


Generated by Claude Code

…on `finding` + `open`

The merge-queue-triage workflow looked its anchor up with
`listForRepo({ state: 'open', labels: 'finding' })`. `finding` is a triage-STATE
label -- first-touch grading takes it off by definition -- so the moment an
anchor was graded the lookup stopped seeing it and the next ejection filed a
fresh one. Measured on the live issues: the surviving anchor is open and carries
the body marker, and lost `finding` to grading; its duplicates are closed with
`state_reason: duplicate`. Correct triage was feeding the loop.

Identity is now the file key plus a machine-owned `queue-flake-anchor` label the
triage protocol never touches, applied on create and adopted onto pre-existing
anchors. Resolution order: an OPEN anchor for the key (oldest wins) is
refreshed; an unestablished absence still creates nothing; a closed-as-duplicate
chain with no open anchor is named rather than added to; a closed-on-its-merits
anchor makes the next ejection a REGRESSION, whose new anchor links the old one.
The counting and aggregation limb is untouched.

The contract test gains A14-A17 for the graded and closed cases, and its
`listForRepo` double now honours `state` and `labels` -- without that a lookup
keyed on `open` + `finding` would satisfy a scenario about a graded anchor and
every one of these pins would be unfalsifiable. Mutations M17-M19 drive the
battery red, M17 by reverting the lookup to the shipped selection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WLJQhde67SeTccsmnBVarV
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 2, 2026
@baozhoutao
baozhoutao marked this pull request as ready for review September 2, 2026 23:25
@baozhoutao
baozhoutao enabled auto-merge September 2, 2026 23:25
@baozhoutao
baozhoutao added this pull request to the merge queue Sep 2, 2026
Merged via the queue into main with commit 96b7723 Sep 3, 2026
36 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-14682-queue-flake-anchor-lookup branch September 3, 2026 00:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants