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