next-issue 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -18
- package/dist/cli.mjs +10 -20
- package/dist/exec.mjs +1 -1
- package/dist/git.mjs +22 -10
- package/dist/github.mjs +49 -10
- package/dist/loop.mjs +27 -0
- package/dist/pipeline.mjs +29 -12
- package/dist/prompts.mjs +2 -2
- package/dist/state.mjs +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,22 +24,27 @@ For each open issue, oldest first:
|
|
|
24
24
|
the claim. An issue with saved state under `.next-issue/` resumes instead,
|
|
25
25
|
whatever its labels, unless another person is now the assignee.
|
|
26
26
|
2. Assign the issue to you and add `status:in-progress`.
|
|
27
|
-
3. Fetch the
|
|
28
|
-
safe, then add a git worktree
|
|
27
|
+
3. Fetch the remote, fast-forward the local copy of the default branch when
|
|
28
|
+
that is safe, then add a git worktree at `../<repo>-issue-<n>` on the
|
|
29
|
+
`issue-<n>` branch. A branch that exists on the remote is the start, and a
|
|
30
|
+
local branch that fell behind the remote is fast-forwarded.
|
|
29
31
|
4. Let the implementer agent do the work, then commit and push.
|
|
30
32
|
5. Open a draft pull request that closes the issue.
|
|
31
|
-
6. Wait for the checks
|
|
32
|
-
`checkTimeoutMinutes`. A
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
6. Wait for the checks, once the pull request shows the pushed commit. A
|
|
34
|
+
pending state waits again, up to `checkTimeoutMinutes`. A read that fails
|
|
35
|
+
is tried three times before the run stops. A repository that reports no
|
|
36
|
+
check keeps asking for `checkGraceSeconds`, because a new pull request often
|
|
37
|
+
shows no check yet, and only then goes straight to the review. The later
|
|
38
|
+
gates of the same issue ask once.
|
|
35
39
|
7. On a red build, give the failed job logs to the fixer agent, then go to step
|
|
36
40
|
6 again. The budget is `maxCiFixes`.
|
|
37
41
|
8. Let the reviewer agent judge the diff, at most `diffMaxChars` of it. The
|
|
38
42
|
reviewer returns a structured verdict that rates each finding `blocking` or
|
|
39
43
|
`minor`. The harness puts the result on the pull request.
|
|
40
44
|
9. No blocking finding means approval: mark the pull request ready for review
|
|
41
|
-
and set `status:done`. The merge stays with you.
|
|
42
|
-
|
|
45
|
+
and set `status:done`. The merge stays with you. The worktree, the local
|
|
46
|
+
branch and the state file go away. The remote branch stays. A blocking
|
|
47
|
+
finding always stops the approval, even when the reviewer also says approve.
|
|
43
48
|
10. With a blocking finding, run the fixer agent and go to step 6 again. The
|
|
44
49
|
budget is `maxReviewRounds`.
|
|
45
50
|
|
|
@@ -110,14 +115,14 @@ A short text summary of the same data goes to standard output. Use `--json` to
|
|
|
110
115
|
get the full summary object there instead, so the harness fits in a pipe. All
|
|
111
116
|
progress goes to standard error.
|
|
112
117
|
|
|
113
|
-
Recorded for each step: `claim`, `comments`, `worktree`, `
|
|
114
|
-
`pull-request`, `checks`, `review` and `fix`, each with a `.start`,
|
|
115
|
-
`.error` event and a duration in `ms`. Every `git` and `gh` command
|
|
116
|
-
with its exit code and duration. A `tool` event holds the tool
|
|
117
|
-
`Bash` tool the command too. Each agent call adds a `usage`
|
|
118
|
-
tokens, the cost estimate in dollars, the turn count, the
|
|
119
|
-
model and the session id, so you can open the session
|
|
120
|
-
transcript.
|
|
118
|
+
Recorded for each step: `claim`, `comments`, `worktree`, `setup`, `implement`,
|
|
119
|
+
`push`, `pull-request`, `checks`, `review` and `fix`, each with a `.start`,
|
|
120
|
+
`.end` or `.error` event and a duration in `ms`. Every `git` and `gh` command
|
|
121
|
+
is recorded with its exit code and duration. A `tool` event holds the tool
|
|
122
|
+
name, and for a `Bash` tool the command too. Each agent call adds a `usage`
|
|
123
|
+
event with the tokens, the cost estimate in dollars, the turn count, the
|
|
124
|
+
tool-call count, the model and the session id, so you can open the session
|
|
125
|
+
again and read the full transcript.
|
|
121
126
|
|
|
122
127
|
```bash
|
|
123
128
|
jq -r 'select(.kind=="usage") | [.issue,.role,.turns,.total] | @tsv' \
|
|
@@ -134,7 +139,7 @@ Three levels of console output:
|
|
|
134
139
|
|
|
135
140
|
## Requirements
|
|
136
141
|
|
|
137
|
-
- Node 24 or later
|
|
142
|
+
- Node 24 or later
|
|
138
143
|
- `gh`, authenticated with write access to the repository
|
|
139
144
|
- Anthropic credentials for the Claude Agent SDK: `ANTHROPIC_API_KEY`, or a
|
|
140
145
|
Claude subscription login
|
|
@@ -221,7 +226,8 @@ A hand-over leaves the state file with the used budgets in it, so the next run
|
|
|
221
226
|
would hand the issue over again at once. To give the issue another try, take the
|
|
222
227
|
`status:needs-human` label off and run with `--reset`. The flag puts the budgets
|
|
223
228
|
and the findings back to zero and keeps the branch, the pull request and the
|
|
224
|
-
step that the last run reached, so the work carries on where it stopped.
|
|
229
|
+
step that the last run reached, so the work carries on where it stopped. A
|
|
230
|
+
commit that you pushed to the pull request in the meantime is fetched first.
|
|
225
231
|
|
|
226
232
|
## Commands and options
|
|
227
233
|
|
package/dist/cli.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { loadConfig, writeDefaultConfig } from "./config.mjs";
|
|
|
6
6
|
import { setCommandObserver, setDefaultTimeout } from "./exec.mjs";
|
|
7
7
|
import { detectRepo, ensureIgnored, repoRoot } from "./git.mjs";
|
|
8
8
|
import { currentLogin, defaultBranch, openIssues } from "./github.mjs";
|
|
9
|
+
import { exitCode, runIssues } from "./loop.mjs";
|
|
9
10
|
import { message, Recorder } from "./observe.mjs";
|
|
10
11
|
import { PORTS, processIssue } from "./pipeline.mjs";
|
|
11
12
|
import { resetState, STATE_DIR, takeStop } from "./state.mjs";
|
|
@@ -59,25 +60,14 @@ async function handle(args, config, repo, recorder) {
|
|
|
59
60
|
recorder.event("run.start", { repo: `${repo.owner}/${repo.name}`, base, login, log: recorder.logFile }, "quiet");
|
|
60
61
|
const issues = await openIssues(repo, config.issueLimit);
|
|
61
62
|
recorder.event("issues", { open: issues.length }, "quiet");
|
|
62
|
-
await
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
await resetState(repo, issue.number);
|
|
71
|
-
}
|
|
72
|
-
const report = await processIssue(context, issue);
|
|
73
|
-
reports.push(report);
|
|
74
|
-
if (await takeStop(repo)) {
|
|
75
|
-
recorder.event("run.stop", { issue: issue.number }, "quiet");
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
if (args.once && report.outcome !== "skipped") {
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
63
|
+
const { reports, stoppedAt } = await runIssues(args, {
|
|
64
|
+
issues,
|
|
65
|
+
process: (issue) => processIssue(context, issue),
|
|
66
|
+
reset: (issue) => resetState(repo, issue),
|
|
67
|
+
stop: () => takeStop(repo),
|
|
68
|
+
});
|
|
69
|
+
if (stoppedAt !== undefined) {
|
|
70
|
+
recorder.event("run.stop", { issue: stoppedAt }, "quiet");
|
|
81
71
|
}
|
|
82
72
|
const summary = { ...recorder.summary(), issues: reports };
|
|
83
73
|
recorder.event("run.end", {
|
|
@@ -90,7 +80,7 @@ async function handle(args, config, repo, recorder) {
|
|
|
90
80
|
const summaryFile = join(repo.root, STATE_DIR, "runs", `${recorder.runId}.summary.json`);
|
|
91
81
|
await writeFile(summaryFile, `${JSON.stringify(summary, null, 2)}\n`);
|
|
92
82
|
await write(process.stdout, args.json ? `${JSON.stringify(summary, null, 2)}\n` : formatSummary(summary));
|
|
93
|
-
return
|
|
83
|
+
return exitCode(reports);
|
|
94
84
|
}
|
|
95
85
|
function count(reports, outcome) {
|
|
96
86
|
return reports.filter((report) => report.outcome === outcome).length;
|
package/dist/exec.mjs
CHANGED
package/dist/git.mjs
CHANGED
|
@@ -30,21 +30,26 @@ export function worktreePath(repo, issue) {
|
|
|
30
30
|
export async function addWorktree(repo, issue, base, remote) {
|
|
31
31
|
const path = worktreePath(repo, issue);
|
|
32
32
|
const branch = branchName(issue);
|
|
33
|
-
|
|
33
|
+
const upstream = `${remote}/${branch}`;
|
|
34
|
+
await must("git", ["fetch", remote], { cwd: repo.root });
|
|
34
35
|
await updateBase(repo, base, remote);
|
|
35
36
|
await must("git", ["worktree", "prune"], { cwd: repo.root });
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
const pushed = await hasBranch(repo, upstream, "--remotes");
|
|
38
|
+
if (!(await hasWorktree(repo, path))) {
|
|
39
|
+
const args = (await hasBranch(repo, branch))
|
|
40
|
+
? ["worktree", "add", path, branch]
|
|
41
|
+
: ["worktree", "add", "-b", branch, path, pushed ? upstream : `${remote}/${base}`];
|
|
42
|
+
await must("git", args, { cwd: repo.root });
|
|
43
|
+
}
|
|
44
|
+
if (pushed) {
|
|
45
|
+
await run("git", ["merge", "--ff-only", upstream], { cwd: path });
|
|
38
46
|
}
|
|
39
|
-
const existing = await run("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
40
|
-
cwd: repo.root,
|
|
41
|
-
});
|
|
42
|
-
const args = existing.code === 0
|
|
43
|
-
? ["worktree", "add", path, branch]
|
|
44
|
-
: ["worktree", "add", "-b", branch, path, `${remote}/${base}`];
|
|
45
|
-
await must("git", args, { cwd: repo.root });
|
|
46
47
|
return path;
|
|
47
48
|
}
|
|
49
|
+
async function hasBranch(repo, name, ...flags) {
|
|
50
|
+
const list = await must("git", ["branch", "--list", ...flags, name], { cwd: repo.root });
|
|
51
|
+
return list.length > 0;
|
|
52
|
+
}
|
|
48
53
|
export async function updateBase(repo, base, remote) {
|
|
49
54
|
const head = await run("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: repo.root });
|
|
50
55
|
const args = head.code === 0 && head.stdout.trim() === base
|
|
@@ -98,3 +103,10 @@ export async function commitAll(cwd, message) {
|
|
|
98
103
|
export async function push(cwd, remote, branch) {
|
|
99
104
|
await must("git", ["push", "-u", remote, branch], { cwd });
|
|
100
105
|
}
|
|
106
|
+
export async function revision(cwd) {
|
|
107
|
+
return must("git", ["rev-parse", "HEAD"], { cwd });
|
|
108
|
+
}
|
|
109
|
+
export async function deleteBranch(repo, branch) {
|
|
110
|
+
const result = await run("git", ["branch", "-D", branch], { cwd: repo.root });
|
|
111
|
+
return result.code === 0;
|
|
112
|
+
}
|
package/dist/github.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { must, run } from "./exec.mjs";
|
|
2
2
|
const CHECK_PENDING_CODE = 8;
|
|
3
|
+
const READ_TRIES = 3;
|
|
4
|
+
const KNOWN_LABELS = new Map();
|
|
3
5
|
const NO_CHECKS = /no checks reported/i;
|
|
4
6
|
function slug(repo) {
|
|
5
7
|
return `${repo.owner}/${repo.name}`;
|
|
@@ -22,6 +24,8 @@ export async function openIssues(repo, limit) {
|
|
|
22
24
|
"list",
|
|
23
25
|
"--state",
|
|
24
26
|
"open",
|
|
27
|
+
"--search",
|
|
28
|
+
"sort:created-asc",
|
|
25
29
|
"--limit",
|
|
26
30
|
String(limit),
|
|
27
31
|
"--json",
|
|
@@ -65,7 +69,17 @@ export async function setLabel(repo, kind, number, add, remove) {
|
|
|
65
69
|
await gh(repo, [kind, "edit", String(number), ...labelArgs(add, remove, current)]);
|
|
66
70
|
}
|
|
67
71
|
export async function ensureLabel(repo, label) {
|
|
72
|
+
let known = KNOWN_LABELS.get(slug(repo));
|
|
73
|
+
if (known === undefined) {
|
|
74
|
+
const raw = await gh(repo, ["label", "list", "--limit", "1000", "--json", "name"]);
|
|
75
|
+
known = new Set(JSON.parse(raw).map((item) => item.name));
|
|
76
|
+
KNOWN_LABELS.set(slug(repo), known);
|
|
77
|
+
}
|
|
78
|
+
if (known.has(label)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
68
81
|
await run("gh", ["label", "create", label, "--repo", slug(repo)], { cwd: repo.root });
|
|
82
|
+
known.add(label);
|
|
69
83
|
}
|
|
70
84
|
export async function findPr(repo, branch) {
|
|
71
85
|
const result = await run("gh", ["pr", "list", "--head", branch, "--state", "open", "--json", "number", "--repo", slug(repo)], { cwd: repo.root });
|
|
@@ -95,6 +109,9 @@ export async function createPr(repo, branch, base, title, body, draft) {
|
|
|
95
109
|
}
|
|
96
110
|
return number;
|
|
97
111
|
}
|
|
112
|
+
async function prHead(repo, branch) {
|
|
113
|
+
return gh(repo, ["pr", "view", branch, "--json", "headRefOid", "--jq", ".headRefOid"]);
|
|
114
|
+
}
|
|
98
115
|
async function readChecks(repo, branch) {
|
|
99
116
|
const result = await run("gh", ["pr", "checks", branch, "--json", "name,bucket,link,description", "--repo", slug(repo)], { cwd: repo.root });
|
|
100
117
|
const output = result.stdout.trim();
|
|
@@ -107,8 +124,8 @@ async function readChecks(repo, branch) {
|
|
|
107
124
|
const checks = JSON.parse(output);
|
|
108
125
|
return checks.length === 0 ? "none" : checks;
|
|
109
126
|
}
|
|
110
|
-
async function watchOnce(repo, branch, intervalSeconds, timeoutMs,
|
|
111
|
-
const watch = await run("gh", ["pr", "checks", branch, "--watch", "--fail-fast", "--interval", String(intervalSeconds), "--repo", slug(repo)], { cwd: repo.root,
|
|
127
|
+
async function watchOnce(repo, branch, intervalSeconds, timeoutMs, show) {
|
|
128
|
+
const watch = await run("gh", ["pr", "checks", branch, "--watch", "--fail-fast", "--interval", String(intervalSeconds), "--repo", slug(repo)], { cwd: repo.root, show, timeoutMs });
|
|
112
129
|
if (watch.timedOut) {
|
|
113
130
|
return "timeout";
|
|
114
131
|
}
|
|
@@ -125,31 +142,53 @@ const REAL_CLOCK = {
|
|
|
125
142
|
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
126
143
|
};
|
|
127
144
|
export async function pollChecks(probe, options, clock = REAL_CLOCK) {
|
|
128
|
-
const
|
|
129
|
-
const deadline = started + options.timeoutMs;
|
|
145
|
+
const deadline = clock.now() + options.timeoutMs;
|
|
130
146
|
const rest = () => Math.max(0, deadline - clock.now());
|
|
147
|
+
let shown;
|
|
148
|
+
let failures = 0;
|
|
131
149
|
for (;;) {
|
|
132
150
|
if (rest() === 0) {
|
|
133
151
|
return "timeout";
|
|
134
152
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
153
|
+
const state = await read(probe, options.head, rest(), () => (shown ??= clock.now()));
|
|
154
|
+
if (state instanceof Error) {
|
|
155
|
+
failures += 1;
|
|
156
|
+
if (failures >= READ_TRIES) {
|
|
157
|
+
throw state;
|
|
138
158
|
}
|
|
139
159
|
}
|
|
140
160
|
else {
|
|
141
|
-
|
|
142
|
-
if (state
|
|
161
|
+
failures = 0;
|
|
162
|
+
if (state === "none" && clock.now() - (shown ?? 0) >= options.graceMs) {
|
|
163
|
+
return "none";
|
|
164
|
+
}
|
|
165
|
+
if (state !== "none" && state !== "behind" && state !== "pending") {
|
|
143
166
|
return state;
|
|
144
167
|
}
|
|
145
168
|
}
|
|
146
169
|
await clock.sleep(Math.min(options.intervalSeconds * 1000, rest()));
|
|
147
170
|
}
|
|
148
171
|
}
|
|
172
|
+
async function read(probe, head, timeoutMs, shown) {
|
|
173
|
+
try {
|
|
174
|
+
if (head !== undefined && (await probe.head()) !== head) {
|
|
175
|
+
return "behind";
|
|
176
|
+
}
|
|
177
|
+
shown();
|
|
178
|
+
if ((await probe.list()) === "none") {
|
|
179
|
+
return "none";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
184
|
+
}
|
|
185
|
+
return probe.watch(timeoutMs);
|
|
186
|
+
}
|
|
149
187
|
export function waitForChecks(repo, branch, options) {
|
|
150
188
|
return pollChecks({
|
|
189
|
+
head: () => prHead(repo, branch),
|
|
151
190
|
list: async () => ((await readChecks(repo, branch)) === "none" ? "none" : "some"),
|
|
152
|
-
watch: (timeoutMs) => watchOnce(repo, branch, options.intervalSeconds, timeoutMs, options.
|
|
191
|
+
watch: (timeoutMs) => watchOnce(repo, branch, options.intervalSeconds, timeoutMs, options.show),
|
|
153
192
|
}, options);
|
|
154
193
|
}
|
|
155
194
|
export async function failedCheckLogs(repo, branch, maxChars) {
|
package/dist/loop.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export async function runIssues(args, loop) {
|
|
2
|
+
await loop.stop();
|
|
3
|
+
const reports = [];
|
|
4
|
+
for (const issue of loop.issues) {
|
|
5
|
+
if (args.max !== undefined && handled(reports) >= args.max) {
|
|
6
|
+
break;
|
|
7
|
+
}
|
|
8
|
+
if (args.reset) {
|
|
9
|
+
await loop.reset(issue.number);
|
|
10
|
+
}
|
|
11
|
+
const report = await loop.process(issue);
|
|
12
|
+
reports.push(report);
|
|
13
|
+
if (await loop.stop()) {
|
|
14
|
+
return { reports, stoppedAt: issue.number };
|
|
15
|
+
}
|
|
16
|
+
if (args.once && report.outcome !== "skipped") {
|
|
17
|
+
break;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { reports };
|
|
21
|
+
}
|
|
22
|
+
export function exitCode(reports) {
|
|
23
|
+
return reports.some((report) => report.outcome === "needs-human" || report.outcome === "error") ? 1 : 0;
|
|
24
|
+
}
|
|
25
|
+
function handled(reports) {
|
|
26
|
+
return reports.filter((report) => report.outcome !== "skipped").length;
|
|
27
|
+
}
|
package/dist/pipeline.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { CODING, READ_ONLY, runAgent } from "./agents.mjs";
|
|
2
2
|
import { managedLabels } from "./config.mjs";
|
|
3
3
|
import { run } from "./exec.mjs";
|
|
4
|
-
import { addWorktree, branchName, commitAll, diff, hasWorktree, push, removeWorktree, worktreePath, } from "./git.mjs";
|
|
4
|
+
import { addWorktree, branchName, commitAll, deleteBranch, diff, hasWorktree, push, removeWorktree, revision, worktreePath, } from "./git.mjs";
|
|
5
5
|
import { assignIssue, commentOnPr, createPr, failedCheckLogs, findPr, issueComments, markPrReady, setLabel, waitForChecks, } from "./github.mjs";
|
|
6
6
|
import { message } from "./observe.mjs";
|
|
7
7
|
import { capDiff, fixPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from "./prompts.mjs";
|
|
8
|
-
import { readState, writeState } from "./state.mjs";
|
|
8
|
+
import { dropState, readState, writeState } from "./state.mjs";
|
|
9
9
|
import { blockingFindings, fingerprint, formatFindings, formatVerdict, isApproved, readVerdict, VERDICT_SCHEMA, } from "./verdict.mjs";
|
|
10
10
|
export const PORTS = {
|
|
11
11
|
addWorktree,
|
|
@@ -13,6 +13,7 @@ export const PORTS = {
|
|
|
13
13
|
commentOnPr,
|
|
14
14
|
commitAll,
|
|
15
15
|
createPr,
|
|
16
|
+
deleteBranch,
|
|
16
17
|
diff,
|
|
17
18
|
failedCheckLogs,
|
|
18
19
|
findPr,
|
|
@@ -21,6 +22,7 @@ export const PORTS = {
|
|
|
21
22
|
markPrReady,
|
|
22
23
|
push,
|
|
23
24
|
removeWorktree,
|
|
25
|
+
revision,
|
|
24
26
|
runAgent,
|
|
25
27
|
runSetup: async (command, cwd, timeoutMs) => run("bash", ["-lc", command], { cwd, timeoutMs }),
|
|
26
28
|
setLabel,
|
|
@@ -101,6 +103,12 @@ async function work(context, issue, log, state) {
|
|
|
101
103
|
const escalate = (reason) => handOver(context, log, issue.number, state, reason);
|
|
102
104
|
const finish = async (outcome, reason) => {
|
|
103
105
|
await cleanUp(context, log, issue.number);
|
|
106
|
+
if (outcome === "done") {
|
|
107
|
+
if (!(await ports.deleteBranch(repo, branch))) {
|
|
108
|
+
log.event("branch.kept", { branch }, "quiet");
|
|
109
|
+
}
|
|
110
|
+
await dropState(repo, issue.number);
|
|
111
|
+
}
|
|
104
112
|
return {
|
|
105
113
|
issue: issue.number,
|
|
106
114
|
outcome,
|
|
@@ -123,6 +131,7 @@ async function work(context, issue, log, state) {
|
|
|
123
131
|
}
|
|
124
132
|
}
|
|
125
133
|
if (state.phase === "claimed") {
|
|
134
|
+
const before = await ports.revision(worktree);
|
|
126
135
|
const result = await log.step("implement", {}, () => ports.runAgent(log, {
|
|
127
136
|
name: "implementer",
|
|
128
137
|
cwd: worktree,
|
|
@@ -132,11 +141,10 @@ async function work(context, issue, log, state) {
|
|
|
132
141
|
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
|
133
142
|
}));
|
|
134
143
|
const subject = parseCommitSubject(result.text, `fix: resolve issue #${issue.number}`);
|
|
135
|
-
if (!(await
|
|
144
|
+
if (!(await commitWork(job, before, subject))) {
|
|
136
145
|
await escalate("The implementer produced no change.");
|
|
137
146
|
return finish("needs-human", "no implementation commit");
|
|
138
147
|
}
|
|
139
|
-
log.event("commit", { subject });
|
|
140
148
|
state.phase = "implemented";
|
|
141
149
|
await writeState(repo, state);
|
|
142
150
|
}
|
|
@@ -149,14 +157,18 @@ async function work(context, issue, log, state) {
|
|
|
149
157
|
state.phase = "review";
|
|
150
158
|
await writeState(repo, state);
|
|
151
159
|
log.event("pull-request.ready", { pr: state.pr }, "quiet");
|
|
160
|
+
let noChecks = false;
|
|
152
161
|
const gate = async () => {
|
|
153
|
-
const
|
|
162
|
+
const head = await ports.revision(worktree);
|
|
163
|
+
const checks = await log.step("checks", { pr: state.pr, head }, () => ports.waitForChecks(repo, branch, {
|
|
164
|
+
head,
|
|
154
165
|
intervalSeconds: config.checkIntervalSeconds,
|
|
155
|
-
graceMs: config.checkGraceSeconds * 1000,
|
|
166
|
+
graceMs: noChecks ? 0 : config.checkGraceSeconds * 1000,
|
|
156
167
|
timeoutMs: config.checkTimeoutMinutes * 60_000,
|
|
157
|
-
|
|
168
|
+
show: log.level === "verbose",
|
|
158
169
|
}));
|
|
159
170
|
log.event("checks.state", { state: checks });
|
|
171
|
+
noChecks ||= checks === "none";
|
|
160
172
|
if (checks === "timeout") {
|
|
161
173
|
await escalate("The checks did not finish in time.");
|
|
162
174
|
return { done: true, report: await finish("needs-human", "checks timeout") };
|
|
@@ -210,7 +222,7 @@ async function work(context, issue, log, state) {
|
|
|
210
222
|
const review = await log.step("review", fields, () => ports.runAgent(log, {
|
|
211
223
|
name: "reviewer",
|
|
212
224
|
cwd: worktree,
|
|
213
|
-
prompt: reviewPrompt(issue, comments, patch,
|
|
225
|
+
prompt: reviewPrompt(issue, comments, patch, history(state.reviewLog)),
|
|
214
226
|
profile: READ_ONLY,
|
|
215
227
|
model: config.models.reviewer,
|
|
216
228
|
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
|
@@ -242,8 +254,6 @@ async function work(context, issue, log, state) {
|
|
|
242
254
|
log.event("pull-request.undraft.fail", { pr: state.pr }, "quiet");
|
|
243
255
|
}
|
|
244
256
|
await setStatus(context, issue.number, state.pr, config.labels.done);
|
|
245
|
-
state.phase = "done";
|
|
246
|
-
await writeState(repo, state);
|
|
247
257
|
return finish("done");
|
|
248
258
|
}
|
|
249
259
|
const mark = fingerprint(blocking);
|
|
@@ -264,6 +274,7 @@ async function work(context, issue, log, state) {
|
|
|
264
274
|
async function fixRound(job, reason, detail, earlier, kind) {
|
|
265
275
|
const { context, issue, log, worktree, branch } = job;
|
|
266
276
|
const { ports } = context;
|
|
277
|
+
const before = await ports.revision(worktree);
|
|
267
278
|
const result = await log.step("fix", { reason }, () => ports.runAgent(log, {
|
|
268
279
|
name: "fixer",
|
|
269
280
|
cwd: worktree,
|
|
@@ -278,14 +289,20 @@ async function fixRound(job, reason, detail, earlier, kind) {
|
|
|
278
289
|
return { committed: false, unrelated };
|
|
279
290
|
}
|
|
280
291
|
const subject = parseCommitSubject(result.text, `fix: address feedback on issue #${issue.number}`);
|
|
281
|
-
if (!(await
|
|
292
|
+
if (!(await commitWork(job, before, subject))) {
|
|
282
293
|
log.event("commit.empty", { reason }, "quiet");
|
|
283
294
|
return { committed: false };
|
|
284
295
|
}
|
|
285
|
-
log.event("commit", { subject });
|
|
286
296
|
await ports.push(worktree, context.config.remote, branch);
|
|
287
297
|
return { committed: true };
|
|
288
298
|
}
|
|
299
|
+
async function commitWork(job, before, subject) {
|
|
300
|
+
const { context, issue, log, worktree } = job;
|
|
301
|
+
if (await context.ports.commitAll(worktree, `${subject}\n\nRefs #${issue.number}`)) {
|
|
302
|
+
log.event("commit", { subject });
|
|
303
|
+
}
|
|
304
|
+
return (await context.ports.revision(worktree)) !== before;
|
|
305
|
+
}
|
|
289
306
|
async function cleanUp(context, log, issue) {
|
|
290
307
|
const path = worktreePath(context.repo, issue);
|
|
291
308
|
try {
|
package/dist/prompts.mjs
CHANGED
|
@@ -26,8 +26,8 @@ export function capDiff(diff, maxChars) {
|
|
|
26
26
|
? diff
|
|
27
27
|
: `${diff.slice(0, maxChars)}\n... The diff is cut here, after ${maxChars} characters.`;
|
|
28
28
|
}
|
|
29
|
-
export function reviewPrompt(issue, comments, diff,
|
|
30
|
-
const scope =
|
|
29
|
+
export function reviewPrompt(issue, comments, diff, earlier) {
|
|
30
|
+
const scope = earlier.length === 0
|
|
31
31
|
? "Check correctness, scope, repository conventions and test cover."
|
|
32
32
|
: [
|
|
33
33
|
"This is a later round. Judge two things only:",
|
package/dist/state.mjs
CHANGED