next-issue 2.4.0 → 2.4.2

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/dist/pipeline.mjs CHANGED
@@ -1,13 +1,13 @@
1
- import { CODING, REVIEWING, runAgent } from "./agents.mjs";
2
- import { managedLabels } from "./config.mjs";
3
- import { run } from "./exec.mjs";
4
- import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, findWorktree, isDirty, push, removeWorktree, resetToBase, revision, } from "./git.mjs";
5
- import { commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, openIssues, prMerged, reopenIssue, setLabel, viewIssue, waitForBaseChecks, waitForChecks, } from "./github.mjs";
6
- import { message } from "./observe.mjs";
7
- import { capDiff, fixPrompt, hookPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from "./prompts.mjs";
8
- import { detectSetupCommand } from "./setup.mjs";
9
- import { dropState, readState, resetState, savedIssues, writeState, } from "./state.mjs";
10
- import { blockingFindings, fingerprint, formatFindings, formatVerdict, openFindings, readVerdict, VERDICT_SCHEMA, } from "./verdict.mjs";
1
+ import { CODING, REVIEWING, runAgent } from './agents.mjs';
2
+ import { managedLabels } from './config.mjs';
3
+ import { run } from './exec.mjs';
4
+ import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, findWorktree, isDirty, push, removeWorktree, resetToBase, revision, } from './git.mjs';
5
+ import { commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, openIssues, prMerged, reopenIssue, setLabel, viewIssue, waitForBaseChecks, waitForChecks, } from './github.mjs';
6
+ import { message } from './observe.mjs';
7
+ import { capDiff, fixPrompt, hookPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from './prompts.mjs';
8
+ import { detectSetupCommand } from './setup.mjs';
9
+ import { dropState, readState, resetState, savedIssues, writeState, } from './state.mjs';
10
+ import { blockingFindings, fingerprint, formatFindings, formatVerdict, openFindings, readVerdict, VERDICT_SCHEMA, } from './verdict.mjs';
11
11
  export const PORTS = {
12
12
  addWorktree,
13
13
  commentOnPr,
@@ -35,7 +35,7 @@ export const PORTS = {
35
35
  resetToBase,
36
36
  revision,
37
37
  runAgent,
38
- runSetup: async (command, cwd, timeoutMs) => run("bash", ["-lc", command], { cwd, timeoutMs }),
38
+ runSetup: async (command, cwd, timeoutMs) => run('bash', ['-lc', command], { cwd, timeoutMs }),
39
39
  setLabel,
40
40
  viewIssue,
41
41
  waitForBaseChecks,
@@ -45,30 +45,36 @@ export function skipReason(issue, config, resuming) {
45
45
  const labels = new Set(issue.labels);
46
46
  const names = config.labels;
47
47
  if (labels.has(names.done) || labels.has(names.needsHuman) || labels.has(names.skip)) {
48
- return "stop-label";
48
+ return 'stop-label';
49
49
  }
50
50
  if (resuming) {
51
51
  return undefined;
52
52
  }
53
53
  if (names.ready.length > 0 && !labels.has(names.ready)) {
54
- return "not-ready";
54
+ return 'not-ready';
55
55
  }
56
56
  if (labels.has(names.inProgress) || labels.has(names.inReview)) {
57
- return "in-flight";
57
+ return 'in-flight';
58
58
  }
59
59
  return undefined;
60
60
  }
61
61
  export async function pendingIssues(context) {
62
62
  const { repo, config, ports } = context;
63
- const issues = await ports.openIssues(repo, config.issueLimit);
63
+ const { ready, inProgress, inReview } = config.labels;
64
+ const labels = ready.length === 0 ? [] : [ready, inProgress, inReview];
65
+ const issues = await ports.openIssues(repo, config.issueLimit, labels);
64
66
  const listed = new Set(issues.map((issue) => issue.number));
65
67
  for (const number of await savedIssues(repo)) {
66
68
  if (listed.has(number)) {
67
69
  continue;
68
70
  }
69
71
  const saved = await readState(repo, number).catch(() => undefined);
70
- if (saved?.pr !== undefined && (await ports.prMerged(repo, saved.pr))) {
71
- issues.push(await ports.viewIssue(repo, number));
72
+ if (saved === undefined) {
73
+ continue;
74
+ }
75
+ const { open, ...issue } = await ports.viewIssue(repo, number);
76
+ if (open || (saved.pr !== undefined && (await ports.prMerged(repo, saved.pr)))) {
77
+ issues.push(issue);
72
78
  }
73
79
  }
74
80
  return issues.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
@@ -81,12 +87,17 @@ export async function processIssue(context, issue) {
81
87
  saved = await readState(context.repo, issue.number);
82
88
  }
83
89
  catch (error) {
84
- log.event("issue.error", { error: message(error), ms: Date.now() - started }, "quiet");
85
- return { ...skipped(issue), outcome: "error", reason: message(error), ms: Date.now() - started };
90
+ log.event('issue.error', { error: message(error), ms: Date.now() - started }, 'quiet');
91
+ return {
92
+ ...skipped(issue),
93
+ outcome: 'error',
94
+ reason: message(error),
95
+ ms: Date.now() - started,
96
+ };
86
97
  }
87
98
  const skip = skipReason(issue, context.config, saved !== undefined);
88
99
  if (skip !== undefined) {
89
- log.event("issue.skip", { title: issue.title, reason: skip });
100
+ log.event('issue.skip', { title: issue.title, reason: skip });
90
101
  return skipped(issue);
91
102
  }
92
103
  if (saved === undefined) {
@@ -95,18 +106,23 @@ export async function processIssue(context, issue) {
95
106
  blockers = await context.ports.openBlockers(context.repo, issue.number);
96
107
  }
97
108
  catch (error) {
98
- log.event("issue.error", { error: message(error), ms: Date.now() - started }, "quiet");
99
- return { ...skipped(issue), outcome: "error", reason: message(error), ms: Date.now() - started };
109
+ log.event('issue.error', { error: message(error), ms: Date.now() - started }, 'quiet');
110
+ return {
111
+ ...skipped(issue),
112
+ outcome: 'error',
113
+ reason: message(error),
114
+ ms: Date.now() - started,
115
+ };
100
116
  }
101
117
  if (blockers.length > 0) {
102
- log.event("issue.skip", { title: issue.title, reason: "blocked", blockers });
118
+ log.event('issue.skip', { title: issue.title, reason: 'blocked', blockers });
103
119
  return skipped(issue);
104
120
  }
105
121
  }
106
- log.event("issue.start", { title: issue.title, createdAt: issue.createdAt }, "quiet");
122
+ log.event('issue.start', { title: issue.title, createdAt: issue.createdAt }, 'quiet');
107
123
  const fresh = {
108
124
  issue: issue.number,
109
- phase: "claimed",
125
+ phase: 'claimed',
110
126
  branch: branchName(issue.number),
111
127
  ciFixes: 0,
112
128
  reviewRounds: 0,
@@ -115,21 +131,21 @@ export async function processIssue(context, issue) {
115
131
  const state = saved === undefined ? fresh : context.reset ? resetState(saved) : saved;
116
132
  try {
117
133
  const report = await work(context, issue, log, state);
118
- log.event("issue.end", { outcome: report.outcome, reason: report.reason, ms: report.ms }, "quiet");
134
+ log.event('issue.end', { outcome: report.outcome, reason: report.reason, ms: report.ms }, 'quiet');
119
135
  return report;
120
136
  }
121
137
  catch (error) {
122
- log.event("issue.error", { error: message(error), ms: Date.now() - started }, "quiet");
138
+ log.event('issue.error', { error: message(error), ms: Date.now() - started }, 'quiet');
123
139
  try {
124
140
  await handOver(context, log, issue.number, state, `The run failed: ${message(error)}`);
125
141
  }
126
142
  catch (failure) {
127
- log.event("hand-over.error", { error: message(failure) }, "quiet");
143
+ log.event('hand-over.error', { error: message(failure) }, 'quiet');
128
144
  }
129
145
  await cleanUp(context, log, state.branch);
130
146
  return {
131
147
  issue: issue.number,
132
- outcome: "error",
148
+ outcome: 'error',
133
149
  reason: message(error),
134
150
  pr: state.pr,
135
151
  ciFixes: state.ciFixes,
@@ -139,21 +155,21 @@ export async function processIssue(context, issue) {
139
155
  }
140
156
  }
141
157
  function skipped(issue) {
142
- return { issue: issue.number, outcome: "skipped", ciFixes: 0, reviewRounds: 0, ms: 0 };
158
+ return { issue: issue.number, outcome: 'skipped', ciFixes: 0, reviewRounds: 0, ms: 0 };
143
159
  }
144
160
  async function work(context, issue, log, state) {
145
161
  const started = Date.now();
146
162
  const { repo, config, ports } = context;
147
163
  const branch = state.branch;
148
- await log.step("claim", { label: config.labels.inProgress }, async () => {
164
+ await log.step('claim', { label: config.labels.inProgress }, async () => {
149
165
  await setStatus(context, issue.number, state.pr, config.labels.inProgress);
150
166
  await writeState(repo, state);
151
167
  });
152
- const comments = await log.step("comments", {}, () => ports.issueComments(repo, issue.number));
153
- const added = await log.step("worktree", { branch }, () => ports.addWorktree(repo, issue.number, branch, context.base, config.remote));
168
+ const comments = await log.step('comments', {}, () => ports.issueComments(repo, issue.number));
169
+ const added = await log.step('worktree', { branch }, () => ports.addWorktree(repo, issue.number, branch, context.base, config.remote));
154
170
  const worktree = added.path;
155
171
  if (!added.baseUpdated) {
156
- log.event("base.behind", { base: context.base }, "quiet");
172
+ log.event('base.behind', { base: context.base }, 'quiet');
157
173
  }
158
174
  const job = {
159
175
  context,
@@ -169,20 +185,20 @@ async function work(context, issue, log, state) {
169
185
  };
170
186
  const setupCommand = config.setupCommand ?? (await ports.detectSetupCommand(worktree));
171
187
  if (setupCommand !== undefined) {
172
- const setup = await log.step("setup", { cmd: setupCommand, found: config.setupCommand === undefined }, () => ports.runSetup(setupCommand, worktree, config.setupTimeoutMinutes * 60_000));
188
+ const setup = await log.step('setup', { cmd: setupCommand, found: config.setupCommand === undefined }, () => ports.runSetup(setupCommand, worktree, config.setupTimeoutMinutes * 60_000));
173
189
  if (setup.code !== 0) {
174
190
  const late = setup.timedOut === true;
175
- log.event("setup.fail", { code: setup.code, timedOut: late, stderr: setup.stderr.trim().slice(-2000) }, "quiet");
191
+ log.event('setup.fail', { code: setup.code, timedOut: late, stderr: setup.stderr.trim().slice(-2000) }, 'quiet');
176
192
  await escalate(job, late
177
193
  ? `The setup command did not finish in ${config.setupTimeoutMinutes} minutes.`
178
- : "The setup command failed in the worktree.");
179
- return finish(job, "needs-human", late ? "setup timeout" : "setup failed");
194
+ : 'The setup command failed in the worktree.');
195
+ return finish(job, 'needs-human', late ? 'setup timeout' : 'setup failed');
180
196
  }
181
197
  }
182
- if (state.phase === "claimed") {
198
+ if (state.phase === 'claimed') {
183
199
  const before = await ports.revision(worktree);
184
- const result = await log.step("implement", {}, () => ports.runAgent(log, {
185
- name: "implementer",
200
+ const result = await log.step('implement', {}, () => ports.runAgent(log, {
201
+ name: 'implementer',
186
202
  cwd: worktree,
187
203
  prompt: implementPrompt(issue, comments),
188
204
  profile: CODING,
@@ -190,11 +206,11 @@ async function work(context, issue, log, state) {
190
206
  timeoutMs: config.agentTimeoutMinutes * 60_000,
191
207
  }));
192
208
  const subject = parseCommitSubject(result.text, `fix: resolve issue #${issue.number}`);
193
- if (!(await commitWork(job, before, subject, "implementer"))) {
194
- await escalate(job, "The implementer produced no change.");
195
- return finish(job, "needs-human", "no implementation commit");
209
+ if (!(await commitWork(job, before, subject, 'implementer'))) {
210
+ await escalate(job, 'The implementer produced no change.');
211
+ return finish(job, 'needs-human', 'no implementation commit');
196
212
  }
197
- state.phase = "implemented";
213
+ state.phase = 'implemented';
198
214
  await writeState(repo, state);
199
215
  }
200
216
  if (context.stopAfterRound()) {
@@ -207,11 +223,13 @@ async function work(context, issue, log, state) {
207
223
  return fixed.report;
208
224
  }
209
225
  }
210
- else if (state.phase === "review" && state.pr !== undefined && (await ports.prMerged(repo, state.pr))) {
211
- log.event("pull-request.merged", { pr: state.pr }, "quiet");
226
+ else if (state.phase === 'review' &&
227
+ state.pr !== undefined &&
228
+ (await ports.prMerged(repo, state.pr))) {
229
+ log.event('pull-request.merged', { pr: state.pr }, 'quiet');
212
230
  if (!context.merge) {
213
231
  await setStatus(context, issue.number, state.pr, config.labels.done);
214
- return finish(job, "done");
232
+ return finish(job, 'done');
215
233
  }
216
234
  const landed = await afterMerge(job);
217
235
  if (landed.done) {
@@ -219,8 +237,8 @@ async function work(context, issue, log, state) {
219
237
  }
220
238
  }
221
239
  else {
222
- await log.step("push", { branch }, () => ports.push(worktree, config.remote, branch));
223
- state.phase = "review";
240
+ await log.step('push', { branch }, () => ports.push(worktree, config.remote, branch));
241
+ state.phase = 'review';
224
242
  await openPr(job);
225
243
  }
226
244
  for (;;) {
@@ -246,52 +264,52 @@ async function work(context, issue, log, state) {
246
264
  async function openPr(job) {
247
265
  const { context, issue, log, state, branch } = job;
248
266
  const { repo, config, ports } = context;
249
- const pr = await log.step("pull-request", { branch }, async () => {
267
+ const pr = await log.step('pull-request', { branch }, async () => {
250
268
  const existing = await ports.findPr(repo, branch);
251
269
  return (existing ??
252
270
  ports.createPr(repo, branch, context.base, state.title ?? `fix: resolve issue #${issue.number}`, `Closes #${issue.number}\n\nOpened by next-issue.`, config.draftPullRequest));
253
271
  });
254
272
  state.pr = pr;
255
273
  await writeState(repo, state);
256
- log.event("pull-request.ready", { pr }, "quiet");
274
+ log.event('pull-request.ready', { pr }, 'quiet');
257
275
  }
258
276
  async function checkGate(job) {
259
277
  const { context, log, state, worktree } = job;
260
278
  const { repo, config, ports } = context;
261
279
  const head = await ports.revision(worktree);
262
- const checks = await log.step("checks", { pr: state.pr, head }, () => ports.waitForChecks(repo, state.pr, {
280
+ const checks = await log.step('checks', { pr: state.pr, head }, () => ports.waitForChecks(repo, state.pr, {
263
281
  head,
264
282
  intervalSeconds: config.checkIntervalSeconds,
265
283
  graceMs: job.noChecks ? 0 : config.checkGraceSeconds * 1000,
266
284
  timeoutMs: config.checkTimeoutMinutes * 60_000,
267
- show: log.level === "verbose",
285
+ show: log.level === 'verbose',
268
286
  }));
269
- log.event("checks.state", { state: checks });
270
- job.noChecks ||= checks === "none";
271
- if (checks === "timeout") {
272
- await escalate(job, "The checks did not finish in time.");
273
- return { done: true, report: await finish(job, "needs-human", "checks timeout") };
287
+ log.event('checks.state', { state: checks });
288
+ job.noChecks ||= checks === 'none';
289
+ if (checks === 'timeout') {
290
+ await escalate(job, 'The checks did not finish in time.');
291
+ return { done: true, report: await finish(job, 'needs-human', 'checks timeout') };
274
292
  }
275
- if (checks !== "fail") {
293
+ if (checks !== 'fail') {
276
294
  return { done: false, fixed: false };
277
295
  }
278
296
  if (state.ciFixes >= config.maxCiFixes) {
279
297
  await escalate(job, `The limit of ${config.maxCiFixes} check fixes was reached.`);
280
- return { done: true, report: await finish(job, "needs-human", "check budget") };
298
+ return { done: true, report: await finish(job, 'needs-human', 'check budget') };
281
299
  }
282
300
  state.ciFixes += 1;
283
301
  await writeState(repo, state);
284
302
  const logs = await ports.failedCheckLogs(repo, state.pr, config.logMaxChars);
285
- log.event("checks.logs", { chars: logs.length });
286
- const fix = await fixRound(job, "The continuous integration checks failed.", logs, history(state.reviewLog), "checks");
303
+ log.event('checks.logs', { chars: logs.length });
304
+ const fix = await fixRound(job, 'The continuous integration checks failed.', logs, history(state.reviewLog), 'checks');
287
305
  if (!fix.committed) {
288
306
  const note = fix.unrelated;
289
307
  await escalate(job, note === undefined
290
- ? "The fixer added no commit for the failed checks."
308
+ ? 'The fixer added no commit for the failed checks.'
291
309
  : `The checks fail for a reason this change did not cause: ${note}`);
292
310
  return {
293
311
  done: true,
294
- report: await finish(job, "needs-human", note === undefined ? "no fix commit" : "unrelated failure"),
312
+ report: await finish(job, 'needs-human', note === undefined ? 'no fix commit' : 'unrelated failure'),
295
313
  };
296
314
  }
297
315
  return { done: false, fixed: true };
@@ -302,7 +320,7 @@ async function reviewRound(job) {
302
320
  const pr = state.pr;
303
321
  if (state.reviewRounds >= config.maxReviewRounds) {
304
322
  await escalate(job, `The limit of ${config.maxReviewRounds} review rounds was reached.`);
305
- return { done: true, report: await finish(job, "needs-human", "review budget") };
323
+ return { done: true, report: await finish(job, 'needs-human', 'review budget') };
306
324
  }
307
325
  state.reviewRounds += 1;
308
326
  await writeState(repo, state);
@@ -315,8 +333,8 @@ async function reviewRound(job) {
315
333
  cut: full.length > config.diffMaxChars,
316
334
  };
317
335
  const before = await ports.revision(worktree);
318
- const review = await log.step("review", fields, () => ports.runAgent(log, {
319
- name: "reviewer",
336
+ const review = await log.step('review', fields, () => ports.runAgent(log, {
337
+ name: 'reviewer',
320
338
  cwd: worktree,
321
339
  prompt: reviewPrompt(issue, comments, patch, history(state.reviewLog)),
322
340
  profile: REVIEWING,
@@ -325,22 +343,22 @@ async function reviewRound(job) {
325
343
  outputSchema: VERDICT_SCHEMA,
326
344
  }));
327
345
  if (await ports.discardChanges(worktree, before)) {
328
- log.event("review.discard", { round: state.reviewRounds }, "quiet");
346
+ log.event('review.discard', { round: state.reviewRounds }, 'quiet');
329
347
  }
330
348
  const verdict = readVerdict(review.structured);
331
349
  if (verdict === undefined) {
332
- await escalate(job, "The reviewer gave no verdict.");
333
- return { done: true, report: await finish(job, "needs-human", "no verdict") };
350
+ await escalate(job, 'The reviewer gave no verdict.');
351
+ return { done: true, report: await finish(job, 'needs-human', 'no verdict') };
334
352
  }
335
353
  const blocking = blockingFindings(verdict);
336
354
  const open = openFindings(verdict, state.reviewRounds >= config.maxReviewRounds);
337
- log.event("verdict", {
355
+ log.event('verdict', {
338
356
  round: state.reviewRounds,
339
357
  blocking: blocking.length,
340
358
  minor: verdict.findings.length - blocking.length,
341
359
  open: open.length,
342
360
  summary: verdict.summary,
343
- }, "quiet");
361
+ }, 'quiet');
344
362
  await ports.commentOnPr(repo, pr, formatVerdict(verdict, open));
345
363
  if (open.length === 0) {
346
364
  const gate = await checkGate(job);
@@ -351,36 +369,40 @@ async function reviewRound(job) {
351
369
  return { done: false };
352
370
  }
353
371
  if (config.draftPullRequest && !(await ports.markPrReady(repo, pr))) {
354
- log.event("pull-request.undraft.fail", { pr }, "quiet");
372
+ log.event('pull-request.undraft.fail', { pr }, 'quiet');
355
373
  }
356
374
  if (context.merge) {
357
375
  if (!(await ports.mergePr(repo, pr))) {
358
- await escalate(job, "The merge of the pull request failed.");
359
- return { done: true, report: await finish(job, "needs-human", "merge failed") };
376
+ await escalate(job, 'The merge of the pull request failed.');
377
+ return { done: true, report: await finish(job, 'needs-human', 'merge failed') };
360
378
  }
361
379
  return afterMerge(job);
362
380
  }
363
381
  await setStatus(context, issue.number, pr, config.labels.done);
364
- return { done: true, report: await finish(job, "done") };
382
+ return { done: true, report: await finish(job, 'done') };
365
383
  }
366
384
  const mark = fingerprint(open);
367
385
  if (state.reviewLog.some((round) => round.fingerprint === mark)) {
368
- await escalate(job, "The reviewer repeated findings that an earlier round did not fix.");
369
- return { done: true, report: await finish(job, "needs-human", "repeated findings") };
386
+ await escalate(job, 'The reviewer repeated findings that an earlier round did not fix.');
387
+ return { done: true, report: await finish(job, 'needs-human', 'repeated findings') };
370
388
  }
371
389
  const earlier = history(state.reviewLog);
372
- state.reviewLog.push({ round: state.reviewRounds, fingerprint: mark, findings: formatFindings(open) });
390
+ state.reviewLog.push({
391
+ round: state.reviewRounds,
392
+ fingerprint: mark,
393
+ findings: formatFindings(open),
394
+ });
373
395
  await writeState(repo, state);
374
- const fix = await fixRound(job, "The reviewer requested changes.", formatFindings(open), earlier, "review");
396
+ const fix = await fixRound(job, 'The reviewer requested changes.', formatFindings(open), earlier, 'review');
375
397
  if (!fix.committed) {
376
398
  const note = fix.unrelated;
377
399
  if (note === undefined) {
378
- await escalate(job, "The fixer added no commit for the review findings.");
379
- return { done: true, report: await finish(job, "needs-human", "no fix commit") };
400
+ await escalate(job, 'The fixer added no commit for the review findings.');
401
+ return { done: true, report: await finish(job, 'needs-human', 'no fix commit') };
380
402
  }
381
403
  if (fix.stray === true) {
382
404
  await escalate(job, `The fixer disputed the findings and left a change behind: ${note}`);
383
- return { done: true, report: await finish(job, "needs-human", "dispute with changes") };
405
+ return { done: true, report: await finish(job, 'needs-human', 'dispute with changes') };
384
406
  }
385
407
  state.reviewLog.at(-1).dispute = note;
386
408
  await writeState(repo, state);
@@ -398,7 +420,7 @@ async function afterMerge(job) {
398
420
  return { done: false };
399
421
  }
400
422
  await setStatus(context, issue.number, state.pr, context.config.labels.done);
401
- return { done: true, report: await finish(job, "done") };
423
+ return { done: true, report: await finish(job, 'done') };
402
424
  }
403
425
  async function watchBase(job) {
404
426
  const { context, log, state, worktree } = job;
@@ -408,26 +430,26 @@ async function watchBase(job) {
408
430
  timeoutMs: config.checkTimeoutMinutes * 60_000,
409
431
  });
410
432
  if (sha === undefined) {
411
- await escalate(job, "The pull request did not merge in time, or it closed without a merge.");
412
- return { done: true, report: await finish(job, "needs-human", "not merged") };
433
+ await escalate(job, 'The pull request did not merge in time, or it closed without a merge.');
434
+ return { done: true, report: await finish(job, 'needs-human', 'not merged') };
413
435
  }
414
- const checks = await log.step("main-checks", { pr: state.pr, sha }, () => ports.waitForBaseChecks(repo, sha, {
436
+ const checks = await log.step('main-checks', { pr: state.pr, sha }, () => ports.waitForBaseChecks(repo, sha, {
415
437
  intervalSeconds: config.checkIntervalSeconds,
416
438
  graceMs: config.checkGraceSeconds * 1000,
417
439
  timeoutMs: config.checkTimeoutMinutes * 60_000,
418
440
  }));
419
- log.event("main-checks.state", { state: checks });
420
- if (checks === "pass" || checks === "none") {
441
+ log.event('main-checks.state', { state: checks });
442
+ if (checks === 'pass' || checks === 'none') {
421
443
  return { done: false, fixed: false };
422
444
  }
423
445
  job.mainBroken = true;
424
- if (checks !== "fail") {
425
- await escalate(job, "The checks on main did not finish in time.");
426
- return { done: true, report: await finish(job, "needs-human", "main checks timeout") };
446
+ if (checks !== 'fail') {
447
+ await escalate(job, 'The checks on main did not finish in time.');
448
+ return { done: true, report: await finish(job, 'needs-human', 'main checks timeout') };
427
449
  }
428
450
  if (state.ciFixes >= config.maxCiFixes) {
429
451
  await escalate(job, `The checks on main failed and the limit of ${config.maxCiFixes} check fixes was reached.`);
430
- return { done: true, report: await finish(job, "needs-human", "check budget") };
452
+ return { done: true, report: await finish(job, 'needs-human', 'check budget') };
431
453
  }
432
454
  await ports.resetToBase(worktree, context.base, config.remote);
433
455
  state.ciFixes += 1;
@@ -440,16 +462,16 @@ async function fixBase(job) {
440
462
  const { repo, config, ports } = context;
441
463
  const { sha, head } = state.baseFix;
442
464
  const logs = await ports.failedBaseLogs(repo, sha, config.logMaxChars);
443
- log.event("main-checks.logs", { chars: logs.length });
444
- const fix = await fixRound(job, "The checks on main failed after the merge.", logs, [], "checks", true, head);
465
+ log.event('main-checks.logs', { chars: logs.length });
466
+ const fix = await fixRound(job, 'The checks on main failed after the merge.', logs, [], 'checks', true, head);
445
467
  if (!fix.committed) {
446
468
  const note = fix.unrelated;
447
469
  await escalate(job, note === undefined
448
- ? "The fixer added no commit for the failed checks on main."
470
+ ? 'The fixer added no commit for the failed checks on main.'
449
471
  : `The checks on main fail for a reason this change did not cause: ${note}`);
450
472
  return {
451
473
  done: true,
452
- report: await finish(job, "needs-human", note === undefined ? "no fix commit" : "unrelated failure"),
474
+ report: await finish(job, 'needs-human', note === undefined ? 'no fix commit' : 'unrelated failure'),
453
475
  };
454
476
  }
455
477
  await ports.reopenIssue(repo, issue.number);
@@ -464,10 +486,10 @@ function escalate(job, reason) {
464
486
  }
465
487
  function stop(job) {
466
488
  const { issue, log, state } = job;
467
- log.event("issue.stop", { phase: state.phase }, "quiet");
489
+ log.event('issue.stop', { phase: state.phase }, 'quiet');
468
490
  return {
469
491
  issue: issue.number,
470
- outcome: "stopped",
492
+ outcome: 'stopped',
471
493
  pr: state.pr,
472
494
  ciFixes: state.ciFixes,
473
495
  reviewRounds: state.reviewRounds,
@@ -478,9 +500,9 @@ async function finish(job, outcome, reason) {
478
500
  const { context, issue, log, state, branch } = job;
479
501
  const { repo, ports } = context;
480
502
  await cleanUp(context, log, branch);
481
- if (outcome === "done") {
503
+ if (outcome === 'done') {
482
504
  if (!(await ports.deleteBranch(repo, branch))) {
483
- log.event("branch.kept", { branch }, "quiet");
505
+ log.event('branch.kept', { branch }, 'quiet');
484
506
  }
485
507
  await dropState(repo, issue.number);
486
508
  }
@@ -492,15 +514,15 @@ async function finish(job, outcome, reason) {
492
514
  ciFixes: state.ciFixes,
493
515
  reviewRounds: state.reviewRounds,
494
516
  ms: Date.now() - job.started,
495
- ...(job.mainBroken && outcome !== "done" ? { halt: true } : {}),
517
+ ...(job.mainBroken && outcome !== 'done' ? { halt: true } : {}),
496
518
  };
497
519
  }
498
520
  async function fixRound(job, reason, detail, earlier, kind, force = false, from) {
499
521
  const { context, issue, log, worktree, branch } = job;
500
522
  const { ports } = context;
501
523
  const before = from ?? (await ports.revision(worktree));
502
- const result = await log.step("fix", { reason }, () => ports.runAgent(log, {
503
- name: "fixer",
524
+ const result = await log.step('fix', { reason }, () => ports.runAgent(log, {
525
+ name: 'fixer',
504
526
  cwd: worktree,
505
527
  prompt: fixPrompt(issue, reason, detail, earlier, kind),
506
528
  profile: CODING,
@@ -510,12 +532,12 @@ async function fixRound(job, reason, detail, earlier, kind, force = false, from)
510
532
  const unrelated = parseUnrelated(result.text);
511
533
  if (unrelated !== undefined) {
512
534
  const stray = (await ports.revision(worktree)) !== before || (await ports.isDirty(worktree));
513
- log.event("fix.unrelated", { reason, note: unrelated, stray }, "quiet");
535
+ log.event('fix.unrelated', { reason, note: unrelated, stray }, 'quiet');
514
536
  return { committed: false, unrelated, stray };
515
537
  }
516
538
  const subject = parseCommitSubject(result.text, `fix: address feedback on issue #${issue.number}`);
517
- if (!(await commitWork(job, before, subject, "fixer"))) {
518
- log.event("commit.empty", { reason }, "quiet");
539
+ if (!(await commitWork(job, before, subject, 'fixer'))) {
540
+ log.event('commit.empty', { reason }, 'quiet');
519
541
  return { committed: false };
520
542
  }
521
543
  await ports.push(worktree, context.config.remote, branch, force);
@@ -527,7 +549,7 @@ async function commitWork(job, before, subject, role) {
527
549
  for (;;) {
528
550
  try {
529
551
  if (await ports.commitAll(worktree, `${subject}\n\nRefs #${issue.number}`)) {
530
- log.event("commit", { subject });
552
+ log.event('commit', { subject });
531
553
  }
532
554
  if ((await ports.revision(worktree)) === before) {
533
555
  return false;
@@ -543,8 +565,8 @@ async function commitWork(job, before, subject, role) {
543
565
  state.commitFixes = used + 1;
544
566
  await writeState(repo, state);
545
567
  const output = message(error).slice(-config.logMaxChars);
546
- log.event("commit.fail", { role, output }, "quiet");
547
- const fix = await log.step("commit-fix", { role }, () => ports.runAgent(log, {
568
+ log.event('commit.fail', { role, output }, 'quiet');
569
+ const fix = await log.step('commit-fix', { role }, () => ports.runAgent(log, {
548
570
  name: role,
549
571
  cwd: worktree,
550
572
  prompt: hookPrompt(issue, comments, role, output),
@@ -563,15 +585,15 @@ async function cleanUp(context, log, branch) {
563
585
  return;
564
586
  }
565
587
  if (!(await context.ports.removeWorktree(context.repo, path))) {
566
- log.event("worktree.kept", { path }, "quiet");
588
+ log.event('worktree.kept', { path }, 'quiet');
567
589
  }
568
590
  }
569
591
  catch (error) {
570
- log.event("worktree.error", { error: message(error) }, "quiet");
592
+ log.event('worktree.error', { error: message(error) }, 'quiet');
571
593
  }
572
594
  }
573
595
  async function handOver(context, log, issue, state, reason) {
574
- log.event("hand-over", { reason }, "quiet");
596
+ log.event('hand-over', { reason }, 'quiet');
575
597
  await writeState(context.repo, state);
576
598
  await setStatus(context, issue, state.pr, context.config.labels.needsHuman);
577
599
  if (state.pr !== undefined) {
@@ -580,9 +602,9 @@ async function handOver(context, log, issue, state, reason) {
580
602
  }
581
603
  async function setStatus(context, issue, pr, label) {
582
604
  const managed = managedLabels(context.config);
583
- await context.ports.setLabel(context.repo, "issue", issue, label, managed);
605
+ await context.ports.setLabel(context.repo, 'issue', issue, label, managed);
584
606
  if (pr !== undefined) {
585
- await context.ports.setLabel(context.repo, "pr", pr, label, managed);
607
+ await context.ports.setLabel(context.repo, 'pr', pr, label, managed);
586
608
  }
587
609
  }
588
610
  function history(rounds) {