@spotpatch/agent 1.0.0 → 1.2.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/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // src/engine/execute-agent-change.ts
2
2
  import {
3
- ERROR_CODES as ERROR_CODES18,
4
- SpotPatchError as SpotPatchError18
3
+ ERROR_CODES as ERROR_CODES19,
4
+ SpotPatchError as SpotPatchError19
5
5
  } from "@spotpatch/shared";
6
6
 
7
7
  // src/provider/openai-compatible-provider.ts
@@ -69,7 +69,10 @@ function jsonStringifyToolOutput(value) {
69
69
  }
70
70
 
71
71
  // src/provider/provider-transport.ts
72
- import { ERROR_CODES as ERROR_CODES4, SpotPatchError as SpotPatchError4 } from "@spotpatch/shared";
72
+ import {
73
+ ERROR_CODES as ERROR_CODES4,
74
+ SpotPatchError as SpotPatchError4
75
+ } from "@spotpatch/shared";
73
76
 
74
77
  // src/provider/provider-credential.ts
75
78
  import { ERROR_CODES as ERROR_CODES2, SpotPatchError as SpotPatchError2 } from "@spotpatch/shared";
@@ -239,6 +242,19 @@ async function readSseEvents(stream, options) {
239
242
  }
240
243
 
241
244
  // src/provider/provider-transport.ts
245
+ function createProviderHeaders(authentication, credential) {
246
+ const headers = new Headers({
247
+ Accept: "text/event-stream",
248
+ "Content-Type": "application/json"
249
+ });
250
+ const value = readProviderCredential(credential);
251
+ if (authentication === "x-api-key") {
252
+ headers.set("x-api-key", value);
253
+ } else {
254
+ headers.set("Authorization", `Bearer ${value}`);
255
+ }
256
+ return headers;
257
+ }
242
258
  function mapStatus(status) {
243
259
  if (status === 401 || status === 403) {
244
260
  return new SpotPatchError4(ERROR_CODES4.PROVIDER_AUTH_FAILED);
@@ -278,11 +294,7 @@ async function postProviderStream(options) {
278
294
  try {
279
295
  response = await options.fetch(options.url, {
280
296
  method: "POST",
281
- headers: {
282
- Accept: "text/event-stream",
283
- Authorization: `Bearer ${readProviderCredential(options.credential)}`,
284
- "Content-Type": "application/json"
285
- },
297
+ headers: createProviderHeaders(options.authentication, options.credential),
286
298
  body: JSON.stringify(options.body),
287
299
  redirect: "error",
288
300
  signal: requestController.signal
@@ -475,6 +487,7 @@ function createChatCompletionsSession(options) {
475
487
  }
476
488
  const parsed = parseChatEvents(
477
489
  await postProviderStream({
490
+ authentication: options.provider.authentication,
478
491
  body: {
479
492
  model: options.model.model,
480
493
  messages,
@@ -646,6 +659,7 @@ function createResponsesSession(options) {
646
659
  };
647
660
  const parsed = parseResponsesEvents(
648
661
  await postProviderStream({
662
+ authentication: options.provider.authentication,
649
663
  body,
650
664
  credential: options.credential,
651
665
  fetch,
@@ -702,7 +716,7 @@ var PROTECTED_FILE_NAMES = /* @__PURE__ */ new Set([
702
716
  "yarn.lock"
703
717
  ]);
704
718
  function deny() {
705
- throw new SpotPatchError8(ERROR_CODES8.TOOL_DENIED);
719
+ throw new SpotPatchError8(ERROR_CODES8.TOOL_PATH_DENIED);
706
720
  }
707
721
  function hasControlCharacter(value) {
708
722
  for (let index = 0; index < value.length; index += 1) {
@@ -807,13 +821,13 @@ async function readAgentTextFile(root, relativePath, maximumBytes) {
807
821
  }
808
822
  const bytes = await readFile(absolutePath);
809
823
  if (bytes.includes(0)) {
810
- throw new SpotPatchError9(ERROR_CODES9.TOOL_DENIED);
824
+ throw new SpotPatchError9(ERROR_CODES9.TOOL_PATH_DENIED);
811
825
  }
812
826
  let content;
813
827
  try {
814
828
  content = utf8Decoder.decode(bytes);
815
829
  } catch {
816
- throw new SpotPatchError9(ERROR_CODES9.TOOL_DENIED);
830
+ throw new SpotPatchError9(ERROR_CODES9.TOOL_PATH_DENIED);
817
831
  }
818
832
  return Object.freeze({ content, relativePath, size: metadata.size });
819
833
  }
@@ -826,7 +840,7 @@ function encodeUtf8Text(content, includeByteOrderMark) {
826
840
  }
827
841
  async function writeAgentTextFileIfContentMatches(root, relativePath, expectedContent, nextContent, maximumBytes) {
828
842
  if (nextContent.includes("\0")) {
829
- throw new SpotPatchError9(ERROR_CODES9.TOOL_DENIED);
843
+ throw new SpotPatchError9(ERROR_CODES9.TOOL_INPUT_INVALID);
830
844
  }
831
845
  const absolutePath = await resolveExistingAgentPath(root, relativePath);
832
846
  const [metadata, currentBytes] = await Promise.all([
@@ -840,7 +854,7 @@ async function writeAgentTextFileIfContentMatches(root, relativePath, expectedCo
840
854
  try {
841
855
  currentContent = utf8Decoder.decode(currentBytes);
842
856
  } catch {
843
- throw new SpotPatchError9(ERROR_CODES9.TOOL_DENIED);
857
+ throw new SpotPatchError9(ERROR_CODES9.TOOL_PATH_DENIED);
844
858
  }
845
859
  if (currentBytes.includes(0) || currentContent !== expectedContent) {
846
860
  throw new SpotPatchError9(ERROR_CODES9.PATCH_REJECTED);
@@ -1099,7 +1113,7 @@ async function runConfiguredCheck(options) {
1099
1113
  function requireConfiguredCheck(checkId, checks) {
1100
1114
  const check = checks[checkId];
1101
1115
  if (check === void 0) {
1102
- throw new SpotPatchError10(ERROR_CODES10.TOOL_DENIED);
1116
+ throw new SpotPatchError10(ERROR_CODES10.TOOL_INPUT_INVALID);
1103
1117
  }
1104
1118
  return check;
1105
1119
  }
@@ -1294,9 +1308,16 @@ async function applyAgentPatch(worktreeRoot, patch, limits, signal) {
1294
1308
  throw new SpotPatchError13(ERROR_CODES13.AGENT_LIMIT_EXCEEDED);
1295
1309
  }
1296
1310
  await Promise.all(
1297
- files.map(
1298
- async (file) => resolveWritableAgentPath(worktreeRoot, file.relativePath)
1299
- )
1311
+ files.map(async (file) => {
1312
+ await resolveWritableAgentPath(worktreeRoot, file.relativePath);
1313
+ if (file.kind !== "added") {
1314
+ await readAgentTextFile(
1315
+ worktreeRoot,
1316
+ file.relativePath,
1317
+ limits.maxReadBytesPerFile
1318
+ );
1319
+ }
1320
+ })
1300
1321
  );
1301
1322
  await runGitCommand({
1302
1323
  cwd: worktreeRoot,
@@ -1410,7 +1431,7 @@ var MAX_DISCOVERED_FILES = 2e4;
1410
1431
  var TEXT_SAMPLE_BYTES = 8192;
1411
1432
  function compileGlob(glob) {
1412
1433
  if (glob.length === 0 || glob.length > 256 || glob.includes("\0") || glob.includes("\\") || glob.startsWith("/") || ["[", "]", "{", "}", "(", ")", "!"].some((character) => glob.includes(character)) || glob.split("/").some((segment) => segment === "..")) {
1413
- throw new SpotPatchError14(ERROR_CODES14.TOOL_DENIED);
1434
+ throw new SpotPatchError14(ERROR_CODES14.TOOL_INPUT_INVALID);
1414
1435
  }
1415
1436
  let expression = "^";
1416
1437
  for (let index = 0; index < glob.length; index += 1) {
@@ -1649,7 +1670,7 @@ var runCheckSchema = z.strictObject({
1649
1670
  checkId: z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
1650
1671
  });
1651
1672
  function invalidTool() {
1652
- throw new SpotPatchError15(ERROR_CODES15.TOOL_DENIED);
1673
+ throw new SpotPatchError15(ERROR_CODES15.TOOL_INPUT_INVALID);
1653
1674
  }
1654
1675
  function parseArguments(schema, value) {
1655
1676
  const parsed = schema.safeParse(value);
@@ -1946,67 +1967,354 @@ function createAgentToolExecutor(options) {
1946
1967
  }
1947
1968
 
1948
1969
  // src/worktree/git-worktree.ts
1949
- import { lstat as lstat3, mkdtemp, realpath as realpath2, rm as rm2 } from "fs/promises";
1970
+ import {
1971
+ copyFile,
1972
+ lstat as lstat4,
1973
+ mkdir,
1974
+ mkdtemp,
1975
+ readFile as readFile2,
1976
+ realpath as realpath3,
1977
+ rm as rm2
1978
+ } from "fs/promises";
1979
+ import { createHash as createHash2 } from "crypto";
1950
1980
  import os from "os";
1981
+ import path6 from "path";
1982
+ import {
1983
+ ERROR_CODES as ERROR_CODES17,
1984
+ SpotPatchError as SpotPatchError17
1985
+ } from "@spotpatch/shared";
1986
+
1987
+ // src/worktree/workspace-health.ts
1988
+ import { lstat as lstat3, realpath as realpath2 } from "fs/promises";
1951
1989
  import path5 from "path";
1952
- import { ERROR_CODES as ERROR_CODES16, SpotPatchError as SpotPatchError16 } from "@spotpatch/shared";
1953
- async function assertCleanGitBaseline(options) {
1954
- const root = await realpath2(options.root).catch(() => {
1955
- throw new SpotPatchError16(ERROR_CODES16.WORKTREE_DIRTY);
1990
+ import {
1991
+ AGENT_WORKSPACE_SNAPSHOT_LIMITS,
1992
+ ERROR_CODES as ERROR_CODES16,
1993
+ SpotPatchError as SpotPatchError16
1994
+ } from "@spotpatch/shared";
1995
+ var CONFLICTED_STATUSES = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
1996
+ var OPERATION_MARKERS = Object.freeze([
1997
+ "MERGE_HEAD",
1998
+ "CHERRY_PICK_HEAD",
1999
+ "REVERT_HEAD",
2000
+ "rebase-apply",
2001
+ "rebase-merge"
2002
+ ]);
2003
+ function emptyChanges() {
2004
+ return Object.freeze({
2005
+ staged: 0,
2006
+ unstaged: 0,
2007
+ untracked: 0,
2008
+ conflicted: 0,
2009
+ total: 0
2010
+ });
2011
+ }
2012
+ function blockedHealth(errorCode) {
2013
+ return Object.freeze({
2014
+ state: "blocked",
2015
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
2016
+ changes: emptyChanges(),
2017
+ canIncludeLocalChanges: false,
2018
+ errorCode
2019
+ });
2020
+ }
2021
+ function parsePorcelainStatus(value) {
2022
+ const records = value.split("\0");
2023
+ const untrackedPaths = [];
2024
+ let staged = 0;
2025
+ let unstaged = 0;
2026
+ let conflicted = 0;
2027
+ let total = 0;
2028
+ for (let index = 0; index < records.length; index += 1) {
2029
+ const record = records[index] ?? "";
2030
+ if (record.length === 0) {
2031
+ continue;
2032
+ }
2033
+ if (record.length < 4 || record[2] !== " ") {
2034
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2035
+ }
2036
+ const status = record.slice(0, 2);
2037
+ const relativePath = record.slice(3);
2038
+ if (relativePath.length === 0) {
2039
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2040
+ }
2041
+ total += 1;
2042
+ if (status === "??") {
2043
+ untrackedPaths.push(relativePath);
2044
+ continue;
2045
+ }
2046
+ const indexStatus = status[0] ?? " ";
2047
+ const worktreeStatus = status[1] ?? " ";
2048
+ if (CONFLICTED_STATUSES.has(status) || indexStatus === "U" || worktreeStatus === "U") {
2049
+ conflicted += 1;
2050
+ }
2051
+ if (indexStatus !== " ") {
2052
+ staged += 1;
2053
+ }
2054
+ if (worktreeStatus !== " ") {
2055
+ unstaged += 1;
2056
+ }
2057
+ if (indexStatus === "R" || indexStatus === "C") {
2058
+ index += 1;
2059
+ if ((records[index] ?? "").length === 0) {
2060
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2061
+ }
2062
+ }
2063
+ }
2064
+ return Object.freeze({
2065
+ changes: Object.freeze({
2066
+ staged,
2067
+ unstaged,
2068
+ untracked: untrackedPaths.length,
2069
+ conflicted,
2070
+ total
2071
+ }),
2072
+ untrackedPaths: Object.freeze(untrackedPaths)
1956
2073
  });
1957
- const topLevel = (await runGitCommand({
2074
+ }
2075
+ async function operationInProgress(root, signal) {
2076
+ for (const marker of OPERATION_MARKERS) {
2077
+ const markerPath = (await runGitCommand({
2078
+ cwd: root,
2079
+ args: ["rev-parse", "--git-path", marker],
2080
+ errorCode: ERROR_CODES16.WORKTREE_NOT_REPOSITORY,
2081
+ ...signal === void 0 ? {} : { signal }
2082
+ })).trim();
2083
+ if (await lstat3(path5.resolve(root, markerPath)).catch(() => void 0) !== void 0) {
2084
+ return true;
2085
+ }
2086
+ }
2087
+ return false;
2088
+ }
2089
+ async function inspectUntrackedFiles(root, relativePaths) {
2090
+ if (relativePaths.length > AGENT_WORKSPACE_SNAPSHOT_LIMITS.maxUntrackedFiles) {
2091
+ return ERROR_CODES16.WORKTREE_LOCAL_CHANGES_TOO_LARGE;
2092
+ }
2093
+ let totalBytes = 0;
2094
+ for (const relativePath of relativePaths) {
2095
+ const absolutePath = path5.resolve(root, relativePath);
2096
+ const relative = path5.relative(root, absolutePath);
2097
+ if (relative === ".." || relative.startsWith(`..${path5.sep}`) || path5.isAbsolute(relative)) {
2098
+ return ERROR_CODES16.WORKTREE_UNTRACKED_UNSUPPORTED;
2099
+ }
2100
+ const metadata = await lstat3(absolutePath).catch(() => void 0);
2101
+ if (metadata === void 0 || !metadata.isFile() || metadata.isSymbolicLink()) {
2102
+ return ERROR_CODES16.WORKTREE_UNTRACKED_UNSUPPORTED;
2103
+ }
2104
+ totalBytes += metadata.size;
2105
+ if (totalBytes > AGENT_WORKSPACE_SNAPSHOT_LIMITS.maxUntrackedBytes) {
2106
+ return ERROR_CODES16.WORKTREE_LOCAL_CHANGES_TOO_LARGE;
2107
+ }
2108
+ }
2109
+ return void 0;
2110
+ }
2111
+ async function inspectGitWorkspace(rootValue, signal) {
2112
+ const root = await realpath2(rootValue).catch(() => {
2113
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_NOT_REPOSITORY);
2114
+ });
2115
+ const topLevelResult = await runRawGitCommand({
1958
2116
  cwd: root,
1959
2117
  args: ["rev-parse", "--show-toplevel"],
1960
- errorCode: ERROR_CODES16.WORKTREE_DIRTY,
1961
- ...options.signal === void 0 ? {} : { signal: options.signal }
1962
- })).trim();
2118
+ ...signal === void 0 ? {} : { signal }
2119
+ });
2120
+ if (topLevelResult.exitCode !== 0 || topLevelResult.cancelled || topLevelResult.timedOut) {
2121
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_NOT_REPOSITORY);
2122
+ }
2123
+ const topLevel = topLevelResult.stdout.trim();
1963
2124
  if (!samePath(root, topLevel)) {
1964
- throw new SpotPatchError16(ERROR_CODES16.WORKTREE_DIRTY);
2125
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_NOT_REPOSITORY);
1965
2126
  }
1966
2127
  const head = (await runGitCommand({
1967
2128
  cwd: root,
1968
2129
  args: ["rev-parse", "--verify", "HEAD"],
1969
- errorCode: ERROR_CODES16.WORKTREE_DIRTY,
1970
- ...options.signal === void 0 ? {} : { signal: options.signal }
2130
+ errorCode: ERROR_CODES16.WORKTREE_NOT_REPOSITORY,
2131
+ ...signal === void 0 ? {} : { signal }
1971
2132
  })).trim();
1972
- if (options.expectedHead !== void 0 && head !== options.expectedHead) {
1973
- throw new SpotPatchError16(ERROR_CODES16.APPLY_CONFLICT);
2133
+ const parsed = parsePorcelainStatus(
2134
+ await runGitCommand({
2135
+ cwd: root,
2136
+ args: ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
2137
+ errorCode: ERROR_CODES16.WORKTREE_NOT_REPOSITORY,
2138
+ ...signal === void 0 ? {} : { signal }
2139
+ })
2140
+ );
2141
+ const operationActive = await operationInProgress(root, signal);
2142
+ const untrackedError = await inspectUntrackedFiles(root, parsed.untrackedPaths);
2143
+ const errorCode = operationActive ? ERROR_CODES16.WORKTREE_OPERATION_IN_PROGRESS : parsed.changes.conflicted > 0 ? ERROR_CODES16.WORKTREE_CONFLICTED : untrackedError;
2144
+ const health = Object.freeze({
2145
+ state: errorCode !== void 0 ? "blocked" : parsed.changes.total === 0 ? "ready" : "consent-required",
2146
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
2147
+ changes: parsed.changes,
2148
+ canIncludeLocalChanges: errorCode === void 0 && parsed.changes.total > 0,
2149
+ ...errorCode === void 0 ? {} : { errorCode }
2150
+ });
2151
+ return Object.freeze({
2152
+ root,
2153
+ head,
2154
+ health,
2155
+ untrackedPaths: parsed.untrackedPaths
2156
+ });
2157
+ }
2158
+ async function inspectAgentWorkspace(root, signal) {
2159
+ try {
2160
+ return (await inspectGitWorkspace(root, signal)).health;
2161
+ } catch (error) {
2162
+ if (error instanceof SpotPatchError16) {
2163
+ return blockedHealth(error.code);
2164
+ }
2165
+ return blockedHealth(ERROR_CODES16.INTERNAL_ERROR);
1974
2166
  }
1975
- const status = await runGitCommand({
1976
- cwd: root,
1977
- args: ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
1978
- errorCode: ERROR_CODES16.WORKTREE_DIRTY,
1979
- ...options.signal === void 0 ? {} : { signal: options.signal }
2167
+ }
2168
+
2169
+ // src/worktree/git-worktree.ts
2170
+ function workspacePath(root, relativePath) {
2171
+ const candidate = path6.resolve(root, relativePath);
2172
+ const relative = path6.relative(root, candidate);
2173
+ if (relative === ".." || relative.startsWith(`..${path6.sep}`) || path6.isAbsolute(relative)) {
2174
+ throw new SpotPatchError17(ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2175
+ }
2176
+ return candidate;
2177
+ }
2178
+ async function fileDigest(filePath) {
2179
+ return createHash2("sha256").update(await readFile2(filePath)).digest("hex");
2180
+ }
2181
+ async function copyUntrackedFiles(sourceRoot, worktreeRoot, relativePaths) {
2182
+ for (const relativePath of relativePaths) {
2183
+ const sourcePath = workspacePath(sourceRoot, relativePath);
2184
+ const targetPath = workspacePath(worktreeRoot, relativePath);
2185
+ const metadata = await lstat4(sourcePath).catch(() => void 0);
2186
+ if (metadata === void 0 || !metadata.isFile() || metadata.isSymbolicLink()) {
2187
+ throw new SpotPatchError17(ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2188
+ }
2189
+ await mkdir(path6.dirname(targetPath), { recursive: true });
2190
+ await copyFile(sourcePath, targetPath);
2191
+ const [sourceDigest, targetDigest] = await Promise.all([
2192
+ fileDigest(sourcePath),
2193
+ fileDigest(targetPath)
2194
+ ]).catch(() => {
2195
+ throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2196
+ });
2197
+ if (sourceDigest !== targetDigest) {
2198
+ throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2199
+ }
2200
+ }
2201
+ }
2202
+ async function assertUntrackedFilesUnchanged(sourceRoot, worktreeRoot, relativePaths) {
2203
+ for (const relativePath of relativePaths) {
2204
+ const sourcePath = workspacePath(sourceRoot, relativePath);
2205
+ const targetPath = workspacePath(worktreeRoot, relativePath);
2206
+ const [sourceDigest, targetDigest] = await Promise.all([
2207
+ fileDigest(sourcePath),
2208
+ fileDigest(targetPath)
2209
+ ]).catch(() => {
2210
+ throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2211
+ });
2212
+ if (sourceDigest !== targetDigest) {
2213
+ throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2214
+ }
2215
+ }
2216
+ }
2217
+ async function materializeLocalBaseline(sourceRoot, worktreeRoot, expectedHead, untrackedPaths, signal) {
2218
+ const sourceDiff = await runGitCommand({
2219
+ cwd: sourceRoot,
2220
+ args: [
2221
+ "diff",
2222
+ "--binary",
2223
+ "--full-index",
2224
+ "--no-ext-diff",
2225
+ "--no-color",
2226
+ "--no-renames",
2227
+ "HEAD",
2228
+ "--"
2229
+ ],
2230
+ signal,
2231
+ errorCode: ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
1980
2232
  });
1981
- if (status.length > 0) {
1982
- throw new SpotPatchError16(
1983
- options.expectedHead === void 0 ? ERROR_CODES16.WORKTREE_DIRTY : ERROR_CODES16.APPLY_CONFLICT
1984
- );
2233
+ if (sourceDiff.length > 0) {
2234
+ await runGitCommand({
2235
+ cwd: worktreeRoot,
2236
+ args: ["apply", "--binary", "--whitespace=nowarn", "-"],
2237
+ stdin: sourceDiff,
2238
+ signal,
2239
+ errorCode: ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2240
+ });
1985
2241
  }
1986
- return Object.freeze({ root, head });
2242
+ await copyUntrackedFiles(sourceRoot, worktreeRoot, untrackedPaths);
2243
+ const confirmation = await inspectGitWorkspace(sourceRoot, signal);
2244
+ const confirmationDiff = await runGitCommand({
2245
+ cwd: sourceRoot,
2246
+ args: [
2247
+ "diff",
2248
+ "--binary",
2249
+ "--full-index",
2250
+ "--no-ext-diff",
2251
+ "--no-color",
2252
+ "--no-renames",
2253
+ "HEAD",
2254
+ "--"
2255
+ ],
2256
+ signal,
2257
+ errorCode: ERROR_CODES17.APPLY_CONFLICT
2258
+ });
2259
+ if (confirmation.head !== expectedHead || confirmationDiff !== sourceDiff || confirmation.untrackedPaths.length !== untrackedPaths.length || confirmation.untrackedPaths.some((value, index) => value !== untrackedPaths[index])) {
2260
+ throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2261
+ }
2262
+ await assertUntrackedFilesUnchanged(sourceRoot, worktreeRoot, untrackedPaths);
2263
+ await runGitCommand({
2264
+ cwd: worktreeRoot,
2265
+ args: ["add", "--all"],
2266
+ signal,
2267
+ errorCode: ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2268
+ });
2269
+ await runGitCommand({
2270
+ cwd: worktreeRoot,
2271
+ args: [
2272
+ "-c",
2273
+ "user.name=SpotPatch Agent",
2274
+ "-c",
2275
+ "user.email=spotpatch-agent@example.invalid",
2276
+ "commit",
2277
+ "--quiet",
2278
+ "-m",
2279
+ "SpotPatch local workspace baseline"
2280
+ ],
2281
+ signal,
2282
+ errorCode: ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2283
+ });
1987
2284
  }
1988
2285
  async function defaultTemporaryBase(root) {
1989
- const dependencyDirectory = path5.join(root, "node_modules");
2286
+ const dependencyDirectory = path6.join(root, "node_modules");
1990
2287
  try {
1991
- const stats = await lstat3(dependencyDirectory);
2288
+ const stats = await lstat4(dependencyDirectory);
1992
2289
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
1993
2290
  return os.tmpdir();
1994
2291
  }
1995
- return await realpath2(dependencyDirectory);
2292
+ return await realpath3(dependencyDirectory);
1996
2293
  } catch {
1997
2294
  return os.tmpdir();
1998
2295
  }
1999
2296
  }
2000
2297
  async function createIsolatedGitWorktree(options) {
2001
- const baseline = await assertCleanGitBaseline({
2002
- root: options.root,
2003
- signal: options.signal
2298
+ const workingTreeMode = options.workingTreeMode ?? "require-clean";
2299
+ const inspection = await inspectGitWorkspace(options.root, options.signal);
2300
+ if (inspection.health.state === "blocked") {
2301
+ throw new SpotPatchError17(
2302
+ inspection.health.errorCode ?? ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2303
+ );
2304
+ }
2305
+ if (inspection.health.state === "consent-required" && workingTreeMode === "require-clean") {
2306
+ throw new SpotPatchError17(ERROR_CODES17.WORKTREE_DIRTY);
2307
+ }
2308
+ const baseline = Object.freeze({
2309
+ root: inspection.root,
2310
+ head: inspection.head,
2311
+ workingTreeMode
2004
2312
  });
2005
2313
  const temporaryBase = options.temporaryBase ?? await defaultTemporaryBase(baseline.root);
2006
2314
  const temporaryDirectory = await mkdtemp(
2007
- path5.join(temporaryBase, "spotpatch-agent-")
2315
+ path6.join(temporaryBase, "spotpatch-agent-")
2008
2316
  );
2009
- const worktreePath = path5.join(temporaryDirectory, "worktree");
2317
+ const worktreePath = path6.join(temporaryDirectory, "worktree");
2010
2318
  let registered = false;
2011
2319
  let cleaned = false;
2012
2320
  const cleanup = async () => {
@@ -2021,7 +2329,7 @@ async function createIsolatedGitWorktree(options) {
2021
2329
  timeoutMs: 3e4
2022
2330
  }).catch(() => void 0);
2023
2331
  }
2024
- if (path5.basename(temporaryDirectory).startsWith("spotpatch-agent-")) {
2332
+ if (path6.basename(temporaryDirectory).startsWith("spotpatch-agent-")) {
2025
2333
  await rm2(temporaryDirectory, { recursive: true, force: true }).catch(
2026
2334
  () => void 0
2027
2335
  );
@@ -2031,12 +2339,12 @@ async function createIsolatedGitWorktree(options) {
2031
2339
  await runGitCommand({
2032
2340
  cwd: baseline.root,
2033
2341
  args: ["worktree", "add", "--detach", worktreePath, baseline.head],
2034
- errorCode: ERROR_CODES16.INTERNAL_ERROR,
2342
+ errorCode: ERROR_CODES17.INTERNAL_ERROR,
2035
2343
  signal: options.signal,
2036
2344
  timeoutMs: 3e4
2037
2345
  });
2038
2346
  registered = true;
2039
- const worktreeRoot = await realpath2(worktreePath);
2347
+ const worktreeRoot = await realpath3(worktreePath);
2040
2348
  const actualHead = (await runGitCommand({
2041
2349
  cwd: worktreeRoot,
2042
2350
  args: ["rev-parse", "--verify", "HEAD"],
@@ -2048,7 +2356,16 @@ async function createIsolatedGitWorktree(options) {
2048
2356
  signal: options.signal
2049
2357
  })).trim();
2050
2358
  if (actualHead !== baseline.head || !samePath(actualRoot, worktreeRoot)) {
2051
- throw new SpotPatchError16(ERROR_CODES16.INTERNAL_ERROR);
2359
+ throw new SpotPatchError17(ERROR_CODES17.INTERNAL_ERROR);
2360
+ }
2361
+ if (inspection.health.state === "consent-required") {
2362
+ await materializeLocalBaseline(
2363
+ baseline.root,
2364
+ worktreeRoot,
2365
+ baseline.head,
2366
+ inspection.untrackedPaths,
2367
+ options.signal
2368
+ );
2052
2369
  }
2053
2370
  return Object.freeze({ baseline, root: worktreeRoot, cleanup });
2054
2371
  } catch (error) {
@@ -2058,17 +2375,19 @@ async function createIsolatedGitWorktree(options) {
2058
2375
  }
2059
2376
 
2060
2377
  // src/worktree/prepared-change.ts
2061
- import { createHash as createHash2 } from "crypto";
2062
- import { lstat as lstat4, readFile as readFile2 } from "fs/promises";
2063
- import { ERROR_CODES as ERROR_CODES17, SpotPatchError as SpotPatchError17 } from "@spotpatch/shared";
2378
+ import { createHash as createHash3 } from "crypto";
2379
+ import { lstat as lstat5, readFile as readFile3 } from "fs/promises";
2380
+ import { ERROR_CODES as ERROR_CODES18, SpotPatchError as SpotPatchError18 } from "@spotpatch/shared";
2064
2381
  var privateChanges = /* @__PURE__ */ new WeakMap();
2065
2382
  var DELETED_HASH = "<deleted>";
2066
2383
  function createPreparedAgentChange(options) {
2067
2384
  const touchedPaths = Object.freeze(
2068
2385
  options.result.files.map((file) => file.relativePath)
2069
2386
  );
2070
- if (options.expectedHashes.size !== touchedPaths.length || touchedPaths.some((relativePath) => !options.expectedHashes.has(relativePath))) {
2071
- throw new SpotPatchError17(ERROR_CODES17.INTERNAL_ERROR);
2387
+ if (options.baselineHashes.size !== touchedPaths.length || options.expectedHashes.size !== touchedPaths.length || touchedPaths.some(
2388
+ (relativePath) => !options.baselineHashes.has(relativePath) || !options.expectedHashes.has(relativePath)
2389
+ )) {
2390
+ throw new SpotPatchError18(ERROR_CODES18.INTERNAL_ERROR);
2072
2391
  }
2073
2392
  const change = Object.freeze({
2074
2393
  kind: "prepared-agent-change",
@@ -2078,6 +2397,7 @@ function createPreparedAgentChange(options) {
2078
2397
  });
2079
2398
  privateChanges.set(change, {
2080
2399
  baselineHead: options.baselineHead,
2400
+ baselineHashes: new Map(options.baselineHashes),
2081
2401
  diff: options.result.diff,
2082
2402
  expectedHashes: new Map(options.expectedHashes),
2083
2403
  root: options.root,
@@ -2089,28 +2409,30 @@ function createPreparedAgentChange(options) {
2089
2409
  function requirePrivateChange(change) {
2090
2410
  const privateChange = privateChanges.get(change);
2091
2411
  if (privateChange === void 0) {
2092
- throw new SpotPatchError17(ERROR_CODES17.INTERNAL_ERROR);
2412
+ throw new SpotPatchError18(ERROR_CODES18.INTERNAL_ERROR);
2093
2413
  }
2094
2414
  return privateChange;
2095
2415
  }
2096
- async function currentHead(root) {
2097
- return (await runGitCommand({
2098
- cwd: root,
2099
- args: ["rev-parse", "--verify", "HEAD"],
2100
- errorCode: ERROR_CODES17.APPLY_CONFLICT
2101
- })).trim();
2416
+ async function assertWorkspaceOperationSafe(root, expectedHead) {
2417
+ const inspection = await inspectGitWorkspace(root).catch(() => {
2418
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2419
+ });
2420
+ const blockingOperation = inspection.health.errorCode === ERROR_CODES18.WORKTREE_OPERATION_IN_PROGRESS || inspection.health.errorCode === ERROR_CODES18.WORKTREE_CONFLICTED;
2421
+ if (inspection.head !== expectedHead || blockingOperation) {
2422
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2423
+ }
2102
2424
  }
2103
2425
  async function fileHash(root, relativePath) {
2104
2426
  const normalized = assertAgentPathAllowed(relativePath);
2105
2427
  const absolutePath = await resolveWritableAgentPath(root, normalized);
2106
- const metadata = await lstat4(absolutePath).catch(() => void 0);
2428
+ const metadata = await lstat5(absolutePath).catch(() => void 0);
2107
2429
  if (metadata === void 0) {
2108
2430
  return DELETED_HASH;
2109
2431
  }
2110
2432
  if (!metadata.isFile() || metadata.isSymbolicLink()) {
2111
- throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2433
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2112
2434
  }
2113
- return createHash2("sha256").update(await readFile2(absolutePath)).digest("hex");
2435
+ return createHash3("sha256").update(await readFile3(absolutePath)).digest("hex");
2114
2436
  }
2115
2437
  async function captureAgentFileHashes(root, paths) {
2116
2438
  const entries = await Promise.all(
@@ -2126,34 +2448,38 @@ function hashesMatch(expected, actual) {
2126
2448
  async function applyPreparedAgentChange(change) {
2127
2449
  const privateChange = requirePrivateChange(change);
2128
2450
  if (privateChange.state !== "prepared" || !change.validationPassed || privateChange.diff.length === 0) {
2129
- throw new SpotPatchError17(
2130
- change.validationPassed ? ERROR_CODES17.APPLY_CONFLICT : ERROR_CODES17.VALIDATION_FAILED
2451
+ throw new SpotPatchError18(
2452
+ change.validationPassed ? ERROR_CODES18.APPLY_CONFLICT : ERROR_CODES18.VALIDATION_FAILED
2131
2453
  );
2132
2454
  }
2133
2455
  privateChange.state = "applying";
2134
2456
  try {
2135
- await assertCleanGitBaseline({
2136
- root: privateChange.root,
2137
- expectedHead: privateChange.baselineHead
2138
- });
2457
+ await assertWorkspaceOperationSafe(privateChange.root, privateChange.baselineHead);
2458
+ const currentHashes = await captureAgentFileHashes(
2459
+ privateChange.root,
2460
+ privateChange.touchedPaths
2461
+ );
2462
+ if (!hashesMatch(privateChange.baselineHashes, currentHashes)) {
2463
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2464
+ }
2139
2465
  await runGitCommand({
2140
2466
  cwd: privateChange.root,
2141
2467
  args: ["apply", "--check", "--whitespace=error-all", "-"],
2142
2468
  stdin: privateChange.diff,
2143
- errorCode: ERROR_CODES17.APPLY_CONFLICT
2469
+ errorCode: ERROR_CODES18.APPLY_CONFLICT
2144
2470
  });
2145
2471
  await runGitCommand({
2146
2472
  cwd: privateChange.root,
2147
2473
  args: ["apply", "--whitespace=error-all", "-"],
2148
2474
  stdin: privateChange.diff,
2149
- errorCode: ERROR_CODES17.APPLY_CONFLICT
2475
+ errorCode: ERROR_CODES18.APPLY_CONFLICT
2150
2476
  });
2151
2477
  const appliedHashes = await captureAgentFileHashes(
2152
2478
  privateChange.root,
2153
2479
  privateChange.touchedPaths
2154
2480
  );
2155
2481
  if (!hashesMatch(privateChange.expectedHashes, appliedHashes)) {
2156
- throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2482
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2157
2483
  }
2158
2484
  privateChange.appliedHashes = appliedHashes;
2159
2485
  privateChange.state = "applied";
@@ -2165,34 +2491,39 @@ async function applyPreparedAgentChange(change) {
2165
2491
  async function revertPreparedAgentChange(change) {
2166
2492
  const privateChange = requirePrivateChange(change);
2167
2493
  if (privateChange.state !== "applied" || privateChange.appliedHashes === void 0) {
2168
- throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2494
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2169
2495
  }
2170
2496
  privateChange.state = "reverting";
2171
2497
  try {
2172
- if (await currentHead(privateChange.root) !== privateChange.baselineHead) {
2173
- throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2174
- }
2498
+ await assertWorkspaceOperationSafe(privateChange.root, privateChange.baselineHead);
2175
2499
  const currentHashes = await captureAgentFileHashes(
2176
2500
  privateChange.root,
2177
2501
  privateChange.touchedPaths
2178
2502
  );
2179
2503
  for (const [relativePath, expectedHash] of privateChange.appliedHashes) {
2180
2504
  if (currentHashes.get(relativePath) !== expectedHash) {
2181
- throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2505
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2182
2506
  }
2183
2507
  }
2184
2508
  await runGitCommand({
2185
2509
  cwd: privateChange.root,
2186
2510
  args: ["apply", "--reverse", "--check", "--whitespace=error-all", "-"],
2187
2511
  stdin: privateChange.diff,
2188
- errorCode: ERROR_CODES17.APPLY_CONFLICT
2512
+ errorCode: ERROR_CODES18.APPLY_CONFLICT
2189
2513
  });
2190
2514
  await runGitCommand({
2191
2515
  cwd: privateChange.root,
2192
2516
  args: ["apply", "--reverse", "--whitespace=error-all", "-"],
2193
2517
  stdin: privateChange.diff,
2194
- errorCode: ERROR_CODES17.APPLY_CONFLICT
2518
+ errorCode: ERROR_CODES18.APPLY_CONFLICT
2195
2519
  });
2520
+ const revertedHashes = await captureAgentFileHashes(
2521
+ privateChange.root,
2522
+ privateChange.touchedPaths
2523
+ );
2524
+ if (!hashesMatch(privateChange.baselineHashes, revertedHashes)) {
2525
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2526
+ }
2196
2527
  privateChange.state = "reverted";
2197
2528
  } catch (error) {
2198
2529
  privateChange.state = "applied";
@@ -2374,11 +2705,11 @@ function isRetryableToolFailure(result) {
2374
2705
  return false;
2375
2706
  }
2376
2707
  const candidate = output;
2377
- return candidate.errorCode === ERROR_CODES18.PATCH_REJECTED && candidate.retryable === true;
2708
+ return candidate.errorCode === ERROR_CODES19.PATCH_REJECTED && candidate.retryable === true;
2378
2709
  }
2379
2710
  function throwIfCancelled(signal) {
2380
2711
  if (signal.aborted) {
2381
- throw new SpotPatchError18(ERROR_CODES18.AGENT_CANCELLED);
2712
+ throw new SpotPatchError19(ERROR_CODES19.AGENT_CANCELLED);
2382
2713
  }
2383
2714
  }
2384
2715
  function linkSignal(source, target) {
@@ -2416,6 +2747,7 @@ async function executeAgentChange(options) {
2416
2747
  worktree = await createIsolatedGitWorktree({
2417
2748
  root: options.root,
2418
2749
  signal: controller.signal,
2750
+ workingTreeMode: options.workingTreeMode ?? "require-clean",
2419
2751
  ...options.temporaryBase === void 0 ? {} : { temporaryBase: options.temporaryBase }
2420
2752
  });
2421
2753
  options.callbacks?.onPhase?.(
@@ -2457,7 +2789,7 @@ async function executeAgentChange(options) {
2457
2789
  }
2458
2790
  toolCallCount += response.toolCalls.length;
2459
2791
  if (toolCallCount > options.execution.limits.maxToolCalls) {
2460
- throw new SpotPatchError18(ERROR_CODES18.AGENT_LIMIT_EXCEEDED);
2792
+ throw new SpotPatchError19(ERROR_CODES19.AGENT_LIMIT_EXCEEDED);
2461
2793
  }
2462
2794
  const results = [];
2463
2795
  for (const call of response.toolCalls) {
@@ -2492,7 +2824,7 @@ async function executeAgentChange(options) {
2492
2824
  pendingResults = Object.freeze(results);
2493
2825
  }
2494
2826
  if (summary === void 0) {
2495
- throw new SpotPatchError18(ERROR_CODES18.AGENT_LIMIT_EXCEEDED);
2827
+ throw new SpotPatchError19(ERROR_CODES19.AGENT_LIMIT_EXCEEDED);
2496
2828
  }
2497
2829
  options.callbacks?.onPhase?.(
2498
2830
  Object.freeze({
@@ -2525,7 +2857,7 @@ async function executeAgentChange(options) {
2525
2857
  controller.signal
2526
2858
  );
2527
2859
  if (afterCheck.diff !== initialChangeSet.diff) {
2528
- throw new SpotPatchError18(ERROR_CODES18.VALIDATION_FAILED);
2860
+ throw new SpotPatchError19(ERROR_CODES19.VALIDATION_FAILED);
2529
2861
  }
2530
2862
  }
2531
2863
  const validationPassed = finalChecks.every((check) => check.status === "passed");
@@ -2541,9 +2873,14 @@ async function executeAgentChange(options) {
2541
2873
  worktree.root,
2542
2874
  initialChangeSet.touchedPaths
2543
2875
  );
2876
+ const baselineHashes = await captureAgentFileHashes(
2877
+ worktree.baseline.root,
2878
+ initialChangeSet.touchedPaths
2879
+ );
2544
2880
  return createPreparedAgentChange({
2545
2881
  autoApplyEligible,
2546
2882
  baselineHead: worktree.baseline.head,
2883
+ baselineHashes,
2547
2884
  expectedHashes,
2548
2885
  result,
2549
2886
  root: worktree.baseline.root,
@@ -2551,15 +2888,15 @@ async function executeAgentChange(options) {
2551
2888
  });
2552
2889
  } catch (error) {
2553
2890
  if (options.signal.aborted) {
2554
- throw new SpotPatchError18(ERROR_CODES18.AGENT_CANCELLED);
2891
+ throw new SpotPatchError19(ERROR_CODES19.AGENT_CANCELLED);
2555
2892
  }
2556
2893
  if (hasJobTimedOut()) {
2557
- throw new SpotPatchError18(ERROR_CODES18.AGENT_LIMIT_EXCEEDED);
2894
+ throw new SpotPatchError19(ERROR_CODES19.AGENT_LIMIT_EXCEEDED);
2558
2895
  }
2559
- if (error instanceof SpotPatchError18) {
2896
+ if (error instanceof SpotPatchError19) {
2560
2897
  throw error;
2561
2898
  }
2562
- throw new SpotPatchError18(ERROR_CODES18.INTERNAL_ERROR);
2899
+ throw new SpotPatchError19(ERROR_CODES19.INTERNAL_ERROR);
2563
2900
  } finally {
2564
2901
  clearTimeout(timeout);
2565
2902
  unlink();
@@ -2569,15 +2906,15 @@ async function executeAgentChange(options) {
2569
2906
 
2570
2907
  // src/provider/capability-probe.ts
2571
2908
  import {
2572
- ERROR_CODES as ERROR_CODES19,
2573
- SpotPatchError as SpotPatchError19
2909
+ ERROR_CODES as ERROR_CODES20,
2910
+ SpotPatchError as SpotPatchError20
2574
2911
  } from "@spotpatch/shared";
2575
2912
  var PROBE_TOOL_NAME = "spotpatch_capability_probe";
2576
2913
  var PROBE_TOKEN = "spotpatch-ready-v1";
2577
2914
  async function probeProviderCapability(options) {
2578
2915
  const model = options.provider.models[options.modelProfileId];
2579
2916
  if (model === void 0) {
2580
- throw new SpotPatchError19(ERROR_CODES19.MODEL_NOT_ALLOWED);
2917
+ throw new SpotPatchError20(ERROR_CODES20.MODEL_NOT_ALLOWED);
2581
2918
  }
2582
2919
  const credential = options.credential ?? resolveProviderCredential(options.provider.apiKeyEnv, options.environment);
2583
2920
  const session = createOpenAICompatibleProviderSession({
@@ -2606,7 +2943,7 @@ async function probeProviderCapability(options) {
2606
2943
  const first = await session.next(void 0, options.signal);
2607
2944
  const probeCall = first.toolCalls[0];
2608
2945
  if (first.toolCalls.length !== 1 || probeCall?.name !== PROBE_TOOL_NAME || probeCall.arguments.token !== PROBE_TOKEN) {
2609
- throw new SpotPatchError19(ERROR_CODES19.MODEL_TOOL_CALL_UNSUPPORTED);
2946
+ throw new SpotPatchError20(ERROR_CODES20.MODEL_TOOL_CALL_UNSUPPORTED);
2610
2947
  }
2611
2948
  const second = await session.next(
2612
2949
  Object.freeze([
@@ -2618,7 +2955,7 @@ async function probeProviderCapability(options) {
2618
2955
  options.signal
2619
2956
  );
2620
2957
  if (second.toolCalls.length !== 0 || second.finalText.trim().length === 0) {
2621
- throw new SpotPatchError19(ERROR_CODES19.MODEL_TOOL_CALL_UNSUPPORTED);
2958
+ throw new SpotPatchError20(ERROR_CODES20.MODEL_TOOL_CALL_UNSUPPORTED);
2622
2959
  }
2623
2960
  return Object.freeze({
2624
2961
  providerProfileId: options.provider.id,
@@ -2640,6 +2977,7 @@ export {
2640
2977
  createOpenAICompatibleProviderSession,
2641
2978
  createProviderCredential,
2642
2979
  executeAgentChange,
2980
+ inspectAgentWorkspace,
2643
2981
  probeProviderCapability,
2644
2982
  resolveProviderCredential,
2645
2983
  revertPreparedAgentChange