next-issue 0.11.1 → 0.12.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 CHANGED
@@ -85,9 +85,10 @@ only between two issues, finishes the issue that it holds, deletes the file and
85
85
  then writes the summary as usual. A file from an earlier run is dropped at the
86
86
  start, so it stops nothing.
87
87
 
88
- `Ctrl-C` is different: the terminal signals every child too, so the run dies in
89
- the middle of a step. The saved state lets the next run continue the issue, but
90
- the issue keeps `status:in-progress` and the run writes no summary.
88
+ `Ctrl-C` is different: the harness passes the signal on to the command that runs
89
+ and to every child of it, so the run dies in the middle of a step. The saved
90
+ state lets the next run continue the issue, but the issue keeps
91
+ `status:in-progress` and the run writes no summary.
91
92
 
92
93
  Every status label that the harness puts on the issue goes on the pull request
93
94
  too, from the first review round. Thus a draft pull request shows work that the
@@ -193,7 +194,11 @@ does not know all stop the run, so a typo cannot pass without a word.
193
194
  "diffMaxChars": 60000,
194
195
  "keepRunDays": 7,
195
196
  "draftPullRequest": true,
196
- "models": {},
197
+ "models": {
198
+ "implementer": null,
199
+ "reviewer": null,
200
+ "fixer": null
201
+ },
197
202
  "labels": {
198
203
  "ready": "status:todo",
199
204
  "inProgress": "status:in-progress",
@@ -222,7 +227,7 @@ does not know all stop the run, so a typo cannot pass without a word.
222
227
  | `keepRunDays` | `7` | The number of days to keep the run files; `0` keeps them all |
223
228
  | `draftPullRequest` | `true` | Open the pull request as a draft, until the review approves |
224
229
  | `setupCommand` | from the lock file | A shell command to run in a new worktree, before the implementer |
225
- | `models` | `{}` | The model per agent role |
230
+ | `models` | `null` per role | The model per agent role: `implementer`, `reviewer`, `fixer` |
226
231
  | `labels` | see above | The names of the labels that the harness reads and sets |
227
232
 
228
233
  An empty `labels.ready` turns the ready requirement off. The harness then
@@ -231,7 +236,8 @@ claims every open issue that no other label and no assignee holds back.
231
236
  The log gives the reason for a skipped issue as `stop-label`, `not-ready`,
232
237
  `in-flight` or `assigned`.
233
238
 
234
- A role without an entry in `models` uses the default model of the SDK.
239
+ A role set to `null`, or with no entry in `models`, uses the default model of
240
+ the SDK.
235
241
 
236
242
  Without a `setupCommand`, the harness looks in the new worktree for a lock file
237
243
  and runs the install command that goes with it:
package/dist/config.mjs CHANGED
@@ -43,11 +43,17 @@ const WHOLE_FIELDS = [
43
43
  ];
44
44
  const MODEL_ROLES = ["implementer", "reviewer", "fixer"];
45
45
  const LABEL_NAMES = ["ready", "inProgress", "inReview", "done", "needsHuman", "skip"];
46
+ const FILE_DEFAULTS = {
47
+ ...DEFAULTS,
48
+ models: Object.fromEntries(MODEL_ROLES.map((role) => [role, null])),
49
+ };
46
50
  export const CONFIG_FILE = "next-issue.config.json";
47
51
  export async function writeDefaultConfig(root, force) {
48
52
  const path = join(root, CONFIG_FILE);
49
53
  try {
50
- await writeFile(path, `${JSON.stringify(DEFAULTS, null, 2)}\n`, { flag: force ? "w" : "wx" });
54
+ await writeFile(path, `${JSON.stringify(FILE_DEFAULTS, null, 2)}\n`, {
55
+ flag: force ? "w" : "wx",
56
+ });
51
57
  }
52
58
  catch (error) {
53
59
  if (error.code === "EEXIST") {
@@ -111,7 +117,7 @@ export function parseConfig(value) {
111
117
  }
112
118
  }
113
119
  for (const role of MODEL_ROLES) {
114
- if (models[role] !== undefined) {
120
+ if (models[role] !== undefined && models[role] !== null) {
115
121
  config.models[role] = text(models[role], `models.${role}`, false);
116
122
  }
117
123
  }
package/dist/exec.mjs CHANGED
@@ -2,6 +2,41 @@ import { spawn } from "node:child_process";
2
2
  let observer;
3
3
  let defaultTimeoutMs = 10 * 60_000;
4
4
  const STDERR_TAIL = 200_000;
5
+ const groups = new Set();
6
+ function forward(signal) {
7
+ for (const pid of groups) {
8
+ try {
9
+ process.kill(-pid, signal);
10
+ }
11
+ catch { }
12
+ }
13
+ groups.clear();
14
+ process.off("SIGINT", onInterrupt);
15
+ process.off("SIGTERM", onTerminate);
16
+ process.kill(process.pid, signal);
17
+ }
18
+ const onInterrupt = () => forward("SIGINT");
19
+ const onTerminate = () => forward("SIGTERM");
20
+ function watch(pid) {
21
+ if (pid === undefined) {
22
+ return;
23
+ }
24
+ if (groups.size === 0) {
25
+ process.on("SIGINT", onInterrupt);
26
+ process.on("SIGTERM", onTerminate);
27
+ }
28
+ groups.add(pid);
29
+ }
30
+ function unwatch(pid) {
31
+ if (pid === undefined) {
32
+ return;
33
+ }
34
+ groups.delete(pid);
35
+ if (groups.size === 0) {
36
+ process.off("SIGINT", onInterrupt);
37
+ process.off("SIGTERM", onTerminate);
38
+ }
39
+ }
5
40
  export function setCommandObserver(next) {
6
41
  observer = next;
7
42
  }
@@ -14,12 +49,15 @@ export function run(command, args, options = {}) {
14
49
  return new Promise((resolve, reject) => {
15
50
  const child = spawn(command, args, {
16
51
  cwd: options.cwd,
52
+ detached: true,
17
53
  stdio: [
18
54
  options.input === undefined ? "ignore" : "pipe",
19
55
  options.show === true ? 2 : "pipe",
20
56
  "pipe",
21
57
  ],
22
58
  });
59
+ const pid = child.pid;
60
+ watch(pid);
23
61
  let stdout = "";
24
62
  let stderr = "";
25
63
  let timedOut = false;
@@ -40,11 +78,21 @@ export function run(command, args, options = {}) {
40
78
  });
41
79
  const timer = setTimeout(() => {
42
80
  timedOut = true;
43
- child.kill("SIGKILL");
81
+ if (pid === undefined) {
82
+ child.kill("SIGKILL");
83
+ return;
84
+ }
85
+ try {
86
+ process.kill(-pid, "SIGKILL");
87
+ }
88
+ catch {
89
+ child.kill("SIGKILL");
90
+ }
44
91
  }, limit);
45
92
  let done = false;
46
93
  const settle = (code) => {
47
94
  clearTimeout(timer);
95
+ unwatch(pid);
48
96
  if (done) {
49
97
  return;
50
98
  }
@@ -63,6 +111,7 @@ export function run(command, args, options = {}) {
63
111
  };
64
112
  child.on("error", (error) => {
65
113
  clearTimeout(timer);
114
+ unwatch(pid);
66
115
  done = true;
67
116
  reject(error);
68
117
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "0.11.1",
3
+ "version": "0.12.1",
4
4
  "description": "Autonomous issue harness driven by the Claude Agent SDK",
5
5
  "keywords": [
6
6
  "github",