next-issue 1.0.6 → 1.0.7

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
@@ -21,11 +21,14 @@ For each open issue, oldest first:
21
21
  closed, if its pull request is merged.
22
22
  2. Add `status:in-progress` to the issue.
23
23
  3. Fetch the remote, fast-forward the local copy of the default branch when
24
- that is safe, then add a git worktree at `../<repo>-issue-<n>` on the
25
- `issue-<n>` branch. A branch that exists on the remote is the start, and a
26
- local branch that fell behind the remote is fast-forwarded. A fast-forward
27
- of the base that is not safe leaves the local base branch as it is and
28
- writes a `base.behind` event. A new branch then starts from the remote base.
24
+ that is safe, then add a git worktree at `.worktrees/<n>` in the
25
+ repository root on the `<n>` branch. An issue that resumes from saved
26
+ state keeps the branch name that the state holds, and a worktree that
27
+ holds that branch already stays in use. A branch that exists on the
28
+ remote is the start, and a local branch that fell behind the remote is
29
+ fast-forwarded. A fast-forward of the base that is not safe leaves the
30
+ local base branch as it is and writes a `base.behind` event. A new branch
31
+ then starts from the remote base.
29
32
  4. Let the implementer agent do the work, then commit and push. When a git
30
33
  hook stops a commit, the agent that made the change gets the hook output,
31
34
  fixes it, and the harness commits again. A `commit:` line in the reply of
@@ -229,9 +232,10 @@ npx -y next-issue@latest init
229
232
  ```
230
233
 
231
234
  The command writes `next-issue.config.json` in the root of the repository and
232
- adds `.next-issue/` to `.gitignore`. It stops when the config file is there
233
- already; `--force` replaces it. The file holds no `setupCommand`, because the
234
- harness deduces one from the lock file of the project.
235
+ adds `.next-issue/` and `.worktrees/` to `.gitignore`. It stops when the
236
+ config file is there already; `--force` replaces it. The file holds no
237
+ `setupCommand`, because the harness deduces one from the lock file of the
238
+ project.
235
239
 
236
240
  With no file, every field keeps its default. A field that is not in the file
237
241
  keeps its default. A file that cannot be read, a file that is not valid
package/dist/cli.mjs CHANGED
@@ -4,7 +4,7 @@ import { join } from "node:path";
4
4
  import { parseArgs, USAGE } from "./args.mjs";
5
5
  import { loadConfig, writeDefaultConfig } from "./config.mjs";
6
6
  import { setCommandObserver, setDefaultTimeout } from "./exec.mjs";
7
- import { detectRepo, ensureIgnored, repoRoot } from "./git.mjs";
7
+ import { detectRepo, ensureIgnored, repoRoot, WORKTREE_DIR } from "./git.mjs";
8
8
  import { defaultBranch } from "./github.mjs";
9
9
  import { listenForInterrupts } from "./interrupt.mjs";
10
10
  import { exitCode, runIssues } from "./loop.mjs";
@@ -58,6 +58,9 @@ async function init(root, force) {
58
58
  if (await ensureIgnored(root, `${STATE_DIR}/`)) {
59
59
  await write(process.stdout, `added ${STATE_DIR}/ to .gitignore\n`);
60
60
  }
61
+ if (await ensureIgnored(root, `${WORKTREE_DIR}/`)) {
62
+ await write(process.stdout, `added ${WORKTREE_DIR}/ to .gitignore\n`);
63
+ }
61
64
  return 0;
62
65
  }
63
66
  async function handle(args, config, repo, recorder) {
package/dist/git.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import { appendFile, readFile } from "node:fs/promises";
2
- import { basename, dirname, join } from "node:path";
2
+ import { join } from "node:path";
3
3
  import { must, run } from "./exec.mjs";
4
4
  const REMOTE_PATTERNS = [
5
5
  /^git@[^:]+:(?<owner>[^/]+)\/(?<name>[^/]+?)(?:\.git)?$/,
@@ -24,18 +24,19 @@ export async function detectRepo(cwd, remote) {
24
24
  const { owner, name } = parseRemote(url);
25
25
  return { owner, name, root };
26
26
  }
27
+ export const WORKTREE_DIR = ".worktrees";
27
28
  export function worktreePath(repo, issue) {
28
- return join(dirname(repo.root), `${basename(repo.root)}-issue-${issue}`);
29
+ return join(repo.root, WORKTREE_DIR, String(issue));
29
30
  }
30
- export async function addWorktree(repo, issue, base, remote) {
31
- const path = worktreePath(repo, issue);
32
- const branch = branchName(issue);
31
+ export async function addWorktree(repo, issue, branch, base, remote) {
33
32
  const upstream = `${remote}/${branch}`;
34
33
  await must("git", ["fetch", remote], { cwd: repo.root });
35
34
  const baseUpdated = await updateBase(repo, base, remote);
36
35
  await must("git", ["worktree", "prune"], { cwd: repo.root });
37
36
  const pushed = await hasBranch(repo, upstream, "--remotes");
38
- if (!(await hasWorktree(repo, path))) {
37
+ const existing = await findWorktree(repo, branch);
38
+ const path = existing ?? worktreePath(repo, issue);
39
+ if (existing === undefined) {
39
40
  const args = (await hasBranch(repo, branch))
40
41
  ? ["worktree", "add", path, branch]
41
42
  : ["worktree", "add", "-b", branch, path, pushed ? upstream : `${remote}/${base}`];
@@ -58,12 +59,16 @@ export async function updateBase(repo, base, remote) {
58
59
  const result = await run("git", args, { cwd: repo.root });
59
60
  return result.code === 0;
60
61
  }
61
- export async function hasWorktree(repo, path) {
62
+ export async function findWorktree(repo, branch) {
62
63
  const list = await must("git", ["worktree", "list", "--porcelain"], { cwd: repo.root });
63
- return list.split("\n").some((line) => line.trim() === `worktree ${path}`);
64
- }
65
- export async function removeWorktree(repo, issue) {
66
- const result = await run("git", ["worktree", "remove", worktreePath(repo, issue)], { cwd: repo.root });
64
+ const entry = list
65
+ .split("\n\n")
66
+ .map((block) => block.split("\n"))
67
+ .find((lines) => lines.includes(`branch refs/heads/${branch}`));
68
+ return entry?.find((line) => line.startsWith("worktree "))?.slice("worktree ".length);
69
+ }
70
+ export async function removeWorktree(repo, path) {
71
+ const result = await run("git", ["worktree", "remove", path], { cwd: repo.root });
67
72
  return result.code === 0;
68
73
  }
69
74
  export async function ensureIgnored(root, entry) {
@@ -83,7 +88,7 @@ export async function ensureIgnored(root, entry) {
83
88
  return true;
84
89
  }
85
90
  export function branchName(issue) {
86
- return `issue-${issue}`;
91
+ return String(issue);
87
92
  }
88
93
  export async function isDirty(cwd) {
89
94
  const status = await must("git", ["status", "--porcelain"], { cwd });
package/dist/pipeline.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { CODING, REVIEWING, runAgent } from "./agents.mjs";
2
2
  import { managedLabels } from "./config.mjs";
3
3
  import { run } from "./exec.mjs";
4
- import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, hasWorktree, isDirty, push, removeWorktree, resetToBase, revision, worktreePath, } from "./git.mjs";
4
+ import { addWorktree, branchName, commitAll, deleteBranch, diff, discardChanges, findWorktree, isDirty, push, removeWorktree, resetToBase, revision, } from "./git.mjs";
5
5
  import { commentOnPr, createPr, failedBaseLogs, failedCheckLogs, findPr, issueComments, markPrReady, mergeCommit, mergePr, openBlockers, openIssues, prMerged, reopenIssue, setLabel, viewIssue, waitForBaseChecks, waitForChecks, } from "./github.mjs";
6
6
  import { message } from "./observe.mjs";
7
7
  import { capDiff, fixPrompt, hookPrompt, implementPrompt, parseCommitSubject, parseUnrelated, reviewPrompt, } from "./prompts.mjs";
@@ -20,7 +20,7 @@ export const PORTS = {
20
20
  failedBaseLogs,
21
21
  failedCheckLogs,
22
22
  findPr,
23
- hasWorktree,
23
+ findWorktree,
24
24
  isDirty,
25
25
  issueComments,
26
26
  markPrReady,
@@ -126,7 +126,7 @@ export async function processIssue(context, issue) {
126
126
  catch (failure) {
127
127
  log.event("hand-over.error", { error: message(failure) }, "quiet");
128
128
  }
129
- await cleanUp(context, log, issue.number);
129
+ await cleanUp(context, log, state.branch);
130
130
  return {
131
131
  issue: issue.number,
132
132
  outcome: "error",
@@ -150,7 +150,7 @@ async function work(context, issue, log, state) {
150
150
  await writeState(repo, state);
151
151
  });
152
152
  const comments = await log.step("comments", {}, () => ports.issueComments(repo, issue.number));
153
- const added = await log.step("worktree", { branch }, () => ports.addWorktree(repo, issue.number, context.base, config.remote));
153
+ const added = await log.step("worktree", { branch }, () => ports.addWorktree(repo, issue.number, branch, context.base, config.remote));
154
154
  const worktree = added.path;
155
155
  if (!added.baseUpdated) {
156
156
  log.event("base.behind", { base: context.base }, "quiet");
@@ -477,7 +477,7 @@ function stop(job) {
477
477
  async function finish(job, outcome, reason) {
478
478
  const { context, issue, log, state, branch } = job;
479
479
  const { repo, ports } = context;
480
- await cleanUp(context, log, issue.number);
480
+ await cleanUp(context, log, branch);
481
481
  if (outcome === "done") {
482
482
  if (!(await ports.deleteBranch(repo, branch))) {
483
483
  log.event("branch.kept", { branch }, "quiet");
@@ -556,13 +556,13 @@ async function commitWork(job, before, subject, role) {
556
556
  }
557
557
  }
558
558
  }
559
- async function cleanUp(context, log, issue) {
560
- const path = worktreePath(context.repo, issue);
559
+ async function cleanUp(context, log, branch) {
561
560
  try {
562
- if (!(await context.ports.hasWorktree(context.repo, path))) {
561
+ const path = await context.ports.findWorktree(context.repo, branch);
562
+ if (path === undefined) {
563
563
  return;
564
564
  }
565
- if (!(await context.ports.removeWorktree(context.repo, issue))) {
565
+ if (!(await context.ports.removeWorktree(context.repo, path))) {
566
566
  log.event("worktree.kept", { path }, "quiet");
567
567
  }
568
568
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-issue",
3
- "version": "1.0.6",
3
+ "version": "1.0.7",
4
4
  "description": "Autonomous issue harness driven by the Claude Agent SDK",
5
5
  "keywords": [
6
6
  "github",