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 +12 -8
- package/dist/cli.mjs +4 -1
- package/dist/git.mjs +17 -12
- package/dist/pipeline.mjs +9 -9
- package/package.json +1 -1
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
233
|
-
already; `--force` replaces it. The file holds no
|
|
234
|
-
harness deduces one from the lock file of the
|
|
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 {
|
|
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(
|
|
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
|
-
|
|
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
|
|
62
|
+
export async function findWorktree(repo, branch) {
|
|
62
63
|
const list = await must("git", ["worktree", "list", "--porcelain"], { cwd: repo.root });
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
560
|
-
const path = worktreePath(context.repo, issue);
|
|
559
|
+
async function cleanUp(context, log, branch) {
|
|
561
560
|
try {
|
|
562
|
-
|
|
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,
|
|
565
|
+
if (!(await context.ports.removeWorktree(context.repo, path))) {
|
|
566
566
|
log.event("worktree.kept", { path }, "quiet");
|
|
567
567
|
}
|
|
568
568
|
}
|