diff --git a/src/resources/extensions/gsd/pre-execution-checks.ts b/src/resources/extensions/gsd/pre-execution-checks.ts index fa32be8c3..ed10ba50b 100644 --- a/src/resources/extensions/gsd/pre-execution-checks.ts +++ b/src/resources/extensions/gsd/pre-execution-checks.ts @@ -238,8 +238,7 @@ export async function checkPackageExistence( export function normalizeFilePath(filePath: string): string { if (!filePath) return filePath; - // Strip backtick wrapping from LLM-generated paths (#3649) - let normalized = filePath.replace(/`/g, ""); + let normalized = extractPathFromAnnotation(filePath); // Normalize path separators to forward slashes normalized = normalized.replace(/\\/g, "/"); @@ -260,6 +259,24 @@ export function normalizeFilePath(filePath: string): string { return normalized; } +function extractPathFromAnnotation(raw: string): string { + const trimmed = raw.trim(); + if (!trimmed) return trimmed; + + const backtickMatch = trimmed.match(/^`([^`]+)`(?:\s+[—–-]\s+.*)?$/); + if (backtickMatch) { + return backtickMatch[1].trim(); + } + + const annotatedMatch = trimmed.match(/^(.+?)\s+[—–-]\s+.+$/); + if (annotatedMatch) { + return annotatedMatch[1].trim(); + } + + // Fall back to the original behavior for already-plain paths. + return trimmed.replace(/`/g, ""); +} + /** * Build a set of files that will be created by tasks up to (but not including) taskIndex. * All paths are normalized for consistent comparison. diff --git a/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts b/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts index fb5d06eea..79ac6a692 100644 --- a/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +++ b/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts @@ -1083,11 +1083,77 @@ describe("checkTaskOrdering false positive regression (#3677)", () => { const results = checkTaskOrdering(tasks, "/tmp"); assert.equal(results.length, 0, "Normalized task.files path should not trigger a false positive"); }); + + test("annotated inputs still trigger ordering violations against later plain outputs", () => { + const tasks = [ + createTask({ + id: "T01", + sequence: 0, + files: [], + inputs: ["`later.ts` — needed first"], + expected_output: [], + }), + createTask({ + id: "T02", + sequence: 1, + files: [], + inputs: [], + expected_output: ["later.ts"], + }), + ]; + + const results = checkTaskOrdering(tasks, "/tmp"); + assert.equal(results.length, 1, "Annotated inputs should still match later plain expected_output entries"); + assert.equal(results[0].target, "`later.ts` — needed first"); + assert.ok(results[0].message.includes("sequence violation")); + }); }); // ─── checkFilePathConsistency additional edge cases ────────────────────────── describe("checkFilePathConsistency additional edge cases", () => { + test("annotated inputs match files that already exist on disk", () => { + const tempDir = join(tmpdir(), `pre-exec-test-annotated-input-${Date.now()}`); + mkdirSync(tempDir, { recursive: true }); + writeFileSync(join(tempDir, "existing.ts"), "// content"); + + try { + const tasks = [ + createTask({ + id: "T01", + files: [], + inputs: ["`existing.ts` — file already on disk"], + expected_output: [], + }), + ]; + + const results = checkFilePathConsistency(tasks, tempDir); + assert.equal(results.length, 0, "Annotated inputs should resolve to the on-disk file path"); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + test("plain inputs match prior annotated expected outputs", () => { + const tasks = [ + createTask({ + id: "T01", + files: [], + inputs: [], + expected_output: ["`generated.ts` — created earlier"], + }), + createTask({ + id: "T02", + files: [], + inputs: ["generated.ts"], + expected_output: [], + }), + ]; + + const results = checkFilePathConsistency(tasks, "/tmp"); + assert.equal(results.length, 0, "Prior annotated expected_output entries should satisfy later plain inputs"); + }); + test("inputs referencing glob-like patterns should not crash", () => { // A glob pattern in inputs is unusual but should be handled gracefully. // The file won't exist on disk, so it should produce a blocking result.