@ravshansbox/pi-next-issue 1.2.0 → 2.3.0

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/src/ports.ts DELETED
@@ -1,43 +0,0 @@
1
- import * as agent from './agent.ts';
2
- import type { Config } from './config.ts';
3
- import * as git from './git.ts';
4
- import * as github from './github.ts';
5
- import type { Ports } from './runner.ts';
6
- import * as state from './state.ts';
7
-
8
- export async function createPorts(root: string, config: Config): Promise<Ports> {
9
- const repo: github.Repo = { root, slug: await git.remoteSlug(root, config.remote) };
10
- const base = await github.defaultBranch(repo);
11
- const timeoutMs = config.checkTimeoutMinutes * 60_000;
12
- return {
13
- openIssues: async () => {
14
- const { open, closed } = await github.openIssues(repo, await state.savedIssues(root));
15
- await Promise.all(closed.map((issue) => state.dropState(root, issue)));
16
- return open;
17
- },
18
- comments: (issue) => github.comments(repo, issue),
19
- setLabel: (issue, add, remove) => github.setLabel(repo, issue, add, remove),
20
- commentOnIssue: (issue, body) => github.commentOnIssue(repo, issue, body),
21
- worktree: (issue, branch) => git.worktree(root, config.remote, base, issue, branch),
22
- commit: (cwd, message) => git.commit(cwd, message),
23
- ahead: (cwd) => git.ahead(cwd, config.remote, base),
24
- push: (cwd, branch) => git.push(cwd, config.remote, branch),
25
- discard: (cwd, branch) => git.discard(cwd, config.remote, branch),
26
- diff: (cwd) => git.diff(cwd, config.remote, base),
27
- openPr: (branch, title, body) => github.openPr(repo, branch, base, title, body),
28
- checks: async (pr, cwd, signal) =>
29
- github.checks(repo, pr, await git.head(cwd), timeoutMs, signal),
30
- failedLogs: (pr) => github.failedLogs(repo, pr, config.logMaxChars),
31
- commentOnPr: (pr, body) => github.commentOnPr(repo, pr, body),
32
- merge: (pr, signal) => github.merge(repo, pr, timeoutMs, signal),
33
- cleanup: (_issue, branch, cwd) => git.cleanup(root, config.remote, branch, cwd),
34
- work: agent.work,
35
- review: agent.review,
36
- readState: (issue) => state.readState(root, issue),
37
- writeState: (value) => state.writeState(root, value),
38
- dropState: (issue) => state.dropState(root, issue),
39
- savedIssues: () => state.savedIssues(root),
40
- lock: () => state.lock(root),
41
- unlock: () => state.unlock(root),
42
- };
43
- }
package/src/prompts.ts DELETED
@@ -1,109 +0,0 @@
1
- import type { Finding, Issue, Verdict } from './runner.ts';
2
-
3
- const COMMIT_RULES = [
4
- 'Do not run git, gh or any push command. The harness commits for you.',
5
- 'End your final message with one line in the form `commit: <conventional commit subject>`.',
6
- ].join('\n');
7
-
8
- function issueBlock(issue: Issue, comments: string[]): string {
9
- return [
10
- `# Issue #${issue.number}: ${issue.title}`,
11
- issue.body.length > 0 ? issue.body : '(no description)',
12
- comments.length > 0 ? `## Comments\n${comments.join('\n\n')}` : '',
13
- ]
14
- .filter((part) => part.length > 0)
15
- .join('\n\n');
16
- }
17
-
18
- export function implementPrompt(issue: Issue, comments: string[]): string {
19
- return [
20
- 'You are the implementer. Solve the issue below in this worktree.',
21
- issueBlock(issue, comments),
22
- '## Rules',
23
- 'Read the repository conventions first. Keep the change minimal and focused on the issue.',
24
- 'Add or update tests when the repository has a test suite.',
25
- COMMIT_RULES,
26
- ].join('\n\n');
27
- }
28
-
29
- export function reviewPrompt(
30
- issue: Issue,
31
- comments: string[],
32
- diff: string,
33
- earlier: string[],
34
- ): string {
35
- const scope =
36
- earlier.length === 0
37
- ? 'Check correctness, scope, repository conventions and test cover.'
38
- : [
39
- 'This is a later round. Judge only whether the earlier findings below are fixed and whether the new commits broke something.',
40
- 'Do not raise a new point of taste. Withdraw a finding when the fixer disputed it correctly.',
41
- `## Earlier rounds\n${earlier.join('\n\n')}`,
42
- ].join('\n');
43
- return [
44
- 'You are the reviewer. Judge whether the diff solves the issue correctly.',
45
- issueBlock(issue, comments),
46
- '## Diff',
47
- '```diff',
48
- diff,
49
- '```',
50
- '## Rules',
51
- 'Read files and run commands to prove what you say: the tests, the type check or the linter.',
52
- 'Do not change a file. Do not write with git or gh. The harness discards each change that you make.',
53
- scope,
54
- 'Mark a finding blocking only for a wrong result, a missing part of the issue, a regression or a broken convention.',
55
- 'Finish by calling the submit_verdict tool once with a short summary and one entry per finding.',
56
- ].join('\n\n');
57
- }
58
-
59
- export function fixPrompt(issue: Issue, reason: string, detail: string, earlier: string[]): string {
60
- return [
61
- `You are the fixer. Correct the work for issue #${issue.number}: ${issue.title}.`,
62
- `## Reason\n${reason}`,
63
- `## Detail\n${detail}`,
64
- earlier.length > 0
65
- ? `## Earlier rounds\n${earlier.join('\n\n')}\n\nKeep those earlier fixes.`
66
- : '',
67
- '## Rules',
68
- 'Fix the cause, not the symptom. Keep the change minimal. Do not weaken, skip or delete a test to make it pass.',
69
- 'If the problem is not caused by this change, or every finding is wrong, change nothing and end your final message with one line in the form `unrelated: <one sentence on why>`.',
70
- COMMIT_RULES,
71
- ]
72
- .filter((part) => part.length > 0)
73
- .join('\n\n');
74
- }
75
-
76
- function lastLine(text: string, key: string): string | undefined {
77
- const match = [...text.matchAll(new RegExp(`^${key}:\\s*(.+)$`, 'gim'))].at(-1);
78
- const value = match?.[1]
79
- ?.trim()
80
- .replace(/^`+|`+$/g, '')
81
- .trim();
82
- return value !== undefined && value.length > 0 ? value : undefined;
83
- }
84
-
85
- export function parseCommitSubject(text: string, fallback: string): string {
86
- return lastLine(text, 'commit') ?? fallback;
87
- }
88
-
89
- export function parseUnrelated(text: string): string | undefined {
90
- return lastLine(text, 'unrelated');
91
- }
92
-
93
- export function cap(text: string, maxChars: number): string {
94
- return text.length <= maxChars
95
- ? text
96
- : `${text.slice(0, maxChars)}\n... cut after ${maxChars} characters.`;
97
- }
98
-
99
- export function formatVerdict(verdict: Verdict, open: Finding[]): string {
100
- const head = open.length === 0 ? 'Review: approved' : 'Review: changes requested';
101
- const lines = verdict.findings.map((finding) => `- **${finding.severity}** ${finding.detail}`);
102
- return [`### ${head}`, verdict.summary, lines.join('\n')]
103
- .filter((part) => part.length > 0)
104
- .join('\n\n');
105
- }
106
-
107
- export function formatFindings(findings: Finding[]): string {
108
- return findings.map((finding) => `- ${finding.detail}`).join('\n');
109
- }
package/src/runner.ts DELETED
@@ -1,358 +0,0 @@
1
- import type { Config } from './config.ts';
2
- import {
3
- cap,
4
- fixPrompt,
5
- formatFindings,
6
- formatVerdict,
7
- implementPrompt,
8
- parseCommitSubject,
9
- parseUnrelated,
10
- reviewPrompt,
11
- } from './prompts.ts';
12
-
13
- export type Issue = { number: number; title: string; body: string; labels: string[] };
14
-
15
- export type Finding = { severity: 'blocking' | 'minor'; detail: string };
16
-
17
- export type Verdict = { summary: string; findings: Finding[] };
18
-
19
- export type ChecksResult = 'pass' | 'fail' | 'timeout';
20
-
21
- export type Phase = 'implement' | 'publish' | 'checks' | 'review' | 'merge';
22
-
23
- export type IssueState = {
24
- issue: number;
25
- branch: string;
26
- phase: Phase;
27
- pr?: number;
28
- title?: string | undefined;
29
- ciFixes: number;
30
- round: number;
31
- earlier: string[];
32
- };
33
-
34
- export type AgentTask = {
35
- cwd: string;
36
- prompt: string;
37
- signal: AbortSignal;
38
- onTool: (label: string) => void;
39
- };
40
-
41
- export type Ports = {
42
- openIssues(): Promise<Issue[]>;
43
- comments(issue: number): Promise<string[]>;
44
- setLabel(issue: number, add: string, remove: string[]): Promise<void>;
45
- commentOnIssue(issue: number, body: string): Promise<void>;
46
- worktree(issue: number, branch: string): Promise<string>;
47
- commit(cwd: string, message: string): Promise<boolean>;
48
- ahead(cwd: string): Promise<string | undefined>;
49
- push(cwd: string, branch: string): Promise<void>;
50
- discard(cwd: string, branch: string): Promise<void>;
51
- diff(cwd: string): Promise<string>;
52
- openPr(branch: string, title: string, body: string): Promise<number>;
53
- checks(pr: number, cwd: string, signal: AbortSignal): Promise<ChecksResult>;
54
- failedLogs(pr: number): Promise<string>;
55
- commentOnPr(pr: number, body: string): Promise<void>;
56
- merge(pr: number, signal: AbortSignal): Promise<MergeResult>;
57
- cleanup(issue: number, branch: string, cwd: string): Promise<void>;
58
- work(task: AgentTask): Promise<string>;
59
- review(task: AgentTask): Promise<Verdict | undefined>;
60
- readState(issue: number): Promise<IssueState | undefined>;
61
- writeState(state: IssueState): Promise<void>;
62
- dropState(issue: number): Promise<void>;
63
- savedIssues(): Promise<number[]>;
64
- lock(): Promise<void>;
65
- unlock(): Promise<void>;
66
- };
67
-
68
- export type MergeResult = { merged: true } | { merged: false; reason: string };
69
-
70
- export type Outcome = 'merged' | 'needs-human';
71
-
72
- export type Status = 'idle' | 'running' | 'stopping' | 'finished' | 'stopped' | 'failed';
73
-
74
- export type Progress = {
75
- status: Status;
76
- issue?: number | undefined;
77
- title?: string | undefined;
78
- stage?: string | undefined;
79
- tools: number;
80
- lastTool?: string | undefined;
81
- issueStartedAt?: number;
82
- handled: Array<{ issue: number; outcome: Outcome }>;
83
- error?: string;
84
- };
85
-
86
- export const LABELS = {
87
- ready: 'status:todo',
88
- inProgress: 'status:in-progress',
89
- done: 'status:done',
90
- needsHuman: 'status:needs-human',
91
- blocked: 'status:blocked',
92
- };
93
-
94
- export const STOP_LABELS = [LABELS.inProgress, LABELS.done, LABELS.needsHuman, LABELS.blocked];
95
-
96
- function ready(issue: Issue): boolean {
97
- return (
98
- issue.labels.includes(LABELS.ready) &&
99
- !issue.labels.some((label) => STOP_LABELS.includes(label))
100
- );
101
- }
102
-
103
- function resumable(issue: Issue): boolean {
104
- return !issue.labels.some((label) => label !== LABELS.inProgress && STOP_LABELS.includes(label));
105
- }
106
-
107
- export class Runner {
108
- progress: Progress = { status: 'idle', tools: 0, handled: [] };
109
- private readonly controller = new AbortController();
110
- private readonly listeners = new Set<(progress: Progress) => void>();
111
-
112
- private readonly ports: Ports;
113
- private readonly config: Config;
114
-
115
- constructor(ports: Ports, config: Config) {
116
- this.ports = ports;
117
- this.config = config;
118
- }
119
-
120
- subscribe(listener: (progress: Progress) => void): () => void {
121
- this.listeners.add(listener);
122
- return () => this.listeners.delete(listener);
123
- }
124
-
125
- stop(): void {
126
- if (this.progress.status === 'running') {
127
- this.update({ status: 'stopping' });
128
- }
129
- this.controller.abort();
130
- }
131
-
132
- async run(): Promise<void> {
133
- this.update({ status: 'running' });
134
- const seen = new Set<number>();
135
- let locked = false;
136
- try {
137
- await this.ports.lock();
138
- locked = true;
139
- for (;;) {
140
- this.controller.signal.throwIfAborted();
141
- const issues = await this.ports.openIssues();
142
- const saved = new Set(await this.ports.savedIssues());
143
- const fresh = issues.filter((item) => !seen.has(item.number));
144
- const next =
145
- fresh.find((item) => saved.has(item.number) && resumable(item)) ??
146
- fresh.find((item) => ready(item));
147
- if (next === undefined) {
148
- this.update({ status: 'finished', stage: undefined, issue: undefined, title: undefined });
149
- return;
150
- }
151
- seen.add(next.number);
152
- const outcome = await this.handle(next, await this.ports.readState(next.number));
153
- this.update({ handled: [...this.progress.handled, { issue: next.number, outcome }] });
154
- }
155
- } catch (error) {
156
- if (this.controller.signal.aborted) {
157
- this.update({ status: 'stopped' });
158
- } else {
159
- this.update({
160
- status: 'failed',
161
- error: error instanceof Error ? error.message : String(error),
162
- });
163
- }
164
- } finally {
165
- if (locked) {
166
- await this.ports.unlock().catch(() => {});
167
- }
168
- }
169
- }
170
-
171
- private update(change: Partial<Progress>): void {
172
- this.progress = { ...this.progress, ...change };
173
- for (const listener of this.listeners) {
174
- listener(this.progress);
175
- }
176
- }
177
-
178
- private stage(stage: string): void {
179
- this.controller.signal.throwIfAborted();
180
- this.update({ stage });
181
- }
182
-
183
- private async agent(kind: 'work', cwd: string, prompt: string): Promise<string>;
184
- private async agent(kind: 'review', cwd: string, prompt: string): Promise<Verdict | undefined>;
185
- private async agent(
186
- kind: 'work' | 'review',
187
- cwd: string,
188
- prompt: string,
189
- ): Promise<string | Verdict | undefined> {
190
- const task: AgentTask = {
191
- cwd,
192
- prompt,
193
- signal: this.controller.signal,
194
- onTool: (label) => this.update({ tools: this.progress.tools + 1, lastTool: label }),
195
- };
196
- const result = kind === 'work' ? await this.ports.work(task) : await this.ports.review(task);
197
- this.controller.signal.throwIfAborted();
198
- return result;
199
- }
200
-
201
- private async handle(issue: Issue, saved: IssueState | undefined): Promise<Outcome> {
202
- const { ports, config } = this;
203
- const number = issue.number;
204
- this.update({
205
- issue: number,
206
- title: issue.title,
207
- stage: 'prepare',
208
- tools: 0,
209
- lastTool: undefined,
210
- issueStartedAt: Date.now(),
211
- });
212
- let state: IssueState = saved ?? {
213
- issue: number,
214
- branch: String(number),
215
- phase: 'implement',
216
- ciFixes: 0,
217
- round: 0,
218
- earlier: [],
219
- };
220
- const save = async (change: Partial<IssueState>) => {
221
- state = { ...state, ...change };
222
- await ports.writeState(state);
223
- };
224
- const needsHuman = async (reason: string): Promise<Outcome> => {
225
- await ports.setLabel(number, LABELS.needsHuman, [LABELS.inProgress]);
226
- await ports.commentOnIssue(number, `next-issue stopped: ${reason}`);
227
- await ports.dropState(number);
228
- return 'needs-human';
229
- };
230
- if (saved === undefined) {
231
- await ports.setLabel(number, LABELS.inProgress, [LABELS.ready]);
232
- await ports.writeState(state);
233
- }
234
- const comments = await ports.comments(number);
235
- const cwd = await ports.worktree(number, state.branch);
236
- const pr = () => {
237
- if (state.pr === undefined) {
238
- throw new Error(`the saved state of #${number} holds no pull request`);
239
- }
240
- return state.pr;
241
- };
242
- for (;;) {
243
- switch (state.phase) {
244
- case 'implement': {
245
- this.stage('implement');
246
- const kept = saved === undefined ? undefined : await ports.ahead(cwd);
247
- if (kept !== undefined) {
248
- await save({ phase: 'publish', title: kept });
249
- break;
250
- }
251
- const reply = await this.agent('work', cwd, implementPrompt(issue, comments));
252
- const title = parseCommitSubject(reply, `feat: ${issue.title}`);
253
- if (!(await ports.commit(cwd, title))) {
254
- return needsHuman('the implementer made no change');
255
- }
256
- await save({ phase: 'publish', title });
257
- break;
258
- }
259
- case 'publish': {
260
- this.stage('pull request');
261
- await ports.push(cwd, state.branch);
262
- const title = state.title ?? `feat: ${issue.title}`;
263
- await save({
264
- phase: 'checks',
265
- pr: await ports.openPr(state.branch, title, `Closes #${number}`),
266
- });
267
- break;
268
- }
269
- case 'checks': {
270
- this.stage('checks');
271
- const result = await ports.checks(pr(), cwd, this.controller.signal);
272
- if (result === 'pass') {
273
- await save({ phase: 'review' });
274
- break;
275
- }
276
- if (result === 'timeout') {
277
- return needsHuman('the checks did not finish in time');
278
- }
279
- if (state.ciFixes >= config.maxCiFixes) {
280
- return needsHuman(`the checks still fail after ${config.maxCiFixes} fixes`);
281
- }
282
- this.stage(`fix checks ${state.ciFixes + 1}/${config.maxCiFixes}`);
283
- const logs = cap(await ports.failedLogs(pr()), config.logMaxChars);
284
- const reply = await this.agent(
285
- 'work',
286
- cwd,
287
- fixPrompt(issue, 'The checks failed.', logs, []),
288
- );
289
- if (!(await ports.commit(cwd, parseCommitSubject(reply, 'fix: make the checks pass')))) {
290
- return needsHuman(
291
- `the fixer made no change: ${parseUnrelated(reply) ?? 'no reason given'}`,
292
- );
293
- }
294
- await ports.push(cwd, state.branch);
295
- await save({ ciFixes: state.ciFixes + 1 });
296
- break;
297
- }
298
- case 'review': {
299
- const round = state.round + 1;
300
- const last = round >= config.maxReviewRounds;
301
- this.stage(`review ${round}/${config.maxReviewRounds}`);
302
- const diff = cap(await ports.diff(cwd), config.diffMaxChars);
303
- const verdict = await this.agent(
304
- 'review',
305
- cwd,
306
- reviewPrompt(issue, comments, diff, state.earlier),
307
- );
308
- await ports.discard(cwd, state.branch);
309
- if (verdict === undefined) {
310
- return needsHuman('the reviewer gave no verdict');
311
- }
312
- const open = last
313
- ? verdict.findings.filter((finding) => finding.severity === 'blocking')
314
- : verdict.findings;
315
- await ports.commentOnPr(pr(), formatVerdict(verdict, open));
316
- if (open.length === 0) {
317
- await save({ phase: 'merge', round });
318
- break;
319
- }
320
- if (last) {
321
- return needsHuman('blocking review findings are left after the last round');
322
- }
323
- this.stage(`fix review ${round}/${config.maxReviewRounds}`);
324
- const findings = formatFindings(open);
325
- const reply = await this.agent(
326
- 'work',
327
- cwd,
328
- fixPrompt(issue, 'The reviewer found problems.', findings, state.earlier),
329
- );
330
- const committed = await ports.commit(
331
- cwd,
332
- parseCommitSubject(reply, 'fix: address the review'),
333
- );
334
- const dispute = committed
335
- ? ''
336
- : `\nThe fixer disputed every finding of this round: ${parseUnrelated(reply) ?? 'no reason given'}`;
337
- const earlier = [...state.earlier, `Round ${round}:\n${findings}${dispute}`];
338
- if (committed) {
339
- await ports.push(cwd, state.branch);
340
- }
341
- await save({ phase: committed ? 'checks' : 'review', round, earlier });
342
- break;
343
- }
344
- case 'merge': {
345
- this.stage('merge');
346
- const result = await ports.merge(pr(), this.controller.signal);
347
- if (!result.merged) {
348
- return needsHuman(`pull request #${pr()} could not be merged: ${result.reason}`);
349
- }
350
- await ports.setLabel(number, LABELS.done, [LABELS.inProgress]);
351
- await ports.cleanup(number, state.branch, cwd);
352
- await ports.dropState(number);
353
- return 'merged';
354
- }
355
- }
356
- }
357
- }
358
- }
package/src/state.ts DELETED
@@ -1,121 +0,0 @@
1
- import { link, mkdir, readdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { WORKTREE_DIR } from './git.ts';
4
- import type { IssueState } from './runner.ts';
5
-
6
- function dir(root: string): string {
7
- return join(root, WORKTREE_DIR);
8
- }
9
-
10
- function file(root: string, issue: number): string {
11
- return join(dir(root), `${issue}.json`);
12
- }
13
-
14
- export async function readState(root: string, issue: number): Promise<IssueState | undefined> {
15
- try {
16
- return JSON.parse(await readFile(file(root, issue), 'utf8')) as IssueState;
17
- } catch (error) {
18
- if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
19
- return undefined;
20
- }
21
- throw new Error(`the state of #${issue} cannot be read: ${(error as Error).message}`);
22
- }
23
- }
24
-
25
- export async function writeState(root: string, state: IssueState): Promise<void> {
26
- await mkdir(dir(root), { recursive: true });
27
- await writeFile(file(root, state.issue), `${JSON.stringify(state, null, 2)}\n`);
28
- }
29
-
30
- export async function dropState(root: string, issue: number): Promise<void> {
31
- await rm(file(root, issue), { force: true });
32
- }
33
-
34
- export async function savedIssues(root: string): Promise<number[]> {
35
- const names = await readdir(dir(root)).catch(() => []);
36
- return names.flatMap((name) => {
37
- const match = /^(\d+)\.json$/.exec(name);
38
- return match === null ? [] : [Number(match[1])];
39
- });
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
- }