@letrquan/book 0.2.0 → 0.4.0-beta.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/CHANGELOG.md +3771 -40
- package/README.md +135 -1311
- package/dist/additional-roots-VAWRQ625.js +32 -0
- package/dist/{app-MOXXWEOY.js → app-J7BVPSUH.js} +9648 -5509
- package/dist/app-J7BVPSUH.js.map +1 -0
- package/dist/chunk-2AAOF5B5.js +28 -0
- package/dist/chunk-2AAOF5B5.js.map +1 -0
- package/dist/chunk-32WBCUAN.js +660 -0
- package/dist/chunk-32WBCUAN.js.map +1 -0
- package/dist/chunk-4DKHLZRI.js +120 -0
- package/dist/chunk-4DKHLZRI.js.map +1 -0
- package/dist/{chunk-CAGOB2N4.js → chunk-62EEAUID.js} +6 -4
- package/dist/chunk-62EEAUID.js.map +1 -0
- package/dist/chunk-AHC7OU75.js +1124 -0
- package/dist/chunk-AHC7OU75.js.map +1 -0
- package/dist/{chunk-E75IPST5.js → chunk-AJZWLKLQ.js} +73 -70
- package/dist/chunk-AJZWLKLQ.js.map +1 -0
- package/dist/chunk-B5FC3MYZ.js +395 -0
- package/dist/chunk-B5FC3MYZ.js.map +1 -0
- package/dist/{chunk-KWHQ2XXL.js → chunk-DNFQTSMJ.js} +55 -13
- package/dist/chunk-DNFQTSMJ.js.map +1 -0
- package/dist/{chunk-J7MIZ5CA.js → chunk-E5WLQSKA.js} +719 -89
- package/dist/chunk-E5WLQSKA.js.map +1 -0
- package/dist/chunk-ECLC4CIJ.js +1181 -0
- package/dist/chunk-ECLC4CIJ.js.map +1 -0
- package/dist/chunk-EEX6QNXV.js +127 -0
- package/dist/chunk-EEX6QNXV.js.map +1 -0
- package/dist/chunk-EFXVSQ5J.js +931 -0
- package/dist/chunk-EFXVSQ5J.js.map +1 -0
- package/dist/{chunk-V4BOYR52.js → chunk-ENLB7NYV.js} +1304 -326
- package/dist/chunk-ENLB7NYV.js.map +1 -0
- package/dist/chunk-FPR7SKIE.js +6926 -0
- package/dist/chunk-FPR7SKIE.js.map +1 -0
- package/dist/{chunk-DGMQEI6Y.js → chunk-HFB74CLM.js} +30 -9
- package/dist/chunk-HFB74CLM.js.map +1 -0
- package/dist/{chunk-5RDRAO4B.js → chunk-HOZXKOE3.js} +37 -1
- package/dist/chunk-HOZXKOE3.js.map +1 -0
- package/dist/chunk-IDYWN37T.js +36 -0
- package/dist/chunk-IDYWN37T.js.map +1 -0
- package/dist/chunk-J4F2QLM2.js +273 -0
- package/dist/chunk-J4F2QLM2.js.map +1 -0
- package/dist/chunk-JGF5HLSI.js +1065 -0
- package/dist/chunk-JGF5HLSI.js.map +1 -0
- package/dist/{chunk-XIFFTATU.js → chunk-KDNWMKXV.js} +7166 -4237
- package/dist/chunk-KDNWMKXV.js.map +1 -0
- package/dist/chunk-KXAYUIKP.js +90 -0
- package/dist/chunk-KXAYUIKP.js.map +1 -0
- package/dist/{chunk-3U5IBM24.js → chunk-MEOB2DZA.js} +12 -2
- package/dist/chunk-MEOB2DZA.js.map +1 -0
- package/dist/{chunk-WKBO5R4O.js → chunk-MV5PQNIA.js} +2 -2
- package/dist/chunk-MV5PQNIA.js.map +1 -0
- package/dist/chunk-N2G4Q7ME.js +2672 -0
- package/dist/chunk-N2G4Q7ME.js.map +1 -0
- package/dist/chunk-OJBMDM5H.js +299 -0
- package/dist/chunk-OJBMDM5H.js.map +1 -0
- package/dist/{chunk-URLQAXTR.js → chunk-PTDC7ZUP.js} +2 -2
- package/dist/chunk-PTDC7ZUP.js.map +1 -0
- package/dist/{chunk-574C6MQR.js → chunk-REASC2QG.js} +173 -64
- package/dist/chunk-REASC2QG.js.map +1 -0
- package/dist/{chunk-7N4J557C.js → chunk-RT5SATF7.js} +1 -1
- package/dist/chunk-RT5SATF7.js.map +1 -0
- package/dist/chunk-SBTU5BEC.js +114 -0
- package/dist/chunk-SBTU5BEC.js.map +1 -0
- package/dist/{chunk-5KLVH3PY.js → chunk-SWPDKLNM.js} +5 -3
- package/dist/chunk-SWPDKLNM.js.map +1 -0
- package/dist/chunk-UCFJ43OS.js +520 -0
- package/dist/chunk-UCFJ43OS.js.map +1 -0
- package/dist/{chunk-VZQXX3WW.js → chunk-W7SLYS3M.js} +80 -16
- package/dist/chunk-W7SLYS3M.js.map +1 -0
- package/dist/{chunk-ANSJPYGI.js → chunk-WV3O5PGX.js} +3 -3
- package/dist/chunk-WV3O5PGX.js.map +1 -0
- package/dist/chunk-XHAO3K7I.js +18 -0
- package/dist/chunk-XHAO3K7I.js.map +1 -0
- package/dist/chunk-XJRJVXJR.js +1050 -0
- package/dist/chunk-XJRJVXJR.js.map +1 -0
- package/dist/chunk-YIL6HTJX.js +663 -0
- package/dist/chunk-YIL6HTJX.js.map +1 -0
- package/dist/{chunk-24AXE6SP.js → chunk-ZPSZZUWF.js} +1 -1
- package/dist/chunk-ZPSZZUWF.js.map +1 -0
- package/dist/{command-approvals-6Y57VCWZ.js → command-approvals-ZRY66LCW.js} +7 -5
- package/dist/context-C3RAP6K5.js +41 -0
- package/dist/{hook-approvals-CTUFKEJT.js → hook-approvals-DPQD5H65.js} +6 -5
- package/dist/index.js +947 -203
- package/dist/index.js.map +1 -1
- package/dist/{interactive-assets-CCAAWVKH.js → interactive-assets-73AQO5WP.js} +18 -9
- package/dist/interactive-assets-73AQO5WP.js.map +1 -0
- package/dist/job-runner.js +166 -111
- package/dist/job-runner.js.map +1 -1
- package/dist/job-supervisor.d.ts +2 -0
- package/dist/job-supervisor.js +121 -0
- package/dist/job-supervisor.js.map +1 -0
- package/dist/loader-DQNPDKOX.js +27 -0
- package/dist/{mcp-approvals-WNPH65BK.js → mcp-approvals-27LJYADQ.js} +6 -5
- package/dist/{mcp-config-YIJIES2C.js → mcp-config-GXSODNCM.js} +4 -3
- package/dist/memory-store-I6Z54VUN.js +51 -0
- package/dist/{model-window-store-YFVNXCIP.js → model-window-store-6VKXXTPJ.js} +9 -4
- package/dist/openai-compatible-IPUNBJ53.js +16 -0
- package/dist/permission-approvals-Q5IMFEZI.js +15 -0
- package/dist/permissions-7Y35X6HZ.js +62 -0
- package/dist/provider-approvals-5AGH3RVT.js +76 -0
- package/dist/sandbox-N2RY72GN.js +42 -0
- package/dist/{scrollback-LUKLIBVI.js → scrollback-GD76GNM4.js} +44 -26
- package/dist/scrollback-GD76GNM4.js.map +1 -0
- package/dist/sdk.d.ts +2739 -2069
- package/dist/sdk.js +77 -42
- package/dist/sdk.js.map +1 -1
- package/dist/settings-loader-KDBH45AQ.js +43 -0
- package/dist/settings-loader-KDBH45AQ.js.map +1 -0
- package/dist/{settings-removed-JWWPT4WA.js → settings-removed-3ATWJEF3.js} +2 -2
- package/dist/settings-removed-3ATWJEF3.js.map +1 -0
- package/dist/shell-selection-Z5WORZU4.js +31 -0
- package/dist/shell-selection-Z5WORZU4.js.map +1 -0
- package/package.json +25 -24
- package/dist/app-MOXXWEOY.js.map +0 -1
- package/dist/chunk-24AXE6SP.js.map +0 -1
- package/dist/chunk-3U5IBM24.js.map +0 -1
- package/dist/chunk-574C6MQR.js.map +0 -1
- package/dist/chunk-5GDZ22YP.js +0 -449
- package/dist/chunk-5GDZ22YP.js.map +0 -1
- package/dist/chunk-5KLVH3PY.js.map +0 -1
- package/dist/chunk-5RDRAO4B.js.map +0 -1
- package/dist/chunk-7N4J557C.js.map +0 -1
- package/dist/chunk-A7JLBK2Y.js +0 -3506
- package/dist/chunk-A7JLBK2Y.js.map +0 -1
- package/dist/chunk-ANSJPYGI.js.map +0 -1
- package/dist/chunk-BTZZUCLI.js +0 -264
- package/dist/chunk-BTZZUCLI.js.map +0 -1
- package/dist/chunk-C7D4NJ2D.js +0 -217
- package/dist/chunk-C7D4NJ2D.js.map +0 -1
- package/dist/chunk-CAGOB2N4.js.map +0 -1
- package/dist/chunk-DGMQEI6Y.js.map +0 -1
- package/dist/chunk-E75IPST5.js.map +0 -1
- package/dist/chunk-EYGD6X2T.js +0 -180
- package/dist/chunk-EYGD6X2T.js.map +0 -1
- package/dist/chunk-GTVB4HEM.js +0 -765
- package/dist/chunk-GTVB4HEM.js.map +0 -1
- package/dist/chunk-J7MIZ5CA.js.map +0 -1
- package/dist/chunk-KWHQ2XXL.js.map +0 -1
- package/dist/chunk-L6LK2QIQ.js +0 -657
- package/dist/chunk-L6LK2QIQ.js.map +0 -1
- package/dist/chunk-MAQXCUR4.js +0 -1007
- package/dist/chunk-MAQXCUR4.js.map +0 -1
- package/dist/chunk-S4XL7HOM.js +0 -402
- package/dist/chunk-S4XL7HOM.js.map +0 -1
- package/dist/chunk-URLQAXTR.js.map +0 -1
- package/dist/chunk-V4BOYR52.js.map +0 -1
- package/dist/chunk-VZQXX3WW.js.map +0 -1
- package/dist/chunk-WKBO5R4O.js.map +0 -1
- package/dist/chunk-XIFFTATU.js.map +0 -1
- package/dist/context-ZC5IFEAX.js +0 -25
- package/dist/interactive-assets-CCAAWVKH.js.map +0 -1
- package/dist/loader-4IDS2RIE.js +0 -15
- package/dist/openai-compatible-RHZUBHV6.js +0 -10
- package/dist/permission-approvals-76PVREFD.js +0 -14
- package/dist/permissions-CHCKTFQQ.js +0 -28
- package/dist/sandbox-CNVAQDA7.js +0 -23
- package/dist/scrollback-LUKLIBVI.js.map +0 -1
- package/dist/settings-loader-M2QP5TX2.js +0 -21
- package/dist/shell-selection-V6QAYXPX.js +0 -17
- package/patches/ink+6.8.0.patch +0 -13
- package/scripts/apply-ink-patch.mjs +0 -29
- package/scripts/ink-patch.mjs +0 -59
- package/scripts/verify-ink-patch.mjs +0 -11
- /package/dist/{command-approvals-6Y57VCWZ.js.map → additional-roots-VAWRQ625.js.map} +0 -0
- /package/dist/{context-ZC5IFEAX.js.map → command-approvals-ZRY66LCW.js.map} +0 -0
- /package/dist/{hook-approvals-CTUFKEJT.js.map → context-C3RAP6K5.js.map} +0 -0
- /package/dist/{loader-4IDS2RIE.js.map → hook-approvals-DPQD5H65.js.map} +0 -0
- /package/dist/{mcp-approvals-WNPH65BK.js.map → loader-DQNPDKOX.js.map} +0 -0
- /package/dist/{mcp-config-YIJIES2C.js.map → mcp-approvals-27LJYADQ.js.map} +0 -0
- /package/dist/{model-window-store-YFVNXCIP.js.map → mcp-config-GXSODNCM.js.map} +0 -0
- /package/dist/{openai-compatible-RHZUBHV6.js.map → memory-store-I6Z54VUN.js.map} +0 -0
- /package/dist/{permission-approvals-76PVREFD.js.map → model-window-store-6VKXXTPJ.js.map} +0 -0
- /package/dist/{permissions-CHCKTFQQ.js.map → openai-compatible-IPUNBJ53.js.map} +0 -0
- /package/dist/{sandbox-CNVAQDA7.js.map → permission-approvals-Q5IMFEZI.js.map} +0 -0
- /package/dist/{settings-loader-M2QP5TX2.js.map → permissions-7Y35X6HZ.js.map} +0 -0
- /package/dist/{settings-removed-JWWPT4WA.js.map → provider-approvals-5AGH3RVT.js.map} +0 -0
- /package/dist/{shell-selection-V6QAYXPX.js.map → sandbox-N2RY72GN.js.map} +0 -0
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HARDENED_DIFF_ARGS,
|
|
3
|
+
hardenedGitArgs,
|
|
4
|
+
hardenedGitEnv,
|
|
5
|
+
noFetchGitEnv,
|
|
6
|
+
parseGitConfig,
|
|
7
|
+
readRepositoryProgramPins
|
|
8
|
+
} from "./chunk-32WBCUAN.js";
|
|
9
|
+
import {
|
|
10
|
+
systemClock
|
|
11
|
+
} from "./chunk-RT5SATF7.js";
|
|
12
|
+
import {
|
|
13
|
+
buildChildEnv
|
|
14
|
+
} from "./chunk-XHAO3K7I.js";
|
|
15
|
+
import {
|
|
16
|
+
ignoresHomeRepository
|
|
17
|
+
} from "./chunk-SBTU5BEC.js";
|
|
18
|
+
import {
|
|
19
|
+
canonicalizePath,
|
|
20
|
+
isOutside
|
|
21
|
+
} from "./chunk-YIL6HTJX.js";
|
|
22
|
+
import {
|
|
23
|
+
resolveBookHome
|
|
24
|
+
} from "./chunk-ZPSZZUWF.js";
|
|
25
|
+
|
|
26
|
+
// src/jobs/persistent-store.ts
|
|
27
|
+
import {
|
|
28
|
+
existsSync as existsSync2,
|
|
29
|
+
mkdirSync as mkdirSync2,
|
|
30
|
+
readFileSync as readFileSync2,
|
|
31
|
+
readdirSync,
|
|
32
|
+
renameSync,
|
|
33
|
+
rmSync as rmSync2,
|
|
34
|
+
writeFileSync
|
|
35
|
+
} from "fs";
|
|
36
|
+
import { createHash as createHash2, randomUUID as randomUUID2 } from "crypto";
|
|
37
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
38
|
+
|
|
39
|
+
// src/agents/git-isolation.ts
|
|
40
|
+
import { createHash, randomUUID } from "crypto";
|
|
41
|
+
import { execFile, spawn } from "child_process";
|
|
42
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from "fs";
|
|
43
|
+
import { tmpdir } from "os";
|
|
44
|
+
import { dirname, join, relative, resolve, sep } from "path";
|
|
45
|
+
|
|
46
|
+
// src/agents/git-signing.ts
|
|
47
|
+
var SIGNING_KEY_PATTERN = "^(commit\\.gpgsign|gpg\\..*|user\\.signingkey)$";
|
|
48
|
+
var MINIMUM_GIT = "2.26";
|
|
49
|
+
var READ_TIMEOUT_MS = 3e4;
|
|
50
|
+
var USER_SCOPES = /* @__PURE__ */ new Set(["system", "global", "command"]);
|
|
51
|
+
var REPOSITORY_SCOPES = /* @__PURE__ */ new Set(["local", "worktree"]);
|
|
52
|
+
var BUILT_IN = {
|
|
53
|
+
"commit.gpgsign": "false",
|
|
54
|
+
"gpg.format": "openpgp",
|
|
55
|
+
gpg: "gpg",
|
|
56
|
+
"gpg.x509.program": "gpgsm",
|
|
57
|
+
"gpg.ssh.program": "ssh-keygen"
|
|
58
|
+
};
|
|
59
|
+
var PIN_UNITS = [
|
|
60
|
+
{ keys: ["commit.gpgsign"], fallback: BUILT_IN["commit.gpgsign"] },
|
|
61
|
+
{ keys: ["gpg.format"], fallback: BUILT_IN["gpg.format"] },
|
|
62
|
+
{ keys: ["gpg.program", "gpg.openpgp.program"], fallback: BUILT_IN.gpg },
|
|
63
|
+
{ keys: ["gpg.x509.program"], fallback: BUILT_IN["gpg.x509.program"] },
|
|
64
|
+
{ keys: ["gpg.ssh.program"], fallback: BUILT_IN["gpg.ssh.program"] },
|
|
65
|
+
{ keys: ["gpg.ssh.defaultkeycommand"], fallback: "" },
|
|
66
|
+
{ keys: ["gpg.ssh.allowedsignersfile"], fallback: "" },
|
|
67
|
+
{ keys: ["gpg.ssh.revocationfile"], fallback: "" }
|
|
68
|
+
];
|
|
69
|
+
async function readSigningConfig(repoRoot, run2, signal) {
|
|
70
|
+
const result = await run2(
|
|
71
|
+
"git",
|
|
72
|
+
["config", "--show-scope", "-z", "--get-regexp", SIGNING_KEY_PATTERN],
|
|
73
|
+
{ cwd: repoRoot, allowExitCodes: [1, 129], timeoutMs: READ_TIMEOUT_MS, signal }
|
|
74
|
+
);
|
|
75
|
+
if (result.code === 129 || /unknown option/i.test(result.stderr)) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Applying an agent's result needs git ${MINIMUM_GIT} or later (for \`git config --show-scope\`); this git is ${await gitVersion(run2, repoRoot, signal)}`
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return parseSigningConfig(result.stdout);
|
|
81
|
+
}
|
|
82
|
+
async function gitVersion(run2, repoRoot, signal) {
|
|
83
|
+
try {
|
|
84
|
+
return (await run2("git", ["--version"], { cwd: repoRoot, timeoutMs: READ_TIMEOUT_MS, signal })).stdout.trim();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (signal?.aborted) throw error;
|
|
87
|
+
return "of an unknown version";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function parseSigningConfig(stdout) {
|
|
91
|
+
return parseGitConfig(stdout);
|
|
92
|
+
}
|
|
93
|
+
function signingFacts(entries) {
|
|
94
|
+
const operatorEntries = [];
|
|
95
|
+
const repositoryKeys = /* @__PURE__ */ new Set();
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (USER_SCOPES.has(entry.scope)) operatorEntries.push(entry);
|
|
98
|
+
else if (REPOSITORY_SCOPES.has(entry.scope)) repositoryKeys.add(entry.key);
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
operatorEntries,
|
|
102
|
+
repositoryKeys,
|
|
103
|
+
signingOn: configBool(operatorValue(operatorEntries, ["commit.gpgsign"])),
|
|
104
|
+
format: (operatorValue(operatorEntries, ["gpg.format"]) ?? BUILT_IN["gpg.format"]).trim().toLowerCase()
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function operatorValue(operatorEntries, keys) {
|
|
108
|
+
let value;
|
|
109
|
+
for (const entry of operatorEntries) {
|
|
110
|
+
if (keys.includes(entry.key)) value = entry.value;
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
function lastOperatorValue(facts, keys) {
|
|
115
|
+
return operatorValue(facts.operatorEntries, keys);
|
|
116
|
+
}
|
|
117
|
+
function configBool(value) {
|
|
118
|
+
return ["true", "yes", "on", "1"].includes((value ?? "").trim().toLowerCase());
|
|
119
|
+
}
|
|
120
|
+
function signingKeySource(facts) {
|
|
121
|
+
if (!facts.repositoryKeys.has("user.signingkey") || !facts.signingOn) return "none";
|
|
122
|
+
if (lastOperatorValue(facts, ["user.signingkey"]) !== void 0) return "operator";
|
|
123
|
+
if (facts.format === "ssh") {
|
|
124
|
+
return lastOperatorValue(facts, ["gpg.ssh.defaultkeycommand"]) === void 0 ? "none" : "ssh-default-key-command";
|
|
125
|
+
}
|
|
126
|
+
return "committer";
|
|
127
|
+
}
|
|
128
|
+
function pinsFor(facts, inputs) {
|
|
129
|
+
const fallbacks = [];
|
|
130
|
+
const operatorValues = [];
|
|
131
|
+
const pin = (into, key, value) => {
|
|
132
|
+
into.push("-c", `${key}=${value}`);
|
|
133
|
+
};
|
|
134
|
+
const pinned = /* @__PURE__ */ new Set();
|
|
135
|
+
for (const unit of PIN_UNITS) {
|
|
136
|
+
for (const key of unit.keys) pinned.add(key);
|
|
137
|
+
const operatorValue2 = lastOperatorValue(facts, unit.keys);
|
|
138
|
+
for (const key of unit.keys) {
|
|
139
|
+
pin(fallbacks, key, unit.fallback);
|
|
140
|
+
if (operatorValue2 !== void 0) pin(operatorValues, key, operatorValue2);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (facts.repositoryKeys.has("user.signingkey")) {
|
|
144
|
+
pin(fallbacks, "user.signingkey", derivedSigningKey(facts, inputs));
|
|
145
|
+
const operatorValue2 = lastOperatorValue(facts, ["user.signingkey"]);
|
|
146
|
+
if (operatorValue2 !== void 0) pin(operatorValues, "user.signingkey", operatorValue2);
|
|
147
|
+
}
|
|
148
|
+
for (const key of facts.repositoryKeys) {
|
|
149
|
+
if (pinned.has(key) || !key.startsWith("gpg.")) continue;
|
|
150
|
+
pin(fallbacks, key, "");
|
|
151
|
+
const operatorValue2 = lastOperatorValue(facts, [key]);
|
|
152
|
+
if (operatorValue2 !== void 0) pin(operatorValues, key, operatorValue2);
|
|
153
|
+
}
|
|
154
|
+
return [...fallbacks, ...operatorValues];
|
|
155
|
+
}
|
|
156
|
+
function derivedSigningKey(facts, inputs) {
|
|
157
|
+
switch (signingKeySource(facts)) {
|
|
158
|
+
case "operator":
|
|
159
|
+
return "";
|
|
160
|
+
case "committer":
|
|
161
|
+
return committerSigningIdentity(inputs.committerIdent);
|
|
162
|
+
case "ssh-default-key-command":
|
|
163
|
+
return inputs.sshDefaultKey;
|
|
164
|
+
default:
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function committerSigningIdentity(ident) {
|
|
169
|
+
return ident.trim().replace(/\s+\d+\s+[+-]\d{4}$/, "");
|
|
170
|
+
}
|
|
171
|
+
async function signingPinArgs(repoRoot, run2, signal) {
|
|
172
|
+
const entries = await readSigningConfig(repoRoot, run2, signal);
|
|
173
|
+
const facts = signingFacts(entries);
|
|
174
|
+
const inputs = { committerIdent: "", sshDefaultKey: "" };
|
|
175
|
+
const source = signingKeySource(facts);
|
|
176
|
+
if (source === "committer") {
|
|
177
|
+
inputs.committerIdent = (await run2("git", ["var", "GIT_COMMITTER_IDENT"], {
|
|
178
|
+
cwd: repoRoot,
|
|
179
|
+
timeoutMs: READ_TIMEOUT_MS,
|
|
180
|
+
signal
|
|
181
|
+
})).stdout;
|
|
182
|
+
} else if (source === "ssh-default-key-command") {
|
|
183
|
+
inputs.sshDefaultKey = await sshDefaultSigningKey(facts, repoRoot, run2, signal);
|
|
184
|
+
}
|
|
185
|
+
return pinsFor(facts, inputs);
|
|
186
|
+
}
|
|
187
|
+
async function sshDefaultSigningKey(facts, repoRoot, run2, signal) {
|
|
188
|
+
const command = lastOperatorValue(facts, ["gpg.ssh.defaultkeycommand"])?.trim();
|
|
189
|
+
if (!command) return "";
|
|
190
|
+
const [program, ...args] = command.split(/\s+/);
|
|
191
|
+
try {
|
|
192
|
+
const result = await run2(program, args, {
|
|
193
|
+
cwd: repoRoot,
|
|
194
|
+
timeoutMs: READ_TIMEOUT_MS,
|
|
195
|
+
signal
|
|
196
|
+
});
|
|
197
|
+
const first = result.stdout.split(/\r?\n/)[0]?.trim() ?? "";
|
|
198
|
+
return first.startsWith("key::") ? first : "";
|
|
199
|
+
} catch (error) {
|
|
200
|
+
if (signal?.aborted) throw error;
|
|
201
|
+
return "";
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/agents/git-isolation.ts
|
|
206
|
+
function run(command, args, options, maxBuffer = DEFAULT_MAX_BUFFER_BYTES) {
|
|
207
|
+
const env = internalGitEnv(options.env);
|
|
208
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
|
|
209
|
+
const signal = options.signal;
|
|
210
|
+
if (signal?.aborted) {
|
|
211
|
+
return Promise.reject(new Error(stoppedDescription(command, args, "aborted", null, timeoutMs)));
|
|
212
|
+
}
|
|
213
|
+
if (options.input !== void 0) {
|
|
214
|
+
return new Promise((resolvePromise, reject) => {
|
|
215
|
+
const child = spawn(command, args, {
|
|
216
|
+
cwd: options.cwd,
|
|
217
|
+
env,
|
|
218
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
219
|
+
});
|
|
220
|
+
let stdout = "";
|
|
221
|
+
let stderr = "";
|
|
222
|
+
let settled = false;
|
|
223
|
+
let cancelEscalation;
|
|
224
|
+
const settle = (outcome) => {
|
|
225
|
+
if (settled) return;
|
|
226
|
+
settled = true;
|
|
227
|
+
if (timer) clearTimeout(timer);
|
|
228
|
+
cancelEscalation?.();
|
|
229
|
+
outcome();
|
|
230
|
+
};
|
|
231
|
+
let stoppedBy;
|
|
232
|
+
const stopChild = (reason) => {
|
|
233
|
+
if (stoppedBy === void 0) stoppedBy = reason;
|
|
234
|
+
cancelEscalation = stopEscalating(
|
|
235
|
+
child,
|
|
236
|
+
() => settle(
|
|
237
|
+
() => reject(
|
|
238
|
+
new Error(stoppedDescription(command, args, stoppedBy, child.signalCode, timeoutMs))
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
);
|
|
242
|
+
};
|
|
243
|
+
const onAbort = () => stopChild("aborted");
|
|
244
|
+
const timer = timeoutMs > 0 ? setTimeout(() => stopChild("timeout"), timeoutMs) : void 0;
|
|
245
|
+
timer?.unref?.();
|
|
246
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
247
|
+
let stdinError;
|
|
248
|
+
child.stdout.on("data", (chunk) => stdout += chunk.toString());
|
|
249
|
+
child.stderr.on("data", (chunk) => stderr += chunk.toString());
|
|
250
|
+
let outputError;
|
|
251
|
+
child.stdout.on("error", (error) => outputError ??= error);
|
|
252
|
+
child.stderr.on("error", (error) => outputError ??= error);
|
|
253
|
+
child.on("error", (error) => {
|
|
254
|
+
if (timer) clearTimeout(timer);
|
|
255
|
+
signal?.removeEventListener("abort", onAbort);
|
|
256
|
+
settle(() => reject(error));
|
|
257
|
+
});
|
|
258
|
+
child.on("close", (codeValue, closedBySignal) => {
|
|
259
|
+
if (timer) clearTimeout(timer);
|
|
260
|
+
signal?.removeEventListener("abort", onAbort);
|
|
261
|
+
settle(() => {
|
|
262
|
+
const code = typeof codeValue === "number" ? codeValue : void 0;
|
|
263
|
+
if (code === void 0) {
|
|
264
|
+
reject(
|
|
265
|
+
new Error(
|
|
266
|
+
stderr.trim() || stdout.trim() || outputError?.message || stdinError?.message || stoppedDescription(command, args, stoppedBy, closedBySignal, timeoutMs)
|
|
267
|
+
)
|
|
268
|
+
);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
if (code === 0 || options.allowExitCodes?.includes(code)) {
|
|
272
|
+
resolvePromise({ stdout, stderr, code });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
reject(
|
|
276
|
+
new Error(
|
|
277
|
+
failureDescription(
|
|
278
|
+
null,
|
|
279
|
+
stderr.trim() || stdout.trim() || stdinError?.message || outputError?.message || "",
|
|
280
|
+
command,
|
|
281
|
+
args,
|
|
282
|
+
timeoutMs,
|
|
283
|
+
stoppedBy === "aborted",
|
|
284
|
+
code
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
child.stdin.on("error", (error) => {
|
|
291
|
+
if (error.code === "EPIPE" || error.code === "EOF") return;
|
|
292
|
+
stdinError ??= error;
|
|
293
|
+
});
|
|
294
|
+
child.stdin.end(options.input);
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return new Promise((resolvePromise, reject) => {
|
|
298
|
+
let settled = false;
|
|
299
|
+
let cancelEscalation;
|
|
300
|
+
const settle = (outcome) => {
|
|
301
|
+
if (settled) return;
|
|
302
|
+
settled = true;
|
|
303
|
+
if (timer) clearTimeout(timer);
|
|
304
|
+
signal?.removeEventListener("abort", onAbort);
|
|
305
|
+
cancelEscalation?.();
|
|
306
|
+
outcome();
|
|
307
|
+
};
|
|
308
|
+
let stoppedBy;
|
|
309
|
+
const stopCall = (reason) => {
|
|
310
|
+
if (stoppedBy === void 0) stoppedBy = reason;
|
|
311
|
+
cancelEscalation = stopEscalating(
|
|
312
|
+
child,
|
|
313
|
+
() => settle(
|
|
314
|
+
() => reject(
|
|
315
|
+
new Error(
|
|
316
|
+
failureDescription(
|
|
317
|
+
{ killed: true },
|
|
318
|
+
"",
|
|
319
|
+
command,
|
|
320
|
+
args,
|
|
321
|
+
timeoutMs,
|
|
322
|
+
reason === "aborted"
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
);
|
|
328
|
+
};
|
|
329
|
+
const onAbort = () => stopCall("aborted");
|
|
330
|
+
const child = execFile(
|
|
331
|
+
command,
|
|
332
|
+
args,
|
|
333
|
+
{
|
|
334
|
+
cwd: options.cwd,
|
|
335
|
+
env,
|
|
336
|
+
encoding: "utf8",
|
|
337
|
+
// Each of stdout and stderr is capped here, and a call that overruns the cap fails rather
|
|
338
|
+
// than resolving on what had arrived. Only this branch is capped: the `input` branch above
|
|
339
|
+
// collects its output without a limit. See {@link DEFAULT_MAX_BUFFER_BYTES}.
|
|
340
|
+
maxBuffer
|
|
341
|
+
// No `timeout` and no `signal` option here. `execFile`'s own timeout kills the child but
|
|
342
|
+
// still waits for its output to close, and a grandchild holding that pipe — a shell script,
|
|
343
|
+
// a `credential.helper` that started something of its own — means it never does, so the
|
|
344
|
+
// callback that would report the timeout may never arrive; its `signal` sends SIGTERM and
|
|
345
|
+
// then stops looking. The timer below bounds the call and `stopEscalating` ends the child.
|
|
346
|
+
},
|
|
347
|
+
(error, stdout, stderr) => {
|
|
348
|
+
const code = exitCodeOf(error);
|
|
349
|
+
if (code === void 0) {
|
|
350
|
+
settle(
|
|
351
|
+
() => reject(
|
|
352
|
+
new Error(
|
|
353
|
+
failureDescription(
|
|
354
|
+
error,
|
|
355
|
+
stderr.trim() || stdout.trim(),
|
|
356
|
+
command,
|
|
357
|
+
args,
|
|
358
|
+
timeoutMs,
|
|
359
|
+
stoppedBy === "aborted"
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
if (code === 0 || options.allowExitCodes?.includes(code)) {
|
|
367
|
+
settle(() => resolvePromise({ stdout, stderr, code }));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
settle(
|
|
371
|
+
() => reject(
|
|
372
|
+
new Error(
|
|
373
|
+
failureDescription(
|
|
374
|
+
error,
|
|
375
|
+
stderr.trim() || stdout.trim(),
|
|
376
|
+
command,
|
|
377
|
+
args,
|
|
378
|
+
timeoutMs,
|
|
379
|
+
stoppedBy === "aborted",
|
|
380
|
+
code
|
|
381
|
+
)
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
);
|
|
387
|
+
const timer = timeoutMs > 0 ? setTimeout(() => stopCall("timeout"), timeoutMs) : void 0;
|
|
388
|
+
timer?.unref?.();
|
|
389
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
390
|
+
child.stdin?.end();
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
var DEFAULT_GIT_TIMEOUT_MS = 12e4;
|
|
394
|
+
var DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;
|
|
395
|
+
var KILL_GRACE_MS = 2e3;
|
|
396
|
+
function stopEscalating(child, settled) {
|
|
397
|
+
child.kill("SIGTERM");
|
|
398
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
399
|
+
return () => {
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const escalation = setTimeout(() => {
|
|
403
|
+
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
|
404
|
+
settled();
|
|
405
|
+
}, KILL_GRACE_MS);
|
|
406
|
+
escalation.unref?.();
|
|
407
|
+
return () => clearTimeout(escalation);
|
|
408
|
+
}
|
|
409
|
+
function subcommandOf(args) {
|
|
410
|
+
for (let index = 0; index < args.length; index++) {
|
|
411
|
+
const arg = args[index];
|
|
412
|
+
if (arg === "-c" || arg === "-C") {
|
|
413
|
+
index++;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (arg.startsWith("-")) continue;
|
|
417
|
+
return arg;
|
|
418
|
+
}
|
|
419
|
+
return "(no subcommand)";
|
|
420
|
+
}
|
|
421
|
+
function stoppedDescription(command, args, stoppedBy, closedBySignal, timeoutMs) {
|
|
422
|
+
const invocation = `${command} ${subcommandOf(args)}`;
|
|
423
|
+
if (stoppedBy === "timeout") {
|
|
424
|
+
return `${invocation} timed out after ${timeoutMs}ms and was killed`;
|
|
425
|
+
}
|
|
426
|
+
if (stoppedBy === "aborted") return `${invocation} was cancelled`;
|
|
427
|
+
if (closedBySignal) return `${invocation} was killed with ${closedBySignal}`;
|
|
428
|
+
return `${invocation} was killed and produced no result`;
|
|
429
|
+
}
|
|
430
|
+
function exitCodeOf(error) {
|
|
431
|
+
if (!error) return 0;
|
|
432
|
+
if (error.killed || error.signal) return void 0;
|
|
433
|
+
if (isAbortError(error)) return void 0;
|
|
434
|
+
return typeof error.code === "number" ? error.code : void 0;
|
|
435
|
+
}
|
|
436
|
+
function isAbortError(error) {
|
|
437
|
+
return error?.name === "AbortError" || error?.code === "ABORT_ERR";
|
|
438
|
+
}
|
|
439
|
+
function failureDescription(error, output, command, args, timeoutMs, aborted, code) {
|
|
440
|
+
const invocation = `${command} ${subcommandOf(args)}`;
|
|
441
|
+
if (aborted || isAbortError(error)) return `${invocation} was cancelled`;
|
|
442
|
+
if (error?.killed) return `${invocation} timed out after ${timeoutMs}ms and was killed`;
|
|
443
|
+
if (error?.signal) return `${invocation} was killed with ${error.signal}`;
|
|
444
|
+
if (typeof error?.code === "string") return `${invocation} failed: ${error.code}`;
|
|
445
|
+
const trimmed = output.trim();
|
|
446
|
+
if (/promisor remote|lazy fetching disabled/i.test(trimmed)) {
|
|
447
|
+
return "this repository is a partial clone; Book's agent isolation does not fetch missing objects, because the fetch would run the transport this checkout configures (`core.sshCommand`, `credential.helper`) with no prompt; fetch the missing objects yourself (for example `git fetch --refetch <remote>` after unsetting `remote.<remote>.partialclonefilter`), then retry.\n" + trimmed;
|
|
448
|
+
}
|
|
449
|
+
if (trimmed) return trimmed;
|
|
450
|
+
if (code !== void 0) return `${invocation} failed (${code})`;
|
|
451
|
+
if (error?.message) return error.message;
|
|
452
|
+
return `${invocation} failed`;
|
|
453
|
+
}
|
|
454
|
+
var GIT_REPOSITORY_SELECTION_VARIABLES = /* @__PURE__ */ new Set([
|
|
455
|
+
"GIT_DIR",
|
|
456
|
+
"GIT_WORK_TREE",
|
|
457
|
+
"GIT_INDEX_FILE",
|
|
458
|
+
// The index *format* rather than the index's location, and kept in this list for the same
|
|
459
|
+
// reason as the index itself: a snapshot stages into a temporary index of its own, and an
|
|
460
|
+
// ambient version would have git write that index in a format the operator's own git may refuse
|
|
461
|
+
// to read back.
|
|
462
|
+
"GIT_INDEX_VERSION",
|
|
463
|
+
"GIT_OBJECT_DIRECTORY",
|
|
464
|
+
"GIT_ALTERNATE_OBJECT_DIRECTORIES",
|
|
465
|
+
"GIT_COMMON_DIR",
|
|
466
|
+
"GIT_NAMESPACE",
|
|
467
|
+
"GIT_PREFIX"
|
|
468
|
+
]);
|
|
469
|
+
function internalGitEnv(overrides) {
|
|
470
|
+
const ambient = buildChildEnv(process.env);
|
|
471
|
+
for (const name of GIT_REPOSITORY_SELECTION_VARIABLES) delete ambient[name];
|
|
472
|
+
return {
|
|
473
|
+
...ambient,
|
|
474
|
+
...overrides,
|
|
475
|
+
...hardenedGitEnv(),
|
|
476
|
+
...noFetchGitEnv()
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
async function git(cwd, args, options = {}) {
|
|
480
|
+
const pins = await readRepositoryProgramPins(
|
|
481
|
+
cwd,
|
|
482
|
+
(pinArgs, pinOpts) => run("git", pinArgs, {
|
|
483
|
+
signal: options.signal,
|
|
484
|
+
timeoutMs: options.timeoutMs,
|
|
485
|
+
env: options.env,
|
|
486
|
+
...pinOpts
|
|
487
|
+
}),
|
|
488
|
+
{
|
|
489
|
+
signal: options.signal,
|
|
490
|
+
timeoutMs: options.timeoutMs,
|
|
491
|
+
env: options.env,
|
|
492
|
+
allowFsmonitor: false
|
|
493
|
+
}
|
|
494
|
+
);
|
|
495
|
+
return run("git", hardenedGitArgs([...pins, ...args]), { ...options, cwd });
|
|
496
|
+
}
|
|
497
|
+
function cherryPickFailureResult(facts) {
|
|
498
|
+
const conflicted = facts.unmergedPaths.length > 0;
|
|
499
|
+
if (facts.abortError) {
|
|
500
|
+
return {
|
|
501
|
+
// Not `conflicted ? 'conflicted' : 'not_applied'`, which is what the conflict claim alone
|
|
502
|
+
// decides: a pick whose rollback did not run is not retryable whether or not it conflicted,
|
|
503
|
+
// and reporting the narrower of the two claims is what made an operator commit into a
|
|
504
|
+
// repository that was still mid-pick.
|
|
505
|
+
status: "conflicted",
|
|
506
|
+
error: `Cherry-pick failed (${facts.pickError}) and the rollback did not complete (${facts.abortError}), so this repository may be left mid-cherry-pick: finish it with git cherry-pick --continue or undo it with git cherry-pick --abort.`
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
status: conflicted ? "conflicted" : "not_applied",
|
|
511
|
+
error: `Cherry-pick failed and was rolled back: ${facts.pickError}`
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
function gitIdentityEnv() {
|
|
515
|
+
return {
|
|
516
|
+
GIT_AUTHOR_NAME: "Book Agent",
|
|
517
|
+
GIT_AUTHOR_EMAIL: "agents@book.local",
|
|
518
|
+
GIT_COMMITTER_NAME: "Book Agent",
|
|
519
|
+
GIT_COMMITTER_EMAIL: "agents@book.local"
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
function tempIndex() {
|
|
523
|
+
const dir = mkdtempSync(join(tmpdir(), "book-index-"));
|
|
524
|
+
return { dir, path: join(dir, "index") };
|
|
525
|
+
}
|
|
526
|
+
async function findGitRoot(workspace, signal) {
|
|
527
|
+
if (!existsSync(workspace)) {
|
|
528
|
+
return void 0;
|
|
529
|
+
}
|
|
530
|
+
try {
|
|
531
|
+
const top = (await git(workspace, ["rev-parse", "--show-toplevel"], { signal })).stdout.trim();
|
|
532
|
+
return ignoresHomeRepository(top, workspace) ? void 0 : top;
|
|
533
|
+
} catch (error) {
|
|
534
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
535
|
+
const code = error?.code;
|
|
536
|
+
if (/not a git repository/i.test(message) || code === "ENOENT" || /no such file or directory/i.test(message)) {
|
|
537
|
+
return void 0;
|
|
538
|
+
}
|
|
539
|
+
throw error;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function repositoryHash(repoRoot) {
|
|
543
|
+
return createHash("sha256").update(resolve(repoRoot).toLowerCase()).digest("hex").slice(0, 20);
|
|
544
|
+
}
|
|
545
|
+
function bookHomeInRepository(repoRoot) {
|
|
546
|
+
const rootReal = canonicalizePath(repoRoot);
|
|
547
|
+
const bookHomeReal = canonicalizePath(resolveBookHome());
|
|
548
|
+
if (isOutside(rootReal, bookHomeReal)) return void 0;
|
|
549
|
+
const inside = relative(rootReal, bookHomeReal);
|
|
550
|
+
return inside === "" ? void 0 : inside.split(sep).join("/");
|
|
551
|
+
}
|
|
552
|
+
async function writeWorkspaceTree(repoRoot, includeUntracked, signal) {
|
|
553
|
+
const index = tempIndex();
|
|
554
|
+
try {
|
|
555
|
+
const env = { GIT_INDEX_FILE: index.path };
|
|
556
|
+
const bookHome = bookHomeInRepository(repoRoot);
|
|
557
|
+
const magic = bookHome === void 0 ? [] : ["--no-literal-pathspecs"];
|
|
558
|
+
const exclusion = bookHome === void 0 ? [] : [`:(top,literal,exclude)${bookHome}`];
|
|
559
|
+
const head = (await git(repoRoot, ["rev-parse", "HEAD"], { signal })).stdout.trim();
|
|
560
|
+
await git(repoRoot, ["read-tree", head], { env, signal });
|
|
561
|
+
await git(
|
|
562
|
+
repoRoot,
|
|
563
|
+
[...magic, "add", includeUntracked ? "-A" : "-u", "--", ".", ...exclusion],
|
|
564
|
+
{
|
|
565
|
+
env,
|
|
566
|
+
signal
|
|
567
|
+
}
|
|
568
|
+
);
|
|
569
|
+
const tree = (await git(repoRoot, ["write-tree"], { env, signal })).stdout.trim();
|
|
570
|
+
const status = [
|
|
571
|
+
(await git(
|
|
572
|
+
repoRoot,
|
|
573
|
+
[
|
|
574
|
+
...magic,
|
|
575
|
+
"status",
|
|
576
|
+
"--porcelain=v1",
|
|
577
|
+
"--untracked-files=all",
|
|
578
|
+
"--ignore-submodules=dirty",
|
|
579
|
+
...bookHome === void 0 ? [] : ["--", ".", ...exclusion]
|
|
580
|
+
],
|
|
581
|
+
{ signal }
|
|
582
|
+
)).stdout
|
|
583
|
+
];
|
|
584
|
+
if (bookHome !== void 0) {
|
|
585
|
+
status.push(
|
|
586
|
+
(await git(
|
|
587
|
+
repoRoot,
|
|
588
|
+
[
|
|
589
|
+
...magic,
|
|
590
|
+
"status",
|
|
591
|
+
"--porcelain=v1",
|
|
592
|
+
"--untracked-files=no",
|
|
593
|
+
"--ignore-submodules=dirty",
|
|
594
|
+
"--",
|
|
595
|
+
`:(top,literal)${bookHome}`
|
|
596
|
+
],
|
|
597
|
+
{ signal }
|
|
598
|
+
)).stdout
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
return { head, tree, status: status.join("") };
|
|
602
|
+
} finally {
|
|
603
|
+
rmSync(index.dir, { recursive: true, force: true });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
async function currentWorkspaceFingerprint(repoRoot, includeUntracked, signal) {
|
|
607
|
+
const { head, tree } = await writeWorkspaceTree(repoRoot, includeUntracked, signal);
|
|
608
|
+
return `${head}:${tree}`;
|
|
609
|
+
}
|
|
610
|
+
async function createSyntheticSnapshot(workspace, includeUntracked = true, signal) {
|
|
611
|
+
const repoRoot = await findGitRoot(workspace, signal);
|
|
612
|
+
if (!repoRoot) {
|
|
613
|
+
throw new Error(
|
|
614
|
+
"Managed agents require a Git workspace with at least one commit. Use --agents off for this workspace."
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
const id = randomUUID();
|
|
618
|
+
const { head, tree, status } = await writeWorkspaceTree(repoRoot, includeUntracked, signal);
|
|
619
|
+
const commit = (await git(repoRoot, ["commit-tree", tree, "-p", head, "-m", `book synthetic snapshot ${id}`], {
|
|
620
|
+
env: gitIdentityEnv(),
|
|
621
|
+
signal
|
|
622
|
+
})).stdout.trim();
|
|
623
|
+
const repoHash = repositoryHash(repoRoot);
|
|
624
|
+
const ref = `refs/book/snapshots/${repoHash}/${id}`;
|
|
625
|
+
await git(repoRoot, ["update-ref", ref, commit], { signal });
|
|
626
|
+
const manifestOutput = (await git(
|
|
627
|
+
repoRoot,
|
|
628
|
+
[
|
|
629
|
+
"-c",
|
|
630
|
+
"core.quotepath=false",
|
|
631
|
+
"diff-tree",
|
|
632
|
+
"--no-commit-id",
|
|
633
|
+
"--name-status",
|
|
634
|
+
"-r",
|
|
635
|
+
"-M",
|
|
636
|
+
head,
|
|
637
|
+
commit
|
|
638
|
+
],
|
|
639
|
+
{ signal }
|
|
640
|
+
)).stdout;
|
|
641
|
+
const manifest = manifestOutput.split(/\r?\n/).filter(Boolean).map((line) => {
|
|
642
|
+
const [statusCode, ...paths] = line.split(" ");
|
|
643
|
+
return { status: statusCode, path: paths.join(" -> ") };
|
|
644
|
+
});
|
|
645
|
+
return {
|
|
646
|
+
id,
|
|
647
|
+
repoRoot,
|
|
648
|
+
repoHash,
|
|
649
|
+
baseHead: head,
|
|
650
|
+
commit,
|
|
651
|
+
tree,
|
|
652
|
+
ref,
|
|
653
|
+
fingerprint: `${head}:${tree}`,
|
|
654
|
+
dirty: status.trim().length > 0,
|
|
655
|
+
includeUntracked,
|
|
656
|
+
manifest,
|
|
657
|
+
createdAt: Date.now()
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function defaultWorktreeRoot() {
|
|
661
|
+
return join(resolveBookHome(), "worktrees");
|
|
662
|
+
}
|
|
663
|
+
async function branchExists(repoRoot, branch, signal) {
|
|
664
|
+
const result = await git(repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], {
|
|
665
|
+
allowExitCodes: [1],
|
|
666
|
+
signal
|
|
667
|
+
});
|
|
668
|
+
return result.code === 0;
|
|
669
|
+
}
|
|
670
|
+
async function deleteBranchBookCreated(repoRoot, branch, existed) {
|
|
671
|
+
if (existed) return;
|
|
672
|
+
await git(repoRoot, ["branch", "-D", branch], { allowExitCodes: [1, 128] }).catch(() => {
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
async function worktreeIsComplete(path, signal) {
|
|
676
|
+
if (!existsSync(join(path, ".git"))) return false;
|
|
677
|
+
try {
|
|
678
|
+
const administrative = (await git(path, ["rev-parse", "--absolute-git-dir"], { signal })).stdout.trim();
|
|
679
|
+
if (!existsSync(join(administrative, "index"))) return false;
|
|
680
|
+
await git(path, ["rev-parse", "--verify", "HEAD"], { signal });
|
|
681
|
+
return true;
|
|
682
|
+
} catch {
|
|
683
|
+
return false;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
async function removeWorktree(repoRoot, path) {
|
|
687
|
+
await git(repoRoot, ["worktree", "remove", "--force", path], { allowExitCodes: [128] }).catch(
|
|
688
|
+
() => {
|
|
689
|
+
}
|
|
690
|
+
);
|
|
691
|
+
await git(repoRoot, ["worktree", "prune"], { allowExitCodes: [128] }).catch(() => {
|
|
692
|
+
});
|
|
693
|
+
if (pointsAtWorktree(path)) rmSync(path, { recursive: true, force: true });
|
|
694
|
+
}
|
|
695
|
+
function pointsAtWorktree(path) {
|
|
696
|
+
return existsSync(join(path, ".git"));
|
|
697
|
+
}
|
|
698
|
+
async function createAgentWorktree(snapshot, agentId, worktreeRoot = defaultWorktreeRoot(), startCommit = snapshot.commit, signal) {
|
|
699
|
+
const path = join(worktreeRoot, snapshot.repoHash, agentId);
|
|
700
|
+
const branch = `book-agent/${snapshot.repoHash}/${agentId}`;
|
|
701
|
+
if (await worktreeIsComplete(path, signal)) return { path, branch };
|
|
702
|
+
const existed = await branchExists(snapshot.repoRoot, branch, signal);
|
|
703
|
+
if (pointsAtWorktree(path)) {
|
|
704
|
+
await removeWorktree(snapshot.repoRoot, path);
|
|
705
|
+
await deleteBranchBookCreated(snapshot.repoRoot, branch, false);
|
|
706
|
+
}
|
|
707
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
708
|
+
try {
|
|
709
|
+
await git(snapshot.repoRoot, ["worktree", "add", "-b", branch, path, startCommit], { signal });
|
|
710
|
+
} catch (error) {
|
|
711
|
+
await removeWorktree(snapshot.repoRoot, path);
|
|
712
|
+
await deleteBranchBookCreated(snapshot.repoRoot, branch, existed);
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
if (!await worktreeIsComplete(path, signal)) {
|
|
716
|
+
await removeWorktree(snapshot.repoRoot, path);
|
|
717
|
+
await deleteBranchBookCreated(snapshot.repoRoot, branch, existed);
|
|
718
|
+
throw new Error(
|
|
719
|
+
`git worktree add reported success but ${path} is not a usable worktree; it was removed so the next attempt starts clean.`
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
return { path, branch };
|
|
723
|
+
}
|
|
724
|
+
function removalRoot(record, repoRoot) {
|
|
725
|
+
if (repoRoot && resolve(repoRoot) !== resolve(record.worktree)) return repoRoot;
|
|
726
|
+
const pointed = repositoryOfWorktree(record.worktree);
|
|
727
|
+
if (pointed && existsSync(pointed)) return pointed;
|
|
728
|
+
return dirname(record.worktree);
|
|
729
|
+
}
|
|
730
|
+
function repositoryOfWorktree(worktree) {
|
|
731
|
+
try {
|
|
732
|
+
const pointer = readFileSync(join(worktree, ".git"), "utf8").trim();
|
|
733
|
+
const match = /^gitdir:\s*(.+)$/m.exec(pointer);
|
|
734
|
+
const gitdir = match?.[1]?.trim();
|
|
735
|
+
if (!gitdir) return void 0;
|
|
736
|
+
return dirname(dirname(resolve(worktree, gitdir)));
|
|
737
|
+
} catch {
|
|
738
|
+
return void 0;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
async function removeAgentWorktree(record, repoRoot) {
|
|
742
|
+
if (!record.worktree || !record.branch) return;
|
|
743
|
+
const root = removalRoot(record, repoRoot);
|
|
744
|
+
await git(root, ["worktree", "remove", "--force", record.worktree], {
|
|
745
|
+
allowExitCodes: [128]
|
|
746
|
+
}).catch(() => {
|
|
747
|
+
});
|
|
748
|
+
await git(root, ["branch", "-D", record.branch], { allowExitCodes: [1, 128] }).catch(() => {
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
async function removeSnapshotRef(snapshot) {
|
|
752
|
+
await git(snapshot.repoRoot, ["update-ref", "-d", snapshot.ref], { allowExitCodes: [1] }).catch(
|
|
753
|
+
() => {
|
|
754
|
+
}
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
async function checkoutAgentCommit(worktree, commit) {
|
|
758
|
+
await git(worktree, ["reset", "--hard", commit]);
|
|
759
|
+
}
|
|
760
|
+
async function commitAgentWork(record, snapshot) {
|
|
761
|
+
if (!record.worktree || !record.branch) return void 0;
|
|
762
|
+
await git(record.worktree, ["add", "-A", "--", "."]);
|
|
763
|
+
const diff = await git(record.worktree, ["diff", ...HARDENED_DIFF_ARGS, "--cached", "--quiet"], {
|
|
764
|
+
allowExitCodes: [1]
|
|
765
|
+
});
|
|
766
|
+
if (diff.code === 0) return void 0;
|
|
767
|
+
await git(
|
|
768
|
+
record.worktree,
|
|
769
|
+
["commit", "--no-verify", "--no-gpg-sign", "-m", `book agent ${record.id}: ${record.name}`],
|
|
770
|
+
{
|
|
771
|
+
env: gitIdentityEnv()
|
|
772
|
+
}
|
|
773
|
+
);
|
|
774
|
+
const headCommit = (await git(record.worktree, ["rev-parse", "HEAD"])).stdout.trim();
|
|
775
|
+
return {
|
|
776
|
+
baseCommit: snapshot.commit,
|
|
777
|
+
headCommit,
|
|
778
|
+
branch: record.branch,
|
|
779
|
+
agentId: record.id
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
async function candidateDelta(snapshot, candidate, signal) {
|
|
783
|
+
return (await git(
|
|
784
|
+
snapshot.repoRoot,
|
|
785
|
+
[
|
|
786
|
+
"diff",
|
|
787
|
+
...HARDENED_DIFF_ARGS,
|
|
788
|
+
"--binary",
|
|
789
|
+
"--full-index",
|
|
790
|
+
candidate.baseCommit,
|
|
791
|
+
candidate.headCommit,
|
|
792
|
+
"--"
|
|
793
|
+
],
|
|
794
|
+
{ signal }
|
|
795
|
+
)).stdout;
|
|
796
|
+
}
|
|
797
|
+
async function precheckDelta(snapshot, patch, signal) {
|
|
798
|
+
const index = tempIndex();
|
|
799
|
+
try {
|
|
800
|
+
const env = { GIT_INDEX_FILE: index.path };
|
|
801
|
+
await git(snapshot.repoRoot, ["read-tree", snapshot.commit], { env, signal });
|
|
802
|
+
await git(snapshot.repoRoot, ["apply", "--3way", "--check", "--cached", "-"], {
|
|
803
|
+
env,
|
|
804
|
+
input: patch,
|
|
805
|
+
signal
|
|
806
|
+
});
|
|
807
|
+
await git(snapshot.repoRoot, ["apply", "--check", "-"], { input: patch, signal });
|
|
808
|
+
} finally {
|
|
809
|
+
rmSync(index.dir, { recursive: true, force: true });
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
var CHERRY_PICK_TIMEOUT_MS = 3e5;
|
|
813
|
+
async function hasPendingCherryPick(repoRoot, signal) {
|
|
814
|
+
const result = await git(repoRoot, ["rev-parse", "-q", "--verify", "CHERRY_PICK_HEAD"], {
|
|
815
|
+
allowExitCodes: [1],
|
|
816
|
+
signal
|
|
817
|
+
});
|
|
818
|
+
return result.code === 0;
|
|
819
|
+
}
|
|
820
|
+
async function unmergedPathsAfterFailure(repoRoot) {
|
|
821
|
+
try {
|
|
822
|
+
const result = await git(repoRoot, [
|
|
823
|
+
"diff",
|
|
824
|
+
...HARDENED_DIFF_ARGS,
|
|
825
|
+
"--name-only",
|
|
826
|
+
"--diff-filter=U"
|
|
827
|
+
]);
|
|
828
|
+
return result.stdout.split(/\r?\n/).filter(Boolean);
|
|
829
|
+
} catch {
|
|
830
|
+
return [];
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async function applyVerifiedCandidate(snapshot, candidate, signal, options) {
|
|
834
|
+
return applyCandidate(snapshot, candidate, signal, DEFAULT_MAX_BUFFER_BYTES, options);
|
|
835
|
+
}
|
|
836
|
+
function stoppedApplyError(signal, step, outcome) {
|
|
837
|
+
const reason = typeof signal?.reason === "string" ? ` (${signal.reason})` : "";
|
|
838
|
+
return new Error(`The apply was stopped${reason} during ${step}. ${outcome}`);
|
|
839
|
+
}
|
|
840
|
+
async function applyCandidate(snapshot, candidate, signal, signingBuffer, options) {
|
|
841
|
+
const mutationSignal = options?.mutationSignal ?? signal;
|
|
842
|
+
if (candidate.baseCommit !== snapshot.commit) {
|
|
843
|
+
return {
|
|
844
|
+
status: "conflicted",
|
|
845
|
+
error: "Patch candidate base does not match the task snapshot."
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
const actualHead = (await git(snapshot.repoRoot, ["rev-parse", candidate.headCommit], { signal })).stdout.trim();
|
|
849
|
+
if (actualHead !== candidate.headCommit) {
|
|
850
|
+
return { status: "conflicted", error: "Patch candidate commit no longer resolves exactly." };
|
|
851
|
+
}
|
|
852
|
+
if (await hasPendingCherryPick(snapshot.repoRoot, signal)) {
|
|
853
|
+
return {
|
|
854
|
+
status: "conflicted",
|
|
855
|
+
error: "A cherry-pick is already in progress in this repository. Book did not start it and will not abort it; finish it (git cherry-pick --continue) or undo it (git cherry-pick --abort), then apply the agent result again."
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
const current = await currentWorkspaceFingerprint(
|
|
859
|
+
snapshot.repoRoot,
|
|
860
|
+
snapshot.includeUntracked,
|
|
861
|
+
signal
|
|
862
|
+
);
|
|
863
|
+
if (current !== snapshot.fingerprint) {
|
|
864
|
+
return {
|
|
865
|
+
status: "conflicted",
|
|
866
|
+
error: "Parent workspace drifted after the agent snapshot; no changes were applied."
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
const patch = await candidateDelta(snapshot, candidate, signal);
|
|
870
|
+
try {
|
|
871
|
+
await precheckDelta(snapshot, patch, signal);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
const stopped = signal?.aborted === true;
|
|
874
|
+
if (stopped) {
|
|
875
|
+
throw stoppedApplyError(signal, "the patch pre-check", "No changes were applied.");
|
|
876
|
+
}
|
|
877
|
+
return {
|
|
878
|
+
status: "conflicted",
|
|
879
|
+
error: `Patch pre-check failed; no changes were applied: ${error instanceof Error ? error.message : String(error)}`
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
const signingRunner = (command, args, options2) => run(command, args, options2, signingBuffer);
|
|
883
|
+
if (!snapshot.dirty) {
|
|
884
|
+
let signingPins;
|
|
885
|
+
try {
|
|
886
|
+
signingPins = await signingPinArgs(snapshot.repoRoot, signingRunner, signal);
|
|
887
|
+
} catch (error) {
|
|
888
|
+
if (signal?.aborted) throw error;
|
|
889
|
+
return {
|
|
890
|
+
status: "conflicted",
|
|
891
|
+
error: `Could not read the signing configuration for the cherry-pick: ${error instanceof Error ? error.message : String(error)}`
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
await git(snapshot.repoRoot, [...signingPins, "cherry-pick", candidate.headCommit], {
|
|
896
|
+
timeoutMs: CHERRY_PICK_TIMEOUT_MS,
|
|
897
|
+
signal: mutationSignal
|
|
898
|
+
});
|
|
899
|
+
} catch (error) {
|
|
900
|
+
const stopped = mutationSignal?.aborted === true;
|
|
901
|
+
const unmergedPaths = await unmergedPathsAfterFailure(snapshot.repoRoot);
|
|
902
|
+
let abortError;
|
|
903
|
+
try {
|
|
904
|
+
await git(snapshot.repoRoot, ["cherry-pick", "--abort"], { allowExitCodes: [128] });
|
|
905
|
+
} catch (abortFailure) {
|
|
906
|
+
abortError = abortFailure instanceof Error ? abortFailure.message : String(abortFailure);
|
|
907
|
+
}
|
|
908
|
+
if (stopped) {
|
|
909
|
+
throw stoppedApplyError(
|
|
910
|
+
mutationSignal,
|
|
911
|
+
"the cherry-pick",
|
|
912
|
+
[
|
|
913
|
+
abortError ? `The rollback did not complete (${abortError}), so this repository may be left mid-cherry-pick: finish it with git cherry-pick --continue or undo it with git cherry-pick --abort.` : "The cherry-pick was rolled back.",
|
|
914
|
+
unmergedPaths.length > 0 ? `Unmerged paths: ${unmergedPaths.join(", ")}.` : void 0
|
|
915
|
+
].filter(Boolean).join(" ")
|
|
916
|
+
);
|
|
917
|
+
}
|
|
918
|
+
return cherryPickFailureResult({
|
|
919
|
+
pickError: error instanceof Error ? error.message : String(error),
|
|
920
|
+
abortError,
|
|
921
|
+
unmergedPaths
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
const appliedCommit = (await git(snapshot.repoRoot, ["rev-parse", "HEAD"])).stdout.trim();
|
|
925
|
+
return { status: "applied", commit: appliedCommit };
|
|
926
|
+
}
|
|
927
|
+
try {
|
|
928
|
+
await git(snapshot.repoRoot, ["apply", "-"], { input: patch, signal: mutationSignal });
|
|
929
|
+
return { status: "applied", commit: candidate.headCommit };
|
|
930
|
+
} catch (error) {
|
|
931
|
+
if (mutationSignal?.aborted) {
|
|
932
|
+
throw stoppedApplyError(
|
|
933
|
+
mutationSignal,
|
|
934
|
+
"git apply",
|
|
935
|
+
"git apply was cut short, so check git status before applying again: the working tree may be partly changed."
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
status: "conflicted",
|
|
940
|
+
error: `Patch application failed: ${error instanceof Error ? error.message : String(error)}`
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// src/fs-contention.ts
|
|
946
|
+
var FILE_CONTENTION_CODES = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
947
|
+
function sleepSync(milliseconds) {
|
|
948
|
+
if (milliseconds <= 0) return;
|
|
949
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/jobs/persistent-store.ts
|
|
953
|
+
function persistentJobPaths(workspace, root = join2(resolveBookHome(), "jobs")) {
|
|
954
|
+
const repositoryRoot = join2(root, repositoryHash(workspace));
|
|
955
|
+
return {
|
|
956
|
+
root: repositoryRoot,
|
|
957
|
+
records: join2(repositoryRoot, "records"),
|
|
958
|
+
controls: join2(repositoryRoot, "controls"),
|
|
959
|
+
logs: join2(repositoryRoot, "logs"),
|
|
960
|
+
specs: join2(repositoryRoot, "specs")
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function ensurePersistentJobPaths(paths) {
|
|
964
|
+
for (const path of [paths.root, paths.records, paths.controls, paths.logs, paths.specs]) {
|
|
965
|
+
mkdirSync2(path, { recursive: true });
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
var DEFAULT_RENAME_RETRY_BUDGET_MS = 100;
|
|
969
|
+
var RENAME_RETRY_STEP_MS = 10;
|
|
970
|
+
function renameWithContentionRetry(from, to, options = {}) {
|
|
971
|
+
const rename = options.rename ?? renameSync;
|
|
972
|
+
const platform = options.platform ?? process.platform;
|
|
973
|
+
const clock = options.clock ?? systemClock;
|
|
974
|
+
const sleep = options.sleep ?? sleepSync;
|
|
975
|
+
const deadline = clock.monotonicNowMs() + (options.budgetMs ?? DEFAULT_RENAME_RETRY_BUDGET_MS);
|
|
976
|
+
for (; ; ) {
|
|
977
|
+
try {
|
|
978
|
+
rename(from, to);
|
|
979
|
+
return;
|
|
980
|
+
} catch (error) {
|
|
981
|
+
const code = error.code ?? "";
|
|
982
|
+
const remainingMs = deadline - clock.monotonicNowMs();
|
|
983
|
+
if (platform !== "win32" || !FILE_CONTENTION_CODES.has(code) || remainingMs <= 0) {
|
|
984
|
+
throw error;
|
|
985
|
+
}
|
|
986
|
+
sleep(Math.min(RENAME_RETRY_STEP_MS, remainingMs));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
function writeJsonAtomic(path, value, options = {}) {
|
|
991
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
992
|
+
const temporary = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
993
|
+
try {
|
|
994
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2), { encoding: "utf8", mode: 384 });
|
|
995
|
+
renameWithContentionRetry(temporary, path, { budgetMs: options.renameRetryBudgetMs });
|
|
996
|
+
} catch (error) {
|
|
997
|
+
try {
|
|
998
|
+
rmSync2(temporary, { force: true });
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
1001
|
+
throw error;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
function readJsonFile(path) {
|
|
1005
|
+
try {
|
|
1006
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
1007
|
+
} catch {
|
|
1008
|
+
return void 0;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
function loadPersistentShellSpec(path) {
|
|
1012
|
+
const spec = readJsonFile(path);
|
|
1013
|
+
if (!spec || spec.version !== 1 || typeof spec.token !== "string" || typeof spec.tokenHash !== "string" || createHash2("sha256").update(spec.token).digest("hex") !== spec.tokenHash) {
|
|
1014
|
+
return { error: "Invalid persistent shell specification." };
|
|
1015
|
+
}
|
|
1016
|
+
if (spec.sandboxed && !spec.exec) {
|
|
1017
|
+
return {
|
|
1018
|
+
error: "Invalid persistent shell specification: sandboxed is set but no sandboxed argv is present."
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
return { spec };
|
|
1022
|
+
}
|
|
1023
|
+
function listPersistentStates(paths) {
|
|
1024
|
+
if (!existsSync2(paths.records)) return [];
|
|
1025
|
+
return readdirSync(paths.records).filter((name) => name.endsWith(".json")).map((name) => readJsonFile(join2(paths.records, name))).filter((state) => state?.version === 1);
|
|
1026
|
+
}
|
|
1027
|
+
function removePersistentJobFiles(paths, id) {
|
|
1028
|
+
for (const path of [
|
|
1029
|
+
join2(paths.records, `${id}.json`),
|
|
1030
|
+
join2(paths.controls, `${id}.json`),
|
|
1031
|
+
join2(paths.logs, `${id}.log`),
|
|
1032
|
+
join2(paths.specs, `${id}.json`)
|
|
1033
|
+
]) {
|
|
1034
|
+
rmSync2(path, { force: true });
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function removePersistentRunnerFiles(paths, id) {
|
|
1038
|
+
for (const path of [join2(paths.controls, `${id}.json`), join2(paths.specs, `${id}.json`)]) {
|
|
1039
|
+
rmSync2(path, { force: true });
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export {
|
|
1044
|
+
findGitRoot,
|
|
1045
|
+
repositoryHash,
|
|
1046
|
+
createSyntheticSnapshot,
|
|
1047
|
+
defaultWorktreeRoot,
|
|
1048
|
+
createAgentWorktree,
|
|
1049
|
+
removeAgentWorktree,
|
|
1050
|
+
removeSnapshotRef,
|
|
1051
|
+
checkoutAgentCommit,
|
|
1052
|
+
commitAgentWork,
|
|
1053
|
+
applyVerifiedCandidate,
|
|
1054
|
+
FILE_CONTENTION_CODES,
|
|
1055
|
+
sleepSync,
|
|
1056
|
+
persistentJobPaths,
|
|
1057
|
+
ensurePersistentJobPaths,
|
|
1058
|
+
writeJsonAtomic,
|
|
1059
|
+
readJsonFile,
|
|
1060
|
+
loadPersistentShellSpec,
|
|
1061
|
+
listPersistentStates,
|
|
1062
|
+
removePersistentJobFiles,
|
|
1063
|
+
removePersistentRunnerFiles
|
|
1064
|
+
};
|
|
1065
|
+
//# sourceMappingURL=chunk-JGF5HLSI.js.map
|