next-issue 2.4.1 → 2.4.3

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
@@ -98,32 +98,32 @@ when:
98
98
  - the check budget or the review budget runs out;
99
99
  - a fixer round adds no commit and gives no reason, so there is no progress;
100
100
  - the fixer reports that the checks fail for a reason the change did not cause,
101
- such as a test already broken on the base branch or one that fails only under
102
- load. It changes nothing and the reason reaches the pull request, rather than
103
- the fixer spending its whole budget on a failure it cannot fix;
101
+ such as a test already broken on the base branch or one that fails only under
102
+ load. It changes nothing and the reason reaches the pull request, rather than
103
+ the fixer spending its whole budget on a failure it cannot fix;
104
104
  - a fixer disputes the findings and leaves a change behind, either in the
105
- worktree or in a commit of its own. A dispute carries a reason, not a diff,
106
- so the harness throws none of it away: an unclean worktree stays on the
107
- disk, and a commit of the fixer stays on the local branch. Neither is
108
- pushed, so the pull request shows no such change. It holds the reason of the
109
- dispute, and the change is on the machine that ran the harness;
105
+ worktree or in a commit of its own. A dispute carries a reason, not a diff,
106
+ so the harness throws none of it away: an unclean worktree stays on the
107
+ disk, and a commit of the fixer stays on the local branch. Neither is
108
+ pushed, so the pull request shows no such change. It holds the reason of the
109
+ dispute, and the change is on the machine that ran the harness;
110
110
  - the reviewer repeats a finding set from an earlier round, which shows a
111
- ping-pong between the fixer and the reviewer, or a finding that the fixer
112
- disputed and the reviewer holds to. The person then decides;
111
+ ping-pong between the fixer and the reviewer, or a finding that the fixer
112
+ disputed and the reviewer holds to. The person then decides;
113
113
  - the reviewer gives no verdict, or the checks do not finish in time;
114
114
  - with `--merge`, the merge of an approved pull request fails, for example
115
- because branch protection asks for a human approval;
115
+ because branch protection asks for a human approval;
116
116
  - with `--merge`, a queued merge does not finish in `checkTimeoutMinutes`, or
117
- the pull request closes without a merge;
117
+ the pull request closes without a merge;
118
118
  - with `--merge`, the checks on the base branch fail after the merge and the
119
- fixer cannot fix them, or they do not finish in time. The base branch is
120
- then broken, so the run stops after this issue;
119
+ fixer cannot fix them, or they do not finish in time. The base branch is
120
+ then broken, so the run stops after this issue;
121
121
  - an agent does not finish in `agentTimeoutMinutes`;
122
122
  - the setup command fails, or does not finish in `setupTimeoutMinutes`;
123
123
  - a step of the run fails with an error. A `gh` call that is safe to repeat,
124
- such as a read or a label edit, is tried up to three times first when the
125
- network fails. A `gh pr create` or `gh pr comment` is tried once, so a lost
126
- reply never makes a duplicate.
124
+ such as a read or a label edit, is tried up to three times first when the
125
+ network fails. A `gh pr create` or `gh pr comment` is tried once, so a lost
126
+ reply never makes a duplicate.
127
127
 
128
128
  The fixer sees the findings of all earlier rounds, not only the last one, with
129
129
  its own disputes next to them. From round two, the reviewer judges only the
@@ -213,11 +213,11 @@ jq -r 'select(.kind=="usage") | [.issue,.role,.turns,.total] | @tsv' \
213
213
 
214
214
  Three levels of console output:
215
215
 
216
- | Level | Shows |
217
- | --- | --- |
218
- | `--quiet` | Run and issue milestones, verdicts, hand-overs, errors |
219
- | default | The above plus each step end, each tool call and failed commands |
220
- | `--verbose` | The above plus every command and all agent text |
216
+ | Level | Shows |
217
+ | ----------- | ---------------------------------------------------------------- |
218
+ | `--quiet` | Run and issue milestones, verdicts, hand-overs, errors |
219
+ | default | The above plus each step end, each tool call and failed commands |
220
+ | `--verbose` | The above plus every command and all agent text |
221
221
 
222
222
  ## Requirements
223
223
 
@@ -281,26 +281,26 @@ does not know all stop the run, so a typo cannot pass without a word.
281
281
  }
282
282
  ```
283
283
 
284
- | Field | Default | Effect |
285
- | --- | --- | --- |
286
- | `remote` | `origin` | The git remote for the repository and the branches |
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
- | `maxCiFixes` | `5` | The budget for check fixes per issue |
289
- | `maxCommitFixes` | `10` | The budget for fixes of a commit that a git hook stopped, per issue |
290
- | `maxReviewRounds` | `5` | The budget for review rounds per issue |
291
- | `checkIntervalSeconds` | `15` | The time between two check states |
292
- | `checkGraceSeconds` | `60` | The time to wait for the first check to show |
293
- | `checkTimeoutMinutes` | `60` | The limit for one check wait |
294
- | `agentTimeoutMinutes` | `30` | The limit for one agent call |
295
- | `setupTimeoutMinutes` | `15` | The limit for the setup command |
296
- | `commandTimeoutMinutes` | `10` | The limit for one `git` or `gh` command |
297
- | `logMaxChars` | `20000` | The maximum length of the failed job logs, shared between the failed jobs |
298
- | `diffMaxChars` | `60000` | The maximum length of the diff that the reviewer reads |
299
- | `keepRunDays` | `7` | The number of days to keep the run files; `0` keeps them all |
300
- | `draftPullRequest` | `true` | Open the pull request as a draft, until the review approves |
301
- | `setupCommand` | from the lock file | A shell command to run in a new worktree, before the implementer |
302
- | `models` | `null` per role | The pi model per agent role, as `provider/model`: `implementer`, `reviewer`, `fixer` |
303
- | `labels` | see above | The names of the labels that the harness reads and sets |
284
+ | Field | Default | Effect |
285
+ | ----------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
286
+ | `remote` | `origin` | The git remote for the repository and the branches |
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
+ | `maxCiFixes` | `5` | The budget for check fixes per issue |
289
+ | `maxCommitFixes` | `10` | The budget for fixes of a commit that a git hook stopped, per issue |
290
+ | `maxReviewRounds` | `5` | The budget for review rounds per issue |
291
+ | `checkIntervalSeconds` | `15` | The time between two check states |
292
+ | `checkGraceSeconds` | `60` | The time to wait for the first check to show |
293
+ | `checkTimeoutMinutes` | `60` | The limit for one check wait |
294
+ | `agentTimeoutMinutes` | `30` | The limit for one agent call |
295
+ | `setupTimeoutMinutes` | `15` | The limit for the setup command |
296
+ | `commandTimeoutMinutes` | `10` | The limit for one `git` or `gh` command |
297
+ | `logMaxChars` | `20000` | The maximum length of the failed job logs, shared between the failed jobs |
298
+ | `diffMaxChars` | `60000` | The maximum length of the diff that the reviewer reads |
299
+ | `keepRunDays` | `7` | The number of days to keep the run files; `0` keeps them all |
300
+ | `draftPullRequest` | `true` | Open the pull request as a draft, until the review approves |
301
+ | `setupCommand` | from the lock file | A shell command to run in a new worktree, before the implementer |
302
+ | `models` | `null` per role | The pi model per agent role, as `provider/model`: `implementer`, `reviewer`, `fixer` |
303
+ | `labels` | see above | The names of the labels that the harness reads and sets |
304
304
 
305
305
  An empty `labels.ready` turns the ready requirement off. The harness then
306
306
  claims every open issue that no other label holds back.
@@ -314,17 +314,17 @@ your pi settings.
314
314
  Without a `setupCommand`, the harness looks in the new worktree for a lock file
315
315
  and runs the install command that goes with it:
316
316
 
317
- | Lock file | Command |
318
- | --- | --- |
319
- | `pnpm-lock.yaml` | `pnpm install --frozen-lockfile` |
320
- | `package-lock.json` | `npm ci` |
321
- | `yarn.lock` | `yarn install --frozen-lockfile`, or `yarn install --immutable` with a `.yarnrc.yml` |
322
- | `bun.lock`, `bun.lockb` | `bun install --frozen-lockfile` |
323
- | `Cargo.lock` | `cargo fetch` |
324
- | `go.sum` | `go mod download` |
325
- | `uv.lock` | `uv sync` |
326
- | `poetry.lock` | `poetry install` |
327
- | `Gemfile.lock` | `bundle install` |
317
+ | Lock file | Command |
318
+ | ----------------------- | ------------------------------------------------------------------------------------ |
319
+ | `pnpm-lock.yaml` | `pnpm install --frozen-lockfile` |
320
+ | `package-lock.json` | `npm ci` |
321
+ | `yarn.lock` | `yarn install --frozen-lockfile`, or `yarn install --immutable` with a `.yarnrc.yml` |
322
+ | `bun.lock`, `bun.lockb` | `bun install --frozen-lockfile` |
323
+ | `Cargo.lock` | `cargo fetch` |
324
+ | `go.sum` | `go mod download` |
325
+ | `uv.lock` | `uv sync` |
326
+ | `poetry.lock` | `poetry install` |
327
+ | `Gemfile.lock` | `bundle install` |
328
328
 
329
329
  The first match in that order wins. A project with no lock file in the list
330
330
  runs no setup, and a `setupCommand` in the config file always wins. Thus the
@@ -353,21 +353,21 @@ that a label still holds back stays as it was.
353
353
 
354
354
  ## Commands and options
355
355
 
356
- | Argument | Effect |
357
- | --- | --- |
358
- | `init` | Write the config file with the defaults |
359
- | `serve` | Run the worker that pi extensions talk to, see [Worker](#worker) |
360
- | `stop` | Stop the worker, see [Worker](#worker) |
361
- | `--force` | Replace an existing config file, with `init` |
362
- | `--once` | Stop after the first handled issue |
363
- | `--max <n>` | Handle at most n issues |
364
- | `--merge` | Squash-merge the pull request after the approval, delete the remote branch and fix a failure of the checks on the base branch |
365
- | `--watch` | Keep running, and read the open issues again after each pass |
366
- | `--reset` | Drop the saved budgets and findings, and keep the rest of the state |
367
- | `--json` | Print the machine summary instead of the text one |
368
- | `--verbose` | Show every command and all agent output |
369
- | `--quiet` | Show only the milestones and the summary |
370
- | `--help` | Show the option list |
356
+ | Argument | Effect |
357
+ | ----------- | ----------------------------------------------------------------------------------------------------------------------------- |
358
+ | `init` | Write the config file with the defaults |
359
+ | `serve` | Run the worker that pi extensions talk to, see [Worker](#worker) |
360
+ | `stop` | Stop the worker, see [Worker](#worker) |
361
+ | `--force` | Replace an existing config file, with `init` |
362
+ | `--once` | Stop after the first handled issue |
363
+ | `--max <n>` | Handle at most n issues |
364
+ | `--merge` | Squash-merge the pull request after the approval, delete the remote branch and fix a failure of the checks on the base branch |
365
+ | `--watch` | Keep running, and read the open issues again after each pass |
366
+ | `--reset` | Drop the saved budgets and findings, and keep the rest of the state |
367
+ | `--json` | Print the machine summary instead of the text one |
368
+ | `--verbose` | Show every command and all agent output |
369
+ | `--quiet` | Show only the milestones and the summary |
370
+ | `--help` | Show the option list |
371
371
 
372
372
  The exit code is 1 when an issue needs a person or failed, and 0 when it does
373
373
  not, so a cron job or a CI step can read the result of the run.
@@ -378,7 +378,8 @@ A run without `--watch` ends when no unseen issue is left. With `--watch`, the
378
378
  harness then waits, reads the open issues again and runs another pass, so it
379
379
  picks up an issue that someone opens during the run, or one whose blocker
380
380
  merges. Each pass tries every open issue again, also one that an earlier pass
381
- skipped.
381
+ skipped. An issue that failed in an earlier pass is not tried again until the
382
+ next run.
382
383
 
383
384
  The waits follow a fixed back-off in minutes: 1, 2, 3, 5, 10, then 10 from then
384
385
  on. A pass that handles an issue, with any outcome other than `skipped`, resets
package/dist/agents.mjs CHANGED
@@ -1,36 +1,36 @@
1
- import { join, sep } from "node:path";
2
- import { createAgentSession, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
3
- import { onStop } from "./exec.mjs";
4
- import { emptyUsage } from "./observe.mjs";
1
+ import { join, sep } from 'node:path';
2
+ import { createAgentSession, DefaultResourceLoader, defineTool, getAgentDir, ModelRuntime, SessionManager, } from '@earendil-works/pi-coding-agent';
3
+ import { onStop } from './exec.mjs';
4
+ import { emptyUsage } from './observe.mjs';
5
5
  const REVIEWING = {
6
- tools: ["read", "bash", "grep", "find", "ls"],
6
+ tools: ['read', 'bash', 'grep', 'find', 'ls'],
7
7
  };
8
8
  const CODING = {
9
- tools: ["read", "bash", "edit", "write", "grep", "find", "ls"],
9
+ tools: ['read', 'bash', 'edit', 'write', 'grep', 'find', 'ls'],
10
10
  };
11
11
  export { CODING, REVIEWING };
12
- export const SUBMIT_TOOL = "submit_result";
12
+ export const SUBMIT_TOOL = 'submit_result';
13
13
  export function isOwnExtension(path) {
14
- return path.split(sep).includes("pi-next-issue");
14
+ return path.split(sep).includes('pi-next-issue');
15
15
  }
16
16
  export async function runAgent(recorder, request) {
17
17
  const log = recorder.scope({ role: request.name });
18
18
  const runtime = request.runtime ?? {};
19
19
  const agentDir = runtime.agentDir ?? getAgentDir();
20
20
  const started = Date.now();
21
- log.event("agent.start", { model: request.model, timeoutMs: request.timeoutMs }, "normal");
21
+ log.event('agent.start', { model: request.model, timeoutMs: request.timeoutMs }, 'normal');
22
22
  let structured;
23
23
  const customTools = [];
24
24
  if (request.outputSchema !== undefined) {
25
25
  customTools.push(defineTool({
26
26
  name: SUBMIT_TOOL,
27
- label: "Submit result",
28
- description: "Give the result. Call it once, as your last action.",
27
+ label: 'Submit result',
28
+ description: 'Give the result. Call it once, as your last action.',
29
29
  parameters: request.outputSchema,
30
30
  async execute(_toolCallId, params) {
31
31
  structured = params;
32
32
  return {
33
- content: [{ type: "text", text: "Result saved." }],
33
+ content: [{ type: 'text', text: 'Result saved.' }],
34
34
  details: {},
35
35
  terminate: true,
36
36
  };
@@ -46,10 +46,11 @@ export async function runAgent(recorder, request) {
46
46
  }),
47
47
  });
48
48
  await resourceLoader.reload();
49
- const modelRuntime = runtime.modelRuntime ?? (await ModelRuntime.create({
50
- authPath: join(agentDir, "auth.json"),
51
- modelsPath: join(agentDir, "models.json"),
52
- }));
49
+ const modelRuntime = runtime.modelRuntime ??
50
+ (await ModelRuntime.create({
51
+ authPath: join(agentDir, 'auth.json'),
52
+ modelsPath: join(agentDir, 'models.json'),
53
+ }));
53
54
  const model = request.model === undefined ? undefined : await findModel(modelRuntime, request.model);
54
55
  const { session, modelFallbackMessage } = await createAgentSession({
55
56
  cwd: request.cwd,
@@ -63,7 +64,7 @@ export async function runAgent(recorder, request) {
63
64
  ...(model === undefined ? {} : { model }),
64
65
  });
65
66
  if (modelFallbackMessage !== undefined) {
66
- log.event("agent.model", { note: modelFallbackMessage }, "quiet");
67
+ log.event('agent.model', { note: modelFallbackMessage }, 'quiet');
67
68
  }
68
69
  let timedOut = false;
69
70
  const timer = request.timeoutMs === undefined
@@ -80,9 +81,9 @@ export async function runAgent(recorder, request) {
80
81
  if (timedOut) {
81
82
  throw new Error(`The ${request.name} agent did not finish in ${request.timeoutMs} ms.`);
82
83
  }
83
- const last = session.messages.findLast((message) => message.role === "assistant");
84
- if (last !== undefined && "stopReason" in last && last.stopReason === "error") {
85
- throw new Error(`The ${request.name} agent failed: ${last.errorMessage ?? "no reason given"}`);
84
+ const last = session.messages.findLast((message) => message.role === 'assistant');
85
+ if (last !== undefined && 'stopReason' in last && last.stopReason === 'error') {
86
+ throw new Error(`The ${request.name} agent failed: ${last.errorMessage ?? 'no reason given'}`);
86
87
  }
87
88
  const stats = session.getSessionStats();
88
89
  const usage = { ...emptyUsage(), ...stats.tokens };
@@ -96,7 +97,7 @@ export async function runAgent(recorder, request) {
96
97
  costUsd: stats.cost,
97
98
  });
98
99
  return {
99
- text: (session.getLastAssistantText() ?? "").trim(),
100
+ text: (session.getLastAssistantText() ?? '').trim(),
100
101
  usage,
101
102
  turns: stats.assistantMessages,
102
103
  toolCalls: stats.toolCalls,
@@ -115,7 +116,7 @@ export async function runAgent(recorder, request) {
115
116
  }
116
117
  }
117
118
  async function findModel(modelRuntime, name) {
118
- const slash = name.indexOf("/");
119
+ const slash = name.indexOf('/');
119
120
  if (slash <= 0) {
120
121
  throw new Error(`The model ${name} is not in the form provider/model.`);
121
122
  }
@@ -126,22 +127,22 @@ async function findModel(modelRuntime, name) {
126
127
  return found;
127
128
  }
128
129
  function observe(log, event) {
129
- if (event.type === "tool_execution_start") {
130
- log.event("tool", toolFields(event.toolName, event.args));
130
+ if (event.type === 'tool_execution_start') {
131
+ log.event('tool', toolFields(event.toolName, event.args));
131
132
  }
132
- else if (event.type === "tool_execution_end" && event.isError) {
133
- log.event("tool.error", { tool: event.toolName }, "quiet");
133
+ else if (event.type === 'tool_execution_end' && event.isError) {
134
+ log.event('tool.error', { tool: event.toolName }, 'quiet');
134
135
  }
135
- else if (event.type === "message_update" &&
136
- event.assistantMessageEvent.type === "text_delta" &&
137
- log.level === "verbose") {
136
+ else if (event.type === 'message_update' &&
137
+ event.assistantMessageEvent.type === 'text_delta' &&
138
+ log.level === 'verbose') {
138
139
  process.stderr.write(event.assistantMessageEvent.delta);
139
140
  }
140
141
  }
141
142
  export function toolFields(name, input) {
142
- if (name !== "bash" || typeof input !== "object" || input === null) {
143
+ if (name !== 'bash' || typeof input !== 'object' || input === null) {
143
144
  return { tool: name };
144
145
  }
145
146
  const { command } = input;
146
- return typeof command === "string" ? { tool: name, command } : { tool: name };
147
+ return typeof command === 'string' ? { tool: name, command } : { tool: name };
147
148
  }
package/dist/args.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { CONFIG_FILE } from "./config.mjs";
1
+ import { CONFIG_FILE } from './config.mjs';
2
2
  export const USAGE = `next-issue [command] [options]
3
3
 
4
4
  init write ${CONFIG_FILE} with the defaults
@@ -18,7 +18,7 @@ export const USAGE = `next-issue [command] [options]
18
18
  `;
19
19
  export function parseArgs(argv) {
20
20
  const args = {
21
- command: "run",
21
+ command: 'run',
22
22
  once: false,
23
23
  help: false,
24
24
  json: false,
@@ -26,44 +26,44 @@ export function parseArgs(argv) {
26
26
  force: false,
27
27
  merge: false,
28
28
  watch: false,
29
- level: "normal",
29
+ level: 'normal',
30
30
  };
31
31
  for (let index = 0; index < argv.length; index += 1) {
32
32
  const arg = argv[index];
33
- if (index === 0 && !arg.startsWith("-")) {
34
- if (arg !== "init" && arg !== "serve" && arg !== "stop") {
33
+ if (index === 0 && !arg.startsWith('-')) {
34
+ if (arg !== 'init' && arg !== 'serve' && arg !== 'stop') {
35
35
  throw new Error(`Unknown command: ${arg}`);
36
36
  }
37
37
  args.command = arg;
38
38
  }
39
- else if (arg === "--force") {
39
+ else if (arg === '--force') {
40
40
  args.force = true;
41
41
  }
42
- else if (arg === "--json") {
42
+ else if (arg === '--json') {
43
43
  args.json = true;
44
44
  }
45
- else if (arg === "--help") {
45
+ else if (arg === '--help') {
46
46
  args.help = true;
47
47
  }
48
- else if (arg === "--merge") {
48
+ else if (arg === '--merge') {
49
49
  args.merge = true;
50
50
  }
51
- else if (arg === "--watch") {
51
+ else if (arg === '--watch') {
52
52
  args.watch = true;
53
53
  }
54
- else if (arg === "--reset") {
54
+ else if (arg === '--reset') {
55
55
  args.reset = true;
56
56
  }
57
- else if (arg === "--once") {
57
+ else if (arg === '--once') {
58
58
  args.once = true;
59
59
  }
60
- else if (arg === "--verbose") {
61
- args.level = "verbose";
60
+ else if (arg === '--verbose') {
61
+ args.level = 'verbose';
62
62
  }
63
- else if (arg === "--quiet") {
64
- args.level = "quiet";
63
+ else if (arg === '--quiet') {
64
+ args.level = 'quiet';
65
65
  }
66
- else if (arg === "--max") {
66
+ else if (arg === '--max') {
67
67
  args.max = whole(argv[++index], arg);
68
68
  }
69
69
  else {
package/dist/cli.mjs CHANGED
@@ -1,16 +1,16 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs, USAGE } from "./args.mjs";
3
- import { writeDefaultConfig } from "./config.mjs";
4
- import { ensureIgnored, repoRoot, WORKTREE_DIR } from "./git.mjs";
5
- import { listenForInterrupts } from "./interrupt.mjs";
6
- import { message } from "./observe.mjs";
7
- import { runRepo } from "./run.mjs";
8
- import { serve, socketPaths, stopWorker } from "./serve.mjs";
9
- import { STATE_DIR } from "./state.mjs";
10
- import { formatSummary } from "./summary.mjs";
2
+ import { parseArgs, USAGE } from './args.mjs';
3
+ import { writeDefaultConfig } from './config.mjs';
4
+ import { ensureIgnored, repoRoot, WORKTREE_DIR } from './git.mjs';
5
+ import { listenForInterrupts } from './interrupt.mjs';
6
+ import { message } from './observe.mjs';
7
+ import { runRepo } from './run.mjs';
8
+ import { serve, socketPaths, stopWorker } from './serve.mjs';
9
+ import { STATE_DIR } from './state.mjs';
10
+ import { formatSummary } from './summary.mjs';
11
11
  function write(stream, text) {
12
12
  return new Promise((resolve, reject) => {
13
- stream.write(text, (error) => (error === null || error === undefined ? resolve() : reject(error)));
13
+ stream.write(text, (error) => error === null || error === undefined ? resolve() : reject(error));
14
14
  });
15
15
  }
16
16
  async function main() {
@@ -19,16 +19,18 @@ async function main() {
19
19
  await write(process.stdout, USAGE);
20
20
  return 0;
21
21
  }
22
- if (args.command === "serve") {
22
+ if (args.command === 'serve') {
23
23
  return serve();
24
24
  }
25
- if (args.command === "stop") {
25
+ if (args.command === 'stop') {
26
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`);
27
+ await write(process.stdout, pid === undefined
28
+ ? 'next-issue: no worker is running\n'
29
+ : `next-issue: stopped the worker with process id ${pid}\n`);
28
30
  return 0;
29
31
  }
30
32
  const root = await repoRoot(process.cwd());
31
- if (args.command === "init") {
33
+ if (args.command === 'init') {
32
34
  return init(root, args.force);
33
35
  }
34
36
  const interrupts = listenForInterrupts((text) => void write(process.stderr, `${text}\n`));
package/dist/config.mjs CHANGED
@@ -1,8 +1,8 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import { join } from "node:path";
3
- import { message } from "./observe.mjs";
1
+ import { readFile, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { message } from './observe.mjs';
4
4
  const DEFAULTS = {
5
- remote: "origin",
5
+ remote: 'origin',
6
6
  issueLimit: 100,
7
7
  maxCiFixes: 5,
8
8
  maxCommitFixes: 10,
@@ -19,49 +19,49 @@ const DEFAULTS = {
19
19
  draftPullRequest: true,
20
20
  models: {},
21
21
  labels: {
22
- ready: "status:todo",
23
- inProgress: "status:in-progress",
24
- inReview: "status:in-review",
25
- done: "status:done",
26
- needsHuman: "status:needs-human",
27
- skip: "status:blocked",
22
+ ready: 'status:todo',
23
+ inProgress: 'status:in-progress',
24
+ inReview: 'status:in-review',
25
+ done: 'status:done',
26
+ needsHuman: 'status:needs-human',
27
+ skip: 'status:blocked',
28
28
  },
29
29
  };
30
30
  function defaults() {
31
31
  return { ...DEFAULTS, models: { ...DEFAULTS.models }, labels: { ...DEFAULTS.labels } };
32
32
  }
33
- const FIELDS = [...Object.keys(DEFAULTS), "setupCommand"];
33
+ const FIELDS = [...Object.keys(DEFAULTS), 'setupCommand'];
34
34
  const WHOLE_FIELDS = [
35
- ["issueLimit", 1],
36
- ["maxCiFixes", 0],
37
- ["maxCommitFixes", 0],
38
- ["maxReviewRounds", 1],
39
- ["checkIntervalSeconds", 1],
40
- ["checkGraceSeconds", 0],
41
- ["checkTimeoutMinutes", 1],
42
- ["agentTimeoutMinutes", 1],
43
- ["setupTimeoutMinutes", 1],
44
- ["commandTimeoutMinutes", 1],
45
- ["logMaxChars", 1],
46
- ["diffMaxChars", 1],
47
- ["keepRunDays", 0],
35
+ ['issueLimit', 1],
36
+ ['maxCiFixes', 0],
37
+ ['maxCommitFixes', 0],
38
+ ['maxReviewRounds', 1],
39
+ ['checkIntervalSeconds', 1],
40
+ ['checkGraceSeconds', 0],
41
+ ['checkTimeoutMinutes', 1],
42
+ ['agentTimeoutMinutes', 1],
43
+ ['setupTimeoutMinutes', 1],
44
+ ['commandTimeoutMinutes', 1],
45
+ ['logMaxChars', 1],
46
+ ['diffMaxChars', 1],
47
+ ['keepRunDays', 0],
48
48
  ];
49
- const MODEL_ROLES = ["implementer", "reviewer", "fixer"];
50
- const LABEL_NAMES = ["ready", "inProgress", "inReview", "done", "needsHuman", "skip"];
49
+ const MODEL_ROLES = ['implementer', 'reviewer', 'fixer'];
50
+ const LABEL_NAMES = ['ready', 'inProgress', 'inReview', 'done', 'needsHuman', 'skip'];
51
51
  const FILE_DEFAULTS = {
52
52
  ...defaults(),
53
53
  models: Object.fromEntries(MODEL_ROLES.map((role) => [role, null])),
54
54
  };
55
- export const CONFIG_FILE = "next-issue.config.json";
55
+ export const CONFIG_FILE = 'next-issue.config.json';
56
56
  export async function writeDefaultConfig(root, force) {
57
57
  const path = join(root, CONFIG_FILE);
58
58
  try {
59
59
  await writeFile(path, `${JSON.stringify(FILE_DEFAULTS, null, 2)}\n`, {
60
- flag: force ? "w" : "wx",
60
+ flag: force ? 'w' : 'wx',
61
61
  });
62
62
  }
63
63
  catch (error) {
64
- if (error.code === "EEXIST") {
64
+ if (error.code === 'EEXIST') {
65
65
  throw new Error(`${CONFIG_FILE} exists already. Use --force to replace it.`);
66
66
  }
67
67
  throw error;
@@ -71,10 +71,10 @@ export async function writeDefaultConfig(root, force) {
71
71
  export async function loadConfig(root) {
72
72
  let raw;
73
73
  try {
74
- raw = await readFile(join(root, CONFIG_FILE), "utf8");
74
+ raw = await readFile(join(root, CONFIG_FILE), 'utf8');
75
75
  }
76
76
  catch (error) {
77
- if (error.code === "ENOENT") {
77
+ if (error.code === 'ENOENT') {
78
78
  return defaults();
79
79
  }
80
80
  throw new Error(`${CONFIG_FILE} cannot be read: ${message(error)}`);
@@ -89,7 +89,7 @@ export async function loadConfig(root) {
89
89
  return parseConfig(parsed);
90
90
  }
91
91
  function parseConfig(value) {
92
- const file = record(value, "the file");
92
+ const file = record(value, 'the file');
93
93
  for (const key of Object.keys(file)) {
94
94
  if (!FIELDS.includes(key)) {
95
95
  throw new Error(`${CONFIG_FILE} holds an unknown field: ${key}`);
@@ -97,7 +97,7 @@ function parseConfig(value) {
97
97
  }
98
98
  const config = defaults();
99
99
  if (file.remote !== undefined) {
100
- config.remote = text(file.remote, "remote", false);
100
+ config.remote = text(file.remote, 'remote', false);
101
101
  }
102
102
  for (const [field, least] of WHOLE_FIELDS) {
103
103
  if (file[field] !== undefined) {
@@ -105,16 +105,16 @@ function parseConfig(value) {
105
105
  }
106
106
  }
107
107
  if (file.draftPullRequest !== undefined) {
108
- if (typeof file.draftPullRequest !== "boolean") {
108
+ if (typeof file.draftPullRequest !== 'boolean') {
109
109
  throw new Error(`${CONFIG_FILE}: draftPullRequest must be true or false.`);
110
110
  }
111
111
  config.draftPullRequest = file.draftPullRequest;
112
112
  }
113
113
  if (file.setupCommand !== undefined) {
114
- config.setupCommand = text(file.setupCommand, "setupCommand", false);
114
+ config.setupCommand = text(file.setupCommand, 'setupCommand', false);
115
115
  }
116
116
  if (file.models !== undefined) {
117
- const models = record(file.models, "models");
117
+ const models = record(file.models, 'models');
118
118
  for (const key of Object.keys(models)) {
119
119
  if (!MODEL_ROLES.includes(key)) {
120
120
  throw new Error(`${CONFIG_FILE} holds an unknown model role: ${key}`);
@@ -127,7 +127,7 @@ function parseConfig(value) {
127
127
  }
128
128
  }
129
129
  if (file.labels !== undefined) {
130
- const labels = record(file.labels, "labels");
130
+ const labels = record(file.labels, 'labels');
131
131
  for (const key of Object.keys(labels)) {
132
132
  if (!LABEL_NAMES.includes(key)) {
133
133
  throw new Error(`${CONFIG_FILE} holds an unknown label name: ${key}`);
@@ -135,27 +135,27 @@ function parseConfig(value) {
135
135
  }
136
136
  for (const name of LABEL_NAMES) {
137
137
  if (labels[name] !== undefined) {
138
- config.labels[name] = text(labels[name], `labels.${name}`, name === "ready");
138
+ config.labels[name] = text(labels[name], `labels.${name}`, name === 'ready');
139
139
  }
140
140
  }
141
141
  }
142
142
  return config;
143
143
  }
144
144
  function record(value, what) {
145
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
145
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
146
146
  throw new Error(`${CONFIG_FILE}: ${what} must be an object.`);
147
147
  }
148
148
  return value;
149
149
  }
150
150
  function whole(value, field, least) {
151
- if (typeof value !== "number" || !Number.isInteger(value) || value < least) {
151
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < least) {
152
152
  throw new Error(`${CONFIG_FILE}: ${field} must be a whole number of ${least} or more.`);
153
153
  }
154
154
  return value;
155
155
  }
156
156
  function text(value, field, allowEmpty) {
157
- if (typeof value !== "string" || (!allowEmpty && value.length === 0)) {
158
- throw new Error(`${CONFIG_FILE}: ${field} must be ${allowEmpty ? "text" : "text that is not empty"}.`);
157
+ if (typeof value !== 'string' || (!allowEmpty && value.length === 0)) {
158
+ throw new Error(`${CONFIG_FILE}: ${field} must be ${allowEmpty ? 'text' : 'text that is not empty'}.`);
159
159
  }
160
160
  return value;
161
161
  }