next-issue 2.3.2 → 2.4.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
@@ -284,7 +284,7 @@ does not know all stop the run, so a typo cannot pass without a word.
284
284
  | Field | Default | Effect |
285
285
  | --- | --- | --- |
286
286
  | `remote` | `origin` | The git remote for the repository and the branches |
287
- | `issueLimit` | `100` | The maximum number of open issues to read |
287
+ | `issueLimit` | `100` | The maximum number of open issues to read. With `labels.ready` set, only issues with the ready, in-progress or in-review label count |
288
288
  | `maxCiFixes` | `5` | The budget for check fixes per issue |
289
289
  | `maxCommitFixes` | `10` | The budget for fixes of a commit that a git hook stopped, per issue |
290
290
  | `maxReviewRounds` | `5` | The budget for review rounds per issue |
@@ -357,6 +357,7 @@ that a label still holds back stays as it was.
357
357
  | --- | --- |
358
358
  | `init` | Write the config file with the defaults |
359
359
  | `serve` | Run the worker that pi extensions talk to, see [Worker](#worker) |
360
+ | `stop` | Stop the worker, see [Worker](#worker) |
360
361
  | `--force` | Replace an existing config file, with `init` |
361
362
  | `--once` | Stop after the first handled issue |
362
363
  | `--max <n>` | Handle at most n issues |
@@ -423,7 +424,10 @@ to it. The worker:
423
424
  - stops every agent and command and exits on `SIGTERM` or `SIGINT`, and
424
425
  removes its socket and lock files.
425
426
 
426
- Stop it by hand with `kill $(cat ~/.pi/agent/next-issue.lock)`.
427
+ Stop it with `next-issue stop`. This sends `SIGTERM` to the process in the
428
+ lock file, so the worker stops at once and a run that is going resumes from
429
+ its saved state next time. When no worker is running, the command says so and
430
+ exits with 0.
427
431
 
428
432
  ## Development
429
433
 
package/dist/args.mjs CHANGED
@@ -3,6 +3,7 @@ export const USAGE = `next-issue [command] [options]
3
3
 
4
4
  init write ${CONFIG_FILE} with the defaults
5
5
  serve run the worker that pi extensions talk to
6
+ stop stop the worker
6
7
 
7
8
  --once stop after the first handled issue
8
9
  --max <n> handle at most n issues
@@ -30,7 +31,7 @@ export function parseArgs(argv) {
30
31
  for (let index = 0; index < argv.length; index += 1) {
31
32
  const arg = argv[index];
32
33
  if (index === 0 && !arg.startsWith("-")) {
33
- if (arg !== "init" && arg !== "serve") {
34
+ if (arg !== "init" && arg !== "serve" && arg !== "stop") {
34
35
  throw new Error(`Unknown command: ${arg}`);
35
36
  }
36
37
  args.command = arg;
package/dist/cli.mjs CHANGED
@@ -5,7 +5,7 @@ import { ensureIgnored, repoRoot, WORKTREE_DIR } from "./git.mjs";
5
5
  import { listenForInterrupts } from "./interrupt.mjs";
6
6
  import { message } from "./observe.mjs";
7
7
  import { runRepo } from "./run.mjs";
8
- import { serve } from "./serve.mjs";
8
+ import { serve, socketPaths, stopWorker } from "./serve.mjs";
9
9
  import { STATE_DIR } from "./state.mjs";
10
10
  import { formatSummary } from "./summary.mjs";
11
11
  function write(stream, text) {
@@ -22,6 +22,11 @@ async function main() {
22
22
  if (args.command === "serve") {
23
23
  return serve();
24
24
  }
25
+ if (args.command === "stop") {
26
+ const pid = await stopWorker(socketPaths().lockPath);
27
+ await write(process.stdout, pid === undefined ? "next-issue: no worker is running\n" : `next-issue: stopped the worker with process id ${pid}\n`);
28
+ return 0;
29
+ }
25
30
  const root = await repoRoot(process.cwd());
26
31
  if (args.command === "init") {
27
32
  return init(root, args.force);
package/dist/github.mjs CHANGED
@@ -44,14 +44,15 @@ function toIssue(item) {
44
44
  labels: item.labels.map((label) => label.name),
45
45
  };
46
46
  }
47
- export async function openIssues(repo, limit) {
47
+ export async function openIssues(repo, limit, labels) {
48
+ const filter = labels.length === 0 ? "" : ` label:${labels.map((label) => `"${label}"`).join(",")}`;
48
49
  const raw = await gh(repo, [
49
50
  "issue",
50
51
  "list",
51
52
  "--state",
52
53
  "open",
53
54
  "--search",
54
- "sort:created-asc",
55
+ `sort:created-asc${filter}`,
55
56
  "--limit",
56
57
  String(limit),
57
58
  "--json",
@@ -61,8 +62,9 @@ export async function openIssues(repo, limit) {
61
62
  return parsed.map(toIssue).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
62
63
  }
63
64
  export async function viewIssue(repo, issue) {
64
- const raw = await gh(repo, ["issue", "view", String(issue), "--json", ISSUE_FIELDS]);
65
- return toIssue(JSON.parse(raw));
65
+ const raw = await gh(repo, ["issue", "view", String(issue), "--json", `${ISSUE_FIELDS},state`]);
66
+ const parsed = JSON.parse(raw);
67
+ return { ...toIssue(parsed), open: parsed.state === "OPEN" };
66
68
  }
67
69
  export async function issueComments(repo, issue) {
68
70
  const raw = await gh(repo, ["issue", "view", String(issue), "--json", "comments"]);
package/dist/pipeline.mjs CHANGED
@@ -60,15 +60,21 @@ export function skipReason(issue, config, resuming) {
60
60
  }
61
61
  export async function pendingIssues(context) {
62
62
  const { repo, config, ports } = context;
63
- const issues = await ports.openIssues(repo, config.issueLimit);
63
+ const { ready, inProgress, inReview } = config.labels;
64
+ const labels = ready.length === 0 ? [] : [ready, inProgress, inReview];
65
+ const issues = await ports.openIssues(repo, config.issueLimit, labels);
64
66
  const listed = new Set(issues.map((issue) => issue.number));
65
67
  for (const number of await savedIssues(repo)) {
66
68
  if (listed.has(number)) {
67
69
  continue;
68
70
  }
69
71
  const saved = await readState(repo, number).catch(() => undefined);
70
- if (saved?.pr !== undefined && (await ports.prMerged(repo, saved.pr))) {
71
- issues.push(await ports.viewIssue(repo, number));
72
+ if (saved === undefined) {
73
+ continue;
74
+ }
75
+ const { open, ...issue } = await ports.viewIssue(repo, number);
76
+ if (open || (saved.pr !== undefined && (await ports.prMerged(repo, saved.pr)))) {
77
+ issues.push(issue);
72
78
  }
73
79
  }
74
80
  return issues.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
package/dist/serve.mjs CHANGED
@@ -36,6 +36,17 @@ function alive(pid) {
36
36
  return error.code === "EPERM";
37
37
  }
38
38
  }
39
+ async function holder(lockPath) {
40
+ const pid = Number((await readFile(lockPath, "utf8").catch(() => "")).trim());
41
+ return pid > 0 && alive(pid) ? pid : undefined;
42
+ }
43
+ export async function stopWorker(lockPath) {
44
+ const pid = await holder(lockPath);
45
+ if (pid !== undefined) {
46
+ process.kill(pid, "SIGTERM");
47
+ }
48
+ return pid;
49
+ }
39
50
  async function lock(path) {
40
51
  for (let attempt = 0; attempt < 2; attempt += 1) {
41
52
  try {
@@ -46,8 +57,7 @@ async function lock(path) {
46
57
  if (error.code !== "EEXIST") {
47
58
  throw error;
48
59
  }
49
- const pid = Number((await readFile(path, "utf8").catch(() => "")).trim());
50
- if (pid > 0 && alive(pid)) {
60
+ if ((await holder(path)) !== undefined) {
51
61
  return false;
52
62
  }
53
63
  await rm(path, { force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "2.3.2",
3
+ "version": "2.4.1",
4
4
  "description": "Autonomous issue harness driven by the pi SDK",
5
5
  "keywords": [
6
6
  "github",