Factory's post-merge "advance to Done" path can close an issue in one repository because a pull request merged in a different repository, and it has already closed real, unfixed work as complete. At least 8 issues were falsely closed; 2 more closures in the same sample were legitimate.
All resolutions in this report were measured on 2026-08-16 against origin/main at f13600f32107e499bfccc6f971f7863fe5822927. Absence claims decay — re-measure before relying on them.
Discovered during the PR-triage sweep of #178 / #146.
The consequence, first
The clearest instance is #211 — "Key dispatch claims on work-unit identity, not surface path."
That issue was closed as completed on 2026-08-12. Its defect is still on origin/main, verbatim, at src/orchestrator/batch-tracker.ts:292:
export const issueKey = (issue: IssueRef): string => `${issue.key}:${issue.uuid}:${issue.path}`
So the mechanism described below closed, as complete, the very ticket that describes why a bare locator is not an identity. This is #228's thesis ("one identity model behind the duplicate-claim, dual-attribution, and 403 symptoms") demonstrated on Factory's own reconciliation path.
Evidence
Every one of these issues was closed by a comment of the exact form:
Factory observed pull request #N merge and completed this issue.
That string is emitted from exactly one place — src/orchestrator/factory.ts:11922 — so the closures are unambiguously attributable to the code path analysed below.
As of 2026-08-16, the highest number in AgentWorkforce/factory across both the issue and PR spaces is #275. Cited numbers above that cannot exist in this repository in any form.
| factory issue |
closed (UTC) |
cites |
resolves in factory as |
same number in AgentWorkforce/cloud |
| #211 |
2026-08-12 |
#274 |
did not exist in any form on the closure date — factory's high-water mark was then ~#236, so the citation pointed ~38 numbers into the future |
cloud#274 — chore: delete dead sandbox Dockerfiles |
| #221 |
2026-08-14 |
#1518 |
does not exist, any type (above factory's whole number space) |
cloud#1518 — neon: split runtime and migration DB |
| #139 |
2026-08-13 |
#2891 |
does not exist, any type (above factory's whole number space) |
cloud#2891 — fix(relayauth): dormant emergency source mint gate |
| #146 |
2026-08-13 |
#2895 |
does not exist, any type (above factory's whole number space) |
cloud#2895 — fix(relayauth): restore bounded mint and webhook recovery |
| #216 |
2026-08-13 |
#219 |
exists, but as an ISSUE, not a merged PR — "Stale-mount auto-refresh targets a stale directory", created 2026-08-07 |
cloud#219 is a PR |
| #145 |
2026-07-21 |
#1346 |
does not exist, any type (above factory's whole number space) |
cloud#1346 — Bump relayfile adapter-core trigger catalog |
| #143 |
2026-07-21 |
#1346 |
does not exist, any type (above factory's whole number space) |
same as above |
| #260 |
2026-08-16 |
#1533 |
does not exist, any type (above factory's whole number space) |
cloud#1533 — Revert #1532 (S6 wiring) |
| #222 |
2026-08-15 |
#250 |
real, merged — legitimate closure |
— |
| #141 |
2026-07-20 |
#148 |
real, merged — legitimate closure |
— |
Two precision notes, because a reviewer who checks will otherwise find the discrepancy and stop reading:
Mechanism
This was read from the code, not inferred from the pairs above.
A merged-PR mount event knows exactly which repository it came from. src/orchestrator/factory.ts:11771 builds it:
const repo = `${parts.owner}/${parts.repo}`
and passes it down at :11833 → #advanceMergedPrToDone(snapshot, repo, record).
Inside #advanceMergedPrToDone (:11893) there are two paths:
- With a tracked in-flight record — repository-qualified and correct. The record lookup at :11877 filters on
recordMatchesGithubRepo(record, eventRepo, this.#config.repos.org).
- Without a record (the agents have already been released) — the fallback at :11905:
const issue = await this.#findMergeAdvanceIssueForPr(snapshot)
repo is in scope on that line and is not passed. #findMergeAdvanceIssueForPr (:11963) takes only the snapshot, then scans every issue path from the configured source (#githubIssuePaths(), :11977) and scores each one with prSnapshotIssueMatchScore(snapshot, issue.key) (:11991). Nothing in that loop compares the issue's repository to the merged PR's repository.
The active contract has issueSource: "github" and 19 repositories in repos.names, so that scan spans all 19 — each with its own independent number space.
The scorer (:17166) is:
const prSnapshotIssueMatchScore = (snapshot: PullSnapshot, issueKey: string): number => {
if (factoryBranchMatchesIssue(snapshot.headRef ?? '', issueKey)) return 30
if (containsIssueKey(snapshot.title ?? '', issueKey)) return 20
if (containsExplicitIssueReference(snapshot.body ?? '', issueKey)) return 10
return 0
}
For a GitHub-native issue, issue.key is a bare number. containsExplicitIssueReference (src/issue-key-match.ts:20-31) then matches that bare number against the PR body via, among others:
#211(?!\d)
https?://github\.com/[^\s/]+/[^\s/]+/issues/211(?!\d)
The URL alternative accepts [^\s/]+/[^\s/]+ — any owner and any repository. A merged PR in cloud whose body references cloud's own #211 therefore scores against factory's issue #211.
Two aggravating details in the same function:
- The in-flight scan de-duplicates score ties and bails out (
babysitterPrDiscoveryAmbiguous, :11886). The fallback scan has no ambiguity guard — on a tie the first match silently wins.
- The closure comment then reports
snapshot.number (:11922), which is the other repository's PR number. That is why the citations look like they point at nonexistent factory PRs: they are real PR numbers, just not from this repository.
Open question
I have not reconstructed which specific merge event fired each individual closure — the mount projection history for those moments is not available to me, and several cited numbers resolve to PRs in other in-scope repos that merged months before the closure date (cloud#274 merged 2026-04-22; factory#211 closed 2026-08-12). So a re-projection or backfill of an already-merged PR is likely part of the trigger, and worth confirming. The missing repository qualification at :11905 is read directly from the code and does not depend on that reconstruction.
Suggested direction
Pass the known repo into #findMergeAdvanceIssueForPr and require the candidate issue's repository to equal the merged PR's repository — the same qualification the in-flight path already applies at :11877. Adding the fallback's own ambiguity guard would close the tie-break gap alongside it.
Per #228, the durable fix is that a claim belongs to the work unit, not to a bare locator: a number is only an identity once qualified by its repository.
Not done here
The 8 falsely-closed issues are being audited separately to determine whether each one's work actually landed on main. Nothing has been reopened — that decision is deliberately left to a human.
Factory's post-merge "advance to Done" path can close an issue in one repository because a pull request merged in a different repository, and it has already closed real, unfixed work as complete. At least 8 issues were falsely closed; 2 more closures in the same sample were legitimate.
All resolutions in this report were measured on 2026-08-16 against
origin/mainatf13600f32107e499bfccc6f971f7863fe5822927. Absence claims decay — re-measure before relying on them.Discovered during the PR-triage sweep of #178 / #146.
The consequence, first
The clearest instance is #211 — "Key dispatch claims on work-unit identity, not surface path."
That issue was closed as completed on 2026-08-12. Its defect is still on
origin/main, verbatim, atsrc/orchestrator/batch-tracker.ts:292:So the mechanism described below closed, as complete, the very ticket that describes why a bare locator is not an identity. This is #228's thesis ("one identity model behind the duplicate-claim, dual-attribution, and 403 symptoms") demonstrated on Factory's own reconciliation path.
Evidence
Every one of these issues was closed by a comment of the exact form:
That string is emitted from exactly one place —
src/orchestrator/factory.ts:11922— so the closures are unambiguously attributable to the code path analysed below.As of 2026-08-16, the highest number in
AgentWorkforce/factoryacross both the issue and PR spaces is #275. Cited numbers above that cannot exist in this repository in any form.factoryasAgentWorkforce/cloudchore: delete dead sandbox Dockerfilesneon: split runtime and migration DBfix(relayauth): dormant emergency source mint gatefix(relayauth): restore bounded mint and webhook recoveryBump relayfile adapter-core trigger catalogRevert #1532 (S6 wiring)Two precision notes, because a reviewer who checks will otherwise find the discrepancy and stop reading:
factory#274does exist today — it was opened 2026-08-16 by an unrelated lane, four days after this closure. The claim is that on 2026-08-12 the cited number did not exist in any form.Mechanism
This was read from the code, not inferred from the pairs above.
A merged-PR mount event knows exactly which repository it came from.
src/orchestrator/factory.ts:11771builds it:and passes it down at :11833 →
#advanceMergedPrToDone(snapshot, repo, record).Inside
#advanceMergedPrToDone(:11893) there are two paths:recordMatchesGithubRepo(record, eventRepo, this.#config.repos.org).repois in scope on that line and is not passed.#findMergeAdvanceIssueForPr(:11963) takes only the snapshot, then scans every issue path from the configured source (#githubIssuePaths(), :11977) and scores each one withprSnapshotIssueMatchScore(snapshot, issue.key)(:11991). Nothing in that loop compares the issue's repository to the merged PR's repository.The active contract has
issueSource: "github"and 19 repositories inrepos.names, so that scan spans all 19 — each with its own independent number space.The scorer (:17166) is:
For a GitHub-native issue,
issue.keyis a bare number.containsExplicitIssueReference(src/issue-key-match.ts:20-31) then matches that bare number against the PR body via, among others:The URL alternative accepts
[^\s/]+/[^\s/]+— any owner and any repository. A merged PR incloudwhose body references cloud's own#211therefore scores againstfactory's issue #211.Two aggravating details in the same function:
babysitterPrDiscoveryAmbiguous, :11886). The fallback scan has no ambiguity guard — on a tie the first match silently wins.snapshot.number(:11922), which is the other repository's PR number. That is why the citations look like they point at nonexistent factory PRs: they are real PR numbers, just not from this repository.Open question
I have not reconstructed which specific merge event fired each individual closure — the mount projection history for those moments is not available to me, and several cited numbers resolve to PRs in other in-scope repos that merged months before the closure date (cloud#274 merged 2026-04-22; factory#211 closed 2026-08-12). So a re-projection or backfill of an already-merged PR is likely part of the trigger, and worth confirming. The missing repository qualification at :11905 is read directly from the code and does not depend on that reconstruction.
Suggested direction
Pass the known
repointo#findMergeAdvanceIssueForPrand require the candidate issue's repository to equal the merged PR's repository — the same qualification the in-flight path already applies at :11877. Adding the fallback's own ambiguity guard would close the tie-break gap alongside it.Per #228, the durable fix is that a claim belongs to the work unit, not to a bare locator: a number is only an identity once qualified by its repository.
Not done here
The 8 falsely-closed issues are being audited separately to determine whether each one's work actually landed on
main. Nothing has been reopened — that decision is deliberately left to a human.