next-issue 0.19.0 → 0.20.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 CHANGED
@@ -33,7 +33,9 @@ For each open issue, oldest first:
33
33
  local branch that fell behind the remote is fast-forwarded. A fast-forward
34
34
  of the base that is not safe leaves the local base branch as it is and
35
35
  writes a `base.behind` event. A new branch then starts from the remote base.
36
- 4. Let the implementer agent do the work, then commit and push.
36
+ 4. Let the implementer agent do the work, then commit and push. When a git
37
+ hook stops a commit, the agent that made the change gets the hook output,
38
+ fixes it, and the harness commits again. The budget is `maxCommitFixes`.
37
39
  5. Open a draft pull request that closes the issue.
38
40
  6. Wait for the checks, once the pull request shows the pushed commit. A
39
41
  pending state waits again, up to `checkTimeoutMinutes`. A read that fails
@@ -238,6 +240,7 @@ does not know all stop the run, so a typo cannot pass without a word.
238
240
  "remote": "origin",
239
241
  "issueLimit": 100,
240
242
  "maxCiFixes": 5,
243
+ "maxCommitFixes": 10,
241
244
  "maxReviewRounds": 5,
242
245
  "checkIntervalSeconds": 15,
243
246
  "checkGraceSeconds": 60,
@@ -271,6 +274,7 @@ does not know all stop the run, so a typo cannot pass without a word.
271
274
  | `remote` | `origin` | The git remote for the repository and the branches |
272
275
  | `issueLimit` | `100` | The maximum number of open issues to read |
273
276
  | `maxCiFixes` | `5` | The budget for check fixes per issue |
277
+ | `maxCommitFixes` | `10` | The budget for fixes of a commit that a git hook stopped, per issue |
274
278
  | `maxReviewRounds` | `5` | The budget for review rounds per issue |
275
279
  | `checkIntervalSeconds` | `15` | The time between two check states |
276
280
  | `checkGraceSeconds` | `60` | The time to wait for the first check to show |
package/dist/config.mjs CHANGED
@@ -7,6 +7,7 @@ const DEFAULTS = {
7
7
  remote: "origin",
8
8
  issueLimit: 100,
9
9
  maxCiFixes: 5,
10
+ maxCommitFixes: 10,
10
11
  maxReviewRounds: 5,
11
12
  checkIntervalSeconds: 15,
12
13
  checkGraceSeconds: 60,
@@ -35,6 +36,7 @@ const FIELDS = [...Object.keys(DEFAULTS), "setupCommand"];
35
36
  const WHOLE_FIELDS = [
36
37
  ["issueLimit", 1],
37
38
  ["maxCiFixes", 0],
39
+ ["maxCommitFixes", 0],
38
40
  ["maxReviewRounds", 1],
39
41
  ["checkIntervalSeconds", 1],
40
42
  ["checkGraceSeconds", 0],
package/dist/pipeline.mjs CHANGED
@@ -4,7 +4,7 @@ import { run } from "./exec.mjs";
4
4
  import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, hasWorktree, isDirty, push, removeWorktree, resetToBase, revision, worktreePath, } from "./git.mjs";
5
5
  import { commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, reopenIssue, setLabel, waitForBaseChecks, waitForChecks, } from "./github.mjs";
6
6
  import { message } from "./observe.mjs";
7
- import { capDiff, fixPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from "./prompts.mjs";
7
+ import { capDiff, fixPrompt, hookPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from "./prompts.mjs";
8
8
  import { detectSetupCommand } from "./setup.mjs";
9
9
  import { dropState, readState, resetState, writeState, } from "./state.mjs";
10
10
  import { blockingFindings, fingerprint, formatFindings, formatVerdict, openFindings, readVerdict, VERDICT_SCHEMA, } from "./verdict.mjs";
@@ -166,7 +166,7 @@ async function work(context, issue, log, state) {
166
166
  timeoutMs: config.agentTimeoutMinutes * 60_000,
167
167
  }));
168
168
  const subject = parseCommitSubject(result.text, `fix: resolve issue #${issue.number}`);
169
- if (!(await commitWork(job, before, subject))) {
169
+ if (!(await commitWork(job, before, subject, "implementer"))) {
170
170
  await escalate(job, "The implementer produced no change.");
171
171
  return finish(job, "needs-human", "no implementation commit");
172
172
  }
@@ -450,19 +450,43 @@ async function fixRound(job, reason, detail, earlier, kind) {
450
450
  return { committed: false, unrelated, stray };
451
451
  }
452
452
  const subject = parseCommitSubject(result.text, `fix: address feedback on issue #${issue.number}`);
453
- if (!(await commitWork(job, before, subject))) {
453
+ if (!(await commitWork(job, before, subject, "fixer"))) {
454
454
  log.event("commit.empty", { reason }, "quiet");
455
455
  return { committed: false };
456
456
  }
457
457
  await ports.push(worktree, context.config.remote, branch);
458
458
  return { committed: true };
459
459
  }
460
- async function commitWork(job, before, subject) {
461
- const { context, issue, log, worktree } = job;
462
- if (await context.ports.commitAll(worktree, `${subject}\n\nRefs #${issue.number}`)) {
463
- log.event("commit", { subject });
460
+ async function commitWork(job, before, subject, role) {
461
+ const { context, issue, log, state, worktree, comments } = job;
462
+ const { repo, config, ports } = context;
463
+ for (;;) {
464
+ try {
465
+ if (await ports.commitAll(worktree, `${subject}\n\nRefs #${issue.number}`)) {
466
+ log.event("commit", { subject });
467
+ }
468
+ return (await ports.revision(worktree)) !== before;
469
+ }
470
+ catch (error) {
471
+ const used = state.commitFixes ?? 0;
472
+ if (used >= config.maxCommitFixes) {
473
+ throw error;
474
+ }
475
+ state.commitFixes = used + 1;
476
+ await writeState(repo, state);
477
+ const output = message(error).slice(-config.logMaxChars);
478
+ log.event("commit.fail", { role, output }, "quiet");
479
+ await log.step("commit-fix", { role }, () => ports.runAgent(log, {
480
+ name: role,
481
+ cwd: worktree,
482
+ prompt: hookPrompt(issue, comments, role, output),
483
+ profile: CODING,
484
+ model: config.models[role],
485
+ agent: config.agent,
486
+ timeoutMs: config.agentTimeoutMinutes * 60_000,
487
+ }));
488
+ }
464
489
  }
465
- return (await context.ports.revision(worktree)) !== before;
466
490
  }
467
491
  async function cleanUp(context, log, issue) {
468
492
  const path = worktreePath(context.repo, issue);
package/dist/prompts.mjs CHANGED
@@ -21,6 +21,16 @@ export function implementPrompt(issue, comments) {
21
21
  COMMIT_RULES,
22
22
  ].join("\n\n");
23
23
  }
24
+ export function hookPrompt(issue, comments, role, output) {
25
+ return [
26
+ `You are the ${role}. The harness could not commit your work for the issue below, because a git hook failed.`,
27
+ issueBlock(issue, comments),
28
+ `## Hook output\n${output}`,
29
+ "## Rules",
30
+ "Fix what the hook reports and keep the rest of the work in this worktree. Do not skip or weaken the hook.",
31
+ COMMIT_RULES,
32
+ ].join("\n\n");
33
+ }
24
34
  export function capDiff(diff, maxChars) {
25
35
  return diff.length <= maxChars
26
36
  ? diff
package/dist/state.mjs CHANGED
@@ -16,7 +16,7 @@ export async function readState(repo, issue) {
16
16
  }
17
17
  }
18
18
  export function resetState(state) {
19
- return { ...state, ciFixes: 0, reviewRounds: 0, reviewLog: [] };
19
+ return { ...state, ciFixes: 0, commitFixes: 0, reviewRounds: 0, reviewLog: [] };
20
20
  }
21
21
  export async function writeState(repo, state) {
22
22
  await mkdir(dir(repo), { recursive: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Autonomous issue harness driven by the Claude Agent SDK",
5
5
  "keywords": [
6
6
  "github",