Skip to content

ci-fix: dedup can't see a sibling triage tracker for the same failed run, and a sibling's rerun gets attributed to the maintainer #867

Description

@worktrunk-bot

Problem

When a consumer keeps a nightly-failure tracker issue, tend-ci-fix and tend-triage both fire on the same failed run, run two full independent diagnoses, and ci-fix opens a second diagnostic issue duplicating the one triage just closed. Two consequences, one of them a publicly posted false claim:

  1. Duplicate diagnosis. Neither session knows the other exists. ci-fix's only issue-side dedup is the repeat-occurrence escalation query in skills/ci-fix/SKILL.md, which searches --search "ci-fix: in:title" — its own artifacts. A tracker titled Nightly tests failed is structurally unreachable by it.
  2. Fabricated actor attribution. When the sibling triage session reruns the failed job, the ci-fix session sees attempt 2 pass without having triggered it, and names an actor it never queried. In the case below it wrote "The maintainer's rerun (attempt 2) of the same job on the same commit passed" — the maintainer did nothing; the bot's own sibling ran gh run rerun.

Evidence — two occurrences of the same shape in max-sixty/worktrunk

2026-08-06 (nightly run 31077344052, schedule, full-tests (macos-15, macos), transient curl [18] Transferred a partial file from crates.io):

time (UTC) event
07:50:16 create-issue-on-nightly-failure opens #3751 Nightly tests failed (label ci)
07:50:20 tend-triage fires on issues: opened (31082458414, $2.51 / 35 turns)
07:50:22 tend-ci-fix fires on workflow_run: completed for the same run (31082460480, $1.58 / 27 turns)
~07:54 triage runs gh run rerun 31077344052 --failed → attempt 2, job 92555098189
07:54:55–08:05:04 both sessions poll that same job to completion; ci-fix spends ~9 min waiting on a rerun it did not trigger
08:06:21 triage posts its diagnosis on #3751 and closes it
08:06:37 ci-fix opens #3752 with a near-identical diagnosis, containing the "maintainer's rerun" claim
08:06:38 ci-fix closes #3752

2026-07-30 (nightly run 30519615581, transient crates.io HTTP/2 framing error): #3667 Nightly tests failed opened 07:37:08, closed by triage 07:40:51 — and ci-fix opened #3668 7 seconds later at 07:40:58. Same duplicate-diagnosis shape, and here the tracker was already closed when ci-fix filed, so even a dedup query scoped to open issues would have missed it.

Both are diagnostic issues, self-closed within seconds, so the waste is bounded — no duplicate fix PR. The attribution error is the sharper defect: it is exactly the class running-in-ci → Grounded Analysis exists to prevent, and it is now permanent public text.

Mechanism for the attribution error

gh api repos/{owner}/{repo}/actions/runs/<id> on a reran scheduled run returns:

"run_attempt": 2, "actor": "max-sixty", "triggering_actor": "worktrunk-bot"

.actor stays the original run's actor (the repo owner, for a schedule run) while .triggering_actor is who reran it. So the field a session would naturally reach for is the misleading one, and the session here queried neither — it inferred "the maintainer" from the fact that a rerun existed. Its own reasoning text said only "The rerun (attempt 2) … I polled it to completion"; the attribution appeared for the first time when the issue body was written.

Why #627's closure doesn't cover this

#627 is the same cross-trigger race, and it was resolved for PRQL by removing that repo's create-issue-on-nightly-failure job so only one path remains. worktrunk deliberately keeps its tracker — .github/nightly-failure.md with update_existing: true, so a streak of nightly failures accumulates on one human-visible issue, and nightly.yaml's link-check job cites it in-comment as the intended destination for link rot. So the race persists in any adopter that keeps a tracker, which is the configuration #627's own proposal 3 was written for:

The ci-fix bundled skill could add a step before the dedup recheck: search for open issues that reference the failed workflow_run.id and exit if a triage issue is open.

That proposal was never landed, and #627 was closed on the consumer-side workaround instead.

Proposed fix

Both are small and in skills/ci-fix/SKILL.md:

  1. Widen the pre-diagnosis dedup beyond ci-fix:-titled issues. Before diagnosing, look for any recent issue — open or recently closed, since the 07-30 case closed 7 s ahead — whose body cites the failed run id, and defer to it:

    gh api "repos/$GITHUB_REPOSITORY/issues?state=all&since=$(date -u -d '2 hours ago' +%Y-%m-%dT%H:%M:%SZ)&per_page=100" \
      --jq "[.[] | select((.body // \"\") | contains(\"/runs/$RUN_ID\"))] | {number, title, state}"

    A hit means a sibling is handling (or has handled) this failure: comment on that issue if there is anything to add, and skip the duplicate diagnostic issue. Replayed against this window with RUN_ID=31077344052 it returns #3751, so the check does fire on the real data — the tracker template is Nightly tests [failed on {{ date }}]({{ env.LINK }}) with LINK pointing at the failed run, and the same URL is in #3667's body for the 07-30 case. Worth noting the timing is genuinely concurrent — the two workflows start ~2 s apart — so this is a defer-on-discovery check, not a race-free gate; it works here because the sibling's tracker is created before either session starts.

  2. Never name who reran a job without reading it. When citing a rerun as evidence, read .triggering_actor (not .actor, which is stale on reruns) — and if the rerun wasn't yours, "attempt 2 passed" is the claim to make, not who caused it.

Happy to send a PR for either if the shape looks right.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions