@davidvornholt/standards 0.17.0 → 0.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidvornholt/standards",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
4
4
  "description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -91,9 +91,8 @@ const acknowledgeItems = async (options: {
91
91
  // biome-ignore lint/performance/noAwaitInLoops: acknowledgement writes are deliberately serialized to avoid GitHub secondary rate limits.
92
92
  const approval = await approvalFor(deps, item, kind);
93
93
  if (
94
- approval !== null &&
95
- typeof approval !== 'string' &&
96
- (await acknowledgeQueuedJob(deps, item.number, approval, kind))
94
+ approval?.kind === 'approved' &&
95
+ (await acknowledgeQueuedJob(deps, item.number, approval.approval, kind))
97
96
  ) {
98
97
  lines.push(
99
98
  `${deps.repo} ${kind === 'fix' ? `#${item.number}` : `PR #${item.number}`}: acknowledged as queued`,
@@ -19,15 +19,45 @@ export type ApprovalBinding = {
19
19
  readonly target: string;
20
20
  };
21
21
 
22
+ export type ApprovalBindingResult =
23
+ | {
24
+ readonly kind: 'approved';
25
+ readonly approval: ApprovalBinding;
26
+ }
27
+ | {
28
+ readonly kind: 'absent';
29
+ }
30
+ | {
31
+ readonly kind: 'rejected';
32
+ readonly generationId: string;
33
+ readonly reason: string;
34
+ };
35
+
22
36
  type ApprovalContext = {
23
37
  readonly token: string | null;
24
38
  readonly repo: string;
25
39
  readonly issueNumber: number;
26
40
  };
27
41
 
42
+ export type ApprovalTargetReader = () => Promise<string | null>;
43
+
28
44
  const stableDigest = (value: unknown): string =>
29
45
  createHash('sha256').update(JSON.stringify(value)).digest('hex');
30
46
 
47
+ const rejectionGenerationId = (
48
+ context: ApprovalContext,
49
+ label: string,
50
+ target: string,
51
+ event: Awaited<ReturnType<typeof lastLabelEvent>>,
52
+ ): string =>
53
+ stableDigest({
54
+ repo: context.repo,
55
+ issueNumber: context.issueNumber,
56
+ label,
57
+ target,
58
+ event,
59
+ });
60
+
31
61
  export const issueRevision = (issue: IssueItem): string =>
32
62
  `issue:${stableDigest({ title: issue.title, body: issue.body })}`;
33
63
 
@@ -41,14 +71,14 @@ export const readApprovalBinding = async (
41
71
  context: ApprovalContext,
42
72
  label: string,
43
73
  target: string,
44
- ): Promise<ApprovalBinding | string> => {
74
+ ): Promise<ApprovalBindingResult> => {
45
75
  const issue = await getIssue(
46
76
  context.token,
47
77
  context.repo,
48
78
  context.issueNumber,
49
79
  );
50
80
  if (!hasLabel(issue.labels, label)) {
51
- return `"${label}" is not currently present on ${context.repo}#${context.issueNumber}`;
81
+ return { kind: 'absent' };
52
82
  }
53
83
  const event = await lastLabelEvent(
54
84
  context.token,
@@ -57,7 +87,11 @@ export const readApprovalBinding = async (
57
87
  label,
58
88
  );
59
89
  if (event === null) {
60
- return `no "${label}" label event found on ${context.repo}#${context.issueNumber}`;
90
+ return {
91
+ kind: 'rejected',
92
+ generationId: rejectionGenerationId(context, label, target, event),
93
+ reason: `no "${label}" label event found on ${context.repo}#${context.issueNumber}`,
94
+ };
61
95
  }
62
96
  const role = await collaboratorRole(
63
97
  context.token,
@@ -65,7 +99,11 @@ export const readApprovalBinding = async (
65
99
  event.actorLogin,
66
100
  );
67
101
  if (!isTrustedRole(role)) {
68
- return `"${label}" on ${context.repo}#${context.issueNumber} was applied by ${event.actorLogin} (role: ${role}); only admin or maintain roles may approve automation`;
102
+ return {
103
+ kind: 'rejected',
104
+ generationId: rejectionGenerationId(context, label, target, event),
105
+ reason: `"${label}" on ${context.repo}#${context.issueNumber} was applied by ${event.actorLogin} (role: ${role}); only admin or maintain roles may approve automation`,
106
+ };
69
107
  }
70
108
  const fields = {
71
109
  repo: context.repo,
@@ -76,5 +114,24 @@ export const readApprovalBinding = async (
76
114
  approvedAt: event.createdAt,
77
115
  target,
78
116
  };
79
- return { id: stableDigest(fields), ...fields };
117
+ return {
118
+ kind: 'approved',
119
+ approval: { id: stableDigest(fields), ...fields },
120
+ };
121
+ };
122
+
123
+ export const approvalIsCurrent = async (
124
+ context: ApprovalContext,
125
+ approval: ApprovalBinding,
126
+ readTarget: ApprovalTargetReader,
127
+ ): Promise<boolean> => {
128
+ const current = await readApprovalBinding(
129
+ context,
130
+ approval.label,
131
+ approval.target,
132
+ );
133
+ if (current.kind !== 'approved' || current.approval.id !== approval.id) {
134
+ return false;
135
+ }
136
+ return (await readTarget()) === approval.target;
80
137
  };
@@ -165,10 +165,13 @@ export const validateClaim = async (
165
165
  claim.approval.label,
166
166
  currentTarget,
167
167
  );
168
- if (typeof current === 'string') {
169
- return current;
168
+ if (current.kind === 'absent') {
169
+ return `"${claim.approval.label}" is not currently present`;
170
170
  }
171
- if (current.id !== claim.approval.id) {
171
+ if (current.kind === 'rejected') {
172
+ return current.reason;
173
+ }
174
+ if (current.approval.id !== claim.approval.id) {
172
175
  return 'approval no longer matches the exact approved revision/head';
173
176
  }
174
177
  const issue = await getIssue(
@@ -1,9 +1,8 @@
1
- // One fix job: a maintainer-approved issue becomes a verified draft PR, a
2
- // precise question, or an explicit failure — never silence. Every transition
3
- // is written back to GitHub so the next tick (or a human) resumes from there.
1
+ // A maintainer-approved issue becomes a verified draft PR, question, or failure.
4
2
 
5
3
  import { join } from 'node:path';
6
- import { issueRevision } from './poller-approval';
4
+ import { hasLabel } from './github-label-identity';
5
+ import { approvalIsCurrent, issueRevision } from './poller-approval';
7
6
  import { acquireClaim } from './poller-claim';
8
7
  import { runCodex } from './poller-codex';
9
8
  import { handleNonFixedOutcome } from './poller-fix-outcome';
@@ -55,29 +54,49 @@ const hasInvalidLocalOutput = (
55
54
  branch: string,
56
55
  ): boolean => sealed === null && localBranchExists(cloneDir, branch);
57
56
 
57
+ const result = (issueNumber: number, message: string): JobResult => ({
58
+ lines: [`#${issueNumber}: ${message}`],
59
+ ranCodex: false,
60
+ });
61
+
62
+ const assertSealedOwnership = (
63
+ sealed: NonNullable<ReturnType<typeof readSealedFixOutput>>,
64
+ issueNumber: number,
65
+ approvalId: string,
66
+ branch: string,
67
+ ): void => {
68
+ if (sealed.issueNumber !== issueNumber || sealed.approvalId !== approvalId) {
69
+ throw new Error(`sealed output on ${branch} has invalid ownership`);
70
+ }
71
+ };
72
+
58
73
  export const runFixJob = async (
59
74
  deps: JobDeps,
60
75
  issue: IssueItem,
61
- defaultBranch: string,
76
+ resolveDefaultBranch: () => Promise<string>,
62
77
  allowCodex = true,
63
78
  ): Promise<JobResult> => {
64
79
  const { config, token, repo } = deps;
65
80
  const currentIssue = await getIssue(token, repo, issue.number);
66
- const preamble = await jobPreamble(
67
- deps,
68
- currentIssue,
69
- FIX_LABELS,
70
- issueRevision(currentIssue),
71
- );
81
+ if (!hasLabel(currentIssue.labels, APPROVED_FOR_FIX)) {
82
+ return result(issue.number, 'approval no longer present; skipped');
83
+ }
84
+ const readTarget = async (): Promise<string> =>
85
+ issueRevision(await getIssue(token, repo, issue.number));
86
+ const preamble = await jobPreamble(deps, currentIssue, FIX_LABELS, {
87
+ approved: issueRevision(currentIssue),
88
+ readCurrent: readTarget,
89
+ });
90
+ if (preamble.kind === 'stale') {
91
+ return result(issue.number, 'approval no longer present; skipped');
92
+ }
72
93
  if (preamble.kind === 'rejected') {
73
- return { lines: [`#${issue.number}: approval rejected`], ranCodex: false };
94
+ return result(issue.number, 'approval rejected');
74
95
  }
75
96
  if (preamble.kind === 'waiting') {
76
- return {
77
- lines: [`#${issue.number}: waiting on an answer`],
78
- ranCodex: false,
79
- };
97
+ return result(issue.number, 'waiting on an answer');
80
98
  }
99
+ const defaultBranch = await resolveDefaultBranch();
81
100
  const cacheClone = ensureCacheClone(config.cacheDir, repo, token);
82
101
  const branch = branchNameForIssue(issue.number, preamble.approval.id);
83
102
  const sealed = readSealedFixOutput(cacheClone, branch);
@@ -86,12 +105,18 @@ export const runFixJob = async (
86
105
  `refusing to overwrite ${branch}: it is not valid sealed output for this approval`,
87
106
  );
88
107
  }
108
+ if (
109
+ !(await approvalIsCurrent(
110
+ { token, repo, issueNumber: issue.number },
111
+ preamble.approval,
112
+ readTarget,
113
+ ))
114
+ ) {
115
+ return result(issue.number, 'approval generation changed; skipped');
116
+ }
89
117
  if (sealed === null && !allowCodex) {
90
118
  await acknowledgeQueuedJob(deps, issue.number, preamble.approval, 'fix');
91
- return {
92
- lines: [`#${issue.number}: waiting for run capacity`],
93
- ranCodex: false,
94
- };
119
+ return result(issue.number, 'waiting for run capacity');
95
120
  }
96
121
  await addLabels(token, repo, issue.number, [FIX_IN_PROGRESS]);
97
122
  const claim = await acquireClaim(
@@ -100,10 +125,7 @@ export const runFixJob = async (
100
125
  FIX_IN_PROGRESS,
101
126
  );
102
127
  if (claim === null) {
103
- return {
104
- lines: [`#${issue.number}: another poller owns the claim`],
105
- ranCodex: false,
106
- };
128
+ return result(issue.number, 'another poller owns the claim');
107
129
  }
108
130
  const resumableJob = {
109
131
  deps,
@@ -114,12 +136,7 @@ export const runFixJob = async (
114
136
  cloneDir: cacheClone,
115
137
  };
116
138
  if (sealed !== null) {
117
- if (
118
- sealed.issueNumber !== issue.number ||
119
- sealed.approvalId !== claim.approval.id
120
- ) {
121
- throw new Error(`sealed output on ${branch} has invalid ownership`);
122
- }
139
+ assertSealedOwnership(sealed, issue.number, claim.approval.id, branch);
123
140
  return {
124
141
  lines: [await publishFixedOutput(resumableJob, FIX_LABELS, sealed, null)],
125
142
  ranCodex: false,
@@ -1,11 +1,7 @@
1
- // Read-side GitHub helpers for the poller. Every function either returns a
2
- // narrow parsed shape or throws with the failing endpoint in the message; the
3
- // tick converts per-job throws into reported problems. Lists are always read
4
- // exhaustively (see github-paginate.ts) — the trust anchor is "the latest
5
- // label event", which a truncated page would silently falsify.
1
+ // Read-side GitHub helpers return narrow parsed shapes or throw.
6
2
 
7
3
  import { apiError, HTTP_OK, request } from './github-api';
8
- import { labelIdentity } from './github-label-identity';
4
+ import { hasLabel, labelIdentity } from './github-label-identity';
9
5
  import { listAllPages } from './github-paginate';
10
6
  import { isNonEmptyString, isRecord } from './github-settings-parse';
11
7
 
@@ -34,23 +30,32 @@ export type LabelEvent = {
34
30
  const asString = (value: unknown): string =>
35
31
  typeof value === 'string' ? value : '';
36
32
 
37
- const labelNames = (raw: unknown): ReadonlyArray<string> =>
38
- Array.isArray(raw)
39
- ? raw
40
- .map((label) => (isRecord(label) ? asString(label.name) : ''))
41
- .filter((name) => name.length > 0)
42
- : [];
33
+ const labelNames = (raw: unknown): ReadonlyArray<string> | null => {
34
+ if (
35
+ !(
36
+ Array.isArray(raw) &&
37
+ raw.every((label) => isRecord(label) && isNonEmptyString(label.name))
38
+ )
39
+ ) {
40
+ return null;
41
+ }
42
+ return raw.map((label) => (label as { readonly name: string }).name);
43
+ };
43
44
 
44
45
  export const parseIssue = (raw: unknown): IssueItem | null => {
45
46
  if (!(isRecord(raw) && typeof raw.number === 'number')) {
46
47
  return null;
47
48
  }
49
+ const labels = labelNames(raw.labels);
50
+ if (labels === null) {
51
+ return null;
52
+ }
48
53
  return {
49
54
  number: raw.number,
50
55
  title: asString(raw.title),
51
56
  body: asString(raw.body),
52
57
  isPullRequest: isRecord(raw.pull_request),
53
- labels: labelNames(raw.labels),
58
+ labels,
54
59
  authorLogin: isRecord(raw.user) ? asString(raw.user.login) : '',
55
60
  };
56
61
  };
@@ -82,9 +87,15 @@ export const listOpenIssuesWithLabel = async (
82
87
  `/repos/${repo}/issues?state=open&labels=${encodeURIComponent(label)}`,
83
88
  `list ${repo} issues labeled ${label}`,
84
89
  );
85
- return items
86
- .map(parseIssue)
87
- .filter((issue): issue is IssueItem => issue !== null);
90
+ return items.flatMap((raw, index) => {
91
+ const issue = parseIssue(raw);
92
+ if (issue === null) {
93
+ throw new Error(
94
+ `list ${repo} issues labeled ${label}: invalid issue at index ${index}`,
95
+ );
96
+ }
97
+ return hasLabel(issue.labels, label) ? [issue] : [];
98
+ });
88
99
  };
89
100
 
90
101
  export const listIssueComments = async (
@@ -151,9 +162,7 @@ export const lastLabelEvent = async (
151
162
  return latest;
152
163
  };
153
164
 
154
- // role_name distinguishes maintain from write; the legacy `permission` field
155
- // collapses both to "write" and cannot express the trust boundary. A 404
156
- // means "not a collaborator" — an untrusted answer, not an API failure.
165
+ // role_name preserves the maintain/write trust boundary. A 404 is untrusted.
157
166
  export const collaboratorRole = async (
158
167
  token: string | null,
159
168
  repo: string,
@@ -3,7 +3,12 @@
3
3
  // approval/progress/failure labels the job family uses.
4
4
 
5
5
  import { hasLabel } from './github-label-identity';
6
- import { type ApprovalBinding, readApprovalBinding } from './poller-approval';
6
+ import {
7
+ type ApprovalBinding,
8
+ type ApprovalTargetReader,
9
+ approvalIsCurrent,
10
+ readApprovalBinding,
11
+ } from './poller-approval';
7
12
  import type { PollerConfig } from './poller-config';
8
13
  import { addLabels, createComment, removeLabel } from './poller-github-write';
9
14
  import {
@@ -31,11 +36,17 @@ export type JobLabels = {
31
36
  readonly failed: string;
32
37
  };
33
38
 
39
+ type JobTarget = {
40
+ readonly approved: string;
41
+ readonly readCurrent: ApprovalTargetReader;
42
+ };
43
+
34
44
  // Must exceed runCodex's failure string at its longest (its stderr snippet
35
45
  // plus the exit-cause prefix), so the diagnostic tail is never cut off.
36
46
  const FAILURE_SNIPPET_LIMIT = 4000;
37
47
 
38
48
  export type JobPreamble =
49
+ | { readonly kind: 'stale' }
39
50
  | { readonly kind: 'rejected' }
40
51
  | { readonly kind: 'waiting' }
41
52
  | {
@@ -51,7 +62,7 @@ export const jobPreamble = async (
51
62
  deps: JobDeps,
52
63
  item: { readonly number: number; readonly labels: ReadonlyArray<string> },
53
64
  labels: JobLabels,
54
- target: string,
65
+ target: JobTarget,
55
66
  ): Promise<JobPreamble> => {
56
67
  const trust = {
57
68
  token: deps.token,
@@ -59,18 +70,44 @@ export const jobPreamble = async (
59
70
  issueNumber: item.number,
60
71
  roleCache: deps.roleCache,
61
72
  };
62
- const approval = await readApprovalBinding(trust, labels.approved, target);
63
- if (typeof approval === 'string') {
73
+ const approval = await readApprovalBinding(
74
+ trust,
75
+ labels.approved,
76
+ target.approved,
77
+ );
78
+ if (approval.kind === 'absent') {
79
+ return { kind: 'stale' };
80
+ }
81
+ if (approval.kind === 'rejected') {
82
+ const current = await readApprovalBinding(
83
+ trust,
84
+ labels.approved,
85
+ target.approved,
86
+ );
87
+ if (
88
+ current.kind !== 'rejected' ||
89
+ current.generationId !== approval.generationId
90
+ ) {
91
+ return { kind: 'stale' };
92
+ }
93
+ if ((await target.readCurrent()) !== target.approved) {
94
+ return { kind: 'stale' };
95
+ }
64
96
  await removeLabel(deps.token, deps.repo, item.number, labels.approved);
65
97
  await createComment(
66
98
  deps.token,
67
99
  deps.repo,
68
100
  item.number,
69
- `${FAILURE_MARKER}\nApproval not honored: ${approval}`,
101
+ `${FAILURE_MARKER}\nApproval not honored: ${approval.reason}`,
70
102
  );
71
103
  return { kind: 'rejected' };
72
104
  }
73
105
  const answers = await answerState(trust);
106
+ if (
107
+ !(await approvalIsCurrent(trust, approval.approval, target.readCurrent))
108
+ ) {
109
+ return { kind: 'stale' };
110
+ }
74
111
  if (answers.waiting) {
75
112
  if (!hasLabel(item.labels, NEEDS_CLARIFICATION)) {
76
113
  await addLabels(deps.token, deps.repo, item.number, [
@@ -83,7 +120,11 @@ export const jobPreamble = async (
83
120
  if (hasLabel(item.labels, NEEDS_CLARIFICATION)) {
84
121
  await removeLabel(deps.token, deps.repo, item.number, NEEDS_CLARIFICATION);
85
122
  }
86
- return { kind: 'go', answers: answers.answers, approval };
123
+ return {
124
+ kind: 'go',
125
+ answers: answers.answers,
126
+ approval: approval.approval,
127
+ };
87
128
  };
88
129
 
89
130
  export const failJob = async (
@@ -4,7 +4,8 @@
4
4
  // verifies and pushes those commits, posts the final report, and flips the PR
5
5
  // ready.
6
6
 
7
- import { prRevision } from './poller-approval';
7
+ import { hasLabel } from './github-label-identity';
8
+ import { approvalIsCurrent, prRevision } from './poller-approval';
8
9
  import { acquireClaim } from './poller-claim';
9
10
  import { runCodex } from './poller-codex';
10
11
  import { getIssue, type IssueItem } from './poller-github';
@@ -49,6 +50,27 @@ const hasInvalidLocalOutput = (
49
50
  ): boolean =>
50
51
  plan === null && sealed === null && localBranchExists(cloneDir, branch);
51
52
 
53
+ const currentReviewTarget = async (
54
+ token: string | null,
55
+ repo: string,
56
+ prNumber: number,
57
+ plan: Awaited<ReturnType<typeof readReviewPlan>>,
58
+ ): Promise<string | null> => {
59
+ const current = await getPullRequest(token, repo, prNumber);
60
+ if (
61
+ plan !== null &&
62
+ current.headSha !== plan.approvedHead &&
63
+ current.headSha !== plan.publishedHead
64
+ ) {
65
+ return null;
66
+ }
67
+ return prRevision(
68
+ current.baseRef,
69
+ current.baseSha,
70
+ plan === null ? current.headSha : plan.approvedHead,
71
+ );
72
+ };
73
+
52
74
  export const runReviewJob = async (
53
75
  deps: JobDeps,
54
76
  prItem: IssueItem,
@@ -56,15 +78,30 @@ export const runReviewJob = async (
56
78
  runAgent: typeof runCodex = runCodex,
57
79
  ): Promise<JobResult> => {
58
80
  const { config, token, repo } = deps;
81
+ const currentItem = await getIssue(token, repo, prItem.number);
82
+ if (!hasLabel(currentItem.labels, APPROVED_FOR_REVIEW)) {
83
+ return {
84
+ lines: [`PR #${prItem.number}: approval no longer present; skipped`],
85
+ ranCodex: false,
86
+ };
87
+ }
59
88
  const pr = await getPullRequest(token, repo, prItem.number);
60
89
  let plan = await currentReviewPlan(deps, pr);
61
- const currentItem = await getIssue(token, repo, prItem.number);
62
- const preamble = await jobPreamble(
63
- deps,
64
- currentItem,
65
- REVIEW_LABELS,
66
- prRevision(pr.baseRef, pr.baseSha, plan?.approvedHead ?? pr.headSha),
67
- );
90
+ const readTarget = () => currentReviewTarget(token, repo, pr.number, plan);
91
+ const preamble = await jobPreamble(deps, currentItem, REVIEW_LABELS, {
92
+ approved: prRevision(
93
+ pr.baseRef,
94
+ pr.baseSha,
95
+ plan?.approvedHead ?? pr.headSha,
96
+ ),
97
+ readCurrent: readTarget,
98
+ });
99
+ if (preamble.kind === 'stale') {
100
+ return {
101
+ lines: [`PR #${prItem.number}: approval no longer present; skipped`],
102
+ ranCodex: false,
103
+ };
104
+ }
68
105
  if (preamble.kind === 'rejected') {
69
106
  return {
70
107
  lines: [`PR #${prItem.number}: approval rejected`],
@@ -101,6 +138,18 @@ export const runReviewJob = async (
101
138
  if (hasInvalidLocalOutput(plan, sealed, cacheClone, outputBranch)) {
102
139
  throw new Error(`sealed output on ${outputBranch} is invalid`);
103
140
  }
141
+ if (
142
+ !(await approvalIsCurrent(
143
+ { token, repo, issueNumber: pr.number },
144
+ preamble.approval,
145
+ readTarget,
146
+ ))
147
+ ) {
148
+ return {
149
+ lines: [`PR #${pr.number}: approval generation changed; skipped`],
150
+ ranCodex: false,
151
+ };
152
+ }
104
153
  if (plan === null && sealed === null && !allowCodex) {
105
154
  await acknowledgeQueuedJob(deps, pr.number, preamble.approval, 'review');
106
155
  return {
@@ -43,7 +43,10 @@ export const readReviewPlan = async (
43
43
  APPROVED_FOR_REVIEW,
44
44
  prRevision(plan.baseRef, plan.baseSha, plan.approvedHead),
45
45
  );
46
- if (typeof binding !== 'string' && binding.id === plan.approvalId) {
46
+ if (
47
+ binding.kind === 'approved' &&
48
+ binding.approval.id === plan.approvalId
49
+ ) {
47
50
  currentPlans.push(plan);
48
51
  }
49
52
  }
@@ -74,7 +74,8 @@ const isStillQueueable = async (
74
74
  target,
75
75
  );
76
76
  return (
77
- typeof currentApproval !== 'string' && currentApproval.id === approval.id
77
+ currentApproval.kind === 'approved' &&
78
+ currentApproval.approval.id === approval.id
78
79
  );
79
80
  };
80
81
 
@@ -87,6 +88,7 @@ export const acknowledgeQueuedJob = async (
87
88
  const expected = queueMarkerFor(approval, kind);
88
89
  const current = await getIssue(deps.token, deps.repo, issueNumber);
89
90
  if (
91
+ !hasLabel(current.labels, approvalLabelFor(kind)) ||
90
92
  hasLabel(current.labels, inProgressLabelFor(kind)) ||
91
93
  hasLabel(current.labels, NEEDS_CLARIFICATION)
92
94
  ) {
@@ -77,15 +77,17 @@ const runQueue = async (options: {
77
77
  // biome-ignore lint/performance/noAwaitInLoops: jobs are heavyweight Codex runs and the shared run budget is the concurrency cap.
78
78
  result = await runners.review(job.deps, job.item, allowCodex);
79
79
  } else {
80
- let defaultBranch = defaultBranches.get(job.deps.repo);
81
- if (defaultBranch === undefined) {
82
- defaultBranch = await repoDefaultBranch(token, job.deps.repo);
83
- defaultBranches.set(job.deps.repo, defaultBranch);
84
- }
85
80
  result = await runners.fix(
86
81
  job.deps,
87
82
  job.item,
88
- defaultBranch,
83
+ async () => {
84
+ let branch = defaultBranches.get(job.deps.repo);
85
+ if (branch === undefined) {
86
+ branch = await repoDefaultBranch(token, job.deps.repo);
87
+ defaultBranches.set(job.deps.repo, branch);
88
+ }
89
+ return branch;
90
+ },
89
91
  allowCodex,
90
92
  );
91
93
  }