@spotpatch/agent 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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");
@@ -278,6 +279,19 @@ async function readSseEvents(stream, options) {
278
279
  }
279
280
 
280
281
  // src/provider/provider-transport.ts
282
+ function createProviderHeaders(authentication, credential) {
283
+ const headers = new Headers({
284
+ Accept: "text/event-stream",
285
+ "Content-Type": "application/json"
286
+ });
287
+ const value = readProviderCredential(credential);
288
+ if (authentication === "x-api-key") {
289
+ headers.set("x-api-key", value);
290
+ } else {
291
+ headers.set("Authorization", `Bearer ${value}`);
292
+ }
293
+ return headers;
294
+ }
281
295
  function mapStatus(status) {
282
296
  if (status === 401 || status === 403) {
283
297
  return new import_shared4.SpotPatchError(import_shared4.ERROR_CODES.PROVIDER_AUTH_FAILED);
@@ -317,11 +331,7 @@ async function postProviderStream(options) {
317
331
  try {
318
332
  response = await options.fetch(options.url, {
319
333
  method: "POST",
320
- headers: {
321
- Accept: "text/event-stream",
322
- Authorization: `Bearer ${readProviderCredential(options.credential)}`,
323
- "Content-Type": "application/json"
324
- },
334
+ headers: createProviderHeaders(options.authentication, options.credential),
325
335
  body: JSON.stringify(options.body),
326
336
  redirect: "error",
327
337
  signal: requestController.signal
@@ -514,6 +524,7 @@ function createChatCompletionsSession(options) {
514
524
  }
515
525
  const parsed = parseChatEvents(
516
526
  await postProviderStream({
527
+ authentication: options.provider.authentication,
517
528
  body: {
518
529
  model: options.model.model,
519
530
  messages,
@@ -685,6 +696,7 @@ function createResponsesSession(options) {
685
696
  };
686
697
  const parsed = parseResponsesEvents(
687
698
  await postProviderStream({
699
+ authentication: options.provider.authentication,
688
700
  body,
689
701
  credential: options.credential,
690
702
  fetch,
@@ -741,7 +753,7 @@ var PROTECTED_FILE_NAMES = /* @__PURE__ */ new Set([
741
753
  "yarn.lock"
742
754
  ]);
743
755
  function deny() {
744
- throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.TOOL_DENIED);
756
+ throw new import_shared8.SpotPatchError(import_shared8.ERROR_CODES.TOOL_PATH_DENIED);
745
757
  }
746
758
  function hasControlCharacter(value) {
747
759
  for (let index = 0; index < value.length; index += 1) {
@@ -843,13 +855,13 @@ async function readAgentTextFile(root, relativePath, maximumBytes) {
843
855
  }
844
856
  const bytes = await (0, import_promises2.readFile)(absolutePath);
845
857
  if (bytes.includes(0)) {
846
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_DENIED);
858
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_PATH_DENIED);
847
859
  }
848
860
  let content;
849
861
  try {
850
862
  content = utf8Decoder.decode(bytes);
851
863
  } catch {
852
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_DENIED);
864
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_PATH_DENIED);
853
865
  }
854
866
  return Object.freeze({ content, relativePath, size: metadata.size });
855
867
  }
@@ -862,7 +874,7 @@ function encodeUtf8Text(content, includeByteOrderMark) {
862
874
  }
863
875
  async function writeAgentTextFileIfContentMatches(root, relativePath, expectedContent, nextContent, maximumBytes) {
864
876
  if (nextContent.includes("\0")) {
865
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_DENIED);
877
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_INPUT_INVALID);
866
878
  }
867
879
  const absolutePath = await resolveExistingAgentPath(root, relativePath);
868
880
  const [metadata, currentBytes] = await Promise.all([
@@ -876,7 +888,7 @@ async function writeAgentTextFileIfContentMatches(root, relativePath, expectedCo
876
888
  try {
877
889
  currentContent = utf8Decoder.decode(currentBytes);
878
890
  } catch {
879
- throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_DENIED);
891
+ throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.TOOL_PATH_DENIED);
880
892
  }
881
893
  if (currentBytes.includes(0) || currentContent !== expectedContent) {
882
894
  throw new import_shared9.SpotPatchError(import_shared9.ERROR_CODES.PATCH_REJECTED);
@@ -1131,7 +1143,7 @@ async function runConfiguredCheck(options) {
1131
1143
  function requireConfiguredCheck(checkId, checks) {
1132
1144
  const check = checks[checkId];
1133
1145
  if (check === void 0) {
1134
- throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.TOOL_DENIED);
1146
+ throw new import_shared10.SpotPatchError(import_shared10.ERROR_CODES.TOOL_INPUT_INVALID);
1135
1147
  }
1136
1148
  return check;
1137
1149
  }
@@ -1320,9 +1332,16 @@ async function applyAgentPatch(worktreeRoot, patch, limits, signal) {
1320
1332
  throw new import_shared13.SpotPatchError(import_shared13.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
1321
1333
  }
1322
1334
  await Promise.all(
1323
- files.map(
1324
- async (file) => resolveWritableAgentPath(worktreeRoot, file.relativePath)
1325
- )
1335
+ files.map(async (file) => {
1336
+ await resolveWritableAgentPath(worktreeRoot, file.relativePath);
1337
+ if (file.kind !== "added") {
1338
+ await readAgentTextFile(
1339
+ worktreeRoot,
1340
+ file.relativePath,
1341
+ limits.maxReadBytesPerFile
1342
+ );
1343
+ }
1344
+ })
1326
1345
  );
1327
1346
  await runGitCommand({
1328
1347
  cwd: worktreeRoot,
@@ -1436,7 +1455,7 @@ var MAX_DISCOVERED_FILES = 2e4;
1436
1455
  var TEXT_SAMPLE_BYTES = 8192;
1437
1456
  function compileGlob(glob) {
1438
1457
  if (glob.length === 0 || glob.length > 256 || glob.includes("\0") || glob.includes("\\") || glob.startsWith("/") || ["[", "]", "{", "}", "(", ")", "!"].some((character) => glob.includes(character)) || glob.split("/").some((segment) => segment === "..")) {
1439
- throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.TOOL_DENIED);
1458
+ throw new import_shared14.SpotPatchError(import_shared14.ERROR_CODES.TOOL_INPUT_INVALID);
1440
1459
  }
1441
1460
  let expression = "^";
1442
1461
  for (let index = 0; index < glob.length; index += 1) {
@@ -1675,7 +1694,7 @@ var runCheckSchema = import_zod.z.strictObject({
1675
1694
  checkId: import_zod.z.string().min(1).max(64).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u)
1676
1695
  });
1677
1696
  function invalidTool() {
1678
- throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.TOOL_DENIED);
1697
+ throw new import_shared15.SpotPatchError(import_shared15.ERROR_CODES.TOOL_INPUT_INVALID);
1679
1698
  }
1680
1699
  function parseArguments(schema, value) {
1681
1700
  const parsed = schema.safeParse(value);
@@ -1972,67 +1991,339 @@ function createAgentToolExecutor(options) {
1972
1991
  }
1973
1992
 
1974
1993
  // src/worktree/git-worktree.ts
1975
- var import_promises5 = require("fs/promises");
1994
+ var import_promises6 = require("fs/promises");
1995
+ var import_node_crypto3 = require("crypto");
1976
1996
  var import_node_os = __toESM(require("os"), 1);
1997
+ var import_node_path6 = __toESM(require("path"), 1);
1998
+ var import_shared17 = require("@spotpatch/shared");
1999
+
2000
+ // src/worktree/workspace-health.ts
2001
+ var import_promises5 = require("fs/promises");
1977
2002
  var import_node_path5 = __toESM(require("path"), 1);
1978
2003
  var import_shared16 = require("@spotpatch/shared");
1979
- async function assertCleanGitBaseline(options) {
1980
- const root = await (0, import_promises5.realpath)(options.root).catch(() => {
1981
- throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_DIRTY);
2004
+ var CONFLICTED_STATUSES = /* @__PURE__ */ new Set(["DD", "AU", "UD", "UA", "DU", "AA", "UU"]);
2005
+ var OPERATION_MARKERS = Object.freeze([
2006
+ "MERGE_HEAD",
2007
+ "CHERRY_PICK_HEAD",
2008
+ "REVERT_HEAD",
2009
+ "rebase-apply",
2010
+ "rebase-merge"
2011
+ ]);
2012
+ function emptyChanges() {
2013
+ return Object.freeze({
2014
+ staged: 0,
2015
+ unstaged: 0,
2016
+ untracked: 0,
2017
+ conflicted: 0,
2018
+ total: 0
2019
+ });
2020
+ }
2021
+ function blockedHealth(errorCode) {
2022
+ return Object.freeze({
2023
+ state: "blocked",
2024
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
2025
+ changes: emptyChanges(),
2026
+ canIncludeLocalChanges: false,
2027
+ errorCode
1982
2028
  });
1983
- const topLevel = (await runGitCommand({
2029
+ }
2030
+ function parsePorcelainStatus(value) {
2031
+ const records = value.split("\0");
2032
+ const untrackedPaths = [];
2033
+ let staged = 0;
2034
+ let unstaged = 0;
2035
+ let conflicted = 0;
2036
+ let total = 0;
2037
+ for (let index = 0; index < records.length; index += 1) {
2038
+ const record = records[index] ?? "";
2039
+ if (record.length === 0) {
2040
+ continue;
2041
+ }
2042
+ if (record.length < 4 || record[2] !== " ") {
2043
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2044
+ }
2045
+ const status = record.slice(0, 2);
2046
+ const relativePath = record.slice(3);
2047
+ if (relativePath.length === 0) {
2048
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2049
+ }
2050
+ total += 1;
2051
+ if (status === "??") {
2052
+ untrackedPaths.push(relativePath);
2053
+ continue;
2054
+ }
2055
+ const indexStatus = status[0] ?? " ";
2056
+ const worktreeStatus = status[1] ?? " ";
2057
+ if (CONFLICTED_STATUSES.has(status) || indexStatus === "U" || worktreeStatus === "U") {
2058
+ conflicted += 1;
2059
+ }
2060
+ if (indexStatus !== " ") {
2061
+ staged += 1;
2062
+ }
2063
+ if (worktreeStatus !== " ") {
2064
+ unstaged += 1;
2065
+ }
2066
+ if (indexStatus === "R" || indexStatus === "C") {
2067
+ index += 1;
2068
+ if ((records[index] ?? "").length === 0) {
2069
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2070
+ }
2071
+ }
2072
+ }
2073
+ return Object.freeze({
2074
+ changes: Object.freeze({
2075
+ staged,
2076
+ unstaged,
2077
+ untracked: untrackedPaths.length,
2078
+ conflicted,
2079
+ total
2080
+ }),
2081
+ untrackedPaths: Object.freeze(untrackedPaths)
2082
+ });
2083
+ }
2084
+ async function operationInProgress(root, signal) {
2085
+ for (const marker of OPERATION_MARKERS) {
2086
+ const markerPath = (await runGitCommand({
2087
+ cwd: root,
2088
+ args: ["rev-parse", "--git-path", marker],
2089
+ errorCode: import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY,
2090
+ ...signal === void 0 ? {} : { signal }
2091
+ })).trim();
2092
+ if (await (0, import_promises5.lstat)(import_node_path5.default.resolve(root, markerPath)).catch(() => void 0) !== void 0) {
2093
+ return true;
2094
+ }
2095
+ }
2096
+ return false;
2097
+ }
2098
+ async function inspectUntrackedFiles(root, relativePaths) {
2099
+ if (relativePaths.length > import_shared16.AGENT_WORKSPACE_SNAPSHOT_LIMITS.maxUntrackedFiles) {
2100
+ return import_shared16.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE;
2101
+ }
2102
+ let totalBytes = 0;
2103
+ for (const relativePath of relativePaths) {
2104
+ const absolutePath = import_node_path5.default.resolve(root, relativePath);
2105
+ const relative = import_node_path5.default.relative(root, absolutePath);
2106
+ if (relative === ".." || relative.startsWith(`..${import_node_path5.default.sep}`) || import_node_path5.default.isAbsolute(relative)) {
2107
+ return import_shared16.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED;
2108
+ }
2109
+ const metadata = await (0, import_promises5.lstat)(absolutePath).catch(() => void 0);
2110
+ if (metadata === void 0 || !metadata.isFile() || metadata.isSymbolicLink()) {
2111
+ return import_shared16.ERROR_CODES.WORKTREE_UNTRACKED_UNSUPPORTED;
2112
+ }
2113
+ totalBytes += metadata.size;
2114
+ if (totalBytes > import_shared16.AGENT_WORKSPACE_SNAPSHOT_LIMITS.maxUntrackedBytes) {
2115
+ return import_shared16.ERROR_CODES.WORKTREE_LOCAL_CHANGES_TOO_LARGE;
2116
+ }
2117
+ }
2118
+ return void 0;
2119
+ }
2120
+ async function inspectGitWorkspace(rootValue, signal) {
2121
+ const root = await (0, import_promises5.realpath)(rootValue).catch(() => {
2122
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY);
2123
+ });
2124
+ const topLevelResult = await runRawGitCommand({
1984
2125
  cwd: root,
1985
2126
  args: ["rev-parse", "--show-toplevel"],
1986
- errorCode: import_shared16.ERROR_CODES.WORKTREE_DIRTY,
1987
- ...options.signal === void 0 ? {} : { signal: options.signal }
1988
- })).trim();
2127
+ ...signal === void 0 ? {} : { signal }
2128
+ });
2129
+ if (topLevelResult.exitCode !== 0 || topLevelResult.cancelled || topLevelResult.timedOut) {
2130
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY);
2131
+ }
2132
+ const topLevel = topLevelResult.stdout.trim();
1989
2133
  if (!samePath(root, topLevel)) {
1990
- throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_DIRTY);
2134
+ throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY);
1991
2135
  }
1992
2136
  const head = (await runGitCommand({
1993
2137
  cwd: root,
1994
2138
  args: ["rev-parse", "--verify", "HEAD"],
1995
- errorCode: import_shared16.ERROR_CODES.WORKTREE_DIRTY,
1996
- ...options.signal === void 0 ? {} : { signal: options.signal }
2139
+ errorCode: import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY,
2140
+ ...signal === void 0 ? {} : { signal }
1997
2141
  })).trim();
1998
- if (options.expectedHead !== void 0 && head !== options.expectedHead) {
1999
- throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.APPLY_CONFLICT);
2142
+ const parsed = parsePorcelainStatus(
2143
+ await runGitCommand({
2144
+ cwd: root,
2145
+ args: ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
2146
+ errorCode: import_shared16.ERROR_CODES.WORKTREE_NOT_REPOSITORY,
2147
+ ...signal === void 0 ? {} : { signal }
2148
+ })
2149
+ );
2150
+ const operationActive = await operationInProgress(root, signal);
2151
+ const untrackedError = await inspectUntrackedFiles(root, parsed.untrackedPaths);
2152
+ const errorCode = operationActive ? import_shared16.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS : parsed.changes.conflicted > 0 ? import_shared16.ERROR_CODES.WORKTREE_CONFLICTED : untrackedError;
2153
+ const health = Object.freeze({
2154
+ state: errorCode !== void 0 ? "blocked" : parsed.changes.total === 0 ? "ready" : "consent-required",
2155
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
2156
+ changes: parsed.changes,
2157
+ canIncludeLocalChanges: errorCode === void 0 && parsed.changes.total > 0,
2158
+ ...errorCode === void 0 ? {} : { errorCode }
2159
+ });
2160
+ return Object.freeze({
2161
+ root,
2162
+ head,
2163
+ health,
2164
+ untrackedPaths: parsed.untrackedPaths
2165
+ });
2166
+ }
2167
+ async function inspectAgentWorkspace(root, signal) {
2168
+ try {
2169
+ return (await inspectGitWorkspace(root, signal)).health;
2170
+ } catch (error) {
2171
+ if (error instanceof import_shared16.SpotPatchError) {
2172
+ return blockedHealth(error.code);
2173
+ }
2174
+ return blockedHealth(import_shared16.ERROR_CODES.INTERNAL_ERROR);
2000
2175
  }
2001
- const status = await runGitCommand({
2002
- cwd: root,
2003
- args: ["status", "--porcelain=v1", "-z", "--untracked-files=all"],
2004
- errorCode: import_shared16.ERROR_CODES.WORKTREE_DIRTY,
2005
- ...options.signal === void 0 ? {} : { signal: options.signal }
2176
+ }
2177
+
2178
+ // src/worktree/git-worktree.ts
2179
+ function workspacePath(root, relativePath) {
2180
+ const candidate = import_node_path6.default.resolve(root, relativePath);
2181
+ const relative = import_node_path6.default.relative(root, candidate);
2182
+ if (relative === ".." || relative.startsWith(`..${import_node_path6.default.sep}`) || import_node_path6.default.isAbsolute(relative)) {
2183
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2184
+ }
2185
+ return candidate;
2186
+ }
2187
+ async function fileDigest(filePath) {
2188
+ return (0, import_node_crypto3.createHash)("sha256").update(await (0, import_promises6.readFile)(filePath)).digest("hex");
2189
+ }
2190
+ async function copyUntrackedFiles(sourceRoot, worktreeRoot, relativePaths) {
2191
+ for (const relativePath of relativePaths) {
2192
+ const sourcePath = workspacePath(sourceRoot, relativePath);
2193
+ const targetPath = workspacePath(worktreeRoot, relativePath);
2194
+ const metadata = await (0, import_promises6.lstat)(sourcePath).catch(() => void 0);
2195
+ if (metadata === void 0 || !metadata.isFile() || metadata.isSymbolicLink()) {
2196
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED);
2197
+ }
2198
+ await (0, import_promises6.mkdir)(import_node_path6.default.dirname(targetPath), { recursive: true });
2199
+ await (0, import_promises6.copyFile)(sourcePath, targetPath);
2200
+ const [sourceDigest, targetDigest] = await Promise.all([
2201
+ fileDigest(sourcePath),
2202
+ fileDigest(targetPath)
2203
+ ]).catch(() => {
2204
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2205
+ });
2206
+ if (sourceDigest !== targetDigest) {
2207
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2208
+ }
2209
+ }
2210
+ }
2211
+ async function assertUntrackedFilesUnchanged(sourceRoot, worktreeRoot, relativePaths) {
2212
+ for (const relativePath of relativePaths) {
2213
+ const sourcePath = workspacePath(sourceRoot, relativePath);
2214
+ const targetPath = workspacePath(worktreeRoot, relativePath);
2215
+ const [sourceDigest, targetDigest] = await Promise.all([
2216
+ fileDigest(sourcePath),
2217
+ fileDigest(targetPath)
2218
+ ]).catch(() => {
2219
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2220
+ });
2221
+ if (sourceDigest !== targetDigest) {
2222
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2223
+ }
2224
+ }
2225
+ }
2226
+ async function materializeLocalBaseline(sourceRoot, worktreeRoot, expectedHead, untrackedPaths, signal) {
2227
+ const sourceDiff = await runGitCommand({
2228
+ cwd: sourceRoot,
2229
+ args: [
2230
+ "diff",
2231
+ "--binary",
2232
+ "--full-index",
2233
+ "--no-ext-diff",
2234
+ "--no-color",
2235
+ "--no-renames",
2236
+ "HEAD",
2237
+ "--"
2238
+ ],
2239
+ signal,
2240
+ errorCode: import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2006
2241
  });
2007
- if (status.length > 0) {
2008
- throw new import_shared16.SpotPatchError(
2009
- options.expectedHead === void 0 ? import_shared16.ERROR_CODES.WORKTREE_DIRTY : import_shared16.ERROR_CODES.APPLY_CONFLICT
2010
- );
2242
+ if (sourceDiff.length > 0) {
2243
+ await runGitCommand({
2244
+ cwd: worktreeRoot,
2245
+ args: ["apply", "--binary", "--whitespace=nowarn", "-"],
2246
+ stdin: sourceDiff,
2247
+ signal,
2248
+ errorCode: import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2249
+ });
2250
+ }
2251
+ await copyUntrackedFiles(sourceRoot, worktreeRoot, untrackedPaths);
2252
+ const confirmation = await inspectGitWorkspace(sourceRoot, signal);
2253
+ const confirmationDiff = await runGitCommand({
2254
+ cwd: sourceRoot,
2255
+ args: [
2256
+ "diff",
2257
+ "--binary",
2258
+ "--full-index",
2259
+ "--no-ext-diff",
2260
+ "--no-color",
2261
+ "--no-renames",
2262
+ "HEAD",
2263
+ "--"
2264
+ ],
2265
+ signal,
2266
+ errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2267
+ });
2268
+ if (confirmation.head !== expectedHead || confirmationDiff !== sourceDiff || confirmation.untrackedPaths.length !== untrackedPaths.length || confirmation.untrackedPaths.some((value, index) => value !== untrackedPaths[index])) {
2269
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2011
2270
  }
2012
- return Object.freeze({ root, head });
2271
+ await assertUntrackedFilesUnchanged(sourceRoot, worktreeRoot, untrackedPaths);
2272
+ await runGitCommand({
2273
+ cwd: worktreeRoot,
2274
+ args: ["add", "--all"],
2275
+ signal,
2276
+ errorCode: import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2277
+ });
2278
+ await runGitCommand({
2279
+ cwd: worktreeRoot,
2280
+ args: [
2281
+ "-c",
2282
+ "user.name=SpotPatch Agent",
2283
+ "-c",
2284
+ "user.email=spotpatch-agent@example.invalid",
2285
+ "commit",
2286
+ "--quiet",
2287
+ "-m",
2288
+ "SpotPatch local workspace baseline"
2289
+ ],
2290
+ signal,
2291
+ errorCode: import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2292
+ });
2013
2293
  }
2014
2294
  async function defaultTemporaryBase(root) {
2015
- const dependencyDirectory = import_node_path5.default.join(root, "node_modules");
2295
+ const dependencyDirectory = import_node_path6.default.join(root, "node_modules");
2016
2296
  try {
2017
- const stats = await (0, import_promises5.lstat)(dependencyDirectory);
2297
+ const stats = await (0, import_promises6.lstat)(dependencyDirectory);
2018
2298
  if (!stats.isDirectory() || stats.isSymbolicLink()) {
2019
2299
  return import_node_os.default.tmpdir();
2020
2300
  }
2021
- return await (0, import_promises5.realpath)(dependencyDirectory);
2301
+ return await (0, import_promises6.realpath)(dependencyDirectory);
2022
2302
  } catch {
2023
2303
  return import_node_os.default.tmpdir();
2024
2304
  }
2025
2305
  }
2026
2306
  async function createIsolatedGitWorktree(options) {
2027
- const baseline = await assertCleanGitBaseline({
2028
- root: options.root,
2029
- signal: options.signal
2307
+ const workingTreeMode = options.workingTreeMode ?? "require-clean";
2308
+ const inspection = await inspectGitWorkspace(options.root, options.signal);
2309
+ if (inspection.health.state === "blocked") {
2310
+ throw new import_shared17.SpotPatchError(
2311
+ inspection.health.errorCode ?? import_shared17.ERROR_CODES.WORKTREE_LOCAL_CHANGES_UNSUPPORTED
2312
+ );
2313
+ }
2314
+ if (inspection.health.state === "consent-required" && workingTreeMode === "require-clean") {
2315
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.WORKTREE_DIRTY);
2316
+ }
2317
+ const baseline = Object.freeze({
2318
+ root: inspection.root,
2319
+ head: inspection.head,
2320
+ workingTreeMode
2030
2321
  });
2031
2322
  const temporaryBase = options.temporaryBase ?? await defaultTemporaryBase(baseline.root);
2032
- const temporaryDirectory = await (0, import_promises5.mkdtemp)(
2033
- import_node_path5.default.join(temporaryBase, "spotpatch-agent-")
2323
+ const temporaryDirectory = await (0, import_promises6.mkdtemp)(
2324
+ import_node_path6.default.join(temporaryBase, "spotpatch-agent-")
2034
2325
  );
2035
- const worktreePath = import_node_path5.default.join(temporaryDirectory, "worktree");
2326
+ const worktreePath = import_node_path6.default.join(temporaryDirectory, "worktree");
2036
2327
  let registered = false;
2037
2328
  let cleaned = false;
2038
2329
  const cleanup = async () => {
@@ -2047,8 +2338,8 @@ async function createIsolatedGitWorktree(options) {
2047
2338
  timeoutMs: 3e4
2048
2339
  }).catch(() => void 0);
2049
2340
  }
2050
- if (import_node_path5.default.basename(temporaryDirectory).startsWith("spotpatch-agent-")) {
2051
- await (0, import_promises5.rm)(temporaryDirectory, { recursive: true, force: true }).catch(
2341
+ if (import_node_path6.default.basename(temporaryDirectory).startsWith("spotpatch-agent-")) {
2342
+ await (0, import_promises6.rm)(temporaryDirectory, { recursive: true, force: true }).catch(
2052
2343
  () => void 0
2053
2344
  );
2054
2345
  }
@@ -2057,12 +2348,12 @@ async function createIsolatedGitWorktree(options) {
2057
2348
  await runGitCommand({
2058
2349
  cwd: baseline.root,
2059
2350
  args: ["worktree", "add", "--detach", worktreePath, baseline.head],
2060
- errorCode: import_shared16.ERROR_CODES.INTERNAL_ERROR,
2351
+ errorCode: import_shared17.ERROR_CODES.INTERNAL_ERROR,
2061
2352
  signal: options.signal,
2062
2353
  timeoutMs: 3e4
2063
2354
  });
2064
2355
  registered = true;
2065
- const worktreeRoot = await (0, import_promises5.realpath)(worktreePath);
2356
+ const worktreeRoot = await (0, import_promises6.realpath)(worktreePath);
2066
2357
  const actualHead = (await runGitCommand({
2067
2358
  cwd: worktreeRoot,
2068
2359
  args: ["rev-parse", "--verify", "HEAD"],
@@ -2074,7 +2365,16 @@ async function createIsolatedGitWorktree(options) {
2074
2365
  signal: options.signal
2075
2366
  })).trim();
2076
2367
  if (actualHead !== baseline.head || !samePath(actualRoot, worktreeRoot)) {
2077
- throw new import_shared16.SpotPatchError(import_shared16.ERROR_CODES.INTERNAL_ERROR);
2368
+ throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INTERNAL_ERROR);
2369
+ }
2370
+ if (inspection.health.state === "consent-required") {
2371
+ await materializeLocalBaseline(
2372
+ baseline.root,
2373
+ worktreeRoot,
2374
+ baseline.head,
2375
+ inspection.untrackedPaths,
2376
+ options.signal
2377
+ );
2078
2378
  }
2079
2379
  return Object.freeze({ baseline, root: worktreeRoot, cleanup });
2080
2380
  } catch (error) {
@@ -2084,17 +2384,19 @@ async function createIsolatedGitWorktree(options) {
2084
2384
  }
2085
2385
 
2086
2386
  // src/worktree/prepared-change.ts
2087
- var import_node_crypto3 = require("crypto");
2088
- var import_promises6 = require("fs/promises");
2089
- var import_shared17 = require("@spotpatch/shared");
2387
+ var import_node_crypto4 = require("crypto");
2388
+ var import_promises7 = require("fs/promises");
2389
+ var import_shared18 = require("@spotpatch/shared");
2090
2390
  var privateChanges = /* @__PURE__ */ new WeakMap();
2091
2391
  var DELETED_HASH = "<deleted>";
2092
2392
  function createPreparedAgentChange(options) {
2093
2393
  const touchedPaths = Object.freeze(
2094
2394
  options.result.files.map((file) => file.relativePath)
2095
2395
  );
2096
- if (options.expectedHashes.size !== touchedPaths.length || touchedPaths.some((relativePath) => !options.expectedHashes.has(relativePath))) {
2097
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INTERNAL_ERROR);
2396
+ if (options.baselineHashes.size !== touchedPaths.length || options.expectedHashes.size !== touchedPaths.length || touchedPaths.some(
2397
+ (relativePath) => !options.baselineHashes.has(relativePath) || !options.expectedHashes.has(relativePath)
2398
+ )) {
2399
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INTERNAL_ERROR);
2098
2400
  }
2099
2401
  const change = Object.freeze({
2100
2402
  kind: "prepared-agent-change",
@@ -2104,6 +2406,7 @@ function createPreparedAgentChange(options) {
2104
2406
  });
2105
2407
  privateChanges.set(change, {
2106
2408
  baselineHead: options.baselineHead,
2409
+ baselineHashes: new Map(options.baselineHashes),
2107
2410
  diff: options.result.diff,
2108
2411
  expectedHashes: new Map(options.expectedHashes),
2109
2412
  root: options.root,
@@ -2115,28 +2418,30 @@ function createPreparedAgentChange(options) {
2115
2418
  function requirePrivateChange(change) {
2116
2419
  const privateChange = privateChanges.get(change);
2117
2420
  if (privateChange === void 0) {
2118
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.INTERNAL_ERROR);
2421
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.INTERNAL_ERROR);
2119
2422
  }
2120
2423
  return privateChange;
2121
2424
  }
2122
- async function currentHead(root) {
2123
- return (await runGitCommand({
2124
- cwd: root,
2125
- args: ["rev-parse", "--verify", "HEAD"],
2126
- errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2127
- })).trim();
2425
+ async function assertWorkspaceOperationSafe(root, expectedHead) {
2426
+ const inspection = await inspectGitWorkspace(root).catch(() => {
2427
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2428
+ });
2429
+ const blockingOperation = inspection.health.errorCode === import_shared18.ERROR_CODES.WORKTREE_OPERATION_IN_PROGRESS || inspection.health.errorCode === import_shared18.ERROR_CODES.WORKTREE_CONFLICTED;
2430
+ if (inspection.head !== expectedHead || blockingOperation) {
2431
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2432
+ }
2128
2433
  }
2129
2434
  async function fileHash(root, relativePath) {
2130
2435
  const normalized = assertAgentPathAllowed(relativePath);
2131
2436
  const absolutePath = await resolveWritableAgentPath(root, normalized);
2132
- const metadata = await (0, import_promises6.lstat)(absolutePath).catch(() => void 0);
2437
+ const metadata = await (0, import_promises7.lstat)(absolutePath).catch(() => void 0);
2133
2438
  if (metadata === void 0) {
2134
2439
  return DELETED_HASH;
2135
2440
  }
2136
2441
  if (!metadata.isFile() || metadata.isSymbolicLink()) {
2137
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2442
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2138
2443
  }
2139
- return (0, import_node_crypto3.createHash)("sha256").update(await (0, import_promises6.readFile)(absolutePath)).digest("hex");
2444
+ return (0, import_node_crypto4.createHash)("sha256").update(await (0, import_promises7.readFile)(absolutePath)).digest("hex");
2140
2445
  }
2141
2446
  async function captureAgentFileHashes(root, paths) {
2142
2447
  const entries = await Promise.all(
@@ -2152,34 +2457,38 @@ function hashesMatch(expected, actual) {
2152
2457
  async function applyPreparedAgentChange(change) {
2153
2458
  const privateChange = requirePrivateChange(change);
2154
2459
  if (privateChange.state !== "prepared" || !change.validationPassed || privateChange.diff.length === 0) {
2155
- throw new import_shared17.SpotPatchError(
2156
- change.validationPassed ? import_shared17.ERROR_CODES.APPLY_CONFLICT : import_shared17.ERROR_CODES.VALIDATION_FAILED
2460
+ throw new import_shared18.SpotPatchError(
2461
+ change.validationPassed ? import_shared18.ERROR_CODES.APPLY_CONFLICT : import_shared18.ERROR_CODES.VALIDATION_FAILED
2157
2462
  );
2158
2463
  }
2159
2464
  privateChange.state = "applying";
2160
2465
  try {
2161
- await assertCleanGitBaseline({
2162
- root: privateChange.root,
2163
- expectedHead: privateChange.baselineHead
2164
- });
2466
+ await assertWorkspaceOperationSafe(privateChange.root, privateChange.baselineHead);
2467
+ const currentHashes = await captureAgentFileHashes(
2468
+ privateChange.root,
2469
+ privateChange.touchedPaths
2470
+ );
2471
+ if (!hashesMatch(privateChange.baselineHashes, currentHashes)) {
2472
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2473
+ }
2165
2474
  await runGitCommand({
2166
2475
  cwd: privateChange.root,
2167
2476
  args: ["apply", "--check", "--whitespace=error-all", "-"],
2168
2477
  stdin: privateChange.diff,
2169
- errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2478
+ errorCode: import_shared18.ERROR_CODES.APPLY_CONFLICT
2170
2479
  });
2171
2480
  await runGitCommand({
2172
2481
  cwd: privateChange.root,
2173
2482
  args: ["apply", "--whitespace=error-all", "-"],
2174
2483
  stdin: privateChange.diff,
2175
- errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2484
+ errorCode: import_shared18.ERROR_CODES.APPLY_CONFLICT
2176
2485
  });
2177
2486
  const appliedHashes = await captureAgentFileHashes(
2178
2487
  privateChange.root,
2179
2488
  privateChange.touchedPaths
2180
2489
  );
2181
2490
  if (!hashesMatch(privateChange.expectedHashes, appliedHashes)) {
2182
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2491
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2183
2492
  }
2184
2493
  privateChange.appliedHashes = appliedHashes;
2185
2494
  privateChange.state = "applied";
@@ -2191,34 +2500,39 @@ async function applyPreparedAgentChange(change) {
2191
2500
  async function revertPreparedAgentChange(change) {
2192
2501
  const privateChange = requirePrivateChange(change);
2193
2502
  if (privateChange.state !== "applied" || privateChange.appliedHashes === void 0) {
2194
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2503
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2195
2504
  }
2196
2505
  privateChange.state = "reverting";
2197
2506
  try {
2198
- if (await currentHead(privateChange.root) !== privateChange.baselineHead) {
2199
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2200
- }
2507
+ await assertWorkspaceOperationSafe(privateChange.root, privateChange.baselineHead);
2201
2508
  const currentHashes = await captureAgentFileHashes(
2202
2509
  privateChange.root,
2203
2510
  privateChange.touchedPaths
2204
2511
  );
2205
2512
  for (const [relativePath, expectedHash] of privateChange.appliedHashes) {
2206
2513
  if (currentHashes.get(relativePath) !== expectedHash) {
2207
- throw new import_shared17.SpotPatchError(import_shared17.ERROR_CODES.APPLY_CONFLICT);
2514
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2208
2515
  }
2209
2516
  }
2210
2517
  await runGitCommand({
2211
2518
  cwd: privateChange.root,
2212
2519
  args: ["apply", "--reverse", "--check", "--whitespace=error-all", "-"],
2213
2520
  stdin: privateChange.diff,
2214
- errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2521
+ errorCode: import_shared18.ERROR_CODES.APPLY_CONFLICT
2215
2522
  });
2216
2523
  await runGitCommand({
2217
2524
  cwd: privateChange.root,
2218
2525
  args: ["apply", "--reverse", "--whitespace=error-all", "-"],
2219
2526
  stdin: privateChange.diff,
2220
- errorCode: import_shared17.ERROR_CODES.APPLY_CONFLICT
2527
+ errorCode: import_shared18.ERROR_CODES.APPLY_CONFLICT
2221
2528
  });
2529
+ const revertedHashes = await captureAgentFileHashes(
2530
+ privateChange.root,
2531
+ privateChange.touchedPaths
2532
+ );
2533
+ if (!hashesMatch(privateChange.baselineHashes, revertedHashes)) {
2534
+ throw new import_shared18.SpotPatchError(import_shared18.ERROR_CODES.APPLY_CONFLICT);
2535
+ }
2222
2536
  privateChange.state = "reverted";
2223
2537
  } catch (error) {
2224
2538
  privateChange.state = "applied";
@@ -2227,7 +2541,7 @@ async function revertPreparedAgentChange(change) {
2227
2541
  }
2228
2542
 
2229
2543
  // src/engine/agent-prompt.ts
2230
- var import_shared18 = require("@spotpatch/shared");
2544
+ var import_shared19 = require("@spotpatch/shared");
2231
2545
  var AGENT_SYSTEM_INSTRUCTIONS = `You are editing code only inside a disposable, isolated Git worktree.
2232
2546
 
2233
2547
  Follow these rules exactly:
@@ -2244,7 +2558,7 @@ Follow these rules exactly:
2244
2558
  function redactedJson(value) {
2245
2559
  return JSON.stringify(
2246
2560
  value,
2247
- (_key, item) => typeof item === "string" ? (0, import_shared18.redactSensitiveText)(item) : item,
2561
+ (_key, item) => typeof item === "string" ? (0, import_shared19.redactSensitiveText)(item) : item,
2248
2562
  2
2249
2563
  );
2250
2564
  }
@@ -2321,7 +2635,7 @@ function createBoundedTarget(target, maximumCharacters) {
2321
2635
  function composeBoundedContext(annotation, maximumCharacters) {
2322
2636
  const page = Object.freeze({
2323
2637
  ...annotation.page,
2324
- url: (0, import_shared18.sanitizeUrl)(annotation.page.url, "http://spotpatch.invalid")
2638
+ url: (0, import_shared19.sanitizeUrl)(annotation.page.url, "http://spotpatch.invalid")
2325
2639
  });
2326
2640
  const fixedCharacters = redactedJson({
2327
2641
  page,
@@ -2377,7 +2691,7 @@ function composeAgentUserPrompt(annotation, maximumCharacters) {
2377
2691
  const minimumContextCharacters = 1024;
2378
2692
  const request = annotation.targets.map(
2379
2693
  (target, index) => `Target ${String(index + 1)}:
2380
- ${(0, import_shared18.redactSensitiveText)(target.instruction.trim())}`
2694
+ ${(0, import_shared19.redactSensitiveText)(target.instruction.trim())}`
2381
2695
  ).join("\n\n");
2382
2696
  const prefix = `${requestPrefix}${request}${contextPrefix}`;
2383
2697
  if (prefix.length + suffix.length + minimumContextCharacters > maximumCharacters) {
@@ -2397,11 +2711,11 @@ function isRetryableToolFailure(result) {
2397
2711
  return false;
2398
2712
  }
2399
2713
  const candidate = output;
2400
- return candidate.errorCode === import_shared19.ERROR_CODES.PATCH_REJECTED && candidate.retryable === true;
2714
+ return candidate.errorCode === import_shared20.ERROR_CODES.PATCH_REJECTED && candidate.retryable === true;
2401
2715
  }
2402
2716
  function throwIfCancelled(signal) {
2403
2717
  if (signal.aborted) {
2404
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AGENT_CANCELLED);
2718
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.AGENT_CANCELLED);
2405
2719
  }
2406
2720
  }
2407
2721
  function linkSignal(source, target) {
@@ -2439,6 +2753,7 @@ async function executeAgentChange(options) {
2439
2753
  worktree = await createIsolatedGitWorktree({
2440
2754
  root: options.root,
2441
2755
  signal: controller.signal,
2756
+ workingTreeMode: options.workingTreeMode ?? "require-clean",
2442
2757
  ...options.temporaryBase === void 0 ? {} : { temporaryBase: options.temporaryBase }
2443
2758
  });
2444
2759
  options.callbacks?.onPhase?.(
@@ -2480,7 +2795,7 @@ async function executeAgentChange(options) {
2480
2795
  }
2481
2796
  toolCallCount += response.toolCalls.length;
2482
2797
  if (toolCallCount > options.execution.limits.maxToolCalls) {
2483
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2798
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2484
2799
  }
2485
2800
  const results = [];
2486
2801
  for (const call of response.toolCalls) {
@@ -2515,7 +2830,7 @@ async function executeAgentChange(options) {
2515
2830
  pendingResults = Object.freeze(results);
2516
2831
  }
2517
2832
  if (summary === void 0) {
2518
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2833
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2519
2834
  }
2520
2835
  options.callbacks?.onPhase?.(
2521
2836
  Object.freeze({
@@ -2548,7 +2863,7 @@ async function executeAgentChange(options) {
2548
2863
  controller.signal
2549
2864
  );
2550
2865
  if (afterCheck.diff !== initialChangeSet.diff) {
2551
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.VALIDATION_FAILED);
2866
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.VALIDATION_FAILED);
2552
2867
  }
2553
2868
  }
2554
2869
  const validationPassed = finalChecks.every((check) => check.status === "passed");
@@ -2564,9 +2879,14 @@ async function executeAgentChange(options) {
2564
2879
  worktree.root,
2565
2880
  initialChangeSet.touchedPaths
2566
2881
  );
2882
+ const baselineHashes = await captureAgentFileHashes(
2883
+ worktree.baseline.root,
2884
+ initialChangeSet.touchedPaths
2885
+ );
2567
2886
  return createPreparedAgentChange({
2568
2887
  autoApplyEligible,
2569
2888
  baselineHead: worktree.baseline.head,
2889
+ baselineHashes,
2570
2890
  expectedHashes,
2571
2891
  result,
2572
2892
  root: worktree.baseline.root,
@@ -2574,15 +2894,15 @@ async function executeAgentChange(options) {
2574
2894
  });
2575
2895
  } catch (error) {
2576
2896
  if (options.signal.aborted) {
2577
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AGENT_CANCELLED);
2897
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.AGENT_CANCELLED);
2578
2898
  }
2579
2899
  if (hasJobTimedOut()) {
2580
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2900
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.AGENT_LIMIT_EXCEEDED);
2581
2901
  }
2582
- if (error instanceof import_shared19.SpotPatchError) {
2902
+ if (error instanceof import_shared20.SpotPatchError) {
2583
2903
  throw error;
2584
2904
  }
2585
- throw new import_shared19.SpotPatchError(import_shared19.ERROR_CODES.INTERNAL_ERROR);
2905
+ throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.INTERNAL_ERROR);
2586
2906
  } finally {
2587
2907
  clearTimeout(timeout);
2588
2908
  unlink();
@@ -2591,13 +2911,13 @@ async function executeAgentChange(options) {
2591
2911
  }
2592
2912
 
2593
2913
  // src/provider/capability-probe.ts
2594
- var import_shared20 = require("@spotpatch/shared");
2914
+ var import_shared21 = require("@spotpatch/shared");
2595
2915
  var PROBE_TOOL_NAME = "spotpatch_capability_probe";
2596
2916
  var PROBE_TOKEN = "spotpatch-ready-v1";
2597
2917
  async function probeProviderCapability(options) {
2598
2918
  const model = options.provider.models[options.modelProfileId];
2599
2919
  if (model === void 0) {
2600
- throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.MODEL_NOT_ALLOWED);
2920
+ throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.MODEL_NOT_ALLOWED);
2601
2921
  }
2602
2922
  const credential = options.credential ?? resolveProviderCredential(options.provider.apiKeyEnv, options.environment);
2603
2923
  const session = createOpenAICompatibleProviderSession({
@@ -2626,7 +2946,7 @@ async function probeProviderCapability(options) {
2626
2946
  const first = await session.next(void 0, options.signal);
2627
2947
  const probeCall = first.toolCalls[0];
2628
2948
  if (first.toolCalls.length !== 1 || probeCall?.name !== PROBE_TOOL_NAME || probeCall.arguments.token !== PROBE_TOKEN) {
2629
- throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED);
2949
+ throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED);
2630
2950
  }
2631
2951
  const second = await session.next(
2632
2952
  Object.freeze([
@@ -2638,7 +2958,7 @@ async function probeProviderCapability(options) {
2638
2958
  options.signal
2639
2959
  );
2640
2960
  if (second.toolCalls.length !== 0 || second.finalText.trim().length === 0) {
2641
- throw new import_shared20.SpotPatchError(import_shared20.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED);
2961
+ throw new import_shared21.SpotPatchError(import_shared21.ERROR_CODES.MODEL_TOOL_CALL_UNSUPPORTED);
2642
2962
  }
2643
2963
  return Object.freeze({
2644
2964
  providerProfileId: options.provider.id,
@@ -2661,6 +2981,7 @@ async function probeProviderCapability(options) {
2661
2981
  createOpenAICompatibleProviderSession,
2662
2982
  createProviderCredential,
2663
2983
  executeAgentChange,
2984
+ inspectAgentWorkspace,
2664
2985
  probeProviderCapability,
2665
2986
  resolveProviderCredential,
2666
2987
  revertPreparedAgentChange