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