next-issue 2.9.0 → 2.10.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 +17 -6
- package/dist/fix.mjs +1 -1
- package/dist/github.mjs +35 -0
- package/dist/handover.mjs +30 -1
- package/dist/pipeline.mjs +8 -4
- package/dist/ports.mjs +3 -1
- package/dist/prompts.mjs +16 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -167,7 +167,9 @@ comment: `### Review: approved`, `### Review: changes requested` and
|
|
|
167
167
|
`### Fixer: no change`. The account is the same for all of them, so the heading,
|
|
168
168
|
not the author, tells them apart. The agents read the comments of the issue, not
|
|
169
169
|
of the pull request, so a comment of an agent never returns to a prompt: the
|
|
170
|
-
dispute of the fixer reaches the reviewer through the state file.
|
|
170
|
+
dispute of the fixer reaches the reviewer through the state file. The one
|
|
171
|
+
exception is the answer to a hand-over, see
|
|
172
|
+
[Retry an issue that waits for a person](#retry-an-issue-that-waits-for-a-person).
|
|
171
173
|
|
|
172
174
|
## Stop a run
|
|
173
175
|
|
|
@@ -233,11 +235,11 @@ A short text summary of the same data goes to standard output. Use `--json` to
|
|
|
233
235
|
get the full summary object there instead, so the harness fits in a pipe. All
|
|
234
236
|
progress goes to standard error.
|
|
235
237
|
|
|
236
|
-
Recorded for each step: `claim`, `comments`, `
|
|
237
|
-
`push`, `pull-request`, `checks`, `review` and `fix`, each with a
|
|
238
|
-
`.end` or `.error` event and a duration in `ms`. Every `git` and `gh`
|
|
239
|
-
is recorded with its exit code and duration. A `tool` event holds the
|
|
240
|
-
name, and for a `bash` tool the command too. Each agent call adds a `usage`
|
|
238
|
+
Recorded for each step: `claim`, `comments`, `reply`, `worktree`, `setup`,
|
|
239
|
+
`implement`, `push`, `pull-request`, `checks`, `review` and `fix`, each with a
|
|
240
|
+
`.start`, `.end` or `.error` event and a duration in `ms`. Every `git` and `gh`
|
|
241
|
+
command is recorded with its exit code and duration. A `tool` event holds the
|
|
242
|
+
tool name, and for a `bash` tool the command too. Each agent call adds a `usage`
|
|
241
243
|
event with the tokens, the cost estimate in dollars, the turn count, the
|
|
242
244
|
tool-call count, the model and the session id.
|
|
243
245
|
|
|
@@ -386,6 +388,15 @@ the pull request and the step that the last run reached, so the work carries on
|
|
|
386
388
|
where it stopped. A commit that you pushed to the pull request in the meantime
|
|
387
389
|
is fetched first. An issue that still has the label is not claimed.
|
|
388
390
|
|
|
391
|
+
To give guidance, answer the hand-over with a comment in the same place: on the
|
|
392
|
+
pull request, or on the issue when there is no pull request. When the run
|
|
393
|
+
resumes the issue, it reads the comments posted after its last
|
|
394
|
+
`next-issue needs help:` comment there. Only comments from users with write
|
|
395
|
+
access to the repository count, for the hand-over comment too. The implementer
|
|
396
|
+
and every fixer of that run get them in their prompt, under the heading
|
|
397
|
+
`Answer of a person to the hand-over`. A short reply such as "ready, take it" is
|
|
398
|
+
enough. With no such comment, the run resumes with no guidance.
|
|
399
|
+
|
|
389
400
|
The `--reset` flag does the same for any issue that the run handles, whether or
|
|
390
401
|
not it was handed over. It touches only an issue that the run handles, so the
|
|
391
402
|
state of an issue that a label still holds back stays as it was.
|
package/dist/fix.mjs
CHANGED
|
@@ -10,7 +10,7 @@ export async function fixRound(job, reason, detail, earlier, kind, force = false
|
|
|
10
10
|
const result = await log.step('fix', { reason }, () => ports.runAgent(log, {
|
|
11
11
|
name: 'fixer',
|
|
12
12
|
cwd: worktree,
|
|
13
|
-
prompt: fixPrompt(issue, reason, detail, earlier, kind),
|
|
13
|
+
prompt: fixPrompt(issue, reason, detail, earlier, kind, job.reply),
|
|
14
14
|
profile: CODING,
|
|
15
15
|
model: context.config.models.fixer,
|
|
16
16
|
timeoutMs: context.config.agentTimeoutMinutes * 60_000,
|
package/dist/github.mjs
CHANGED
|
@@ -74,6 +74,41 @@ export async function issueComments(repo, issue) {
|
|
|
74
74
|
const parsed = JSON.parse(raw);
|
|
75
75
|
return parsed.comments.map((comment) => `@${comment.author.login}: ${comment.body}`);
|
|
76
76
|
}
|
|
77
|
+
export async function threadComments(repo, number) {
|
|
78
|
+
const raw = await repeatable([
|
|
79
|
+
'api',
|
|
80
|
+
'--paginate',
|
|
81
|
+
`repos/${slug(repo)}/issues/${number}/comments`,
|
|
82
|
+
'--jq',
|
|
83
|
+
'.[] | [.user.login, .body] | @json',
|
|
84
|
+
], { cwd: repo.root });
|
|
85
|
+
return raw
|
|
86
|
+
.split('\n')
|
|
87
|
+
.filter((line) => line.length > 0)
|
|
88
|
+
.map((line) => {
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(line);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new Error(`gh gave a comment of #${number} that is not valid JSON: ${line}`);
|
|
95
|
+
}
|
|
96
|
+
const [author, body] = parsed;
|
|
97
|
+
return { author, body: body ?? '' };
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export async function canWrite(repo, login) {
|
|
101
|
+
try {
|
|
102
|
+
const permission = await repeatable(['api', `repos/${slug(repo)}/collaborators/${login}/permission`, '--jq', '.permission'], { cwd: repo.root });
|
|
103
|
+
return ['admin', 'write'].includes(permission.trim());
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error instanceof Error && /HTTP 404/.test(error.message)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
77
112
|
export async function openBlockers(repo, issue) {
|
|
78
113
|
const raw = await repeatable([
|
|
79
114
|
'api',
|
package/dist/handover.mjs
CHANGED
|
@@ -1,6 +1,35 @@
|
|
|
1
1
|
import { setStatus } from './job.mjs';
|
|
2
2
|
import { message } from './observe.mjs';
|
|
3
3
|
import { dropState, writeState } from './state.mjs';
|
|
4
|
+
export const HAND_OVER = 'next-issue needs help:';
|
|
5
|
+
export async function handOverReply(context, issue, pr) {
|
|
6
|
+
const { repo, ports } = context;
|
|
7
|
+
const comments = await ports.threadComments(repo, pr ?? issue);
|
|
8
|
+
const access = new Map();
|
|
9
|
+
const writer = async (author) => {
|
|
10
|
+
if (!access.has(author)) {
|
|
11
|
+
access.set(author, await ports.canWrite(repo, author));
|
|
12
|
+
}
|
|
13
|
+
return access.get(author) === true;
|
|
14
|
+
};
|
|
15
|
+
let last = -1;
|
|
16
|
+
for (let index = comments.length - 1; index >= 0 && last === -1; index -= 1) {
|
|
17
|
+
const comment = comments[index];
|
|
18
|
+
if (comment.body.startsWith(HAND_OVER) && (await writer(comment.author))) {
|
|
19
|
+
last = index;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (last === -1) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const reply = [];
|
|
26
|
+
for (const comment of comments.slice(last + 1)) {
|
|
27
|
+
if (await writer(comment.author)) {
|
|
28
|
+
reply.push(`@${comment.author}: ${comment.body}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return reply;
|
|
32
|
+
}
|
|
4
33
|
export function escalate(job, reason) {
|
|
5
34
|
return handOver(job.context, job.log, job.issue.number, job.state, reason);
|
|
6
35
|
}
|
|
@@ -57,7 +86,7 @@ export async function handOver(context, log, issue, state, reason) {
|
|
|
57
86
|
log.event('hand-over', { reason }, 'quiet');
|
|
58
87
|
await writeState(context.repo, { ...state, handedOver: true });
|
|
59
88
|
await setStatus(context, issue, state.pr, context.config.labels.needsHuman);
|
|
60
|
-
const body =
|
|
89
|
+
const body = `${HAND_OVER} ${reason}`;
|
|
61
90
|
if (state.pr === undefined) {
|
|
62
91
|
await context.ports.commentOnIssue(context.repo, issue, body);
|
|
63
92
|
}
|
package/dist/pipeline.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { CODING } from './agents.mjs';
|
|
|
2
2
|
import { commitWork } from './fix.mjs';
|
|
3
3
|
import { checkGate } from './gate.mjs';
|
|
4
4
|
import { branchName } from './git.mjs';
|
|
5
|
-
import { cleanUp, escalate, finish, handOver, skipped, stop } from './handover.mjs';
|
|
5
|
+
import { cleanUp, escalate, finish, handOver, handOverReply, skipped, stop } from './handover.mjs';
|
|
6
6
|
import { openPr, setStatus } from './job.mjs';
|
|
7
7
|
import { afterMerge, fixBase } from './merge.mjs';
|
|
8
8
|
import { message } from './observe.mjs';
|
|
@@ -112,7 +112,7 @@ export async function processIssue(context, issue) {
|
|
|
112
112
|
? resetState(saved)
|
|
113
113
|
: saved;
|
|
114
114
|
try {
|
|
115
|
-
const report = await work(context, issue, log, state, saved === undefined);
|
|
115
|
+
const report = await work(context, issue, log, state, saved === undefined, saved?.handedOver === true);
|
|
116
116
|
log.event('issue.end', { outcome: report.outcome, reason: report.reason, ms: report.ms }, 'quiet');
|
|
117
117
|
return report;
|
|
118
118
|
}
|
|
@@ -136,7 +136,7 @@ export async function processIssue(context, issue) {
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
|
-
async function work(context, issue, log, state, start) {
|
|
139
|
+
async function work(context, issue, log, state, start, handedOver) {
|
|
140
140
|
const started = Date.now();
|
|
141
141
|
const { repo, config, ports } = context;
|
|
142
142
|
const branch = state.branch;
|
|
@@ -145,6 +145,9 @@ async function work(context, issue, log, state, start) {
|
|
|
145
145
|
await writeState(repo, state);
|
|
146
146
|
});
|
|
147
147
|
const comments = await log.step('comments', {}, () => ports.issueComments(repo, issue.number));
|
|
148
|
+
const reply = handedOver
|
|
149
|
+
? await log.step('reply', {}, () => handOverReply(context, issue.number, state.pr))
|
|
150
|
+
: [];
|
|
148
151
|
const added = await log.step('worktree', { branch }, () => ports.addWorktree(repo, issue.number, branch, context.base, config.remote, start));
|
|
149
152
|
const worktree = added.path;
|
|
150
153
|
if (!added.baseUpdated) {
|
|
@@ -158,6 +161,7 @@ async function work(context, issue, log, state, start) {
|
|
|
158
161
|
branch,
|
|
159
162
|
worktree,
|
|
160
163
|
comments,
|
|
164
|
+
reply,
|
|
161
165
|
started,
|
|
162
166
|
noChecks: false,
|
|
163
167
|
mainBroken: false,
|
|
@@ -179,7 +183,7 @@ async function work(context, issue, log, state, start) {
|
|
|
179
183
|
const result = await log.step('implement', {}, () => ports.runAgent(log, {
|
|
180
184
|
name: 'implementer',
|
|
181
185
|
cwd: worktree,
|
|
182
|
-
prompt: implementPrompt(issue, comments),
|
|
186
|
+
prompt: implementPrompt(issue, comments, reply),
|
|
183
187
|
profile: CODING,
|
|
184
188
|
model: config.models.implementer,
|
|
185
189
|
timeoutMs: config.agentTimeoutMinutes * 60_000,
|
package/dist/ports.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { runAgent } from './agents.mjs';
|
|
2
2
|
import { run } from './exec.mjs';
|
|
3
3
|
import { addWorktree, commitAll, deleteBranch, diff, discardChanges, disputeBranches, findWorktree, isDirty, latestSubject, push, removeWorktree, resetToBase, revision, setAside, } from './git.mjs';
|
|
4
|
-
import { commentOnIssue, commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, openIssues, prConflicts, prMerged, reopenIssue, setLabel, viewIssue, waitForBaseChecks, waitForChecks, } from './github.mjs';
|
|
4
|
+
import { canWrite, commentOnIssue, commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, openIssues, prConflicts, prMerged, reopenIssue, setLabel, threadComments, viewIssue, waitForBaseChecks, waitForChecks, } from './github.mjs';
|
|
5
5
|
import { detectSetupCommand } from './setup.mjs';
|
|
6
6
|
export const PORTS = {
|
|
7
7
|
addWorktree,
|
|
8
|
+
canWrite,
|
|
8
9
|
commentOnIssue,
|
|
9
10
|
commentOnPr,
|
|
10
11
|
commitAll,
|
|
@@ -37,6 +38,7 @@ export const PORTS = {
|
|
|
37
38
|
runSetup: async (command, cwd, timeoutMs) => run('bash', ['-lc', command], { cwd, timeoutMs }),
|
|
38
39
|
setAside,
|
|
39
40
|
setLabel,
|
|
41
|
+
threadComments,
|
|
40
42
|
viewIssue,
|
|
41
43
|
waitForBaseChecks,
|
|
42
44
|
waitForChecks,
|
package/dist/prompts.mjs
CHANGED
|
@@ -11,15 +11,27 @@ function issueBlock(issue, comments) {
|
|
|
11
11
|
.filter((part) => part.length > 0)
|
|
12
12
|
.join('\n\n');
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
function replyBlock(reply) {
|
|
15
|
+
return reply.length > 0
|
|
16
|
+
? [
|
|
17
|
+
'## Answer of a person to the hand-over',
|
|
18
|
+
'The last run handed the issue to a person. A person with write access to the repository answered as below. Follow this guidance.',
|
|
19
|
+
reply.join('\n\n'),
|
|
20
|
+
].join('\n\n')
|
|
21
|
+
: '';
|
|
22
|
+
}
|
|
23
|
+
export function implementPrompt(issue, comments, reply = []) {
|
|
15
24
|
return [
|
|
16
25
|
'You are the implementer. Solve the issue below in this worktree.',
|
|
17
26
|
issueBlock(issue, comments),
|
|
27
|
+
replyBlock(reply),
|
|
18
28
|
'## Rules',
|
|
19
29
|
'Read the repository conventions first. Keep the change minimal and focused on the issue.',
|
|
20
30
|
'Add or update tests when the repository has a test suite.',
|
|
21
31
|
COMMIT_RULES,
|
|
22
|
-
]
|
|
32
|
+
]
|
|
33
|
+
.filter((part) => part.length > 0)
|
|
34
|
+
.join('\n\n');
|
|
23
35
|
}
|
|
24
36
|
export function hookPrompt(issue, comments, role, output) {
|
|
25
37
|
return [
|
|
@@ -75,7 +87,7 @@ const DISPUTE_RULE = [
|
|
|
75
87
|
'If only some of the findings are wrong, fix the correct ones and change nothing for the wrong ones. Write no `unrelated` line in that round. The next round holds only the findings that are left, and you can dispute them there.',
|
|
76
88
|
'The reviewer reads that line in the next round. Do not make a change that you hold to be wrong only to end the round.',
|
|
77
89
|
].join('\n');
|
|
78
|
-
export function fixPrompt(issue, reason, detail, earlier, kind) {
|
|
90
|
+
export function fixPrompt(issue, reason, detail, earlier, kind, reply = []) {
|
|
79
91
|
return [
|
|
80
92
|
`You are the fixer. Correct the work for issue #${issue.number}: ${issue.title}.`,
|
|
81
93
|
`## Reason\n${reason}`,
|
|
@@ -87,6 +99,7 @@ export function fixPrompt(issue, reason, detail, earlier, kind) {
|
|
|
87
99
|
'Keep those earlier fixes. Do not undo work that a later round did not question.',
|
|
88
100
|
].join('\n')
|
|
89
101
|
: '',
|
|
102
|
+
replyBlock(reply),
|
|
90
103
|
'## Rules',
|
|
91
104
|
'Fix the cause, not the symptom. Keep the change minimal.',
|
|
92
105
|
kind === 'checks' ? UNRELATED_RULE : DISPUTE_RULE,
|