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