@spotpatch/agent 1.1.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
@@ -29,9 +29,27 @@ function parseJsonRecord(value) {
29
29
  }
30
30
  function parseToolArguments(value) {
31
31
  if (typeof value !== "string") {
32
- throw new SpotPatchError(ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED);
32
+ throw new SpotPatchError(ERROR_CODES.TOOL_ARGUMENTS_INVALID);
33
+ }
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(value);
37
+ } catch {
38
+ throw new SpotPatchError(ERROR_CODES.TOOL_ARGUMENTS_INVALID);
39
+ }
40
+ if (!isRecord(parsed)) {
41
+ throw new SpotPatchError(ERROR_CODES.TOOL_ARGUMENTS_INVALID);
42
+ }
43
+ return parsed;
44
+ }
45
+ function assertUniqueToolCallIds(calls) {
46
+ const ids = /* @__PURE__ */ new Set();
47
+ for (const call of calls) {
48
+ if (ids.has(call.id)) {
49
+ throw new SpotPatchError(ERROR_CODES.TOOL_CALL_ID_CONFLICT);
50
+ }
51
+ ids.add(call.id);
33
52
  }
34
- return parseJsonRecord(value);
35
53
  }
36
54
  function requireString(record, field) {
37
55
  const value = record[field];
@@ -41,6 +59,7 @@ function requireString(record, field) {
41
59
  return value;
42
60
  }
43
61
  function validateToolResults(pendingCalls, results) {
62
+ assertUniqueToolCallIds(pendingCalls);
44
63
  if (pendingCalls.length === 0) {
45
64
  if (results !== void 0 && results.length > 0) {
46
65
  throw new SpotPatchError(ERROR_CODES.INTERNAL_ERROR);
@@ -441,6 +460,7 @@ function parseChatEvents(events) {
441
460
  }
442
461
  const finalText = content.join("");
443
462
  const toolCalls = finalizeToolCalls(calls);
463
+ assertUniqueToolCallIds(toolCalls);
444
464
  if (toolCalls.length === 0 && finalText.trim().length === 0) {
445
465
  throw new SpotPatchError5(ERROR_CODES5.PROVIDER_PROTOCOL_UNSUPPORTED);
446
466
  }
@@ -537,7 +557,7 @@ function collectFunctionCall(item, calls) {
537
557
  });
538
558
  const existing = calls.get(id);
539
559
  if (existing !== void 0 && (existing.name !== call.name || JSON.stringify(existing.arguments) !== JSON.stringify(call.arguments))) {
540
- throw new SpotPatchError6(ERROR_CODES6.PROVIDER_PROTOCOL_UNSUPPORTED);
560
+ throw new SpotPatchError6(ERROR_CODES6.TOOL_CALL_ID_CONFLICT);
541
561
  }
542
562
  calls.set(id, call);
543
563
  }
@@ -716,7 +736,7 @@ var PROTECTED_FILE_NAMES = /* @__PURE__ */ new Set([
716
736
  "yarn.lock"
717
737
  ]);
718
738
  function deny() {
719
- throw new SpotPatchError8(ERROR_CODES8.TOOL_DENIED);
739
+ throw new SpotPatchError8(ERROR_CODES8.TOOL_PATH_DENIED);
720
740
  }
721
741
  function hasControlCharacter(value) {
722
742
  for (let index = 0; index < value.length; index += 1) {
@@ -821,13 +841,13 @@ async function readAgentTextFile(root, relativePath, maximumBytes) {
821
841
  }
822
842
  const bytes = await readFile(absolutePath);
823
843
  if (bytes.includes(0)) {
824
- throw new SpotPatchError9(ERROR_CODES9.TOOL_DENIED);
844
+ throw new SpotPatchError9(ERROR_CODES9.TOOL_PATH_DENIED);
825
845
  }
826
846
  let content;
827
847
  try {
828
848
  content = utf8Decoder.decode(bytes);
829
849
  } catch {
830
- throw new SpotPatchError9(ERROR_CODES9.TOOL_DENIED);
850
+ throw new SpotPatchError9(ERROR_CODES9.TOOL_PATH_DENIED);
831
851
  }
832
852
  return Object.freeze({ content, relativePath, size: metadata.size });
833
853
  }
@@ -840,7 +860,7 @@ function encodeUtf8Text(content, includeByteOrderMark) {
840
860
  }
841
861
  async function writeAgentTextFileIfContentMatches(root, relativePath, expectedContent, nextContent, maximumBytes) {
842
862
  if (nextContent.includes("\0")) {
843
- throw new SpotPatchError9(ERROR_CODES9.TOOL_DENIED);
863
+ throw new SpotPatchError9(ERROR_CODES9.TOOL_ARGUMENTS_INVALID);
844
864
  }
845
865
  const absolutePath = await resolveExistingAgentPath(root, relativePath);
846
866
  const [metadata, currentBytes] = await Promise.all([
@@ -854,7 +874,7 @@ async function writeAgentTextFileIfContentMatches(root, relativePath, expectedCo
854
874
  try {
855
875
  currentContent = utf8Decoder.decode(currentBytes);
856
876
  } catch {
857
- throw new SpotPatchError9(ERROR_CODES9.TOOL_DENIED);
877
+ throw new SpotPatchError9(ERROR_CODES9.TOOL_PATH_DENIED);
858
878
  }
859
879
  if (currentBytes.includes(0) || currentContent !== expectedContent) {
860
880
  throw new SpotPatchError9(ERROR_CODES9.PATCH_REJECTED);
@@ -1113,7 +1133,7 @@ async function runConfiguredCheck(options) {
1113
1133
  function requireConfiguredCheck(checkId, checks) {
1114
1134
  const check = checks[checkId];
1115
1135
  if (check === void 0) {
1116
- throw new SpotPatchError10(ERROR_CODES10.TOOL_DENIED);
1136
+ throw new SpotPatchError10(ERROR_CODES10.TOOL_ARGUMENTS_INVALID);
1117
1137
  }
1118
1138
  return check;
1119
1139
  }
@@ -1308,9 +1328,16 @@ async function applyAgentPatch(worktreeRoot, patch, limits, signal) {
1308
1328
  throw new SpotPatchError13(ERROR_CODES13.AGENT_LIMIT_EXCEEDED);
1309
1329
  }
1310
1330
  await Promise.all(
1311
- files.map(
1312
- async (file) => resolveWritableAgentPath(worktreeRoot, file.relativePath)
1313
- )
1331
+ files.map(async (file) => {
1332
+ await resolveWritableAgentPath(worktreeRoot, file.relativePath);
1333
+ if (file.kind !== "added") {
1334
+ await readAgentTextFile(
1335
+ worktreeRoot,
1336
+ file.relativePath,
1337
+ limits.maxReadBytesPerFile
1338
+ );
1339
+ }
1340
+ })
1314
1341
  );
1315
1342
  await runGitCommand({
1316
1343
  cwd: worktreeRoot,
@@ -1424,7 +1451,7 @@ var MAX_DISCOVERED_FILES = 2e4;
1424
1451
  var TEXT_SAMPLE_BYTES = 8192;
1425
1452
  function compileGlob(glob) {
1426
1453
  if (glob.length === 0 || glob.length > 256 || glob.includes("\0") || glob.includes("\\") || glob.startsWith("/") || ["[", "]", "{", "}", "(", ")", "!"].some((character) => glob.includes(character)) || glob.split("/").some((segment) => segment === "..")) {
1427
- throw new SpotPatchError14(ERROR_CODES14.TOOL_DENIED);
1454
+ throw new SpotPatchError14(ERROR_CODES14.TOOL_ARGUMENTS_INVALID);
1428
1455
  }
1429
1456
  let expression = "^";
1430
1457
  for (let index = 0; index < glob.length; index += 1) {
@@ -1663,7 +1690,7 @@ var runCheckSchema = z.strictObject({
1663
1690
  checkId: z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
1664
1691
  });
1665
1692
  function invalidTool() {
1666
- throw new SpotPatchError15(ERROR_CODES15.TOOL_DENIED);
1693
+ throw new SpotPatchError15(ERROR_CODES15.TOOL_ARGUMENTS_INVALID);
1667
1694
  }
1668
1695
  function parseArguments(schema, value) {
1669
1696
  const parsed = schema.safeParse(value);
@@ -1720,8 +1747,16 @@ function retryableWriteRejection(reason, guidance) {
1720
1747
  guidance
1721
1748
  });
1722
1749
  }
1750
+ function retryableArgumentsRejection() {
1751
+ return Object.freeze({
1752
+ errorCode: ERROR_CODES15.TOOL_ARGUMENTS_INVALID,
1753
+ retryable: true,
1754
+ reason: "ARGUMENTS_DO_NOT_MATCH_CONTRACT",
1755
+ guidance: "No files changed. Retry once with a new tool call ID and only the declared fields and value types."
1756
+ });
1757
+ }
1723
1758
  function createAgentToolExecutor(options) {
1724
- const cache = /* @__PURE__ */ new Map();
1759
+ const cacheByTurn = /* @__PURE__ */ new Map();
1725
1760
  const touchedPaths = /* @__PURE__ */ new Set();
1726
1761
  const executeUncached = async (call, signal) => {
1727
1762
  switch (call.name) {
@@ -1937,20 +1972,32 @@ function createAgentToolExecutor(options) {
1937
1972
  }
1938
1973
  };
1939
1974
  return Object.freeze({
1940
- async execute(call, signal) {
1975
+ async execute(call, scope, signal) {
1976
+ if (!Number.isSafeInteger(scope.turn) || scope.turn < 1) {
1977
+ throw new SpotPatchError15(ERROR_CODES15.INTERNAL_ERROR);
1978
+ }
1979
+ const turnCache = cacheByTurn.get(scope.turn) ?? /* @__PURE__ */ new Map();
1980
+ cacheByTurn.set(scope.turn, turnCache);
1941
1981
  const signature = `${call.name}\0${JSON.stringify(call.arguments)}`;
1942
- const cached = cache.get(call.id);
1982
+ const cached = turnCache.get(call.id);
1943
1983
  if (cached !== void 0) {
1944
1984
  if (cached.signature !== signature) {
1945
- return invalidTool();
1985
+ throw new SpotPatchError15(ERROR_CODES15.TOOL_CALL_ID_CONFLICT);
1946
1986
  }
1947
1987
  return cached.result;
1948
1988
  }
1949
- const result = Object.freeze({
1950
- toolCallId: call.id,
1951
- output: await executeUncached(call, signal)
1952
- });
1953
- cache.set(call.id, Object.freeze({ signature, result }));
1989
+ let output;
1990
+ try {
1991
+ output = await executeUncached(call, signal);
1992
+ } catch (error) {
1993
+ if (error instanceof SpotPatchError15 && error.code === ERROR_CODES15.TOOL_ARGUMENTS_INVALID) {
1994
+ output = retryableArgumentsRejection();
1995
+ } else {
1996
+ throw error;
1997
+ }
1998
+ }
1999
+ const result = Object.freeze({ toolCallId: call.id, output });
2000
+ turnCache.set(call.id, Object.freeze({ signature, result }));
1954
2001
  return result;
1955
2002
  },
1956
2003
  touchedPaths() {
@@ -1960,67 +2007,354 @@ function createAgentToolExecutor(options) {
1960
2007
  }
1961
2008
 
1962
2009
  // src/worktree/git-worktree.ts
1963
- import { lstat as lstat3, mkdtemp, realpath as realpath2, rm as rm2 } from "fs/promises";
2010
+ import {
2011
+ copyFile,
2012
+ lstat as lstat4,
2013
+ mkdir,
2014
+ mkdtemp,
2015
+ readFile as readFile2,
2016
+ realpath as realpath3,
2017
+ rm as rm2
2018
+ } from "fs/promises";
2019
+ import { createHash as createHash2 } from "crypto";
1964
2020
  import os from "os";
2021
+ import path6 from "path";
2022
+ import {
2023
+ ERROR_CODES as ERROR_CODES17,
2024
+ SpotPatchError as SpotPatchError17
2025
+ } from "@spotpatch/shared";
2026
+
2027
+ // src/worktree/workspace-health.ts
2028
+ import { lstat as lstat3, realpath as realpath2 } from "fs/promises";
1965
2029
  import path5 from "path";
1966
- import { ERROR_CODES as ERROR_CODES16, SpotPatchError as SpotPatchError16 } from "@spotpatch/shared";
1967
- async function assertCleanGitBaseline(options) {
1968
- const root = await realpath2(options.root).catch(() => {
1969
- throw new SpotPatchError16(ERROR_CODES16.WORKTREE_DIRTY);
2030
+ import {
2031
+ AGENT_WORKSPACE_SNAPSHOT_LIMITS,
2032
+ ERROR_CODES as ERROR_CODES16,
2033
+ SpotPatchError as SpotPatchError16
2034
+ } from "@spotpatch/shared";
2035
+ var CONFLICTED_STATUSES = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
2036
+ var OPERATION_MARKERS = Object.freeze([
2037
+ "MERGE_HEAD",
2038
+ "CHERRY_PICK_HEAD",
2039
+ "REVERT_HEAD",
2040
+ "rebase-apply",
2041
+ "rebase-merge"
2042
+ ]);
2043
+ function emptyChanges() {
2044
+ return Object.freeze({
2045
+ staged: 0,
2046
+ unstaged: 0,
2047
+ untracked: 0,
2048
+ conflicted: 0,
2049
+ total: 0
2050
+ });
2051
+ }
2052
+ function blockedHealth(errorCode) {
2053
+ return Object.freeze({
2054
+ state: "blocked",
2055
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
2056
+ changes: emptyChanges(),
2057
+ canIncludeLocalChanges: false,
2058
+ errorCode
2059
+ });
2060
+ }
2061
+ function parsePorcelainStatus(value) {
2062
+ const records = value.split("\0");
2063
+ const untrackedPaths = [];
2064
+ let staged = 0;
2065
+ let unstaged = 0;
2066
+ let conflicted = 0;
2067
+ let total = 0;
2068
+ for (let index = 0; index < records.length; index += 1) {
2069
+ const record = records[index] ?? "";
2070
+ if (record.length === 0) {
2071
+ continue;
2072
+ }
2073
+ if (record.length < 4 || record[2] !== " ") {
2074
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2075
+ }
2076
+ const status = record.slice(0, 2);
2077
+ const relativePath = record.slice(3);
2078
+ if (relativePath.length === 0) {
2079
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2080
+ }
2081
+ total += 1;
2082
+ if (status === "??") {
2083
+ untrackedPaths.push(relativePath);
2084
+ continue;
2085
+ }
2086
+ const indexStatus = status[0] ?? " ";
2087
+ const worktreeStatus = status[1] ?? " ";
2088
+ if (CONFLICTED_STATUSES.has(status) || indexStatus === "U" || worktreeStatus === "U") {
2089
+ conflicted += 1;
2090
+ }
2091
+ if (indexStatus !== " ") {
2092
+ staged += 1;
2093
+ }
2094
+ if (worktreeStatus !== " ") {
2095
+ unstaged += 1;
2096
+ }
2097
+ if (indexStatus === "R" || indexStatus === "C") {
2098
+ index += 1;
2099
+ if ((records[index] ?? "").length === 0) {
2100
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2101
+ }
2102
+ }
2103
+ }
2104
+ return Object.freeze({
2105
+ changes: Object.freeze({
2106
+ staged,
2107
+ unstaged,
2108
+ untracked: untrackedPaths.length,
2109
+ conflicted,
2110
+ total
2111
+ }),
2112
+ untrackedPaths: Object.freeze(untrackedPaths)
1970
2113
  });
1971
- const topLevel = (await runGitCommand({
2114
+ }
2115
+ async function operationInProgress(root, signal) {
2116
+ for (const marker of OPERATION_MARKERS) {
2117
+ const markerPath = (await runGitCommand({
2118
+ cwd: root,
2119
+ args: ["rev-parse", "--git-path", marker],
2120
+ errorCode: ERROR_CODES16.WORKTREE_NOT_REPOSITORY,
2121
+ ...signal === void 0 ? {} : { signal }
2122
+ })).trim();
2123
+ if (await lstat3(path5.resolve(root, markerPath)).catch(() => void 0) !== void 0) {
2124
+ return true;
2125
+ }
2126
+ }
2127
+ return false;
2128
+ }
2129
+ async function inspectUntrackedFiles(root, relativePaths) {
2130
+ if (relativePaths.length > AGENT_WORKSPACE_SNAPSHOT_LIMITS.maxUntrackedFiles) {
2131
+ return ERROR_CODES16.WORKTREE_LOCAL_CHANGES_TOO_LARGE;
2132
+ }
2133
+ let totalBytes = 0;
2134
+ for (const relativePath of relativePaths) {
2135
+ const absolutePath = path5.resolve(root, relativePath);
2136
+ const relative = path5.relative(root, absolutePath);
2137
+ if (relative === ".." || relative.startsWith(`..${path5.sep}`) || path5.isAbsolute(relative)) {
2138
+ return ERROR_CODES16.WORKTREE_UNTRACKED_UNSUPPORTED;
2139
+ }
2140
+ const metadata = await lstat3(absolutePath).catch(() => void 0);
2141
+ if (metadata === void 0 || !metadata.isFile() || metadata.isSymbolicLink()) {
2142
+ return ERROR_CODES16.WORKTREE_UNTRACKED_UNSUPPORTED;
2143
+ }
2144
+ totalBytes += metadata.size;
2145
+ if (totalBytes > AGENT_WORKSPACE_SNAPSHOT_LIMITS.maxUntrackedBytes) {
2146
+ return ERROR_CODES16.WORKTREE_LOCAL_CHANGES_TOO_LARGE;
2147
+ }
2148
+ }
2149
+ return void 0;
2150
+ }
2151
+ async function inspectGitWorkspace(rootValue, signal) {
2152
+ const root = await realpath2(rootValue).catch(() => {
2153
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_NOT_REPOSITORY);
2154
+ });
2155
+ const topLevelResult = await runRawGitCommand({
1972
2156
  cwd: root,
1973
2157
  args: ["rev-parse", "--show-toplevel"],
1974
- errorCode: ERROR_CODES16.WORKTREE_DIRTY,
1975
- ...options.signal === void 0 ? {} : { signal: options.signal }
1976
- })).trim();
2158
+ ...signal === void 0 ? {} : { signal }
2159
+ });
2160
+ if (topLevelResult.exitCode !== 0 || topLevelResult.cancelled || topLevelResult.timedOut) {
2161
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_NOT_REPOSITORY);
2162
+ }
2163
+ const topLevel = topLevelResult.stdout.trim();
1977
2164
  if (!samePath(root, topLevel)) {
1978
- throw new SpotPatchError16(ERROR_CODES16.WORKTREE_DIRTY);
2165
+ throw new SpotPatchError16(ERROR_CODES16.WORKTREE_NOT_REPOSITORY);
1979
2166
  }
1980
2167
  const head = (await runGitCommand({
1981
2168
  cwd: root,
1982
2169
  args: ["rev-parse", "--verify", "HEAD"],
1983
- errorCode: ERROR_CODES16.WORKTREE_DIRTY,
1984
- ...options.signal === void 0 ? {} : { signal: options.signal }
2170
+ errorCode: ERROR_CODES16.WORKTREE_NOT_REPOSITORY,
2171
+ ...signal === void 0 ? {} : { signal }
1985
2172
  })).trim();
1986
- if (options.expectedHead !== void 0 && head !== options.expectedHead) {
1987
- throw new SpotPatchError16(ERROR_CODES16.APPLY_CONFLICT);
2173
+ const parsed = parsePorcelainStatus(
2174
+ await runGitCommand({
2175
+ cwd: root,
2176
+ args: ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
2177
+ errorCode: ERROR_CODES16.WORKTREE_NOT_REPOSITORY,
2178
+ ...signal === void 0 ? {} : { signal }
2179
+ })
2180
+ );
2181
+ const operationActive = await operationInProgress(root, signal);
2182
+ const untrackedError = await inspectUntrackedFiles(root, parsed.untrackedPaths);
2183
+ const errorCode = operationActive ? ERROR_CODES16.WORKTREE_OPERATION_IN_PROGRESS : parsed.changes.conflicted > 0 ? ERROR_CODES16.WORKTREE_CONFLICTED : untrackedError;
2184
+ const health = Object.freeze({
2185
+ state: errorCode !== void 0 ? "blocked" : parsed.changes.total === 0 ? "ready" : "consent-required",
2186
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
2187
+ changes: parsed.changes,
2188
+ canIncludeLocalChanges: errorCode === void 0 && parsed.changes.total > 0,
2189
+ ...errorCode === void 0 ? {} : { errorCode }
2190
+ });
2191
+ return Object.freeze({
2192
+ root,
2193
+ head,
2194
+ health,
2195
+ untrackedPaths: parsed.untrackedPaths
2196
+ });
2197
+ }
2198
+ async function inspectAgentWorkspace(root, signal) {
2199
+ try {
2200
+ return (await inspectGitWorkspace(root, signal)).health;
2201
+ } catch (error) {
2202
+ if (error instanceof SpotPatchError16) {
2203
+ return blockedHealth(error.code);
2204
+ }
2205
+ return blockedHealth(ERROR_CODES16.INTERNAL_ERROR);
1988
2206
  }
1989
- const status = await runGitCommand({
1990
- cwd: root,
1991
- args: ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
1992
- errorCode: ERROR_CODES16.WORKTREE_DIRTY,
1993
- ...options.signal === void 0 ? {} : { signal: options.signal }
2207
+ }
2208
+
2209
+ // src/worktree/git-worktree.ts
2210
+ function workspacePath(root, relativePath) {
2211
+ const candidate = path6.resolve(root, relativePath);
2212
+ const relative = path6.relative(root, candidate);
2213
+ if (relative === ".." || relative.startsWith(`..${path6.sep}`) || path6.isAbsolute(relative)) {
2214
+ throw new SpotPatchError17(ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2215
+ }
2216
+ return candidate;
2217
+ }
2218
+ async function fileDigest(filePath) {
2219
+ return createHash2("sha256").update(await readFile2(filePath)).digest("hex");
2220
+ }
2221
+ async function copyUntrackedFiles(sourceRoot, worktreeRoot, relativePaths) {
2222
+ for (const relativePath of relativePaths) {
2223
+ const sourcePath = workspacePath(sourceRoot, relativePath);
2224
+ const targetPath = workspacePath(worktreeRoot, relativePath);
2225
+ const metadata = await lstat4(sourcePath).catch(() => void 0);
2226
+ if (metadata === void 0 || !metadata.isFile() || metadata.isSymbolicLink()) {
2227
+ throw new SpotPatchError17(ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2228
+ }
2229
+ await mkdir(path6.dirname(targetPath), { recursive: true });
2230
+ await copyFile(sourcePath, targetPath);
2231
+ const [sourceDigest, targetDigest] = await Promise.all([
2232
+ fileDigest(sourcePath),
2233
+ fileDigest(targetPath)
2234
+ ]).catch(() => {
2235
+ throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2236
+ });
2237
+ if (sourceDigest !== targetDigest) {
2238
+ throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2239
+ }
2240
+ }
2241
+ }
2242
+ async function assertUntrackedFilesUnchanged(sourceRoot, worktreeRoot, relativePaths) {
2243
+ for (const relativePath of relativePaths) {
2244
+ const sourcePath = workspacePath(sourceRoot, relativePath);
2245
+ const targetPath = workspacePath(worktreeRoot, relativePath);
2246
+ const [sourceDigest, targetDigest] = await Promise.all([
2247
+ fileDigest(sourcePath),
2248
+ fileDigest(targetPath)
2249
+ ]).catch(() => {
2250
+ throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2251
+ });
2252
+ if (sourceDigest !== targetDigest) {
2253
+ throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2254
+ }
2255
+ }
2256
+ }
2257
+ async function materializeLocalBaseline(sourceRoot, worktreeRoot, expectedHead, untrackedPaths, signal) {
2258
+ const sourceDiff = await runGitCommand({
2259
+ cwd: sourceRoot,
2260
+ args: [
2261
+ "diff",
2262
+ "--binary",
2263
+ "--full-index",
2264
+ "--no-ext-diff",
2265
+ "--no-color",
2266
+ "--no-renames",
2267
+ "HEAD",
2268
+ "--"
2269
+ ],
2270
+ signal,
2271
+ errorCode: ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
1994
2272
  });
1995
- if (status.length > 0) {
1996
- throw new SpotPatchError16(
1997
- options.expectedHead === void 0 ? ERROR_CODES16.WORKTREE_DIRTY : ERROR_CODES16.APPLY_CONFLICT
1998
- );
2273
+ if (sourceDiff.length > 0) {
2274
+ await runGitCommand({
2275
+ cwd: worktreeRoot,
2276
+ args: ["apply", "--binary", "--whitespace=nowarn", "-"],
2277
+ stdin: sourceDiff,
2278
+ signal,
2279
+ errorCode: ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2280
+ });
1999
2281
  }
2000
- return Object.freeze({ root, head });
2282
+ await copyUntrackedFiles(sourceRoot, worktreeRoot, untrackedPaths);
2283
+ const confirmation = await inspectGitWorkspace(sourceRoot, signal);
2284
+ const confirmationDiff = await runGitCommand({
2285
+ cwd: sourceRoot,
2286
+ args: [
2287
+ "diff",
2288
+ "--binary",
2289
+ "--full-index",
2290
+ "--no-ext-diff",
2291
+ "--no-color",
2292
+ "--no-renames",
2293
+ "HEAD",
2294
+ "--"
2295
+ ],
2296
+ signal,
2297
+ errorCode: ERROR_CODES17.APPLY_CONFLICT
2298
+ });
2299
+ if (confirmation.head !== expectedHead || confirmationDiff !== sourceDiff || confirmation.untrackedPaths.length !== untrackedPaths.length || confirmation.untrackedPaths.some((value, index) => value !== untrackedPaths[index])) {
2300
+ throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2301
+ }
2302
+ await assertUntrackedFilesUnchanged(sourceRoot, worktreeRoot, untrackedPaths);
2303
+ await runGitCommand({
2304
+ cwd: worktreeRoot,
2305
+ args: ["add", "--all"],
2306
+ signal,
2307
+ errorCode: ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2308
+ });
2309
+ await runGitCommand({
2310
+ cwd: worktreeRoot,
2311
+ args: [
2312
+ "-c",
2313
+ "user.name=SpotPatch Agent",
2314
+ "-c",
2315
+ "user.email=spotpatch-agent@example.invalid",
2316
+ "commit",
2317
+ "--quiet",
2318
+ "-m",
2319
+ "SpotPatch local workspace baseline"
2320
+ ],
2321
+ signal,
2322
+ errorCode: ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2323
+ });
2001
2324
  }
2002
2325
  async function defaultTemporaryBase(root) {
2003
- const dependencyDirectory = path5.join(root, "node_modules");
2326
+ const dependencyDirectory = path6.join(root, "node_modules");
2004
2327
  try {
2005
- const stats = await lstat3(dependencyDirectory);
2328
+ const stats = await lstat4(dependencyDirectory);
2006
2329
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
2007
2330
  return os.tmpdir();
2008
2331
  }
2009
- return await realpath2(dependencyDirectory);
2332
+ return await realpath3(dependencyDirectory);
2010
2333
  } catch {
2011
2334
  return os.tmpdir();
2012
2335
  }
2013
2336
  }
2014
2337
  async function createIsolatedGitWorktree(options) {
2015
- const baseline = await assertCleanGitBaseline({
2016
- root: options.root,
2017
- signal: options.signal
2338
+ const workingTreeMode = options.workingTreeMode ?? "require-clean";
2339
+ const inspection = await inspectGitWorkspace(options.root, options.signal);
2340
+ if (inspection.health.state === "blocked") {
2341
+ throw new SpotPatchError17(
2342
+ inspection.health.errorCode ?? ERROR_CODES17.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2343
+ );
2344
+ }
2345
+ if (inspection.health.state === "consent-required" && workingTreeMode === "require-clean") {
2346
+ throw new SpotPatchError17(ERROR_CODES17.WORKTREE_DIRTY);
2347
+ }
2348
+ const baseline = Object.freeze({
2349
+ root: inspection.root,
2350
+ head: inspection.head,
2351
+ workingTreeMode
2018
2352
  });
2019
2353
  const temporaryBase = options.temporaryBase ?? await defaultTemporaryBase(baseline.root);
2020
2354
  const temporaryDirectory = await mkdtemp(
2021
- path5.join(temporaryBase, "spotpatch-agent-")
2355
+ path6.join(temporaryBase, "spotpatch-agent-")
2022
2356
  );
2023
- const worktreePath = path5.join(temporaryDirectory, "worktree");
2357
+ const worktreePath = path6.join(temporaryDirectory, "worktree");
2024
2358
  let registered = false;
2025
2359
  let cleaned = false;
2026
2360
  const cleanup = async () => {
@@ -2035,7 +2369,7 @@ async function createIsolatedGitWorktree(options) {
2035
2369
  timeoutMs: 3e4
2036
2370
  }).catch(() => void 0);
2037
2371
  }
2038
- if (path5.basename(temporaryDirectory).startsWith("spotpatch-agent-")) {
2372
+ if (path6.basename(temporaryDirectory).startsWith("spotpatch-agent-")) {
2039
2373
  await rm2(temporaryDirectory, { recursive: true, force: true }).catch(
2040
2374
  () => void 0
2041
2375
  );
@@ -2045,12 +2379,12 @@ async function createIsolatedGitWorktree(options) {
2045
2379
  await runGitCommand({
2046
2380
  cwd: baseline.root,
2047
2381
  args: ["worktree", "add", "--detach", worktreePath, baseline.head],
2048
- errorCode: ERROR_CODES16.INTERNAL_ERROR,
2382
+ errorCode: ERROR_CODES17.INTERNAL_ERROR,
2049
2383
  signal: options.signal,
2050
2384
  timeoutMs: 3e4
2051
2385
  });
2052
2386
  registered = true;
2053
- const worktreeRoot = await realpath2(worktreePath);
2387
+ const worktreeRoot = await realpath3(worktreePath);
2054
2388
  const actualHead = (await runGitCommand({
2055
2389
  cwd: worktreeRoot,
2056
2390
  args: ["rev-parse", "--verify", "HEAD"],
@@ -2062,7 +2396,16 @@ async function createIsolatedGitWorktree(options) {
2062
2396
  signal: options.signal
2063
2397
  })).trim();
2064
2398
  if (actualHead !== baseline.head || !samePath(actualRoot, worktreeRoot)) {
2065
- throw new SpotPatchError16(ERROR_CODES16.INTERNAL_ERROR);
2399
+ throw new SpotPatchError17(ERROR_CODES17.INTERNAL_ERROR);
2400
+ }
2401
+ if (inspection.health.state === "consent-required") {
2402
+ await materializeLocalBaseline(
2403
+ baseline.root,
2404
+ worktreeRoot,
2405
+ baseline.head,
2406
+ inspection.untrackedPaths,
2407
+ options.signal
2408
+ );
2066
2409
  }
2067
2410
  return Object.freeze({ baseline, root: worktreeRoot, cleanup });
2068
2411
  } catch (error) {
@@ -2072,17 +2415,19 @@ async function createIsolatedGitWorktree(options) {
2072
2415
  }
2073
2416
 
2074
2417
  // src/worktree/prepared-change.ts
2075
- import { createHash as createHash2 } from "crypto";
2076
- import { lstat as lstat4, readFile as readFile2 } from "fs/promises";
2077
- import { ERROR_CODES as ERROR_CODES17, SpotPatchError as SpotPatchError17 } from "@spotpatch/shared";
2418
+ import { createHash as createHash3 } from "crypto";
2419
+ import { lstat as lstat5, readFile as readFile3 } from "fs/promises";
2420
+ import { ERROR_CODES as ERROR_CODES18, SpotPatchError as SpotPatchError18 } from "@spotpatch/shared";
2078
2421
  var privateChanges = /* @__PURE__ */ new WeakMap();
2079
2422
  var DELETED_HASH = "<deleted>";
2080
2423
  function createPreparedAgentChange(options) {
2081
2424
  const touchedPaths = Object.freeze(
2082
2425
  options.result.files.map((file) => file.relativePath)
2083
2426
  );
2084
- if (options.expectedHashes.size !== touchedPaths.length || touchedPaths.some((relativePath) => !options.expectedHashes.has(relativePath))) {
2085
- throw new SpotPatchError17(ERROR_CODES17.INTERNAL_ERROR);
2427
+ if (options.baselineHashes.size !== touchedPaths.length || options.expectedHashes.size !== touchedPaths.length || touchedPaths.some(
2428
+ (relativePath) => !options.baselineHashes.has(relativePath) || !options.expectedHashes.has(relativePath)
2429
+ )) {
2430
+ throw new SpotPatchError18(ERROR_CODES18.INTERNAL_ERROR);
2086
2431
  }
2087
2432
  const change = Object.freeze({
2088
2433
  kind: "prepared-agent-change",
@@ -2092,6 +2437,7 @@ function createPreparedAgentChange(options) {
2092
2437
  });
2093
2438
  privateChanges.set(change, {
2094
2439
  baselineHead: options.baselineHead,
2440
+ baselineHashes: new Map(options.baselineHashes),
2095
2441
  diff: options.result.diff,
2096
2442
  expectedHashes: new Map(options.expectedHashes),
2097
2443
  root: options.root,
@@ -2103,28 +2449,30 @@ function createPreparedAgentChange(options) {
2103
2449
  function requirePrivateChange(change) {
2104
2450
  const privateChange = privateChanges.get(change);
2105
2451
  if (privateChange === void 0) {
2106
- throw new SpotPatchError17(ERROR_CODES17.INTERNAL_ERROR);
2452
+ throw new SpotPatchError18(ERROR_CODES18.INTERNAL_ERROR);
2107
2453
  }
2108
2454
  return privateChange;
2109
2455
  }
2110
- async function currentHead(root) {
2111
- return (await runGitCommand({
2112
- cwd: root,
2113
- args: ["rev-parse", "--verify", "HEAD"],
2114
- errorCode: ERROR_CODES17.APPLY_CONFLICT
2115
- })).trim();
2456
+ async function assertWorkspaceOperationSafe(root, expectedHead) {
2457
+ const inspection = await inspectGitWorkspace(root).catch(() => {
2458
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2459
+ });
2460
+ const blockingOperation = inspection.health.errorCode === ERROR_CODES18.WORKTREE_OPERATION_IN_PROGRESS || inspection.health.errorCode === ERROR_CODES18.WORKTREE_CONFLICTED;
2461
+ if (inspection.head !== expectedHead || blockingOperation) {
2462
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2463
+ }
2116
2464
  }
2117
2465
  async function fileHash(root, relativePath) {
2118
2466
  const normalized = assertAgentPathAllowed(relativePath);
2119
2467
  const absolutePath = await resolveWritableAgentPath(root, normalized);
2120
- const metadata = await lstat4(absolutePath).catch(() => void 0);
2468
+ const metadata = await lstat5(absolutePath).catch(() => void 0);
2121
2469
  if (metadata === void 0) {
2122
2470
  return DELETED_HASH;
2123
2471
  }
2124
2472
  if (!metadata.isFile() || metadata.isSymbolicLink()) {
2125
- throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2473
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2126
2474
  }
2127
- return createHash2("sha256").update(await readFile2(absolutePath)).digest("hex");
2475
+ return createHash3("sha256").update(await readFile3(absolutePath)).digest("hex");
2128
2476
  }
2129
2477
  async function captureAgentFileHashes(root, paths) {
2130
2478
  const entries = await Promise.all(
@@ -2140,34 +2488,38 @@ function hashesMatch(expected, actual) {
2140
2488
  async function applyPreparedAgentChange(change) {
2141
2489
  const privateChange = requirePrivateChange(change);
2142
2490
  if (privateChange.state !== "prepared" || !change.validationPassed || privateChange.diff.length === 0) {
2143
- throw new SpotPatchError17(
2144
- change.validationPassed ? ERROR_CODES17.APPLY_CONFLICT : ERROR_CODES17.VALIDATION_FAILED
2491
+ throw new SpotPatchError18(
2492
+ change.validationPassed ? ERROR_CODES18.APPLY_CONFLICT : ERROR_CODES18.VALIDATION_FAILED
2145
2493
  );
2146
2494
  }
2147
2495
  privateChange.state = "applying";
2148
2496
  try {
2149
- await assertCleanGitBaseline({
2150
- root: privateChange.root,
2151
- expectedHead: privateChange.baselineHead
2152
- });
2497
+ await assertWorkspaceOperationSafe(privateChange.root, privateChange.baselineHead);
2498
+ const currentHashes = await captureAgentFileHashes(
2499
+ privateChange.root,
2500
+ privateChange.touchedPaths
2501
+ );
2502
+ if (!hashesMatch(privateChange.baselineHashes, currentHashes)) {
2503
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2504
+ }
2153
2505
  await runGitCommand({
2154
2506
  cwd: privateChange.root,
2155
2507
  args: ["apply", "--check", "--whitespace=error-all", "-"],
2156
2508
  stdin: privateChange.diff,
2157
- errorCode: ERROR_CODES17.APPLY_CONFLICT
2509
+ errorCode: ERROR_CODES18.APPLY_CONFLICT
2158
2510
  });
2159
2511
  await runGitCommand({
2160
2512
  cwd: privateChange.root,
2161
2513
  args: ["apply", "--whitespace=error-all", "-"],
2162
2514
  stdin: privateChange.diff,
2163
- errorCode: ERROR_CODES17.APPLY_CONFLICT
2515
+ errorCode: ERROR_CODES18.APPLY_CONFLICT
2164
2516
  });
2165
2517
  const appliedHashes = await captureAgentFileHashes(
2166
2518
  privateChange.root,
2167
2519
  privateChange.touchedPaths
2168
2520
  );
2169
2521
  if (!hashesMatch(privateChange.expectedHashes, appliedHashes)) {
2170
- throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2522
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2171
2523
  }
2172
2524
  privateChange.appliedHashes = appliedHashes;
2173
2525
  privateChange.state = "applied";
@@ -2179,34 +2531,39 @@ async function applyPreparedAgentChange(change) {
2179
2531
  async function revertPreparedAgentChange(change) {
2180
2532
  const privateChange = requirePrivateChange(change);
2181
2533
  if (privateChange.state !== "applied" || privateChange.appliedHashes === void 0) {
2182
- throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2534
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2183
2535
  }
2184
2536
  privateChange.state = "reverting";
2185
2537
  try {
2186
- if (await currentHead(privateChange.root) !== privateChange.baselineHead) {
2187
- throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2188
- }
2538
+ await assertWorkspaceOperationSafe(privateChange.root, privateChange.baselineHead);
2189
2539
  const currentHashes = await captureAgentFileHashes(
2190
2540
  privateChange.root,
2191
2541
  privateChange.touchedPaths
2192
2542
  );
2193
2543
  for (const [relativePath, expectedHash] of privateChange.appliedHashes) {
2194
2544
  if (currentHashes.get(relativePath) !== expectedHash) {
2195
- throw new SpotPatchError17(ERROR_CODES17.APPLY_CONFLICT);
2545
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2196
2546
  }
2197
2547
  }
2198
2548
  await runGitCommand({
2199
2549
  cwd: privateChange.root,
2200
2550
  args: ["apply", "--reverse", "--check", "--whitespace=error-all", "-"],
2201
2551
  stdin: privateChange.diff,
2202
- errorCode: ERROR_CODES17.APPLY_CONFLICT
2552
+ errorCode: ERROR_CODES18.APPLY_CONFLICT
2203
2553
  });
2204
2554
  await runGitCommand({
2205
2555
  cwd: privateChange.root,
2206
2556
  args: ["apply", "--reverse", "--whitespace=error-all", "-"],
2207
2557
  stdin: privateChange.diff,
2208
- errorCode: ERROR_CODES17.APPLY_CONFLICT
2558
+ errorCode: ERROR_CODES18.APPLY_CONFLICT
2209
2559
  });
2560
+ const revertedHashes = await captureAgentFileHashes(
2561
+ privateChange.root,
2562
+ privateChange.touchedPaths
2563
+ );
2564
+ if (!hashesMatch(privateChange.baselineHashes, revertedHashes)) {
2565
+ throw new SpotPatchError18(ERROR_CODES18.APPLY_CONFLICT);
2566
+ }
2210
2567
  privateChange.state = "reverted";
2211
2568
  } catch (error) {
2212
2569
  privateChange.state = "applied";
@@ -2229,6 +2586,7 @@ Follow these rules exactly:
2229
2586
  - Use apply_patch only when creating or deleting a file, or when the change cannot be expressed as one exact replacement. apply_patch accepts only a raw canonical unified Git diff.
2230
2587
  - Every patch must begin with 'diff --git a/<path> b/<path>', include matching '--- a/<path>' and '+++ b/<path>' headers and valid '@@' hunks. Send only the raw diff: no Markdown fences, prose, shell commands, or '*** Begin Patch' markers.
2231
2588
  - If a write tool returns a retryable PATCH_REJECTED result, no file changed. Follow its guidance, re-read the current file, and retry once with a new tool call ID.
2589
+ - If any tool returns a retryable TOOL_ARGUMENTS_INVALID result, no file changed. Retry once with a new tool call ID using only the declared fields and value types.
2232
2590
  - Never modify credentials, environment files, lockfiles, generated output, Git metadata, or dependencies.
2233
2591
  - Do not claim a check passed unless run_check returned a passed status.
2234
2592
  - Finish with a concise factual summary after all needed tool calls. Do not include secrets or absolute paths.`;
@@ -2388,11 +2746,11 @@ function isRetryableToolFailure(result) {
2388
2746
  return false;
2389
2747
  }
2390
2748
  const candidate = output;
2391
- return candidate.errorCode === ERROR_CODES18.PATCH_REJECTED && candidate.retryable === true;
2749
+ return candidate.retryable === true && (candidate.errorCode === ERROR_CODES19.PATCH_REJECTED || candidate.errorCode === ERROR_CODES19.TOOL_ARGUMENTS_INVALID);
2392
2750
  }
2393
2751
  function throwIfCancelled(signal) {
2394
2752
  if (signal.aborted) {
2395
- throw new SpotPatchError18(ERROR_CODES18.AGENT_CANCELLED);
2753
+ throw new SpotPatchError19(ERROR_CODES19.AGENT_CANCELLED);
2396
2754
  }
2397
2755
  }
2398
2756
  function linkSignal(source, target) {
@@ -2430,6 +2788,7 @@ async function executeAgentChange(options) {
2430
2788
  worktree = await createIsolatedGitWorktree({
2431
2789
  root: options.root,
2432
2790
  signal: controller.signal,
2791
+ workingTreeMode: options.workingTreeMode ?? "require-clean",
2433
2792
  ...options.temporaryBase === void 0 ? {} : { temporaryBase: options.temporaryBase }
2434
2793
  });
2435
2794
  options.callbacks?.onPhase?.(
@@ -2463,30 +2822,38 @@ async function executeAgentChange(options) {
2463
2822
  let summary;
2464
2823
  let toolCallCount = 0;
2465
2824
  for (let turn = 0; turn < options.execution.limits.maxTurns; turn += 1) {
2825
+ const turnNumber = turn + 1;
2466
2826
  throwIfCancelled(controller.signal);
2467
2827
  const response = await session.next(pendingResults, controller.signal);
2828
+ assertUniqueToolCallIds(response.toolCalls);
2468
2829
  if (response.toolCalls.length === 0) {
2469
2830
  summary = response.finalText.trim().slice(0, options.execution.limits.maxToolOutputCharacters);
2470
2831
  break;
2471
2832
  }
2472
2833
  toolCallCount += response.toolCalls.length;
2473
2834
  if (toolCallCount > options.execution.limits.maxToolCalls) {
2474
- throw new SpotPatchError18(ERROR_CODES18.AGENT_LIMIT_EXCEEDED);
2835
+ throw new SpotPatchError19(ERROR_CODES19.AGENT_LIMIT_EXCEEDED);
2475
2836
  }
2476
2837
  const results = [];
2477
2838
  for (const call of response.toolCalls) {
2478
2839
  options.callbacks?.onTool?.(
2479
2840
  Object.freeze({
2841
+ turn: turnNumber,
2480
2842
  toolCallId: call.id,
2481
2843
  toolName: call.name,
2482
2844
  state: "started"
2483
2845
  })
2484
2846
  );
2485
2847
  try {
2486
- const result2 = await executor.execute(call, controller.signal);
2848
+ const result2 = await executor.execute(
2849
+ call,
2850
+ Object.freeze({ turn: turnNumber }),
2851
+ controller.signal
2852
+ );
2487
2853
  results.push(result2);
2488
2854
  options.callbacks?.onTool?.(
2489
2855
  Object.freeze({
2856
+ turn: turnNumber,
2490
2857
  toolCallId: call.id,
2491
2858
  toolName: call.name,
2492
2859
  state: isRetryableToolFailure(result2) ? "failed" : "succeeded"
@@ -2495,6 +2862,7 @@ async function executeAgentChange(options) {
2495
2862
  } catch (error) {
2496
2863
  options.callbacks?.onTool?.(
2497
2864
  Object.freeze({
2865
+ turn: turnNumber,
2498
2866
  toolCallId: call.id,
2499
2867
  toolName: call.name,
2500
2868
  state: "failed"
@@ -2506,7 +2874,7 @@ async function executeAgentChange(options) {
2506
2874
  pendingResults = Object.freeze(results);
2507
2875
  }
2508
2876
  if (summary === void 0) {
2509
- throw new SpotPatchError18(ERROR_CODES18.AGENT_LIMIT_EXCEEDED);
2877
+ throw new SpotPatchError19(ERROR_CODES19.AGENT_LIMIT_EXCEEDED);
2510
2878
  }
2511
2879
  options.callbacks?.onPhase?.(
2512
2880
  Object.freeze({
@@ -2539,7 +2907,7 @@ async function executeAgentChange(options) {
2539
2907
  controller.signal
2540
2908
  );
2541
2909
  if (afterCheck.diff !== initialChangeSet.diff) {
2542
- throw new SpotPatchError18(ERROR_CODES18.VALIDATION_FAILED);
2910
+ throw new SpotPatchError19(ERROR_CODES19.VALIDATION_FAILED);
2543
2911
  }
2544
2912
  }
2545
2913
  const validationPassed = finalChecks.every((check) => check.status === "passed");
@@ -2555,9 +2923,14 @@ async function executeAgentChange(options) {
2555
2923
  worktree.root,
2556
2924
  initialChangeSet.touchedPaths
2557
2925
  );
2926
+ const baselineHashes = await captureAgentFileHashes(
2927
+ worktree.baseline.root,
2928
+ initialChangeSet.touchedPaths
2929
+ );
2558
2930
  return createPreparedAgentChange({
2559
2931
  autoApplyEligible,
2560
2932
  baselineHead: worktree.baseline.head,
2933
+ baselineHashes,
2561
2934
  expectedHashes,
2562
2935
  result,
2563
2936
  root: worktree.baseline.root,
@@ -2565,15 +2938,15 @@ async function executeAgentChange(options) {
2565
2938
  });
2566
2939
  } catch (error) {
2567
2940
  if (options.signal.aborted) {
2568
- throw new SpotPatchError18(ERROR_CODES18.AGENT_CANCELLED);
2941
+ throw new SpotPatchError19(ERROR_CODES19.AGENT_CANCELLED);
2569
2942
  }
2570
2943
  if (hasJobTimedOut()) {
2571
- throw new SpotPatchError18(ERROR_CODES18.AGENT_LIMIT_EXCEEDED);
2944
+ throw new SpotPatchError19(ERROR_CODES19.AGENT_LIMIT_EXCEEDED);
2572
2945
  }
2573
- if (error instanceof SpotPatchError18) {
2946
+ if (error instanceof SpotPatchError19) {
2574
2947
  throw error;
2575
2948
  }
2576
- throw new SpotPatchError18(ERROR_CODES18.INTERNAL_ERROR);
2949
+ throw new SpotPatchError19(ERROR_CODES19.INTERNAL_ERROR);
2577
2950
  } finally {
2578
2951
  clearTimeout(timeout);
2579
2952
  unlink();
@@ -2583,15 +2956,15 @@ async function executeAgentChange(options) {
2583
2956
 
2584
2957
  // src/provider/capability-probe.ts
2585
2958
  import {
2586
- ERROR_CODES as ERROR_CODES19,
2587
- SpotPatchError as SpotPatchError19
2959
+ ERROR_CODES as ERROR_CODES20,
2960
+ SpotPatchError as SpotPatchError20
2588
2961
  } from "@spotpatch/shared";
2589
2962
  var PROBE_TOOL_NAME = "spotpatch_capability_probe";
2590
2963
  var PROBE_TOKEN = "spotpatch-ready-v1";
2591
2964
  async function probeProviderCapability(options) {
2592
2965
  const model = options.provider.models[options.modelProfileId];
2593
2966
  if (model === void 0) {
2594
- throw new SpotPatchError19(ERROR_CODES19.MODEL_NOT_ALLOWED);
2967
+ throw new SpotPatchError20(ERROR_CODES20.MODEL_NOT_ALLOWED);
2595
2968
  }
2596
2969
  const credential = options.credential ?? resolveProviderCredential(options.provider.apiKeyEnv, options.environment);
2597
2970
  const session = createOpenAICompatibleProviderSession({
@@ -2620,7 +2993,7 @@ async function probeProviderCapability(options) {
2620
2993
  const first = await session.next(void 0, options.signal);
2621
2994
  const probeCall = first.toolCalls[0];
2622
2995
  if (first.toolCalls.length !== 1 || probeCall?.name !== PROBE_TOOL_NAME || probeCall.arguments.token !== PROBE_TOKEN) {
2623
- throw new SpotPatchError19(ERROR_CODES19.MODEL_TOOL_CALL_UNSUPPORTED);
2996
+ throw new SpotPatchError20(ERROR_CODES20.MODEL_TOOL_CALL_UNSUPPORTED);
2624
2997
  }
2625
2998
  const second = await session.next(
2626
2999
  Object.freeze([
@@ -2632,7 +3005,7 @@ async function probeProviderCapability(options) {
2632
3005
  options.signal
2633
3006
  );
2634
3007
  if (second.toolCalls.length !== 0 || second.finalText.trim().length === 0) {
2635
- throw new SpotPatchError19(ERROR_CODES19.MODEL_TOOL_CALL_UNSUPPORTED);
3008
+ throw new SpotPatchError20(ERROR_CODES20.MODEL_TOOL_CALL_UNSUPPORTED);
2636
3009
  }
2637
3010
  return Object.freeze({
2638
3011
  providerProfileId: options.provider.id,
@@ -2654,6 +3027,7 @@ export {
2654
3027
  createOpenAICompatibleProviderSession,
2655
3028
  createProviderCredential,
2656
3029
  executeAgentChange,
3030
+ inspectAgentWorkspace,
2657
3031
  probeProviderCapability,
2658
3032
  resolveProviderCredential,
2659
3033
  revertPreparedAgentChange