@letrquan/book 0.2.0 → 0.3.0

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