@ravshansbox/pi-next-issue 1.0.3 → 1.0.4
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 +1 -1
- package/package.json +1 -1
- package/src/ports.ts +2 -0
- package/src/runner.ts +9 -0
- package/src/state.ts +82 -1
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ The implementer and the reviewer read the issue title, the body and the comments
|
|
|
39
39
|
|
|
40
40
|
Each agent is a pi session in the worktree. It uses the model and thinking level from your pi settings, and loads all of your extensions except this one.
|
|
41
41
|
|
|
42
|
-
The state of each issue is saved in `.worktrees/<n>.json`. The extension adds `/.worktrees/` to `.git/info/exclude`.
|
|
42
|
+
The state of each issue is saved in `.worktrees/<n>.json`. A run holds `.worktrees/run.lock`, which keeps the process id, so that only one pi at a time works on a repository. A second run fails at once. A lock whose process is no longer alive is taken over. If the operating system gives that process id to another process, the lock stays; remove `.worktrees/run.lock` by hand when no run is going. The extension adds `/.worktrees/` to `.git/info/exclude`.
|
|
43
43
|
|
|
44
44
|
## Config
|
|
45
45
|
|
package/package.json
CHANGED
package/src/ports.ts
CHANGED
|
@@ -32,5 +32,7 @@ export async function createPorts(root: string, config: Config): Promise<Ports>
|
|
|
32
32
|
writeState: (value) => state.writeState(root, value),
|
|
33
33
|
dropState: (issue) => state.dropState(root, issue),
|
|
34
34
|
savedIssues: () => state.savedIssues(root),
|
|
35
|
+
lock: () => state.lock(root),
|
|
36
|
+
unlock: () => state.unlock(root),
|
|
35
37
|
};
|
|
36
38
|
}
|
package/src/runner.ts
CHANGED
|
@@ -60,6 +60,8 @@ export type Ports = {
|
|
|
60
60
|
writeState(state: IssueState): Promise<void>;
|
|
61
61
|
dropState(issue: number): Promise<void>;
|
|
62
62
|
savedIssues(): Promise<number[]>;
|
|
63
|
+
lock(): Promise<void>;
|
|
64
|
+
unlock(): Promise<void>;
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
export type Outcome = 'merged' | 'needs-human';
|
|
@@ -123,7 +125,10 @@ export class Runner {
|
|
|
123
125
|
async run(): Promise<void> {
|
|
124
126
|
this.update({ status: 'running' });
|
|
125
127
|
const seen = new Set<number>();
|
|
128
|
+
let locked = false;
|
|
126
129
|
try {
|
|
130
|
+
await this.ports.lock();
|
|
131
|
+
locked = true;
|
|
127
132
|
for (;;) {
|
|
128
133
|
this.controller.signal.throwIfAborted();
|
|
129
134
|
const issues = await this.ports.openIssues();
|
|
@@ -148,6 +153,10 @@ export class Runner {
|
|
|
148
153
|
error: error instanceof Error ? error.message : String(error),
|
|
149
154
|
});
|
|
150
155
|
}
|
|
156
|
+
} finally {
|
|
157
|
+
if (locked) {
|
|
158
|
+
await this.ports.unlock().catch(() => {});
|
|
159
|
+
}
|
|
151
160
|
}
|
|
152
161
|
}
|
|
153
162
|
|
package/src/state.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
1
|
+
import { link, mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { WORKTREE_DIR } from './git.ts';
|
|
4
4
|
import type { IssueState } from './runner.ts';
|
|
@@ -38,3 +38,84 @@ export async function savedIssues(root: string): Promise<number[]> {
|
|
|
38
38
|
return match === null ? [] : [Number(match[1])];
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
function lockFile(root: string): string {
|
|
43
|
+
return join(dir(root), 'run.lock');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function alive(pid: number): boolean {
|
|
47
|
+
try {
|
|
48
|
+
process.kill(pid, 0);
|
|
49
|
+
return true;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return (error as NodeJS.ErrnoException).code === 'EPERM';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function holder(path: string): Promise<number> {
|
|
56
|
+
return Number((await readFile(path, 'utf8').catch(() => '')).trim());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function busy(pid: number, cause: unknown): Error {
|
|
60
|
+
return new Error(`another run works on this repository (pid ${pid})`, { cause });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function place(root: string): Promise<void> {
|
|
64
|
+
const draft = `${lockFile(root)}.${process.pid}.draft`;
|
|
65
|
+
await writeFile(draft, `${process.pid}\n`);
|
|
66
|
+
try {
|
|
67
|
+
await link(draft, lockFile(root));
|
|
68
|
+
} finally {
|
|
69
|
+
await rm(draft, { force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function claim(root: string): Promise<void> {
|
|
74
|
+
try {
|
|
75
|
+
await place(root);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
78
|
+
throw busy(await holder(lockFile(root)), error);
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function lock(root: string): Promise<void> {
|
|
85
|
+
await mkdir(dir(root), { recursive: true });
|
|
86
|
+
try {
|
|
87
|
+
await place(root);
|
|
88
|
+
return;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
const pid = await holder(lockFile(root));
|
|
94
|
+
if (Number.isInteger(pid) && pid > 0 && alive(pid)) {
|
|
95
|
+
throw busy(pid, error);
|
|
96
|
+
}
|
|
97
|
+
const stale = `${lockFile(root)}.${process.pid}`;
|
|
98
|
+
try {
|
|
99
|
+
await rename(lockFile(root), stale);
|
|
100
|
+
} catch (renameError) {
|
|
101
|
+
if ((renameError as NodeJS.ErrnoException).code !== 'ENOENT') {
|
|
102
|
+
throw renameError;
|
|
103
|
+
}
|
|
104
|
+
return claim(root);
|
|
105
|
+
}
|
|
106
|
+
const moved = await holder(stale);
|
|
107
|
+
if (!Object.is(moved, pid)) {
|
|
108
|
+
await link(stale, lockFile(root)).catch(() => undefined);
|
|
109
|
+
await rm(stale, { force: true });
|
|
110
|
+
throw busy(moved, error);
|
|
111
|
+
}
|
|
112
|
+
await rm(stale, { force: true });
|
|
113
|
+
}
|
|
114
|
+
await claim(root);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function unlock(root: string): Promise<void> {
|
|
118
|
+
if ((await holder(lockFile(root))) === process.pid) {
|
|
119
|
+
await rm(lockFile(root), { force: true });
|
|
120
|
+
}
|
|
121
|
+
}
|