next-issue 2.4.1 → 2.4.3

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