next-issue 2.4.1 → 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/README.md +68 -68
- package/dist/agents.mjs +32 -31
- package/dist/args.mjs +17 -17
- package/dist/cli.mjs +16 -14
- package/dist/config.mjs +42 -42
- package/dist/exec.mjs +26 -26
- package/dist/git.mjs +40 -40
- package/dist/github.mjs +107 -78
- package/dist/interrupt.mjs +7 -7
- package/dist/loop.mjs +5 -3
- package/dist/observe.mjs +19 -19
- package/dist/pipeline.mjs +129 -113
- package/dist/progress.mjs +22 -22
- package/dist/prompts.mjs +52 -52
- package/dist/run.mjs +28 -28
- package/dist/serve.mjs +49 -45
- package/dist/setup.mjs +13 -13
- package/dist/state.mjs +20 -20
- package/dist/summary.mjs +7 -7
- package/dist/verdict.mjs +30 -25
- package/dist/version.mjs +3 -3
- package/package.json +7 -1
package/dist/pipeline.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { CODING, REVIEWING, runAgent } from
|
|
2
|
-
import { managedLabels } from
|
|
3
|
-
import { run } from
|
|
4
|
-
import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, findWorktree, isDirty, push, removeWorktree, resetToBase, revision, } from
|
|
5
|
-
import { commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, openIssues, prMerged, reopenIssue, setLabel, viewIssue, waitForBaseChecks, waitForChecks, } from
|
|
6
|
-
import { message } from
|
|
7
|
-
import { capDiff, fixPrompt, hookPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from
|
|
8
|
-
import { detectSetupCommand } from
|
|
9
|
-
import { dropState, readState, resetState, savedIssues, writeState, } from
|
|
10
|
-
import { blockingFindings, fingerprint, formatFindings, formatVerdict, openFindings, readVerdict, VERDICT_SCHEMA, } from
|
|
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(
|
|
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
|
|
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
|
|
54
|
+
return 'not-ready';
|
|
55
55
|
}
|
|
56
56
|
if (labels.has(names.inProgress) || labels.has(names.inReview)) {
|
|
57
|
-
return
|
|
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(
|
|
91
|
-
return {
|
|
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(
|
|
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(
|
|
105
|
-
return {
|
|
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(
|
|
118
|
+
log.event('issue.skip', { title: issue.title, reason: 'blocked', blockers });
|
|
109
119
|
return skipped(issue);
|
|
110
120
|
}
|
|
111
121
|
}
|
|
112
|
-
log.event(
|
|
122
|
+
log.event('issue.start', { title: issue.title, createdAt: issue.createdAt }, 'quiet');
|
|
113
123
|
const fresh = {
|
|
114
124
|
issue: issue.number,
|
|
115
|
-
phase:
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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(
|
|
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(
|
|
159
|
-
const added = await log.step(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
:
|
|
185
|
-
return finish(job,
|
|
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 ===
|
|
198
|
+
if (state.phase === 'claimed') {
|
|
189
199
|
const before = await ports.revision(worktree);
|
|
190
|
-
const result = await log.step(
|
|
191
|
-
name:
|
|
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,
|
|
200
|
-
await escalate(job,
|
|
201
|
-
return finish(job,
|
|
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 =
|
|
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 ===
|
|
217
|
-
|
|
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,
|
|
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(
|
|
229
|
-
state.phase =
|
|
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(
|
|
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(
|
|
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(
|
|
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 ===
|
|
285
|
+
show: log.level === 'verbose',
|
|
274
286
|
}));
|
|
275
|
-
log.event(
|
|
276
|
-
job.noChecks ||= checks ===
|
|
277
|
-
if (checks ===
|
|
278
|
-
await escalate(job,
|
|
279
|
-
return { done: true, report: await finish(job,
|
|
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 !==
|
|
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,
|
|
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(
|
|
292
|
-
const fix = await fixRound(job,
|
|
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
|
-
?
|
|
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,
|
|
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,
|
|
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(
|
|
325
|
-
name:
|
|
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(
|
|
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,
|
|
339
|
-
return { done: true, report: await finish(job,
|
|
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(
|
|
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
|
-
},
|
|
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(
|
|
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,
|
|
365
|
-
return { done: true, report: await finish(job,
|
|
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,
|
|
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,
|
|
375
|
-
return { done: true, report: await finish(job,
|
|
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({
|
|
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,
|
|
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,
|
|
385
|
-
return { done: true, report: await finish(job,
|
|
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,
|
|
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,
|
|
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,
|
|
418
|
-
return { done: true, report: await finish(job,
|
|
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(
|
|
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(
|
|
426
|
-
if (checks ===
|
|
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 !==
|
|
431
|
-
await escalate(job,
|
|
432
|
-
return { done: true, report: await finish(job,
|
|
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,
|
|
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(
|
|
450
|
-
const fix = await fixRound(job,
|
|
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
|
-
?
|
|
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,
|
|
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(
|
|
489
|
+
log.event('issue.stop', { phase: state.phase }, 'quiet');
|
|
474
490
|
return {
|
|
475
491
|
issue: issue.number,
|
|
476
|
-
outcome:
|
|
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 ===
|
|
503
|
+
if (outcome === 'done') {
|
|
488
504
|
if (!(await ports.deleteBranch(repo, branch))) {
|
|
489
|
-
log.event(
|
|
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 !==
|
|
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(
|
|
509
|
-
name:
|
|
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(
|
|
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,
|
|
524
|
-
log.event(
|
|
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(
|
|
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(
|
|
553
|
-
const fix = await log.step(
|
|
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(
|
|
588
|
+
log.event('worktree.kept', { path }, 'quiet');
|
|
573
589
|
}
|
|
574
590
|
}
|
|
575
591
|
catch (error) {
|
|
576
|
-
log.event(
|
|
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(
|
|
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,
|
|
605
|
+
await context.ports.setLabel(context.repo, 'issue', issue, label, managed);
|
|
590
606
|
if (pr !== undefined) {
|
|
591
|
-
await context.ports.setLabel(context.repo,
|
|
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([
|
|
2
|
+
const NOT_STAGES = new Set(['issue.start', 'agent.start', 'run.start']);
|
|
3
3
|
export function startProgress() {
|
|
4
|
-
return { status:
|
|
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[
|
|
12
|
-
const command = record[
|
|
13
|
-
const line = typeof command ===
|
|
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[
|
|
18
|
-
if (kind ===
|
|
17
|
+
const kind = String(record['kind']);
|
|
18
|
+
if (kind === 'issue.start') {
|
|
19
19
|
const next = {
|
|
20
20
|
...idle(progress),
|
|
21
|
-
issue: Number(record[
|
|
22
|
-
issueStartedAt: Date.parse(String(record[
|
|
21
|
+
issue: Number(record['issue']),
|
|
22
|
+
issueStartedAt: Date.parse(String(record['ts'])),
|
|
23
23
|
};
|
|
24
|
-
if (typeof record[
|
|
25
|
-
next.title = record[
|
|
24
|
+
if (typeof record['title'] === 'string') {
|
|
25
|
+
next.title = record['title'];
|
|
26
26
|
}
|
|
27
27
|
return next;
|
|
28
28
|
}
|
|
29
|
-
if (kind ===
|
|
29
|
+
if (kind === 'tool') {
|
|
30
30
|
return { ...progress, tools: progress.tools + 1, lastTool: toolLine(record) };
|
|
31
31
|
}
|
|
32
|
-
if (kind ===
|
|
33
|
-
const outcome = kind ===
|
|
34
|
-
const handled = outcome ===
|
|
35
|
-
? [...progress.handled, { issue: Number(record[
|
|
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 ===
|
|
40
|
-
return { ...progress, error: String(record[
|
|
39
|
+
if (kind === 'issues.fail') {
|
|
40
|
+
return { ...progress, error: String(record['error']) };
|
|
41
41
|
}
|
|
42
|
-
if (kind.endsWith(
|
|
43
|
-
const stage = kind.slice(0, -
|
|
44
|
-
const round = record[
|
|
45
|
-
return { ...progress, stage: typeof round ===
|
|
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
|
}
|