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:
- 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.
- 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:
-
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.
-
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.
Problem
When a consumer keeps a nightly-failure tracker issue,
tend-ci-fixandtend-triageboth fire on the same failed run, run two full independent diagnoses, andci-fixopens a second diagnostic issue duplicating the onetriagejust closed. Two consequences, one of them a publicly posted false claim:ci-fix's only issue-side dedup is the repeat-occurrence escalation query inskills/ci-fix/SKILL.md, which searches--search "ci-fix: in:title"— its own artifacts. A tracker titledNightly tests failedis structurally unreachable by it.triagesession reruns the failed job, theci-fixsession 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 rangh 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), transientcurl [18] Transferred a partial filefrom crates.io):create-issue-on-nightly-failureopens #3751Nightly tests failed(labelci)tend-triagefires onissues: opened(31082458414, $2.51 / 35 turns)tend-ci-fixfires onworkflow_run: completedfor the same run (31082460480, $1.58 / 27 turns)gh run rerun 31077344052 --failed→ attempt 2, job925550981892026-07-30 (nightly run 30519615581, transient crates.io HTTP/2 framing error): #3667
Nightly tests failedopened 07:37:08, closed by triage 07:40:51 — andci-fixopened #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:.actorstays the original run's actor (the repo owner, for aschedulerun) while.triggering_actoris 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-failurejob so only one path remains. worktrunk deliberately keeps its tracker —.github/nightly-failure.mdwithupdate_existing: true, so a streak of nightly failures accumulates on one human-visible issue, andnightly.yaml'slink-checkjob 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: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: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: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=31077344052it returns #3751, so the check does fire on the real data — the tracker template isNightly tests [failed on {{ date }}]({{ env.LINK }})withLINKpointing 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.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.