@anhlp/alp-runtime 0.0.0-beta.1
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/LICENSE +202 -0
- package/NOTICE +7 -0
- package/README.md +7 -0
- package/dist/acp-transport.d.ts +114 -0
- package/dist/acp-transport.js +478 -0
- package/dist/antigravity-transport.d.ts +80 -0
- package/dist/antigravity-transport.js +609 -0
- package/dist/board-domain.d.ts +56 -0
- package/dist/board-domain.js +157 -0
- package/dist/board.d.ts +21 -0
- package/dist/board.js +69 -0
- package/dist/catalog.d.ts +31 -0
- package/dist/catalog.js +38 -0
- package/dist/claude-transport.d.ts +157 -0
- package/dist/claude-transport.js +671 -0
- package/dist/contracts.d.ts +163 -0
- package/dist/contracts.js +5 -0
- package/dist/events.d.ts +1 -0
- package/dist/events.js +0 -0
- package/dist/github.d.ts +10 -0
- package/dist/github.js +39 -0
- package/dist/handoff.d.ts +36 -0
- package/dist/handoff.js +73 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +64 -0
- package/dist/language.d.ts +41 -0
- package/dist/language.js +68 -0
- package/dist/live.d.ts +48 -0
- package/dist/live.js +45 -0
- package/dist/mailbox.d.ts +24 -0
- package/dist/mailbox.js +36 -0
- package/dist/mcp-tools.d.ts +115 -0
- package/dist/mcp-tools.js +71 -0
- package/dist/permission-path.d.ts +2 -0
- package/dist/permission-path.js +44 -0
- package/dist/process-info.d.ts +14 -0
- package/dist/process-info.js +32 -0
- package/dist/providers.d.ts +28 -0
- package/dist/providers.js +71 -0
- package/dist/recall.d.ts +42 -0
- package/dist/recall.js +60 -0
- package/dist/resolve.d.ts +121 -0
- package/dist/resolve.js +238 -0
- package/dist/runtime-context.d.ts +43 -0
- package/dist/runtime-context.js +61 -0
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.js +2979 -0
- package/dist/session-config.d.ts +1973 -0
- package/dist/session-config.js +205 -0
- package/dist/session-state.d.ts +140 -0
- package/dist/session-state.js +0 -0
- package/dist/tool-results.d.ts +12 -0
- package/dist/tool-results.js +25 -0
- package/dist/tools.d.ts +674 -0
- package/dist/tools.js +375 -0
- package/dist/transport.d.ts +47 -0
- package/dist/transport.js +169 -0
- package/dist/usage.d.ts +15 -0
- package/dist/usage.js +35 -0
- package/dist/workspace.d.ts +101 -0
- package/dist/workspace.js +259 -0
- package/dist/worktree-domain.d.ts +88 -0
- package/dist/worktree-domain.js +196 -0
- package/package.json +256 -0
- package/src/acp-transport.ts +473 -0
- package/src/antigravity-transport.ts +658 -0
- package/src/board-domain.ts +185 -0
- package/src/board.ts +80 -0
- package/src/catalog.ts +32 -0
- package/src/claude-transport.ts +771 -0
- package/src/contracts.ts +166 -0
- package/src/events.ts +1 -0
- package/src/github.ts +37 -0
- package/src/handoff.ts +86 -0
- package/src/index.ts +26 -0
- package/src/language.ts +102 -0
- package/src/live.ts +86 -0
- package/src/mailbox.ts +46 -0
- package/src/mcp-tools.ts +73 -0
- package/src/permission-path.ts +37 -0
- package/src/process-info.ts +40 -0
- package/src/providers.ts +84 -0
- package/src/recall.ts +85 -0
- package/src/resolve.ts +230 -0
- package/src/runtime-context.ts +49 -0
- package/src/runtime.ts +3579 -0
- package/src/session-config.ts +301 -0
- package/src/session-state.ts +141 -0
- package/src/tool-results.ts +26 -0
- package/src/tools.ts +379 -0
- package/src/transport.ts +116 -0
- package/src/usage.ts +42 -0
- package/src/workspace.ts +318 -0
- package/src/worktree-domain.ts +216 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase C isolation (plans/reference/ALPD.md §16): a writing assignment may run in its
|
|
3
|
+
* own git worktree, so writers do not share a checkout. Its work is committed to a
|
|
4
|
+
* branch when it ends, and the requester merges the change into its checkout or
|
|
5
|
+
* discards it. Branches outlive their worktrees, so work is never deleted silently.
|
|
6
|
+
*/
|
|
7
|
+
export type Worktree = {
|
|
8
|
+
/** The requester's checkout, which receives the change on merge. */
|
|
9
|
+
checkout: string;
|
|
10
|
+
/** The worktree's top level, and the assignment's working directory inside it. */
|
|
11
|
+
path: string;
|
|
12
|
+
workdir: string;
|
|
13
|
+
branch: string;
|
|
14
|
+
/** The requester's state when the assignment started: HEAD plus uncommitted tracked changes. */
|
|
15
|
+
base: string;
|
|
16
|
+
/** checkoutFingerprint of the requester's checkout then, to tell whether it changed since. */
|
|
17
|
+
fingerprint?: string;
|
|
18
|
+
};
|
|
19
|
+
export type WorktreeChange = {
|
|
20
|
+
commit: string;
|
|
21
|
+
files: string[];
|
|
22
|
+
stat: string;
|
|
23
|
+
};
|
|
24
|
+
/** The environment ALP runs git in: the caller's, without git's own context, and never prompting. */
|
|
25
|
+
export declare function gitEnvironment(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
26
|
+
/** The top level of the git checkout containing `directory`, or undefined outside git. */
|
|
27
|
+
export declare function checkoutOf(directory: string): Promise<string | undefined>;
|
|
28
|
+
/** The key that writers of one checkout share: its top level, or the directory itself outside git. */
|
|
29
|
+
export declare function checkoutKey(directory: string): Promise<string>;
|
|
30
|
+
/** Whether the checkout containing `directory` has a local branch of that name. */
|
|
31
|
+
export declare function branchExists(directory: string, branch: string): Promise<boolean>;
|
|
32
|
+
/** Creates a worktree for `id` from the current state of the checkout containing `workdir`. */
|
|
33
|
+
/**
|
|
34
|
+
* Creates a worktree for `id` from the current state of the checkout containing `workdir`,
|
|
35
|
+
* or, with `from`, from an earlier assignment's committed change, keeping that assignment's
|
|
36
|
+
* base so a merge applies both changes together.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createWorktree(workdir: string, root: string, id: string, from?: Pick<Worktree, 'base' | 'fingerprint'> & {
|
|
39
|
+
commit: string;
|
|
40
|
+
}): Promise<Worktree>;
|
|
41
|
+
/**
|
|
42
|
+
* Checks out an assignment's existing branch again at its worktree's place, after
|
|
43
|
+
* reclaimWorktrees committed and removed it, so the assignment continues there.
|
|
44
|
+
*/
|
|
45
|
+
export declare function reattachWorktree(worktree: Worktree): Promise<Worktree>;
|
|
46
|
+
/** A digest of a checkout's state: HEAD, its uncommitted changes, and the names of untracked files. */
|
|
47
|
+
export declare function checkoutFingerprint(checkout: string): Promise<string | undefined>;
|
|
48
|
+
/**
|
|
49
|
+
* Links the project's node_modules into the same place in a worktree that has none,
|
|
50
|
+
* so its build and tests run; returns what undoes it. Commits never see the link,
|
|
51
|
+
* because ALP removes it before the worktree is committed again.
|
|
52
|
+
*/
|
|
53
|
+
export declare function linkModules(projectDir: string, worktreeDir: string): Promise<() => Promise<void>>;
|
|
54
|
+
/** Commits whatever the assignment left in its worktree and describes the change from its base. */
|
|
55
|
+
export declare function commitWorktree(worktree: Worktree, message: string): Promise<WorktreeChange>;
|
|
56
|
+
/**
|
|
57
|
+
* Applies the change to the requester's working tree without committing or touching
|
|
58
|
+
* its index. A patch that applies cleanly is applied as is; otherwise each file is
|
|
59
|
+
* merged three ways against the requester's current file, leaving conflict markers.
|
|
60
|
+
* Where a file was deleted on one side or is binary, the requester's version is kept
|
|
61
|
+
* and reported as a conflict.
|
|
62
|
+
*/
|
|
63
|
+
export declare function mergeWorktree(worktree: Worktree, change: WorktreeChange): Promise<{
|
|
64
|
+
status: "empty";
|
|
65
|
+
files: never[];
|
|
66
|
+
conflicts?: undefined;
|
|
67
|
+
} | {
|
|
68
|
+
status: "applied";
|
|
69
|
+
files: string[];
|
|
70
|
+
conflicts?: undefined;
|
|
71
|
+
} | {
|
|
72
|
+
status: "conflicts";
|
|
73
|
+
files: string[];
|
|
74
|
+
conflicts: string[];
|
|
75
|
+
}>;
|
|
76
|
+
/** Removes the worktree directory; the branch stays unless `deleteBranch`. */
|
|
77
|
+
export declare function removeWorktree(worktree: Worktree, { deleteBranch }?: {
|
|
78
|
+
deleteBranch?: boolean | undefined;
|
|
79
|
+
}): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* After a crash: commits work left in worktrees under `root` to their branches and
|
|
82
|
+
* removes the directories. Returns the branches that were kept.
|
|
83
|
+
*/
|
|
84
|
+
export declare function reclaimWorktrees(root: string): Promise<string[]>;
|
|
85
|
+
/**
|
|
86
|
+
* A disposable copy of a checkout for an assignment that may build and test but
|
|
87
|
+
* must not touch the requester's tree (ALPD §26): a detached worktree at the
|
|
88
|
+
* requester's HEAD with its uncommitted changes applied as uncommitted changes, so
|
|
89
|
+
* `git diff` and `git status` there show what they show the requester, plus its
|
|
90
|
+
* untracked files.
|
|
91
|
+
* A top-level node_modules is linked, not copied. Nothing in it is ever merged.
|
|
92
|
+
*/
|
|
93
|
+
export type Copy = {
|
|
94
|
+
checkout: string;
|
|
95
|
+
path: string;
|
|
96
|
+
workdir: string;
|
|
97
|
+
};
|
|
98
|
+
export declare function createCopy(workdir: string, root: string, id: string): Promise<Copy>;
|
|
99
|
+
export declare function removeCopy(copy: Copy): Promise<void>;
|
|
100
|
+
/** After a crash: removes copies left under `root`; they hold nothing to keep. */
|
|
101
|
+
export declare function reclaimCopies(root: string): Promise<number>;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// packages/runtime/src/workspace.ts
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { copyFile, lstat, mkdir, mkdtemp, readdir, readFile, realpath, rm, stat, symlink, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
var IDENTITY = ["-c", "user.name=ALP", "-c", "user.email=alp@localhost", "-c", "commit.gpgsign=false"];
|
|
8
|
+
var GIT_CONTEXT = [
|
|
9
|
+
"GIT_DIR",
|
|
10
|
+
"GIT_WORK_TREE",
|
|
11
|
+
"GIT_INDEX_FILE",
|
|
12
|
+
"GIT_COMMON_DIR",
|
|
13
|
+
"GIT_OBJECT_DIRECTORY",
|
|
14
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
15
|
+
"GIT_PREFIX",
|
|
16
|
+
"GIT_NAMESPACE",
|
|
17
|
+
"GIT_IMPLICIT_WORK_TREE",
|
|
18
|
+
"GIT_GRAFT_FILE",
|
|
19
|
+
"GIT_REPLACE_REF_BASE",
|
|
20
|
+
"GIT_NO_REPLACE_OBJECTS",
|
|
21
|
+
"GIT_SHALLOW_FILE",
|
|
22
|
+
"GIT_CONFIG",
|
|
23
|
+
"GIT_CONFIG_PARAMETERS",
|
|
24
|
+
"GIT_CONFIG_COUNT"
|
|
25
|
+
];
|
|
26
|
+
function gitEnvironment(env = process.env) {
|
|
27
|
+
const clean = { ...env, GIT_TERMINAL_PROMPT: "0" };
|
|
28
|
+
for (const key of Object.keys(clean)) {
|
|
29
|
+
if (GIT_CONTEXT.includes(key) || /^GIT_CONFIG_(KEY|VALUE)_\d+$/.test(key)) delete clean[key];
|
|
30
|
+
}
|
|
31
|
+
return clean;
|
|
32
|
+
}
|
|
33
|
+
function git(cwd, args, input) {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const child = spawn("git", args, { cwd, stdio: [input === void 0 ? "ignore" : "pipe", "pipe", "pipe"], windowsHide: true, env: gitEnvironment() });
|
|
36
|
+
let stdout = "";
|
|
37
|
+
let stderr = "";
|
|
38
|
+
child.stdout.on("data", (chunk) => {
|
|
39
|
+
stdout += chunk;
|
|
40
|
+
});
|
|
41
|
+
child.stderr.on("data", (chunk) => {
|
|
42
|
+
stderr += chunk;
|
|
43
|
+
});
|
|
44
|
+
child.once("error", reject);
|
|
45
|
+
child.once("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
46
|
+
if (child.stdin) {
|
|
47
|
+
child.stdin.on("error", () => {
|
|
48
|
+
});
|
|
49
|
+
child.stdin.end(input);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async function checked(cwd, args, input) {
|
|
54
|
+
const result = await git(cwd, args, input);
|
|
55
|
+
if (result.code !== 0) throw new Error(`git ${args[0]} failed: ${(result.stderr || result.stdout).trim()}`);
|
|
56
|
+
return result.stdout.trim();
|
|
57
|
+
}
|
|
58
|
+
function blob(cwd, revision, file) {
|
|
59
|
+
return new Promise((resolve, reject) => {
|
|
60
|
+
const child = spawn("git", ["cat-file", "blob", `${revision}:${file}`], { cwd, stdio: ["ignore", "pipe", "ignore"], windowsHide: true, env: gitEnvironment() });
|
|
61
|
+
const chunks = [];
|
|
62
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
63
|
+
child.once("error", reject);
|
|
64
|
+
child.once("close", (code) => resolve(code === 0 ? Buffer.concat(chunks) : void 0));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
var binary = (content) => content.subarray(0, 8e3).includes(0);
|
|
68
|
+
var same = (a, b) => a === b || !!a && !!b && a.equals(b);
|
|
69
|
+
async function mergeText(ours, base, theirs) {
|
|
70
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "alp-merge-"));
|
|
71
|
+
try {
|
|
72
|
+
const [o, b, t] = ["ours", "base", "theirs"].map((name) => path.join(directory, name));
|
|
73
|
+
await Promise.all([writeFile(o, ours), writeFile(b, base), writeFile(t, theirs)]);
|
|
74
|
+
const result = await git(directory, ["merge-file", "-p", "-L", "yours", "-L", "base", "-L", "assignment", o, b, t]);
|
|
75
|
+
if (result.code < 0 || result.code > 127) throw new Error(`git merge-file failed: ${result.stderr.trim()}`);
|
|
76
|
+
return { content: result.stdout, conflict: result.code > 0 };
|
|
77
|
+
} finally {
|
|
78
|
+
await rm(directory, { recursive: true, force: true });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function checkoutOf(directory) {
|
|
82
|
+
const result = await git(directory, ["rev-parse", "--show-toplevel"]).catch(() => void 0);
|
|
83
|
+
return result?.code === 0 ? realpath(result.stdout.trim()) : void 0;
|
|
84
|
+
}
|
|
85
|
+
async function checkoutKey(directory) {
|
|
86
|
+
return await checkoutOf(directory) ?? realpath(directory).catch(() => path.resolve(directory));
|
|
87
|
+
}
|
|
88
|
+
async function branchExists(directory, branch) {
|
|
89
|
+
const result = await git(directory, ["rev-parse", "--verify", "--quiet", `refs/heads/${branch}`]).catch(() => void 0);
|
|
90
|
+
return result?.code === 0;
|
|
91
|
+
}
|
|
92
|
+
async function createWorktree(workdir, root, id, from) {
|
|
93
|
+
const checkout = await checkoutOf(workdir);
|
|
94
|
+
if (!checkout) throw new Error("Worktree isolation needs a git repository");
|
|
95
|
+
const head = await git(checkout, ["rev-parse", "--verify", "HEAD"]);
|
|
96
|
+
if (head.code !== 0) throw new Error("Worktree isolation needs at least one commit");
|
|
97
|
+
const fingerprint = from ? from.fingerprint : await checkoutFingerprint(checkout);
|
|
98
|
+
const base = from ? from.base : await checked(checkout, ["stash", "create"]) || head.stdout.trim();
|
|
99
|
+
const target = path.join(root, id);
|
|
100
|
+
const branch = `alp/${id}`;
|
|
101
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
102
|
+
await checked(checkout, ["worktree", "add", "--quiet", "-b", branch, target, from ? from.commit : base]);
|
|
103
|
+
const relative = path.relative(checkout, await realpath(workdir));
|
|
104
|
+
return { checkout, path: target, workdir: path.join(target, relative), branch, base, ...fingerprint ? { fingerprint } : {} };
|
|
105
|
+
}
|
|
106
|
+
async function reattachWorktree(worktree) {
|
|
107
|
+
if (!await branchExists(worktree.checkout, worktree.branch)) throw new Error(`Branch ${worktree.branch} of the worktree no longer exists`);
|
|
108
|
+
if (await stat(worktree.path).catch(() => void 0)) return worktree;
|
|
109
|
+
await mkdir(path.dirname(worktree.path), { recursive: true, mode: 448 });
|
|
110
|
+
await git(worktree.checkout, ["worktree", "prune"]);
|
|
111
|
+
await checked(worktree.checkout, ["worktree", "add", "--quiet", worktree.path, worktree.branch]);
|
|
112
|
+
return worktree;
|
|
113
|
+
}
|
|
114
|
+
async function checkoutFingerprint(checkout) {
|
|
115
|
+
const head = await git(checkout, ["rev-parse", "--verify", "HEAD"]);
|
|
116
|
+
if (head.code !== 0) return void 0;
|
|
117
|
+
const diff = await git(checkout, ["diff", "--binary", "HEAD"]);
|
|
118
|
+
const untracked = await git(checkout, ["ls-files", "--others", "--exclude-standard", "-z"]);
|
|
119
|
+
return createHash("sha256").update(head.stdout).update("\0").update(diff.stdout).update("\0").update(untracked.stdout).digest("hex");
|
|
120
|
+
}
|
|
121
|
+
async function linkModules(projectDir, worktreeDir) {
|
|
122
|
+
const source = path.join(projectDir, "node_modules");
|
|
123
|
+
const target = path.join(worktreeDir, "node_modules");
|
|
124
|
+
if (!(await stat(source).catch(() => void 0))?.isDirectory() || await lstat(target).catch(() => void 0)) return async () => {
|
|
125
|
+
};
|
|
126
|
+
await symlink(source, target, "dir");
|
|
127
|
+
return async () => {
|
|
128
|
+
await unlink(target).catch(() => {
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
async function commitWorktree(worktree, message) {
|
|
133
|
+
await checked(worktree.path, ["add", "-A"]);
|
|
134
|
+
const staged = await git(worktree.path, ["diff", "--cached", "--quiet"]);
|
|
135
|
+
if (staged.code === 1) await checked(worktree.path, [...IDENTITY, "commit", "--quiet", "--no-verify", "-m", message]);
|
|
136
|
+
const commit = await checked(worktree.path, ["rev-parse", "HEAD"]);
|
|
137
|
+
const names = await checked(worktree.path, ["diff", "--name-only", worktree.base, commit]);
|
|
138
|
+
const stat2 = await checked(worktree.path, ["diff", "--shortstat", worktree.base, commit]);
|
|
139
|
+
return { commit, files: names ? names.split("\n") : [], stat: stat2 };
|
|
140
|
+
}
|
|
141
|
+
async function mergeWorktree(worktree, change) {
|
|
142
|
+
if (!change.files.length) return { status: "empty", files: [] };
|
|
143
|
+
const patch = await checked(worktree.checkout, ["diff", "--binary", worktree.base, change.commit]);
|
|
144
|
+
if ((await git(worktree.checkout, ["apply", "--whitespace=nowarn"], `${patch}
|
|
145
|
+
`)).code === 0) return { status: "applied", files: change.files };
|
|
146
|
+
const conflicts = [];
|
|
147
|
+
for (const file of change.files) {
|
|
148
|
+
const target = path.join(worktree.checkout, file);
|
|
149
|
+
const ours = await readFile(target).catch(() => void 0);
|
|
150
|
+
const base = await blob(worktree.checkout, worktree.base, file);
|
|
151
|
+
const theirs = await blob(worktree.checkout, change.commit, file);
|
|
152
|
+
if (same(ours, theirs)) continue;
|
|
153
|
+
if (same(ours, base)) {
|
|
154
|
+
if (theirs) {
|
|
155
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
156
|
+
await writeFile(target, theirs);
|
|
157
|
+
} else {
|
|
158
|
+
await rm(target, { force: true });
|
|
159
|
+
}
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (!ours || !theirs || binary(ours) || binary(theirs) || base && binary(base)) {
|
|
163
|
+
conflicts.push(file);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const merged = await mergeText(ours, base ?? Buffer.alloc(0), theirs);
|
|
167
|
+
await writeFile(target, merged.content);
|
|
168
|
+
if (merged.conflict) conflicts.push(file);
|
|
169
|
+
}
|
|
170
|
+
return conflicts.length ? { status: "conflicts", files: change.files, conflicts } : { status: "applied", files: change.files };
|
|
171
|
+
}
|
|
172
|
+
async function removeWorktree(worktree, { deleteBranch = false } = {}) {
|
|
173
|
+
await git(worktree.checkout, ["worktree", "remove", "--force", worktree.path]);
|
|
174
|
+
await rm(worktree.path, { recursive: true, force: true });
|
|
175
|
+
await git(worktree.checkout, ["worktree", "prune"]);
|
|
176
|
+
if (deleteBranch) await git(worktree.checkout, ["branch", "-D", worktree.branch]);
|
|
177
|
+
}
|
|
178
|
+
async function reclaimWorktrees(root) {
|
|
179
|
+
const kept = [];
|
|
180
|
+
const entries = await readdir(root).catch(() => []);
|
|
181
|
+
for (const name of entries) {
|
|
182
|
+
const target = path.join(root, name);
|
|
183
|
+
if (!(await stat(target).catch(() => void 0))?.isDirectory()) continue;
|
|
184
|
+
const common = await git(target, ["rev-parse", "--path-format=absolute", "--git-common-dir"]).catch(() => void 0);
|
|
185
|
+
if (common?.code !== 0) {
|
|
186
|
+
await rm(target, { recursive: true, force: true });
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const checkout = path.dirname(common.stdout.trim());
|
|
190
|
+
const branch = (await git(target, ["rev-parse", "--abbrev-ref", "HEAD"])).stdout.trim();
|
|
191
|
+
await git(target, ["add", "-A"]);
|
|
192
|
+
if ((await git(target, ["diff", "--cached", "--quiet"])).code === 1) {
|
|
193
|
+
await git(target, [...IDENTITY, "commit", "--quiet", "--no-verify", "-m", "alp: work left by an interrupted assignment"]);
|
|
194
|
+
}
|
|
195
|
+
await removeWorktree({ checkout, path: target, workdir: target, branch, base: "" });
|
|
196
|
+
kept.push(branch);
|
|
197
|
+
}
|
|
198
|
+
return kept;
|
|
199
|
+
}
|
|
200
|
+
async function createCopy(workdir, root, id) {
|
|
201
|
+
const checkout = await checkoutOf(workdir);
|
|
202
|
+
if (!checkout) throw new Error("A review copy needs a git repository");
|
|
203
|
+
const head = await git(checkout, ["rev-parse", "--verify", "HEAD"]);
|
|
204
|
+
if (head.code !== 0) throw new Error("A review copy needs at least one commit");
|
|
205
|
+
const target = path.join(root, id);
|
|
206
|
+
await mkdir(root, { recursive: true, mode: 448 });
|
|
207
|
+
await checked(checkout, ["worktree", "add", "--quiet", "--detach", target, head.stdout.trim()]);
|
|
208
|
+
try {
|
|
209
|
+
const patch = (await git(checkout, ["diff", "--binary", "HEAD"])).stdout;
|
|
210
|
+
if (patch.trim()) await checked(target, ["apply", "--whitespace=nowarn", "--binary"], patch);
|
|
211
|
+
const untracked = (await checked(checkout, ["ls-files", "--others", "--exclude-standard", "-z"])).split("\0").filter(Boolean);
|
|
212
|
+
for (const file of untracked) {
|
|
213
|
+
const from = path.join(checkout, file);
|
|
214
|
+
if (!(await lstat(from).catch(() => void 0))?.isFile()) continue;
|
|
215
|
+
await mkdir(path.dirname(path.join(target, file)), { recursive: true });
|
|
216
|
+
await copyFile(from, path.join(target, file));
|
|
217
|
+
}
|
|
218
|
+
const modules = path.join(checkout, "node_modules");
|
|
219
|
+
if ((await stat(modules).catch(() => void 0))?.isDirectory()) await symlink(modules, path.join(target, "node_modules"), "dir");
|
|
220
|
+
} catch (error) {
|
|
221
|
+
await removeCopy({ checkout, path: target, workdir: target });
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
const relative = path.relative(checkout, await realpath(workdir));
|
|
225
|
+
return { checkout, path: target, workdir: path.join(target, relative) };
|
|
226
|
+
}
|
|
227
|
+
async function removeCopy(copy) {
|
|
228
|
+
await git(copy.checkout, ["worktree", "remove", "--force", copy.path]).catch(() => void 0);
|
|
229
|
+
await rm(copy.path, { recursive: true, force: true });
|
|
230
|
+
await git(copy.checkout, ["worktree", "prune"]).catch(() => void 0);
|
|
231
|
+
}
|
|
232
|
+
async function reclaimCopies(root) {
|
|
233
|
+
let removed = 0;
|
|
234
|
+
for (const name of await readdir(root).catch(() => [])) {
|
|
235
|
+
const target = path.join(root, name);
|
|
236
|
+
const common = await git(target, ["rev-parse", "--path-format=absolute", "--git-common-dir"]).catch(() => void 0);
|
|
237
|
+
if (common?.code === 0) await removeCopy({ checkout: path.dirname(common.stdout.trim()), path: target, workdir: target });
|
|
238
|
+
else await rm(target, { recursive: true, force: true });
|
|
239
|
+
removed++;
|
|
240
|
+
}
|
|
241
|
+
return removed;
|
|
242
|
+
}
|
|
243
|
+
export {
|
|
244
|
+
branchExists,
|
|
245
|
+
checkoutFingerprint,
|
|
246
|
+
checkoutKey,
|
|
247
|
+
checkoutOf,
|
|
248
|
+
commitWorktree,
|
|
249
|
+
createCopy,
|
|
250
|
+
createWorktree,
|
|
251
|
+
gitEnvironment,
|
|
252
|
+
linkModules,
|
|
253
|
+
mergeWorktree,
|
|
254
|
+
reattachWorktree,
|
|
255
|
+
reclaimCopies,
|
|
256
|
+
reclaimWorktrees,
|
|
257
|
+
removeCopy,
|
|
258
|
+
removeWorktree
|
|
259
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type Verification, type VerifyConfig } from '@anhlp/alp-core/verify';
|
|
2
|
+
import { type WorktreeChange } from './workspace.js';
|
|
3
|
+
import type { RuntimeOptions } from './contracts.js';
|
|
4
|
+
import type { Session, Assignment } from './session-state.js';
|
|
5
|
+
type WorktreeContext = {
|
|
6
|
+
options: RuntimeOptions;
|
|
7
|
+
leases: Map<string, {
|
|
8
|
+
assignment: string;
|
|
9
|
+
agent: string;
|
|
10
|
+
}>;
|
|
11
|
+
rootOf(sessionId: string): string;
|
|
12
|
+
lineage(sessionId: string): string[];
|
|
13
|
+
runLog(rootId: string, entry: Record<string, unknown>): void;
|
|
14
|
+
runHooks(sessionId: string, session: Session, event: string, details?: Record<string, unknown>, taskId?: string): Promise<string | undefined>;
|
|
15
|
+
touchTask(rootId: string, id: string): void;
|
|
16
|
+
};
|
|
17
|
+
/** Checkout mutation and verification retain one shared queue per checkout. */
|
|
18
|
+
export declare function createWorktreeDomain({ options, leases, rootOf, lineage, runLog, runHooks, touchTask }: WorktreeContext): {
|
|
19
|
+
settleWorktree: (parentId: string, parent: Session, assignment: Assignment) => Promise<{
|
|
20
|
+
branch: string;
|
|
21
|
+
files: never[];
|
|
22
|
+
note: string;
|
|
23
|
+
error?: undefined;
|
|
24
|
+
} | {
|
|
25
|
+
note: string;
|
|
26
|
+
branch: string;
|
|
27
|
+
base: string;
|
|
28
|
+
commit: string;
|
|
29
|
+
files: string[];
|
|
30
|
+
stat: string;
|
|
31
|
+
error?: undefined;
|
|
32
|
+
} | {
|
|
33
|
+
next: string;
|
|
34
|
+
branch: string;
|
|
35
|
+
base: string;
|
|
36
|
+
commit: string;
|
|
37
|
+
files: string[];
|
|
38
|
+
stat: string;
|
|
39
|
+
note?: undefined;
|
|
40
|
+
error?: undefined;
|
|
41
|
+
} | {
|
|
42
|
+
branch: string;
|
|
43
|
+
error: string;
|
|
44
|
+
files?: undefined;
|
|
45
|
+
note?: undefined;
|
|
46
|
+
}>;
|
|
47
|
+
worktreeTool: (sessionId: string, session: Session, args: unknown, action: "merge" | "discard") => Promise<{
|
|
48
|
+
success: boolean;
|
|
49
|
+
contentItems: {
|
|
50
|
+
type: string;
|
|
51
|
+
text: string;
|
|
52
|
+
}[];
|
|
53
|
+
}>;
|
|
54
|
+
inCheckout: <T>(checkout: string, work: () => Promise<T>) => Promise<T>;
|
|
55
|
+
verifyIn: (project: string, directory: string, config: VerifyConfig) => Promise<Verification>;
|
|
56
|
+
verificationResult: (verification: Verification | {
|
|
57
|
+
passed: boolean;
|
|
58
|
+
commands: Array<Record<string, any>>;
|
|
59
|
+
cwd?: string;
|
|
60
|
+
skipped?: string;
|
|
61
|
+
}) => {
|
|
62
|
+
output?: any;
|
|
63
|
+
steps: {
|
|
64
|
+
idle?: any;
|
|
65
|
+
timedOut?: any;
|
|
66
|
+
step: any;
|
|
67
|
+
command: any;
|
|
68
|
+
exitCode: any;
|
|
69
|
+
ms: any;
|
|
70
|
+
}[];
|
|
71
|
+
skipped?: string | undefined;
|
|
72
|
+
passed: boolean;
|
|
73
|
+
};
|
|
74
|
+
noteVerification: (rootId: string, project: string, taskId: string | undefined, assignmentId: string | undefined, where: "worktree" | "checkout", verification: Verification) => void;
|
|
75
|
+
continuedBrief: (from: string, continued: {
|
|
76
|
+
agent: string;
|
|
77
|
+
change: WorktreeChange;
|
|
78
|
+
verification?: Verification;
|
|
79
|
+
}) => string;
|
|
80
|
+
verifyTool: (sessionId: string, session: Session, args: unknown) => Promise<{
|
|
81
|
+
success: boolean;
|
|
82
|
+
contentItems: {
|
|
83
|
+
type: string;
|
|
84
|
+
text: string;
|
|
85
|
+
}[];
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
88
|
+
export {};
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
// packages/runtime/src/worktree-domain.ts
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { describeVerification, runVerify, verifyConfig } from "@anhlp/alp-core/verify";
|
|
5
|
+
import { recordVerification, loadTasks } from "@anhlp/alp-core/tasks";
|
|
6
|
+
import { checkoutFingerprint, checkoutKey, commitWorktree, linkModules, mergeWorktree, removeWorktree } from "./workspace.js";
|
|
7
|
+
import { writes } from "./catalog.js";
|
|
8
|
+
import { nativeEnvironment } from "./providers.js";
|
|
9
|
+
import { mainOf } from "./tools.js";
|
|
10
|
+
import { errorData, plainObject, toolResult } from "./tool-results.js";
|
|
11
|
+
function createWorktreeDomain({ options, leases, rootOf, lineage, runLog, runHooks, touchTask }) {
|
|
12
|
+
const merging = /* @__PURE__ */ new Map();
|
|
13
|
+
async function settleWorktree(parentId, parent, assignment) {
|
|
14
|
+
const worktree = assignment.worktree;
|
|
15
|
+
try {
|
|
16
|
+
const change = await commitWorktree(worktree, `alp: ${assignment.agent} assignment ${assignment.id}`);
|
|
17
|
+
if (!change.files.length) {
|
|
18
|
+
await removeWorktree(worktree, { deleteBranch: true });
|
|
19
|
+
return { branch: worktree.branch, files: [], note: "No changes; the worktree was removed" };
|
|
20
|
+
}
|
|
21
|
+
const described = { branch: worktree.branch, base: worktree.base, commit: change.commit, files: change.files, stat: change.stat };
|
|
22
|
+
if (parent.closed) {
|
|
23
|
+
await removeWorktree(worktree);
|
|
24
|
+
runLog(rootOf(parentId), { event: "worktree.kept", assignmentId: assignment.id, branch: worktree.branch });
|
|
25
|
+
return { ...described, note: "The requester closed; the change stays on the branch" };
|
|
26
|
+
}
|
|
27
|
+
parent.worktrees.set(assignment.id, { agent: assignment.agent, worktree, change, ...assignment.taskId ? { taskId: assignment.taskId } : {} });
|
|
28
|
+
return { ...described, next: "Apply it with alp_merge, or drop it with alp_discard" };
|
|
29
|
+
} catch (error) {
|
|
30
|
+
await removeWorktree(worktree).catch(() => {
|
|
31
|
+
});
|
|
32
|
+
return { branch: worktree.branch, error: errorData(error).message };
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
async function worktreeTool(sessionId, session, args, action) {
|
|
36
|
+
if (!plainObject(args, ["assignmentId", "skipVerify"]) || typeof args.assignmentId !== "string" || args.skipVerify !== void 0 && (action !== "merge" || typeof args.skipVerify !== "string" || !args.skipVerify.trim())) {
|
|
37
|
+
return toolResult(false, { error: action === "merge" ? "assignmentId is required; skipVerify, when given, says why to merge without verification" : "assignmentId is required" });
|
|
38
|
+
}
|
|
39
|
+
const pending = session.worktrees.get(args.assignmentId);
|
|
40
|
+
if (!pending) {
|
|
41
|
+
return toolResult(false, { error: session.assignments.has(args.assignmentId) ? "The assignment is still running; wait for its result" : "No unmerged worktree change with that assignment id" });
|
|
42
|
+
}
|
|
43
|
+
const { worktree, change } = pending;
|
|
44
|
+
if (action === "discard") {
|
|
45
|
+
session.worktrees.delete(args.assignmentId);
|
|
46
|
+
await removeWorktree(worktree, { deleteBranch: true });
|
|
47
|
+
runLog(rootOf(sessionId), { event: "worktree.discarded", assignmentId: args.assignmentId, branch: worktree.branch });
|
|
48
|
+
return toolResult(true, { discarded: args.assignmentId, branch: worktree.branch });
|
|
49
|
+
}
|
|
50
|
+
if (!writes(session.mapping.mode)) return toolResult(false, { error: "Merging needs workspace-write or full-access" });
|
|
51
|
+
if ([...session.assignments.values()].some((assignment) => assignment.mode !== "read-only" && assignment.isolation === "shared")) {
|
|
52
|
+
return toolResult(false, { error: "A writer assignment is working in this checkout; merge after its result" });
|
|
53
|
+
}
|
|
54
|
+
session.worktrees.delete(args.assignmentId);
|
|
55
|
+
const checkout = await checkoutKey(session.mapping.workdir);
|
|
56
|
+
const holder = leases.get(checkout);
|
|
57
|
+
if (holder && !lineage(sessionId).includes(holder.assignment)) {
|
|
58
|
+
session.worktrees.set(args.assignmentId, pending);
|
|
59
|
+
return toolResult(false, { error: `${holder.agent} in another session is writing this checkout; merge after it finishes` });
|
|
60
|
+
}
|
|
61
|
+
const project = session.mapping.agent.projectRoot;
|
|
62
|
+
const rootId = rootOf(sessionId);
|
|
63
|
+
let config;
|
|
64
|
+
try {
|
|
65
|
+
config = await verifyConfig(project);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
session.worktrees.set(args.assignmentId, pending);
|
|
68
|
+
return toolResult(false, { error: errorData(error).message });
|
|
69
|
+
}
|
|
70
|
+
let verified;
|
|
71
|
+
if (config && args.skipVerify === void 0) {
|
|
72
|
+
const verification = verified = await verifyIn(project, worktreeDirectory(worktree, project), config);
|
|
73
|
+
noteVerification(rootId, project, pending.taskId, args.assignmentId, "worktree", verification);
|
|
74
|
+
if (!verification.passed) {
|
|
75
|
+
session.worktrees.set(args.assignmentId, { ...pending, verification });
|
|
76
|
+
return toolResult(false, {
|
|
77
|
+
assignmentId: args.assignmentId,
|
|
78
|
+
error: `${describeVerification(verification)} in the assignment's worktree; nothing was applied`,
|
|
79
|
+
verification: verificationResult(verification),
|
|
80
|
+
branch: worktree.branch,
|
|
81
|
+
next: verification.skipped ? "The check could not run, which says nothing about the change; call alp_merge again, or alp_merge it with skipVerify saying why" : `Delegate again with continueFrom "${args.assignmentId}"${pending.taskId ? ` and taskId ${pending.taskId}` : ""} to fix it in a worktree that starts from this change; or alp_discard it; or alp_merge it with skipVerify saying why`
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
} else if (config && pending.taskId) {
|
|
85
|
+
await recordVerification(project, pending.taskId, { passed: false, skipped: args.skipVerify }, session.mapping.agent.name).catch(() => {
|
|
86
|
+
});
|
|
87
|
+
runLog(rootId, { event: "verify", assignmentId: args.assignmentId, where: "worktree", skipped: args.skipVerify });
|
|
88
|
+
}
|
|
89
|
+
const refused = await runHooks(sessionId, session, "merge", { assignment: args.assignmentId, branch: worktree.branch }, pending.taskId);
|
|
90
|
+
if (refused) {
|
|
91
|
+
session.worktrees.set(args.assignmentId, pending);
|
|
92
|
+
return toolResult(false, { assignmentId: args.assignmentId, error: `A merge hook refused it; nothing was applied:
|
|
93
|
+
${refused}`, branch: worktree.branch, next: `Fix what it reports in a new assignment with continueFrom "${args.assignmentId}", or alp_discard it` });
|
|
94
|
+
}
|
|
95
|
+
let merged;
|
|
96
|
+
let moved = false;
|
|
97
|
+
try {
|
|
98
|
+
({ merged, moved } = await inCheckout(checkout, async () => {
|
|
99
|
+
const before = await checkoutFingerprint(worktree.checkout).catch(() => void 0);
|
|
100
|
+
return { merged: await mergeWorktree(worktree, change), moved: !worktree.fingerprint || before !== worktree.fingerprint };
|
|
101
|
+
}));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
session.worktrees.set(args.assignmentId, pending);
|
|
104
|
+
return toolResult(false, { error: errorData(error).message, branch: worktree.branch, next: "Merge the branch yourself, or alp_discard it" });
|
|
105
|
+
}
|
|
106
|
+
await removeWorktree(worktree, { deleteBranch: merged.status !== "conflicts" });
|
|
107
|
+
runLog(rootId, { event: "worktree.merged", assignmentId: args.assignmentId, branch: worktree.branch, ...merged });
|
|
108
|
+
let checked;
|
|
109
|
+
if (config && args.skipVerify === void 0 && merged.status === "applied" && moved) {
|
|
110
|
+
checked = await inCheckout(checkout, () => verifyIn(project, session.mapping.workdir, config));
|
|
111
|
+
noteVerification(rootId, project, pending.taskId, args.assignmentId, "checkout", checked);
|
|
112
|
+
}
|
|
113
|
+
return toolResult(true, {
|
|
114
|
+
assignmentId: args.assignmentId,
|
|
115
|
+
...merged,
|
|
116
|
+
...merged.status === "conflicts" ? { branch: worktree.branch } : {},
|
|
117
|
+
...verified ? { verification: verificationResult(checked ?? verified), ...checked ? { verifiedIn: "your checkout, which changed since the assignment started" } : {} } : {},
|
|
118
|
+
...args.skipVerify !== void 0 && config ? { verification: { skipped: args.skipVerify } } : {},
|
|
119
|
+
next: merged.status === "conflicts" ? `Resolve the conflict markers in the listed files, then verify${config ? " with alp_verify" : ""}` : checked && !checked.passed ? `The change passed in its worktree but ${describeVerification(checked)} in your checkout, which changed meanwhile; fix it, then run alp_verify` : "Review the applied change; it is not committed"
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function inCheckout(checkout, work) {
|
|
123
|
+
const previous = merging.get(checkout) ?? Promise.resolve();
|
|
124
|
+
const attempt = previous.catch(() => {
|
|
125
|
+
}).then(work);
|
|
126
|
+
merging.set(checkout, attempt);
|
|
127
|
+
void attempt.finally(() => {
|
|
128
|
+
if (merging.get(checkout) === attempt) merging.delete(checkout);
|
|
129
|
+
}).catch(() => {
|
|
130
|
+
});
|
|
131
|
+
return attempt;
|
|
132
|
+
}
|
|
133
|
+
function worktreeDirectory(worktree, project) {
|
|
134
|
+
let real = project;
|
|
135
|
+
try {
|
|
136
|
+
real = realpathSync(project);
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
return path.join(worktree.path, path.relative(worktree.checkout, real));
|
|
140
|
+
}
|
|
141
|
+
async function verifyIn(project, directory, config) {
|
|
142
|
+
const same = path.resolve(directory) === path.resolve(project);
|
|
143
|
+
const unlinkModules = same ? async () => {
|
|
144
|
+
} : await linkModules(project, directory).catch(() => async () => {
|
|
145
|
+
});
|
|
146
|
+
try {
|
|
147
|
+
return await runVerify(directory, config, { env: nativeEnvironment(options) });
|
|
148
|
+
} finally {
|
|
149
|
+
await unlinkModules();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function verificationResult(verification) {
|
|
153
|
+
const failed = verification.commands.find((command) => command.exitCode !== 0);
|
|
154
|
+
return {
|
|
155
|
+
passed: verification.passed,
|
|
156
|
+
...verification.skipped ? { skipped: verification.skipped } : {},
|
|
157
|
+
steps: verification.commands.map(({ step, command, exitCode, ms, timedOut, idle }) => ({ step, command, exitCode, ms, ...timedOut ? { timedOut } : {}, ...idle ? { idle } : {} })),
|
|
158
|
+
...failed?.output ? { output: failed.output } : {}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function noteVerification(rootId, project, taskId, assignmentId, where, verification) {
|
|
162
|
+
runLog(rootId, { event: "verify", ...assignmentId ? { assignmentId } : {}, ...taskId ? { taskId } : {}, where, passed: verification.passed, detail: describeVerification(verification) });
|
|
163
|
+
if (taskId) void recordVerification(project, taskId, { ...verification, where }, "alpd").then(() => touchTask(rootId, taskId), () => {
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function continuedBrief(from, continued) {
|
|
167
|
+
const failed = continued.verification?.commands.find((command) => command.exitCode !== 0);
|
|
168
|
+
return `Your worktree starts from the change of ${continued.agent}'s assignment ${from}, committed on your branch (${continued.change.files.length} files: ${continued.change.files.slice(0, 20).join(", ")}). Build on it rather than starting over.` + (continued.verification && !continued.verification.passed ? ` The project's checks failed on it: ${describeVerification(continued.verification)}, running \`${failed?.command}\`. The end of its output:
|
|
169
|
+
${failed?.output.slice(-1500) ?? ""}` : "");
|
|
170
|
+
}
|
|
171
|
+
async function verifyTool(sessionId, session, args) {
|
|
172
|
+
if (!plainObject(args, ["taskId"]) || args.taskId !== void 0 && typeof args.taskId !== "string") return toolResult(false, { error: "alp_verify takes only taskId" });
|
|
173
|
+
if (args.taskId !== void 0 && (session.parent || session.mapping.agent.name !== mainOf(session.mapping))) return toolResult(false, { error: "Only main records a verification on a task; call alp_verify without taskId" });
|
|
174
|
+
const project = session.mapping.agent.projectRoot;
|
|
175
|
+
let config;
|
|
176
|
+
try {
|
|
177
|
+
config = await verifyConfig(project);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
return toolResult(false, { error: errorData(error).message });
|
|
180
|
+
}
|
|
181
|
+
if (!config) return toolResult(false, { error: "This project has no verify commands (verify in .alp/settings.json); run its checks yourself" });
|
|
182
|
+
if (args.taskId !== void 0) {
|
|
183
|
+
const task = (await loadTasks(project)).tasks.find((entry) => entry.id === args.taskId);
|
|
184
|
+
if (!task) return toolResult(false, { error: `No task ${args.taskId}` });
|
|
185
|
+
if (task.status === "closed") return toolResult(false, { error: `${args.taskId} is closed` });
|
|
186
|
+
}
|
|
187
|
+
const checkout = await checkoutKey(session.mapping.workdir);
|
|
188
|
+
const verification = await inCheckout(checkout, () => verifyIn(project, session.mapping.workdir, config));
|
|
189
|
+
noteVerification(rootOf(sessionId), project, args.taskId, void 0, "checkout", verification);
|
|
190
|
+
return toolResult(true, { ...verificationResult(verification), summary: describeVerification(verification), ...args.taskId ? { recordedOn: args.taskId } : {} });
|
|
191
|
+
}
|
|
192
|
+
return { settleWorktree, worktreeTool, inCheckout, verifyIn, verificationResult, noteVerification, continuedBrief, verifyTool };
|
|
193
|
+
}
|
|
194
|
+
export {
|
|
195
|
+
createWorktreeDomain
|
|
196
|
+
};
|