@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.cjs CHANGED
@@ -34,6 +34,7 @@ __export(index_exports, {
34
34
  createOpenAICompatibleProviderSession: () => createOpenAICompatibleProviderSession,
35
35
  createProviderCredential: () => createProviderCredential,
36
36
  executeAgentChange: () => executeAgentChange,
37
+ inspectAgentWorkspace: () => inspectAgentWorkspace,
37
38
  probeProviderCapability: () => probeProviderCapability,
38
39
  resolveProviderCredential: () => resolveProviderCredential,
39
40
  revertPreparedAgentChange: () => revertPreparedAgentChange
@@ -41,7 +42,7 @@ __export(index_exports, {
41
42
  module.exports = __toCommonJS(index_exports);
42
43
 
43
44
  // src/engine/execute-agent-change.ts
44
- var import_shared19 = require("@spotpatch/shared");
45
+ var import_shared20 = require("@spotpatch/shared");
45
46
 
46
47
  // src/provider/openai-compatible-provider.ts
47
48
  var import_shared7 = require("@spotpatch/shared");
@@ -68,9 +69,27 @@ function parseJsonRecord(value) {
68
69
  }
69
70
  function parseToolArguments(value) {
70
71
  if (typeof value !== "string") {
71
- throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED);
72
+ throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
73
+ }
74
+ let parsed;
75
+ try {
76
+ parsed = JSON.parse(value);
77
+ } catch {
78
+ throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
79
+ }
80
+ if (!isRecord(parsed)) {
81
+ throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
82
+ }
83
+ return parsed;
84
+ }
85
+ function assertUniqueToolCallIds(calls) {
86
+ const ids = /* @__PURE__ */ new Set();
87
+ for (const call of calls) {
88
+ if (ids.has(call.id)) {
89
+ throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.TOOL_CALL_ID_CONFLICT);
90
+ }
91
+ ids.add(call.id);
72
92
  }
73
- return parseJsonRecord(value);
74
93
  }
75
94
  function requireString(record, field) {
76
95
  const value = record[field];
@@ -80,6 +99,7 @@ function requireString(record, field) {
80
99
  return value;
81
100
  }
82
101
  function validateToolResults(pendingCalls, results) {
102
+ assertUniqueToolCallIds(pendingCalls);
83
103
  if (pendingCalls.length === 0) {
84
104
  if (results !== void 0 && results.length > 0) {
85
105
  throw new import_shared.SpotPatchError(import_shared.ERROR_CODES.INTERNAL_ERROR);
@@ -477,6 +497,7 @@ function parseChatEvents(events) {
477
497
  }
478
498
  const finalText = content.join("");
479
499
  const toolCalls = finalizeToolCalls(calls);
500
+ assertUniqueToolCallIds(toolCalls);
480
501
  if (toolCalls.length === 0 && finalText.trim().length === 0) {
481
502
  throw new import_shared5.SpotPatchError(import_shared5.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED);
482
503
  }
@@ -573,7 +594,7 @@ function collectFunctionCall(item, calls) {
573
594
  });
574
595
  const existing = calls.get(id);
575
596
  if (existing !== void 0 && (existing.name !== call.name || JSON.stringify(existing.arguments) !== JSON.stringify(call.arguments))) {
576
- throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.PROVIDER_PROTOCOL_UNSUPPORTED);
597
+ throw new import_shared6.SpotPatchError(import_shared6.ERROR_CODES.TOOL_CALL_ID_CONFLICT);
577
598
  }
578
599
  calls.set(id, call);
579
600
  }
@@ -752,7 +773,7 @@ var PROTECTED_FILE_NAMES = /* @__PURE__ */ new Set([
752
773
  "yarn.lock"
753
774
  ]);
754
775
  function deny() {
755
- throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.TOOL_DENIED);
776
+ throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.TOOL_PATH_DENIED);
756
777
  }
757
778
  function hasControlCharacter(value) {
758
779
  for (let index = 0; index < value.length; index += 1) {
@@ -854,13 +875,13 @@ async function readAgentTextFile(root, relativePath, maximumBytes) {
854
875
  }
855
876
  const bytes = await (0, import_promises2.readFile)(absolutePath);
856
877
  if (bytes.includes(0)) {
857
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_DENIED);
878
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_PATH_DENIED);
858
879
  }
859
880
  let content;
860
881
  try {
861
882
  content = utf8Decoder.decode(bytes);
862
883
  } catch {
863
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_DENIED);
884
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_PATH_DENIED);
864
885
  }
865
886
  return Object.freeze({ content, relativePath, size: metadata.size });
866
887
  }
@@ -873,7 +894,7 @@ function encodeUtf8Text(content, includeByteOrderMark) {
873
894
  }
874
895
  async function writeAgentTextFileIfContentMatches(root, relativePath, expectedContent, nextContent, maximumBytes) {
875
896
  if (nextContent.includes("\0")) {
876
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_DENIED);
897
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
877
898
  }
878
899
  const absolutePath = await resolveExistingAgentPath(root, relativePath);
879
900
  const [metadata, currentBytes] = await Promise.all([
@@ -887,7 +908,7 @@ async function writeAgentTextFileIfContentMatches(root, relativePath, expectedCo
887
908
  try {
888
909
  currentContent = utf8Decoder.decode(currentBytes);
889
910
  } catch {
890
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_DENIED);
911
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_PATH_DENIED);
891
912
  }
892
913
  if (currentBytes.includes(0) || currentContent !== expectedContent) {
893
914
  throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.PATCH_REJECTED);
@@ -1142,7 +1163,7 @@ async function runConfiguredCheck(options) {
1142
1163
  function requireConfiguredCheck(checkId, checks) {
1143
1164
  const check = checks[checkId];
1144
1165
  if (check === void 0) {
1145
- throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.TOOL_DENIED);
1166
+ throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
1146
1167
  }
1147
1168
  return check;
1148
1169
  }
@@ -1331,9 +1352,16 @@ async function applyAgentPatch(worktreeRoot, patch, limits, signal) {
1331
1352
  throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
1332
1353
  }
1333
1354
  await Promise.all(
1334
- files.map(
1335
- async (file) => resolveWritableAgentPath(worktreeRoot, file.relativePath)
1336
- )
1355
+ files.map(async (file) => {
1356
+ await resolveWritableAgentPath(worktreeRoot, file.relativePath);
1357
+ if (file.kind !== "added") {
1358
+ await readAgentTextFile(
1359
+ worktreeRoot,
1360
+ file.relativePath,
1361
+ limits.maxReadBytesPerFile
1362
+ );
1363
+ }
1364
+ })
1337
1365
  );
1338
1366
  await runGitCommand({
1339
1367
  cwd: worktreeRoot,
@@ -1447,7 +1475,7 @@ var MAX_DISCOVERED_FILES = 2e4;
1447
1475
  var TEXT_SAMPLE_BYTES = 8192;
1448
1476
  function compileGlob(glob) {
1449
1477
  if (glob.length === 0 || glob.length > 256 || glob.includes("\0") || glob.includes("\\") || glob.startsWith("/") || ["[", "]", "{", "}", "(", ")", "!"].some((character) => glob.includes(character)) || glob.split("/").some((segment) => segment === "..")) {
1450
- throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.TOOL_DENIED);
1478
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
1451
1479
  }
1452
1480
  let expression = "^";
1453
1481
  for (let index = 0; index < glob.length; index += 1) {
@@ -1686,7 +1714,7 @@ var runCheckSchema = import_zod.z.strictObject({
1686
1714
  checkId: import_zod.z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
1687
1715
  });
1688
1716
  function invalidTool() {
1689
- throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.TOOL_DENIED);
1717
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
1690
1718
  }
1691
1719
  function parseArguments(schema, value) {
1692
1720
  const parsed = schema.safeParse(value);
@@ -1743,8 +1771,16 @@ function retryableWriteRejection(reason, guidance) {
1743
1771
  guidance
1744
1772
  });
1745
1773
  }
1774
+ function retryableArgumentsRejection() {
1775
+ return Object.freeze({
1776
+ errorCode: import_shared15.ERROR_CODES.TOOL_ARGUMENTS_INVALID,
1777
+ retryable: true,
1778
+ reason: "ARGUMENTS_DO_NOT_MATCH_CONTRACT",
1779
+ guidance: "No files changed. Retry once with a new tool call ID and only the declared fields and value types."
1780
+ });
1781
+ }
1746
1782
  function createAgentToolExecutor(options) {
1747
- const cache = /* @__PURE__ */ new Map();
1783
+ const cacheByTurn = /* @__PURE__ */ new Map();
1748
1784
  const touchedPaths = /* @__PURE__ */ new Set();
1749
1785
  const executeUncached = async (call, signal) => {
1750
1786
  switch (call.name) {
@@ -1960,20 +1996,32 @@ function createAgentToolExecutor(options) {
1960
1996
  }
1961
1997
  };
1962
1998
  return Object.freeze({
1963
- async execute(call, signal) {
1999
+ async execute(call, scope, signal) {
2000
+ if (!Number.isSafeInteger(scope.turn) || scope.turn < 1) {
2001
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.INTERNAL_ERROR);
2002
+ }
2003
+ const turnCache = cacheByTurn.get(scope.turn) ?? /* @__PURE__ */ new Map();
2004
+ cacheByTurn.set(scope.turn, turnCache);
1964
2005
  const signature = `${call.name}\0${JSON.stringify(call.arguments)}`;
1965
- const cached = cache.get(call.id);
2006
+ const cached = turnCache.get(call.id);
1966
2007
  if (cached !== void 0) {
1967
2008
  if (cached.signature !== signature) {
1968
- return invalidTool();
2009
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.TOOL_CALL_ID_CONFLICT);
1969
2010
  }
1970
2011
  return cached.result;
1971
2012
  }
1972
- const result = Object.freeze({
1973
- toolCallId: call.id,
1974
- output: await executeUncached(call, signal)
1975
- });
1976
- cache.set(call.id, Object.freeze({ signature, result }));
2013
+ let output;
2014
+ try {
2015
+ output = await executeUncached(call, signal);
2016
+ } catch (error) {
2017
+ if (error instanceof import_shared15.SpotPatchError && error.code === import_shared15.ERROR_CODES.TOOL_ARGUMENTS_INVALID) {
2018
+ output = retryableArgumentsRejection();
2019
+ } else {
2020
+ throw error;
2021
+ }
2022
+ }
2023
+ const result = Object.freeze({ toolCallId: call.id, output });
2024
+ turnCache.set(call.id, Object.freeze({ signature, result }));
1977
2025
  return result;
1978
2026
  },
1979
2027
  touchedPaths() {
@@ -1983,67 +2031,339 @@ function createAgentToolExecutor(options) {
1983
2031
  }
1984
2032
 
1985
2033
  // src/worktree/git-worktree.ts
1986
- var import_promises5 = require("fs/promises");
2034
+ var import_promises6 = require("fs/promises");
2035
+ var import_node_crypto3 = require("crypto");
1987
2036
  var import_node_os = __toESM(require("os"), 1);
2037
+ var import_node_path6 = __toESM(require("path"), 1);
2038
+ var import_shared17 = require("@spotpatch/shared");
2039
+
2040
+ // src/worktree/workspace-health.ts
2041
+ var import_promises5 = require("fs/promises");
1988
2042
  var import_node_path5 = __toESM(require("path"), 1);
1989
2043
  var import_shared16 = require("@spotpatch/shared");
1990
- async function assertCleanGitBaseline(options) {
1991
- const root = await (0, import_promises5.realpath)(options.root).catch(() => {
1992
- throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_DIRTY);
2044
+ var CONFLICTED_STATUSES = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
2045
+ var OPERATION_MARKERS = Object.freeze([
2046
+ "MERGE_HEAD",
2047
+ "CHERRY_PICK_HEAD",
2048
+ "REVERT_HEAD",
2049
+ "rebase-apply",
2050
+ "rebase-merge"
2051
+ ]);
2052
+ function emptyChanges() {
2053
+ return Object.freeze({
2054
+ staged: 0,
2055
+ unstaged: 0,
2056
+ untracked: 0,
2057
+ conflicted: 0,
2058
+ total: 0
2059
+ });
2060
+ }
2061
+ function blockedHealth(errorCode) {
2062
+ return Object.freeze({
2063
+ state: "blocked",
2064
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
2065
+ changes: emptyChanges(),
2066
+ canIncludeLocalChanges: false,
2067
+ errorCode
2068
+ });
2069
+ }
2070
+ function parsePorcelainStatus(value) {
2071
+ const records = value.split("\0");
2072
+ const untrackedPaths = [];
2073
+ let staged = 0;
2074
+ let unstaged = 0;
2075
+ let conflicted = 0;
2076
+ let total = 0;
2077
+ for (let index = 0; index < records.length; index += 1) {
2078
+ const record = records[index] ?? "";
2079
+ if (record.length === 0) {
2080
+ continue;
2081
+ }
2082
+ if (record.length < 4 || record[2] !== " ") {
2083
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2084
+ }
2085
+ const status = record.slice(0, 2);
2086
+ const relativePath = record.slice(3);
2087
+ if (relativePath.length === 0) {
2088
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2089
+ }
2090
+ total += 1;
2091
+ if (status === "??") {
2092
+ untrackedPaths.push(relativePath);
2093
+ continue;
2094
+ }
2095
+ const indexStatus = status[0] ?? " ";
2096
+ const worktreeStatus = status[1] ?? " ";
2097
+ if (CONFLICTED_STATUSES.has(status) || indexStatus === "U" || worktreeStatus === "U") {
2098
+ conflicted += 1;
2099
+ }
2100
+ if (indexStatus !== " ") {
2101
+ staged += 1;
2102
+ }
2103
+ if (worktreeStatus !== " ") {
2104
+ unstaged += 1;
2105
+ }
2106
+ if (indexStatus === "R" || indexStatus === "C") {
2107
+ index += 1;
2108
+ if ((records[index] ?? "").length === 0) {
2109
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2110
+ }
2111
+ }
2112
+ }
2113
+ return Object.freeze({
2114
+ changes: Object.freeze({
2115
+ staged,
2116
+ unstaged,
2117
+ untracked: untrackedPaths.length,
2118
+ conflicted,
2119
+ total
2120
+ }),
2121
+ untrackedPaths: Object.freeze(untrackedPaths)
1993
2122
  });
1994
- const topLevel = (await runGitCommand({
2123
+ }
2124
+ async function operationInProgress(root, signal) {
2125
+ for (const marker of OPERATION_MARKERS) {
2126
+ const markerPath = (await runGitCommand({
2127
+ cwd: root,
2128
+ args: ["rev-parse", "--git-path", marker],
2129
+ errorCode: import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY,
2130
+ ...signal === void 0 ? {} : { signal }
2131
+ })).trim();
2132
+ if (await (0, import_promises5.lstat)(import_node_path5.default.resolve(root, markerPath)).catch(() => void 0) !== void 0) {
2133
+ return true;
2134
+ }
2135
+ }
2136
+ return false;
2137
+ }
2138
+ async function inspectUntrackedFiles(root, relativePaths) {
2139
+ if (relativePaths.length > import_shared16.AGENT_WORKSPACE_SNAPSHOT_LIMITS.maxUntrackedFiles) {
2140
+ return import_shared16.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE;
2141
+ }
2142
+ let totalBytes = 0;
2143
+ for (const relativePath of relativePaths) {
2144
+ const absolutePath = import_node_path5.default.resolve(root, relativePath);
2145
+ const relative = import_node_path5.default.relative(root, absolutePath);
2146
+ if (relative === ".." || relative.startsWith(`..${import_node_path5.default.sep}`) || import_node_path5.default.isAbsolute(relative)) {
2147
+ return import_shared16.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED;
2148
+ }
2149
+ const metadata = await (0, import_promises5.lstat)(absolutePath).catch(() => void 0);
2150
+ if (metadata === void 0 || !metadata.isFile() || metadata.isSymbolicLink()) {
2151
+ return import_shared16.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED;
2152
+ }
2153
+ totalBytes += metadata.size;
2154
+ if (totalBytes > import_shared16.AGENT_WORKSPACE_SNAPSHOT_LIMITS.maxUntrackedBytes) {
2155
+ return import_shared16.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE;
2156
+ }
2157
+ }
2158
+ return void 0;
2159
+ }
2160
+ async function inspectGitWorkspace(rootValue, signal) {
2161
+ const root = await (0, import_promises5.realpath)(rootValue).catch(() => {
2162
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY);
2163
+ });
2164
+ const topLevelResult = await runRawGitCommand({
1995
2165
  cwd: root,
1996
2166
  args: ["rev-parse", "--show-toplevel"],
1997
- errorCode: import_shared16.ERROR_CODES.WORKTREE_DIRTY,
1998
- ...options.signal === void 0 ? {} : { signal: options.signal }
1999
- })).trim();
2167
+ ...signal === void 0 ? {} : { signal }
2168
+ });
2169
+ if (topLevelResult.exitCode !== 0 || topLevelResult.cancelled || topLevelResult.timedOut) {
2170
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY);
2171
+ }
2172
+ const topLevel = topLevelResult.stdout.trim();
2000
2173
  if (!samePath(root, topLevel)) {
2001
- throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_DIRTY);
2174
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY);
2002
2175
  }
2003
2176
  const head = (await runGitCommand({
2004
2177
  cwd: root,
2005
2178
  args: ["rev-parse", "--verify", "HEAD"],
2006
- errorCode: import_shared16.ERROR_CODES.WORKTREE_DIRTY,
2007
- ...options.signal === void 0 ? {} : { signal: options.signal }
2179
+ errorCode: import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY,
2180
+ ...signal === void 0 ? {} : { signal }
2008
2181
  })).trim();
2009
- if (options.expectedHead !== void 0 && head !== options.expectedHead) {
2010
- throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.APPLY_CONFLICT);
2182
+ const parsed = parsePorcelainStatus(
2183
+ await runGitCommand({
2184
+ cwd: root,
2185
+ args: ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
2186
+ errorCode: import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY,
2187
+ ...signal === void 0 ? {} : { signal }
2188
+ })
2189
+ );
2190
+ const operationActive = await operationInProgress(root, signal);
2191
+ const untrackedError = await inspectUntrackedFiles(root, parsed.untrackedPaths);
2192
+ const errorCode = operationActive ? import_shared16.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS : parsed.changes.conflicted > 0 ? import_shared16.ERROR_CODES.WORKTREE_CONFLICTED : untrackedError;
2193
+ const health = Object.freeze({
2194
+ state: errorCode !== void 0 ? "blocked" : parsed.changes.total === 0 ? "ready" : "consent-required",
2195
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
2196
+ changes: parsed.changes,
2197
+ canIncludeLocalChanges: errorCode === void 0 && parsed.changes.total > 0,
2198
+ ...errorCode === void 0 ? {} : { errorCode }
2199
+ });
2200
+ return Object.freeze({
2201
+ root,
2202
+ head,
2203
+ health,
2204
+ untrackedPaths: parsed.untrackedPaths
2205
+ });
2206
+ }
2207
+ async function inspectAgentWorkspace(root, signal) {
2208
+ try {
2209
+ return (await inspectGitWorkspace(root, signal)).health;
2210
+ } catch (error) {
2211
+ if (error instanceof import_shared16.SpotPatchError) {
2212
+ return blockedHealth(error.code);
2213
+ }
2214
+ return blockedHealth(import_shared16.ERROR_CODES.INTERNAL_ERROR);
2011
2215
  }
2012
- const status = await runGitCommand({
2013
- cwd: root,
2014
- args: ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
2015
- errorCode: import_shared16.ERROR_CODES.WORKTREE_DIRTY,
2016
- ...options.signal === void 0 ? {} : { signal: options.signal }
2216
+ }
2217
+
2218
+ // src/worktree/git-worktree.ts
2219
+ function workspacePath(root, relativePath) {
2220
+ const candidate = import_node_path6.default.resolve(root, relativePath);
2221
+ const relative = import_node_path6.default.relative(root, candidate);
2222
+ if (relative === ".." || relative.startsWith(`..${import_node_path6.default.sep}`) || import_node_path6.default.isAbsolute(relative)) {
2223
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2224
+ }
2225
+ return candidate;
2226
+ }
2227
+ async function fileDigest(filePath) {
2228
+ return (0, import_node_crypto3.createHash)("sha256").update(await (0, import_promises6.readFile)(filePath)).digest("hex");
2229
+ }
2230
+ async function copyUntrackedFiles(sourceRoot, worktreeRoot, relativePaths) {
2231
+ for (const relativePath of relativePaths) {
2232
+ const sourcePath = workspacePath(sourceRoot, relativePath);
2233
+ const targetPath = workspacePath(worktreeRoot, relativePath);
2234
+ const metadata = await (0, import_promises6.lstat)(sourcePath).catch(() => void 0);
2235
+ if (metadata === void 0 || !metadata.isFile() || metadata.isSymbolicLink()) {
2236
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2237
+ }
2238
+ await (0, import_promises6.mkdir)(import_node_path6.default.dirname(targetPath), { recursive: true });
2239
+ await (0, import_promises6.copyFile)(sourcePath, targetPath);
2240
+ const [sourceDigest, targetDigest] = await Promise.all([
2241
+ fileDigest(sourcePath),
2242
+ fileDigest(targetPath)
2243
+ ]).catch(() => {
2244
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2245
+ });
2246
+ if (sourceDigest !== targetDigest) {
2247
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2248
+ }
2249
+ }
2250
+ }
2251
+ async function assertUntrackedFilesUnchanged(sourceRoot, worktreeRoot, relativePaths) {
2252
+ for (const relativePath of relativePaths) {
2253
+ const sourcePath = workspacePath(sourceRoot, relativePath);
2254
+ const targetPath = workspacePath(worktreeRoot, relativePath);
2255
+ const [sourceDigest, targetDigest] = await Promise.all([
2256
+ fileDigest(sourcePath),
2257
+ fileDigest(targetPath)
2258
+ ]).catch(() => {
2259
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2260
+ });
2261
+ if (sourceDigest !== targetDigest) {
2262
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2263
+ }
2264
+ }
2265
+ }
2266
+ async function materializeLocalBaseline(sourceRoot, worktreeRoot, expectedHead, untrackedPaths, signal) {
2267
+ const sourceDiff = await runGitCommand({
2268
+ cwd: sourceRoot,
2269
+ args: [
2270
+ "diff",
2271
+ "--binary",
2272
+ "--full-index",
2273
+ "--no-ext-diff",
2274
+ "--no-color",
2275
+ "--no-renames",
2276
+ "HEAD",
2277
+ "--"
2278
+ ],
2279
+ signal,
2280
+ errorCode: import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2017
2281
  });
2018
- if (status.length > 0) {
2019
- throw new import_shared16.SpotPatchError(
2020
- options.expectedHead === void 0 ? import_shared16.ERROR_CODES.WORKTREE_DIRTY : import_shared16.ERROR_CODES.APPLY_CONFLICT
2021
- );
2282
+ if (sourceDiff.length > 0) {
2283
+ await runGitCommand({
2284
+ cwd: worktreeRoot,
2285
+ args: ["apply", "--binary", "--whitespace=nowarn", "-"],
2286
+ stdin: sourceDiff,
2287
+ signal,
2288
+ errorCode: import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2289
+ });
2022
2290
  }
2023
- return Object.freeze({ root, head });
2291
+ await copyUntrackedFiles(sourceRoot, worktreeRoot, untrackedPaths);
2292
+ const confirmation = await inspectGitWorkspace(sourceRoot, signal);
2293
+ const confirmationDiff = await runGitCommand({
2294
+ cwd: sourceRoot,
2295
+ args: [
2296
+ "diff",
2297
+ "--binary",
2298
+ "--full-index",
2299
+ "--no-ext-diff",
2300
+ "--no-color",
2301
+ "--no-renames",
2302
+ "HEAD",
2303
+ "--"
2304
+ ],
2305
+ signal,
2306
+ errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2307
+ });
2308
+ if (confirmation.head !== expectedHead || confirmationDiff !== sourceDiff || confirmation.untrackedPaths.length !== untrackedPaths.length || confirmation.untrackedPaths.some((value, index) => value !== untrackedPaths[index])) {
2309
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2310
+ }
2311
+ await assertUntrackedFilesUnchanged(sourceRoot, worktreeRoot, untrackedPaths);
2312
+ await runGitCommand({
2313
+ cwd: worktreeRoot,
2314
+ args: ["add", "--all"],
2315
+ signal,
2316
+ errorCode: import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2317
+ });
2318
+ await runGitCommand({
2319
+ cwd: worktreeRoot,
2320
+ args: [
2321
+ "-c",
2322
+ "user.name=SpotPatch Agent",
2323
+ "-c",
2324
+ "user.email=spotpatch-agent@example.invalid",
2325
+ "commit",
2326
+ "--quiet",
2327
+ "-m",
2328
+ "SpotPatch local workspace baseline"
2329
+ ],
2330
+ signal,
2331
+ errorCode: import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2332
+ });
2024
2333
  }
2025
2334
  async function defaultTemporaryBase(root) {
2026
- const dependencyDirectory = import_node_path5.default.join(root, "node_modules");
2335
+ const dependencyDirectory = import_node_path6.default.join(root, "node_modules");
2027
2336
  try {
2028
- const stats = await (0, import_promises5.lstat)(dependencyDirectory);
2337
+ const stats = await (0, import_promises6.lstat)(dependencyDirectory);
2029
2338
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
2030
2339
  return import_node_os.default.tmpdir();
2031
2340
  }
2032
- return await (0, import_promises5.realpath)(dependencyDirectory);
2341
+ return await (0, import_promises6.realpath)(dependencyDirectory);
2033
2342
  } catch {
2034
2343
  return import_node_os.default.tmpdir();
2035
2344
  }
2036
2345
  }
2037
2346
  async function createIsolatedGitWorktree(options) {
2038
- const baseline = await assertCleanGitBaseline({
2039
- root: options.root,
2040
- signal: options.signal
2347
+ const workingTreeMode = options.workingTreeMode ?? "require-clean";
2348
+ const inspection = await inspectGitWorkspace(options.root, options.signal);
2349
+ if (inspection.health.state === "blocked") {
2350
+ throw new import_shared17.SpotPatchError(
2351
+ inspection.health.errorCode ?? import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2352
+ );
2353
+ }
2354
+ if (inspection.health.state === "consent-required" && workingTreeMode === "require-clean") {
2355
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.WORKTREE_DIRTY);
2356
+ }
2357
+ const baseline = Object.freeze({
2358
+ root: inspection.root,
2359
+ head: inspection.head,
2360
+ workingTreeMode
2041
2361
  });
2042
2362
  const temporaryBase = options.temporaryBase ?? await defaultTemporaryBase(baseline.root);
2043
- const temporaryDirectory = await (0, import_promises5.mkdtemp)(
2044
- import_node_path5.default.join(temporaryBase, "spotpatch-agent-")
2363
+ const temporaryDirectory = await (0, import_promises6.mkdtemp)(
2364
+ import_node_path6.default.join(temporaryBase, "spotpatch-agent-")
2045
2365
  );
2046
- const worktreePath = import_node_path5.default.join(temporaryDirectory, "worktree");
2366
+ const worktreePath = import_node_path6.default.join(temporaryDirectory, "worktree");
2047
2367
  let registered = false;
2048
2368
  let cleaned = false;
2049
2369
  const cleanup = async () => {
@@ -2058,8 +2378,8 @@ async function createIsolatedGitWorktree(options) {
2058
2378
  timeoutMs: 3e4
2059
2379
  }).catch(() => void 0);
2060
2380
  }
2061
- if (import_node_path5.default.basename(temporaryDirectory).startsWith("spotpatch-agent-")) {
2062
- await (0, import_promises5.rm)(temporaryDirectory, { recursive: true, force: true }).catch(
2381
+ if (import_node_path6.default.basename(temporaryDirectory).startsWith("spotpatch-agent-")) {
2382
+ await (0, import_promises6.rm)(temporaryDirectory, { recursive: true, force: true }).catch(
2063
2383
  () => void 0
2064
2384
  );
2065
2385
  }
@@ -2068,12 +2388,12 @@ async function createIsolatedGitWorktree(options) {
2068
2388
  await runGitCommand({
2069
2389
  cwd: baseline.root,
2070
2390
  args: ["worktree", "add", "--detach", worktreePath, baseline.head],
2071
- errorCode: import_shared16.ERROR_CODES.INTERNAL_ERROR,
2391
+ errorCode: import_shared17.ERROR_CODES.INTERNAL_ERROR,
2072
2392
  signal: options.signal,
2073
2393
  timeoutMs: 3e4
2074
2394
  });
2075
2395
  registered = true;
2076
- const worktreeRoot = await (0, import_promises5.realpath)(worktreePath);
2396
+ const worktreeRoot = await (0, import_promises6.realpath)(worktreePath);
2077
2397
  const actualHead = (await runGitCommand({
2078
2398
  cwd: worktreeRoot,
2079
2399
  args: ["rev-parse", "--verify", "HEAD"],
@@ -2085,7 +2405,16 @@ async function createIsolatedGitWorktree(options) {
2085
2405
  signal: options.signal
2086
2406
  })).trim();
2087
2407
  if (actualHead !== baseline.head || !samePath(actualRoot, worktreeRoot)) {
2088
- throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INTERNAL_ERROR);
2408
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INTERNAL_ERROR);
2409
+ }
2410
+ if (inspection.health.state === "consent-required") {
2411
+ await materializeLocalBaseline(
2412
+ baseline.root,
2413
+ worktreeRoot,
2414
+ baseline.head,
2415
+ inspection.untrackedPaths,
2416
+ options.signal
2417
+ );
2089
2418
  }
2090
2419
  return Object.freeze({ baseline, root: worktreeRoot, cleanup });
2091
2420
  } catch (error) {
@@ -2095,17 +2424,19 @@ async function createIsolatedGitWorktree(options) {
2095
2424
  }
2096
2425
 
2097
2426
  // src/worktree/prepared-change.ts
2098
- var import_node_crypto3 = require("crypto");
2099
- var import_promises6 = require("fs/promises");
2100
- var import_shared17 = require("@spotpatch/shared");
2427
+ var import_node_crypto4 = require("crypto");
2428
+ var import_promises7 = require("fs/promises");
2429
+ var import_shared18 = require("@spotpatch/shared");
2101
2430
  var privateChanges = /* @__PURE__ */ new WeakMap();
2102
2431
  var DELETED_HASH = "<deleted>";
2103
2432
  function createPreparedAgentChange(options) {
2104
2433
  const touchedPaths = Object.freeze(
2105
2434
  options.result.files.map((file) => file.relativePath)
2106
2435
  );
2107
- if (options.expectedHashes.size !== touchedPaths.length || touchedPaths.some((relativePath) => !options.expectedHashes.has(relativePath))) {
2108
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INTERNAL_ERROR);
2436
+ if (options.baselineHashes.size !== touchedPaths.length || options.expectedHashes.size !== touchedPaths.length || touchedPaths.some(
2437
+ (relativePath) => !options.baselineHashes.has(relativePath) || !options.expectedHashes.has(relativePath)
2438
+ )) {
2439
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INTERNAL_ERROR);
2109
2440
  }
2110
2441
  const change = Object.freeze({
2111
2442
  kind: "prepared-agent-change",
@@ -2115,6 +2446,7 @@ function createPreparedAgentChange(options) {
2115
2446
  });
2116
2447
  privateChanges.set(change, {
2117
2448
  baselineHead: options.baselineHead,
2449
+ baselineHashes: new Map(options.baselineHashes),
2118
2450
  diff: options.result.diff,
2119
2451
  expectedHashes: new Map(options.expectedHashes),
2120
2452
  root: options.root,
@@ -2126,28 +2458,30 @@ function createPreparedAgentChange(options) {
2126
2458
  function requirePrivateChange(change) {
2127
2459
  const privateChange = privateChanges.get(change);
2128
2460
  if (privateChange === void 0) {
2129
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INTERNAL_ERROR);
2461
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INTERNAL_ERROR);
2130
2462
  }
2131
2463
  return privateChange;
2132
2464
  }
2133
- async function currentHead(root) {
2134
- return (await runGitCommand({
2135
- cwd: root,
2136
- args: ["rev-parse", "--verify", "HEAD"],
2137
- errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2138
- })).trim();
2465
+ async function assertWorkspaceOperationSafe(root, expectedHead) {
2466
+ const inspection = await inspectGitWorkspace(root).catch(() => {
2467
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2468
+ });
2469
+ const blockingOperation = inspection.health.errorCode === import_shared18.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS || inspection.health.errorCode === import_shared18.ERROR_CODES.WORKTREE_CONFLICTED;
2470
+ if (inspection.head !== expectedHead || blockingOperation) {
2471
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2472
+ }
2139
2473
  }
2140
2474
  async function fileHash(root, relativePath) {
2141
2475
  const normalized = assertAgentPathAllowed(relativePath);
2142
2476
  const absolutePath = await resolveWritableAgentPath(root, normalized);
2143
- const metadata = await (0, import_promises6.lstat)(absolutePath).catch(() => void 0);
2477
+ const metadata = await (0, import_promises7.lstat)(absolutePath).catch(() => void 0);
2144
2478
  if (metadata === void 0) {
2145
2479
  return DELETED_HASH;
2146
2480
  }
2147
2481
  if (!metadata.isFile() || metadata.isSymbolicLink()) {
2148
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2482
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2149
2483
  }
2150
- return (0, import_node_crypto3.createHash)("sha256").update(await (0, import_promises6.readFile)(absolutePath)).digest("hex");
2484
+ return (0, import_node_crypto4.createHash)("sha256").update(await (0, import_promises7.readFile)(absolutePath)).digest("hex");
2151
2485
  }
2152
2486
  async function captureAgentFileHashes(root, paths) {
2153
2487
  const entries = await Promise.all(
@@ -2163,34 +2497,38 @@ function hashesMatch(expected, actual) {
2163
2497
  async function applyPreparedAgentChange(change) {
2164
2498
  const privateChange = requirePrivateChange(change);
2165
2499
  if (privateChange.state !== "prepared" || !change.validationPassed || privateChange.diff.length === 0) {
2166
- throw new import_shared17.SpotPatchError(
2167
- change.validationPassed ? import_shared17.ERROR_CODES.APPLY_CONFLICT : import_shared17.ERROR_CODES.VALIDATION_FAILED
2500
+ throw new import_shared18.SpotPatchError(
2501
+ change.validationPassed ? import_shared18.ERROR_CODES.APPLY_CONFLICT : import_shared18.ERROR_CODES.VALIDATION_FAILED
2168
2502
  );
2169
2503
  }
2170
2504
  privateChange.state = "applying";
2171
2505
  try {
2172
- await assertCleanGitBaseline({
2173
- root: privateChange.root,
2174
- expectedHead: privateChange.baselineHead
2175
- });
2506
+ await assertWorkspaceOperationSafe(privateChange.root, privateChange.baselineHead);
2507
+ const currentHashes = await captureAgentFileHashes(
2508
+ privateChange.root,
2509
+ privateChange.touchedPaths
2510
+ );
2511
+ if (!hashesMatch(privateChange.baselineHashes, currentHashes)) {
2512
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2513
+ }
2176
2514
  await runGitCommand({
2177
2515
  cwd: privateChange.root,
2178
2516
  args: ["apply", "--check", "--whitespace=error-all", "-"],
2179
2517
  stdin: privateChange.diff,
2180
- errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2518
+ errorCode: import_shared18.ERROR_CODES.APPLY_CONFLICT
2181
2519
  });
2182
2520
  await runGitCommand({
2183
2521
  cwd: privateChange.root,
2184
2522
  args: ["apply", "--whitespace=error-all", "-"],
2185
2523
  stdin: privateChange.diff,
2186
- errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2524
+ errorCode: import_shared18.ERROR_CODES.APPLY_CONFLICT
2187
2525
  });
2188
2526
  const appliedHashes = await captureAgentFileHashes(
2189
2527
  privateChange.root,
2190
2528
  privateChange.touchedPaths
2191
2529
  );
2192
2530
  if (!hashesMatch(privateChange.expectedHashes, appliedHashes)) {
2193
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2531
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2194
2532
  }
2195
2533
  privateChange.appliedHashes = appliedHashes;
2196
2534
  privateChange.state = "applied";
@@ -2202,34 +2540,39 @@ async function applyPreparedAgentChange(change) {
2202
2540
  async function revertPreparedAgentChange(change) {
2203
2541
  const privateChange = requirePrivateChange(change);
2204
2542
  if (privateChange.state !== "applied" || privateChange.appliedHashes === void 0) {
2205
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2543
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2206
2544
  }
2207
2545
  privateChange.state = "reverting";
2208
2546
  try {
2209
- if (await currentHead(privateChange.root) !== privateChange.baselineHead) {
2210
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2211
- }
2547
+ await assertWorkspaceOperationSafe(privateChange.root, privateChange.baselineHead);
2212
2548
  const currentHashes = await captureAgentFileHashes(
2213
2549
  privateChange.root,
2214
2550
  privateChange.touchedPaths
2215
2551
  );
2216
2552
  for (const [relativePath, expectedHash] of privateChange.appliedHashes) {
2217
2553
  if (currentHashes.get(relativePath) !== expectedHash) {
2218
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2554
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2219
2555
  }
2220
2556
  }
2221
2557
  await runGitCommand({
2222
2558
  cwd: privateChange.root,
2223
2559
  args: ["apply", "--reverse", "--check", "--whitespace=error-all", "-"],
2224
2560
  stdin: privateChange.diff,
2225
- errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2561
+ errorCode: import_shared18.ERROR_CODES.APPLY_CONFLICT
2226
2562
  });
2227
2563
  await runGitCommand({
2228
2564
  cwd: privateChange.root,
2229
2565
  args: ["apply", "--reverse", "--whitespace=error-all", "-"],
2230
2566
  stdin: privateChange.diff,
2231
- errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2567
+ errorCode: import_shared18.ERROR_CODES.APPLY_CONFLICT
2232
2568
  });
2569
+ const revertedHashes = await captureAgentFileHashes(
2570
+ privateChange.root,
2571
+ privateChange.touchedPaths
2572
+ );
2573
+ if (!hashesMatch(privateChange.baselineHashes, revertedHashes)) {
2574
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2575
+ }
2233
2576
  privateChange.state = "reverted";
2234
2577
  } catch (error) {
2235
2578
  privateChange.state = "applied";
@@ -2238,7 +2581,7 @@ async function revertPreparedAgentChange(change) {
2238
2581
  }
2239
2582
 
2240
2583
  // src/engine/agent-prompt.ts
2241
- var import_shared18 = require("@spotpatch/shared");
2584
+ var import_shared19 = require("@spotpatch/shared");
2242
2585
  var AGENT_SYSTEM_INSTRUCTIONS = `You are editing code only inside a disposable, isolated Git worktree.
2243
2586
 
2244
2587
  Follow these rules exactly:
@@ -2249,13 +2592,14 @@ Follow these rules exactly:
2249
2592
  - 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.
2250
2593
  - 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.
2251
2594
  - 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.
2595
+ - 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.
2252
2596
  - Never modify credentials, environment files, lockfiles, generated output, Git metadata, or dependencies.
2253
2597
  - Do not claim a check passed unless run_check returned a passed status.
2254
2598
  - Finish with a concise factual summary after all needed tool calls. Do not include secrets or absolute paths.`;
2255
2599
  function redactedJson(value) {
2256
2600
  return JSON.stringify(
2257
2601
  value,
2258
- (_key, item) => typeof item === "string" ? (0, import_shared18.redactSensitiveText)(item) : item,
2602
+ (_key, item) => typeof item === "string" ? (0, import_shared19.redactSensitiveText)(item) : item,
2259
2603
  2
2260
2604
  );
2261
2605
  }
@@ -2332,7 +2676,7 @@ function createBoundedTarget(target, maximumCharacters) {
2332
2676
  function composeBoundedContext(annotation, maximumCharacters) {
2333
2677
  const page = Object.freeze({
2334
2678
  ...annotation.page,
2335
- url: (0, import_shared18.sanitizeUrl)(annotation.page.url, "http://spotpatch.invalid")
2679
+ url: (0, import_shared19.sanitizeUrl)(annotation.page.url, "http://spotpatch.invalid")
2336
2680
  });
2337
2681
  const fixedCharacters = redactedJson({
2338
2682
  page,
@@ -2388,7 +2732,7 @@ function composeAgentUserPrompt(annotation, maximumCharacters) {
2388
2732
  const minimumContextCharacters = 1024;
2389
2733
  const request = annotation.targets.map(
2390
2734
  (target, index) => `Target ${String(index + 1)}:
2391
- ${(0, import_shared18.redactSensitiveText)(target.instruction.trim())}`
2735
+ ${(0, import_shared19.redactSensitiveText)(target.instruction.trim())}`
2392
2736
  ).join("\n\n");
2393
2737
  const prefix = `${requestPrefix}${request}${contextPrefix}`;
2394
2738
  if (prefix.length + suffix.length + minimumContextCharacters > maximumCharacters) {
@@ -2408,11 +2752,11 @@ function isRetryableToolFailure(result) {
2408
2752
  return false;
2409
2753
  }
2410
2754
  const candidate = output;
2411
- return candidate.errorCode === import_shared19.ERROR_CODES.PATCH_REJECTED && candidate.retryable === true;
2755
+ return candidate.retryable === true && (candidate.errorCode === import_shared20.ERROR_CODES.PATCH_REJECTED || candidate.errorCode === import_shared20.ERROR_CODES.TOOL_ARGUMENTS_INVALID);
2412
2756
  }
2413
2757
  function throwIfCancelled(signal) {
2414
2758
  if (signal.aborted) {
2415
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AGENT_CANCELLED);
2759
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.AGENT_CANCELLED);
2416
2760
  }
2417
2761
  }
2418
2762
  function linkSignal(source, target) {
@@ -2450,6 +2794,7 @@ async function executeAgentChange(options) {
2450
2794
  worktree = await createIsolatedGitWorktree({
2451
2795
  root: options.root,
2452
2796
  signal: controller.signal,
2797
+ workingTreeMode: options.workingTreeMode ?? "require-clean",
2453
2798
  ...options.temporaryBase === void 0 ? {} : { temporaryBase: options.temporaryBase }
2454
2799
  });
2455
2800
  options.callbacks?.onPhase?.(
@@ -2483,30 +2828,38 @@ async function executeAgentChange(options) {
2483
2828
  let summary;
2484
2829
  let toolCallCount = 0;
2485
2830
  for (let turn = 0; turn < options.execution.limits.maxTurns; turn += 1) {
2831
+ const turnNumber = turn + 1;
2486
2832
  throwIfCancelled(controller.signal);
2487
2833
  const response = await session.next(pendingResults, controller.signal);
2834
+ assertUniqueToolCallIds(response.toolCalls);
2488
2835
  if (response.toolCalls.length === 0) {
2489
2836
  summary = response.finalText.trim().slice(0, options.execution.limits.maxToolOutputCharacters);
2490
2837
  break;
2491
2838
  }
2492
2839
  toolCallCount += response.toolCalls.length;
2493
2840
  if (toolCallCount > options.execution.limits.maxToolCalls) {
2494
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2841
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2495
2842
  }
2496
2843
  const results = [];
2497
2844
  for (const call of response.toolCalls) {
2498
2845
  options.callbacks?.onTool?.(
2499
2846
  Object.freeze({
2847
+ turn: turnNumber,
2500
2848
  toolCallId: call.id,
2501
2849
  toolName: call.name,
2502
2850
  state: "started"
2503
2851
  })
2504
2852
  );
2505
2853
  try {
2506
- const result2 = await executor.execute(call, controller.signal);
2854
+ const result2 = await executor.execute(
2855
+ call,
2856
+ Object.freeze({ turn: turnNumber }),
2857
+ controller.signal
2858
+ );
2507
2859
  results.push(result2);
2508
2860
  options.callbacks?.onTool?.(
2509
2861
  Object.freeze({
2862
+ turn: turnNumber,
2510
2863
  toolCallId: call.id,
2511
2864
  toolName: call.name,
2512
2865
  state: isRetryableToolFailure(result2) ? "failed" : "succeeded"
@@ -2515,6 +2868,7 @@ async function executeAgentChange(options) {
2515
2868
  } catch (error) {
2516
2869
  options.callbacks?.onTool?.(
2517
2870
  Object.freeze({
2871
+ turn: turnNumber,
2518
2872
  toolCallId: call.id,
2519
2873
  toolName: call.name,
2520
2874
  state: "failed"
@@ -2526,7 +2880,7 @@ async function executeAgentChange(options) {
2526
2880
  pendingResults = Object.freeze(results);
2527
2881
  }
2528
2882
  if (summary === void 0) {
2529
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2883
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2530
2884
  }
2531
2885
  options.callbacks?.onPhase?.(
2532
2886
  Object.freeze({
@@ -2559,7 +2913,7 @@ async function executeAgentChange(options) {
2559
2913
  controller.signal
2560
2914
  );
2561
2915
  if (afterCheck.diff !== initialChangeSet.diff) {
2562
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.VALIDATION_FAILED);
2916
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.VALIDATION_FAILED);
2563
2917
  }
2564
2918
  }
2565
2919
  const validationPassed = finalChecks.every((check) => check.status === "passed");
@@ -2575,9 +2929,14 @@ async function executeAgentChange(options) {
2575
2929
  worktree.root,
2576
2930
  initialChangeSet.touchedPaths
2577
2931
  );
2932
+ const baselineHashes = await captureAgentFileHashes(
2933
+ worktree.baseline.root,
2934
+ initialChangeSet.touchedPaths
2935
+ );
2578
2936
  return createPreparedAgentChange({
2579
2937
  autoApplyEligible,
2580
2938
  baselineHead: worktree.baseline.head,
2939
+ baselineHashes,
2581
2940
  expectedHashes,
2582
2941
  result,
2583
2942
  root: worktree.baseline.root,
@@ -2585,15 +2944,15 @@ async function executeAgentChange(options) {
2585
2944
  });
2586
2945
  } catch (error) {
2587
2946
  if (options.signal.aborted) {
2588
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AGENT_CANCELLED);
2947
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.AGENT_CANCELLED);
2589
2948
  }
2590
2949
  if (hasJobTimedOut()) {
2591
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2950
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2592
2951
  }
2593
- if (error instanceof import_shared19.SpotPatchError) {
2952
+ if (error instanceof import_shared20.SpotPatchError) {
2594
2953
  throw error;
2595
2954
  }
2596
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INTERNAL_ERROR);
2955
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INTERNAL_ERROR);
2597
2956
  } finally {
2598
2957
  clearTimeout(timeout);
2599
2958
  unlink();
@@ -2602,13 +2961,13 @@ async function executeAgentChange(options) {
2602
2961
  }
2603
2962
 
2604
2963
  // src/provider/capability-probe.ts
2605
- var import_shared20 = require("@spotpatch/shared");
2964
+ var import_shared21 = require("@spotpatch/shared");
2606
2965
  var PROBE_TOOL_NAME = "spotpatch_capability_probe";
2607
2966
  var PROBE_TOKEN = "spotpatch-ready-v1";
2608
2967
  async function probeProviderCapability(options) {
2609
2968
  const model = options.provider.models[options.modelProfileId];
2610
2969
  if (model === void 0) {
2611
- throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.MODEL_NOT_ALLOWED);
2970
+ throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.MODEL_NOT_ALLOWED);
2612
2971
  }
2613
2972
  const credential = options.credential ?? resolveProviderCredential(options.provider.apiKeyEnv, options.environment);
2614
2973
  const session = createOpenAICompatibleProviderSession({
@@ -2637,7 +2996,7 @@ async function probeProviderCapability(options) {
2637
2996
  const first = await session.next(void 0, options.signal);
2638
2997
  const probeCall = first.toolCalls[0];
2639
2998
  if (first.toolCalls.length !== 1 || probeCall?.name !== PROBE_TOOL_NAME || probeCall.arguments.token !== PROBE_TOKEN) {
2640
- throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED);
2999
+ throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED);
2641
3000
  }
2642
3001
  const second = await session.next(
2643
3002
  Object.freeze([
@@ -2649,7 +3008,7 @@ async function probeProviderCapability(options) {
2649
3008
  options.signal
2650
3009
  );
2651
3010
  if (second.toolCalls.length !== 0 || second.finalText.trim().length === 0) {
2652
- throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED);
3011
+ throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED);
2653
3012
  }
2654
3013
  return Object.freeze({
2655
3014
  providerProfileId: options.provider.id,
@@ -2672,6 +3031,7 @@ async function probeProviderCapability(options) {
2672
3031
  createOpenAICompatibleProviderSession,
2673
3032
  createProviderCredential,
2674
3033
  executeAgentChange,
3034
+ inspectAgentWorkspace,
2675
3035
  probeProviderCapability,
2676
3036
  resolveProviderCredential,
2677
3037
  revertPreparedAgentChange