next-issue 0.15.0 → 0.17.0
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 +13 -6
- package/dist/args.mjs +5 -0
- package/dist/cli.mjs +1 -1
- package/dist/github.mjs +17 -0
- package/dist/pipeline.mjs +25 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -22,8 +22,10 @@ For each open issue, oldest first:
|
|
|
22
22
|
1. Claim the issue only when it holds the `status:todo` label and holds no
|
|
23
23
|
`status:in-progress` or `status:in-review` label. A `status:done`,
|
|
24
24
|
`status:needs-human` or `status:blocked` label always stops the claim, a
|
|
25
|
-
resume too. An issue
|
|
26
|
-
|
|
25
|
+
resume too. An issue that GitHub lists as blocked by an open issue waits,
|
|
26
|
+
because its blocker closes only when the pull request of the blocker merges.
|
|
27
|
+
An issue with saved state under `.next-issue/` resumes instead, whatever its
|
|
28
|
+
other labels and blockers.
|
|
27
29
|
2. Add `status:in-progress` to the issue.
|
|
28
30
|
3. Fetch the remote, fast-forward the local copy of the default branch when
|
|
29
31
|
that is safe, then add a git worktree at `../<repo>-issue-<n>` on the
|
|
@@ -56,8 +58,10 @@ For each open issue, oldest first:
|
|
|
56
58
|
round gets no review. Thus `maxReviewRounds` of `1` never fixes a minor
|
|
57
59
|
finding.
|
|
58
60
|
10. No open finding means approval: mark the pull request ready for review and
|
|
59
|
-
set `status:done`. The merge stays with you
|
|
60
|
-
|
|
61
|
+
set `status:done`. The merge stays with you, unless you pass `--merge`:
|
|
62
|
+
then the harness squash-merges the pull request and deletes the remote
|
|
63
|
+
branch. The worktree, the local branch and the state file go away. Without
|
|
64
|
+
`--merge`, the remote branch stays. A problem that the
|
|
61
65
|
reviewer leaves out of the findings thus gets an approval: only the findings
|
|
62
66
|
count.
|
|
63
67
|
11. With an open finding, run the fixer agent and go to step 6 again. The
|
|
@@ -89,6 +93,8 @@ when:
|
|
|
89
93
|
ping-pong between the fixer and the reviewer, or a finding that the fixer
|
|
90
94
|
disputed and the reviewer holds to. The person then decides;
|
|
91
95
|
- the reviewer gives no verdict, or the checks do not finish in time;
|
|
96
|
+
- with `--merge`, the merge of an approved pull request fails, for example
|
|
97
|
+
because branch protection asks for a human approval;
|
|
92
98
|
- an agent does not finish in `agentTimeoutMinutes`;
|
|
93
99
|
- the setup command fails, or does not finish in `setupTimeoutMinutes`;
|
|
94
100
|
- a step of the run fails with an error.
|
|
@@ -264,8 +270,8 @@ does not know all stop the run, so a typo cannot pass without a word.
|
|
|
264
270
|
An empty `labels.ready` turns the ready requirement off. The harness then
|
|
265
271
|
claims every open issue that no other label holds back.
|
|
266
272
|
|
|
267
|
-
The log gives the reason for a skipped issue as `stop-label`, `not-ready
|
|
268
|
-
`in-flight
|
|
273
|
+
The log gives the reason for a skipped issue as `stop-label`, `not-ready`,
|
|
274
|
+
`in-flight` or `blocked`, the last one with the open blockers.
|
|
269
275
|
|
|
270
276
|
A role set to `null`, or with no entry in `models`, uses the default model of
|
|
271
277
|
the agent.
|
|
@@ -333,6 +339,7 @@ that a label still holds back stays as it was.
|
|
|
333
339
|
| `--once` | Stop after the first handled issue |
|
|
334
340
|
| `--max <n>` | Handle at most n issues |
|
|
335
341
|
| `--agent <a>` | Run the agents with `claude` or `pi`, whatever the config file says |
|
|
342
|
+
| `--merge` | Squash-merge the pull request after the approval, and delete the remote branch |
|
|
336
343
|
| `--reset` | Drop the saved budgets and findings, and keep the rest of the state |
|
|
337
344
|
| `--json` | Print the machine summary instead of the text one |
|
|
338
345
|
| `--verbose` | Show every command and all agent output |
|
package/dist/args.mjs
CHANGED
|
@@ -6,6 +6,7 @@ export const USAGE = `next-issue [command] [options]
|
|
|
6
6
|
--once stop after the first handled issue
|
|
7
7
|
--max <n> handle at most n issues
|
|
8
8
|
--agent <a> run the agents with claude or pi
|
|
9
|
+
--merge squash-merge the pull request after the approval
|
|
9
10
|
--reset drop the saved budgets and findings first
|
|
10
11
|
--json print the machine summary instead of the text one
|
|
11
12
|
--force replace an existing config file, with init
|
|
@@ -21,6 +22,7 @@ export function parseArgs(argv) {
|
|
|
21
22
|
json: false,
|
|
22
23
|
reset: false,
|
|
23
24
|
force: false,
|
|
25
|
+
merge: false,
|
|
24
26
|
level: "normal",
|
|
25
27
|
};
|
|
26
28
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -40,6 +42,9 @@ export function parseArgs(argv) {
|
|
|
40
42
|
else if (arg === "--help") {
|
|
41
43
|
args.help = true;
|
|
42
44
|
}
|
|
45
|
+
else if (arg === "--merge") {
|
|
46
|
+
args.merge = true;
|
|
47
|
+
}
|
|
43
48
|
else if (arg === "--reset") {
|
|
44
49
|
args.reset = true;
|
|
45
50
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -62,7 +62,7 @@ async function init(root, force) {
|
|
|
62
62
|
}
|
|
63
63
|
async function handle(args, config, repo, recorder) {
|
|
64
64
|
const base = await defaultBranch(repo);
|
|
65
|
-
const context = { repo, config, base, reset: args.reset, recorder, ports: PORTS };
|
|
65
|
+
const context = { repo, config, base, reset: args.reset, merge: args.merge, recorder, ports: PORTS };
|
|
66
66
|
recorder.event("run.start", {
|
|
67
67
|
version: await packageVersion(),
|
|
68
68
|
repo: `${repo.owner}/${repo.name}`,
|
package/dist/github.mjs
CHANGED
|
@@ -44,6 +44,19 @@ export async function issueComments(repo, issue) {
|
|
|
44
44
|
const parsed = JSON.parse(raw);
|
|
45
45
|
return parsed.comments.map((comment) => `@${comment.author.login}: ${comment.body}`);
|
|
46
46
|
}
|
|
47
|
+
export async function openBlockers(repo, issue) {
|
|
48
|
+
const raw = await must("gh", [
|
|
49
|
+
"api",
|
|
50
|
+
"--paginate",
|
|
51
|
+
`repos/${slug(repo)}/issues/${issue}/dependencies/blocked_by`,
|
|
52
|
+
"--jq",
|
|
53
|
+
'.[] | select(.state == "open") | (.repository_url | split("/") | .[-2:] | join("/")) + "#" + (.number | tostring)',
|
|
54
|
+
], { cwd: repo.root });
|
|
55
|
+
return raw
|
|
56
|
+
.split("\n")
|
|
57
|
+
.filter((line) => line.length > 0)
|
|
58
|
+
.map((ref) => (ref.startsWith(`${slug(repo)}#`) ? ref.slice(slug(repo).length) : ref));
|
|
59
|
+
}
|
|
47
60
|
async function labelsOf(repo, kind, number) {
|
|
48
61
|
const raw = await gh(repo, [kind, "view", String(number), "--json", "labels"]);
|
|
49
62
|
const parsed = JSON.parse(raw);
|
|
@@ -227,6 +240,10 @@ export async function markPrReady(repo, pr) {
|
|
|
227
240
|
});
|
|
228
241
|
return result.code === 0;
|
|
229
242
|
}
|
|
243
|
+
export async function mergePr(repo, pr) {
|
|
244
|
+
const result = await run("gh", ["pr", "merge", String(pr), "--squash", "--delete-branch", "--repo", slug(repo)], { cwd: repo.root });
|
|
245
|
+
return result.code === 0;
|
|
246
|
+
}
|
|
230
247
|
export async function commentOnPr(repo, pr, body) {
|
|
231
248
|
await gh(repo, ["pr", "comment", String(pr), "--body-file", "-"], body);
|
|
232
249
|
}
|
package/dist/pipeline.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { CODING, REVIEWING, runAgent } from "./agents.mjs";
|
|
|
2
2
|
import { managedLabels } from "./config.mjs";
|
|
3
3
|
import { run } from "./exec.mjs";
|
|
4
4
|
import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, hasWorktree, isDirty, push, removeWorktree, revision, worktreePath, } from "./git.mjs";
|
|
5
|
-
import { commentOnPr, createPr, failedCheckLogs, findPr, issueComments, markPrReady, setLabel, waitForChecks, } from "./github.mjs";
|
|
5
|
+
import { commentOnPr, createPr, failedCheckLogs, findPr, issueComments, markPrReady, mergePr, openBlockers, 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
8
|
import { detectSetupCommand } from "./setup.mjs";
|
|
@@ -23,6 +23,8 @@ export const PORTS = {
|
|
|
23
23
|
isDirty,
|
|
24
24
|
issueComments,
|
|
25
25
|
markPrReady,
|
|
26
|
+
mergePr,
|
|
27
|
+
openBlockers,
|
|
26
28
|
push,
|
|
27
29
|
removeWorktree,
|
|
28
30
|
revision,
|
|
@@ -55,7 +57,21 @@ export async function processIssue(context, issue) {
|
|
|
55
57
|
const skip = skipReason(issue, context.config, saved !== undefined);
|
|
56
58
|
if (skip !== undefined) {
|
|
57
59
|
log.event("issue.skip", { title: issue.title, reason: skip });
|
|
58
|
-
return
|
|
60
|
+
return skipped(issue);
|
|
61
|
+
}
|
|
62
|
+
if (saved === undefined) {
|
|
63
|
+
let blockers;
|
|
64
|
+
try {
|
|
65
|
+
blockers = await context.ports.openBlockers(context.repo, issue.number);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
log.event("issue.error", { error: message(error), ms: Date.now() - started }, "quiet");
|
|
69
|
+
return { ...skipped(issue), outcome: "error", reason: message(error), ms: Date.now() - started };
|
|
70
|
+
}
|
|
71
|
+
if (blockers.length > 0) {
|
|
72
|
+
log.event("issue.skip", { title: issue.title, reason: "blocked", blockers });
|
|
73
|
+
return skipped(issue);
|
|
74
|
+
}
|
|
59
75
|
}
|
|
60
76
|
log.event("issue.start", { title: issue.title, createdAt: issue.createdAt }, "quiet");
|
|
61
77
|
const fresh = {
|
|
@@ -92,6 +108,9 @@ export async function processIssue(context, issue) {
|
|
|
92
108
|
};
|
|
93
109
|
}
|
|
94
110
|
}
|
|
111
|
+
function skipped(issue) {
|
|
112
|
+
return { issue: issue.number, outcome: "skipped", ciFixes: 0, reviewRounds: 0, ms: 0 };
|
|
113
|
+
}
|
|
95
114
|
async function work(context, issue, log, state) {
|
|
96
115
|
const started = Date.now();
|
|
97
116
|
const { repo, config, ports } = context;
|
|
@@ -270,6 +289,10 @@ async function reviewRound(job) {
|
|
|
270
289
|
if (config.draftPullRequest && !(await ports.markPrReady(repo, pr))) {
|
|
271
290
|
log.event("pull-request.undraft.fail", { pr }, "quiet");
|
|
272
291
|
}
|
|
292
|
+
if (context.merge && !(await ports.mergePr(repo, pr))) {
|
|
293
|
+
await escalate(job, "The merge of the pull request failed.");
|
|
294
|
+
return { done: true, report: await finish(job, "needs-human", "merge failed") };
|
|
295
|
+
}
|
|
273
296
|
await setStatus(context, issue.number, pr, config.labels.done);
|
|
274
297
|
return { done: true, report: await finish(job, "done") };
|
|
275
298
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-issue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Autonomous issue harness driven by the Claude Agent SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"prepublishOnly": "npm run typecheck && npm test"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "^26.6.
|
|
40
|
+
"@types/node": "^26.6.6",
|
|
41
41
|
"typescript": "^7.0.2"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
44
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.316"
|
|
45
45
|
}
|
|
46
46
|
}
|