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