fix: improve apply-patch add-file guidance

This commit is contained in:
Codex
2026-06-03 09:30:10 +00:00
parent 4dd75fff29
commit 1de0e08ae7
2 changed files with 70 additions and 1 deletions
+52 -1
View File
@@ -115,6 +115,37 @@ export function applyPatchV2HelpPayload() {
firstLine: beginMarker,
lastLine: endMarker
},
rules: [
"Add File has no @@ hunk marker: put content immediately after `*** Add File: <path>` and prefix every content line with +.",
"A blank line in Add File is a line containing only +.",
"Update File uses @@ or @@ context markers, followed by context lines starting with space and changed lines starting with - or +.",
"Prefer `trans <route> apply-patch < /tmp/patch.diff` for long patches, Windows paths, or quoting-sensitive content."
],
examples: {
addFile: [
beginMarker,
`${addFileMarker}path/to/new.txt`,
"+first line",
"+",
"+third line after a blank line",
endMarker
].join("\n"),
updateFile: [
beginMarker,
`${updateFileMarker}path/to/existing.txt`,
emptyChangeContextMarker,
" context line",
"-old line",
"+new line",
" more context",
endMarker
].join("\n")
},
commonPitfalls: [
"Do not put @@ after `*** Add File:`; @@ is only for Update File.",
"Do not paste unified diff headers such as `@@ -1,3 +1,4 @@`.",
"Do not use remote Python/Perl/sed heredocs for text patches when `trans <route> apply-patch` is available."
],
note: "apply-patch reads patch text from stdin and uses the v2 engine by default. Use `apply-patch-v1` only for the legacy helper."
};
}
@@ -148,7 +179,7 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult {
while (index < lines.length - 1 && !isFileHeader(lines[index] ?? "")) {
const addLine = lines[index] ?? "";
if (!addLine.startsWith("+")) {
throw new ApplyPatchV2Error("invalid add file line; every added line must start with +", { line: index + 1, path: filePath });
throw invalidAddFileLineError(addLine, index + 1, filePath);
}
added.push(addLine.slice(1));
index += 1;
@@ -189,6 +220,26 @@ export function parseApplyPatchV2(patchText: string): PatchParseResult {
return { patch, hunks };
}
function invalidAddFileLineError(lineText: string, line: number, filePath: string): ApplyPatchV2Error {
const details = { line, path: filePath, text: lineText };
if (lineText.trimStart().startsWith("@@")) {
return new ApplyPatchV2Error(
"invalid add file line: remove @@ for Add File. Add File sections do not use hunk markers; put content directly after the header and prefix every content line with +.",
details,
);
}
if (lineText.trim().length === 0) {
return new ApplyPatchV2Error(
"invalid add file line: blank lines in Add File must be written as a line containing only +.",
details,
);
}
return new ApplyPatchV2Error(
"invalid add file line: every Add File content line must start with +. Add File has no @@ marker; use @@ only under Update File.",
details,
);
}
export function deriveUpdatedContent(filePath: string, originalContent: string, chunks: UpdateChunk[]): PatchUpdateResult {
const originalLines = splitContentLines(originalContent);
const replacements = computeReplacements(filePath, originalLines, chunks);
@@ -905,6 +905,24 @@ export async function runSshArgvGuidanceContract(): Promise<JsonRecord> {
assertCondition(addBeforeFailedUpdateV2.files.hwpod?.includes("device-pod-cli.mjs"), "v2 should preserve an earlier Add File from a large patch when a later hunk misses", addBeforeFailedUpdateV2);
assertCondition(addBeforeFailedUpdateV2.files["scripts/artifact-publish.mjs"] === "actual artifact line\n", "v2 must leave the failed later file unchanged", addBeforeFailedUpdateV2);
const addFileWithHunkMarkerV2 = await applyPatchV2FixtureAttempt([
"*** Begin Patch",
"*** Add File: bad-add.txt",
"@@",
"+content",
"*** End Patch",
"",
].join("\n"), {}, { stderrOutput: true });
assertCondition(addFileWithHunkMarkerV2.exitCode === 1 && addFileWithHunkMarkerV2.error === null, "v2 CLI path should return a visible parse error for Add File @@ misuse", addFileWithHunkMarkerV2);
assertCondition(addFileWithHunkMarkerV2.stdout === "", "v2 Add File parse failures should keep Codex-style empty stdout", addFileWithHunkMarkerV2);
assertCondition(
addFileWithHunkMarkerV2.stderr.includes("remove @@ for Add File")
&& addFileWithHunkMarkerV2.stderr.includes("prefix every content line with +"),
"v2 Add File @@ misuse should tell agents exactly how to repair the patch",
addFileWithHunkMarkerV2.stderr,
);
assertCondition(!addFileWithHunkMarkerV2.commands.some((command) => command.startsWith("write-b64") || command.startsWith("delete")), "v2 Add File parse failures must not touch remote files", addFileWithHunkMarkerV2.commands);
const sequentialCompoundV2 = await applyPatchV2Fixture([
"*** Begin Patch",
"*** Update File: sequence.txt",