From 4559643be7d5dd5537e589e9d026c6d95a8a292d Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 14:15:43 +0000 Subject: [PATCH 01/28] fix(patch): preserve nested checkout snapshot context --- .../codex-security/scripts/workbench_db.py | 5 +- .../codex-security/tests/test_workbench_db.py | 58 ++++ sdk/typescript/src/cli.ts | 76 ++++- sdk/typescript/src/targets.ts | 2 +- sdk/typescript/tests-ts/cli-patch.test.ts | 280 +++++++++++++++++- 5 files changed, 396 insertions(+), 25 deletions(-) diff --git a/plugins/codex-security/scripts/workbench_db.py b/plugins/codex-security/scripts/workbench_db.py index 8f76e21326..a27c089ace 100644 --- a/plugins/codex-security/scripts/workbench_db.py +++ b/plugins/codex-security/scripts/workbench_db.py @@ -3169,7 +3169,8 @@ def patch_artifact_preview( hunk_lines = 0 elif match := re.match(rb"@@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? @@", chunk): hunk_lines = sum( - int(count) if count is not None else 1 for count in match.groups() + int(count.lstrip(b"0") or b"0") if count is not None else 1 + for count in match.groups() ) elif hunk_lines == 0 and chunk.startswith(b"+++ "): new_headers += 1 @@ -3184,7 +3185,7 @@ def patch_artifact_preview( elif chunk.startswith((b" ", b"\n")): hunk_lines = max(0, hunk_lines - 2) at_line_start = chunk.endswith(b"\n") - except SystemExit: + except (SystemExit, ValueError): return None, None if f"sha256:{digest.hexdigest()}" != expected_digest: return None, None diff --git a/plugins/codex-security/tests/test_workbench_db.py b/plugins/codex-security/tests/test_workbench_db.py index 7996a4174d..b4520e13d8 100644 --- a/plugins/codex-security/tests/test_workbench_db.py +++ b/plugins/codex-security/tests/test_workbench_db.py @@ -4196,3 +4196,61 @@ def test_patch_statistics_count_blank_context_lines(tmp_path: Path, blank_contex ) assert preview == patch.decode() assert stats == {"additions": 2, "deletions": 2, "fileCount": 2, "previewTruncated": False} + + +@pytest.mark.parametrize("padded", [False, True]) +def test_saved_scan_remains_readable_with_large_hunk_counts(tmp_path: Path, padded: bool) -> None: + state_dir = tmp_path / "state" + target = tmp_path / "target" + target.mkdir() + (target / "source.txt").write_text("original\n") + scan_id, scan_dir = start_saved_scan(state_dir, target, tmp_path / "scans") + write_completed_contract(scan_dir, scan_id, target, relative_path="source.txt") + completed = run_workbench(state_dir, "complete-scan", "--scan-id", scan_id)["scan"] + occurrence_id = completed["findings"][0]["occurrenceId"] + count = "0" * 4300 + "1" if padded else "9" * 4301 + patch = f"--- a/source.txt\n+++ b/source.txt\n@@ -1,{count} +1,{count} @@\n-original\n+fixed\n" + patch_path = scan_dir / "remediation.patch" + patch_path.write_text(patch) + if padded: + subprocess.run(["git", "apply", "--check", str(patch_path)], cwd=target, check=True) + request_id = str(uuid.uuid4()) + token = str(uuid.uuid4()) + run_workbench( + state_dir, + "request-finding-remediation", + "--occurrence-id", + occurrence_id, + "--request-id", + request_id, + "--action-token", + token, + ) + run_workbench( + state_dir, + "set-finding-remediation", + "--occurrence-id", + occurrence_id, + "--request-id", + request_id, + "--action-token", + token, + "--expected-version", + "1", + "--state", + "generated", + "--patch-path", + patch_path.name, + "--patch-digest", + f"sha256:{hashlib.sha256(patch_path.read_bytes()).hexdigest()}", + ) + saved = run_workbench(state_dir, "get-scan", "--scan-id", scan_id)["scan"] + remediation = saved["findings"][0]["remediationState"] + assert remediation["state"] == "generated" + if padded: + assert remediation["patch"] == patch + assert remediation["patchStats"]["additions"] == 1 + assert remediation["patchStats"]["deletions"] == 1 + else: + assert remediation["patch"] is None + assert remediation["patchStats"] is None diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 8d3a118a17..43e09c2553 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -239,7 +239,10 @@ import { import { abortable, DiffTarget, + enclosingGitWorktreeRoot, enclosingGitWorktreeRoots, + gitMarkerRoot, + GIT_REPOSITORY_ENVIRONMENT, type ScanTarget, relativePathIsOutside as isOutsidePath, } from "./targets.js"; @@ -5159,9 +5162,10 @@ export async function main( const patchRiskBase = options.assessPatchRisk ? await snapshotPatchTree(selected.repository, dependencies) : undefined; - const patchBase = - patchRiskBase ?? - (await snapshotPatchState(selected.repository, dependencies)); + const patchBase = await snapshotPatchState( + selected.repository, + dependencies, + ); const patchRun = await runFindingPatches( selected, options, @@ -5273,9 +5277,7 @@ export async function main( options.assessPatchRisk || options.createPr ? await snapshotPatchTree(repository, dependencies) : undefined; - const patchBase = - patchGitBase ?? - (await snapshotPatchState(repository, dependencies)); + const patchBase = await snapshotPatchState(repository, dependencies); if (options.createPr) { await requireCleanPatchPullRequestBase( repository, @@ -7048,7 +7050,7 @@ interface GitPatchState { async function changedPatchFiles( repository: string, - base: string | GitPatchState | Map, + base: GitPatchState | Map, dependencies: CliDependencies, ): Promise { if (base instanceof Map) { @@ -7057,16 +7059,15 @@ async function changedPatchFiles( .filter((path) => base.get(path) !== head.get(path)) .sort(); } - const bases = typeof base === "string" ? new Map([["", base]]) : base.trees; - const heads = - typeof base === "string" - ? new Map([["", await snapshotPatchTree(repository, dependencies)]]) - : (await snapshotGitPatchState(repository, dependencies)).trees; + const heads = (await snapshotGitPatchState(repository, dependencies)).trees; const files = new Set(); - for (const [directory, tree] of bases) { + for (const [directory, tree] of base.trees) { const head = heads.get(directory); if (head === undefined) continue; - const output = await dependencies.runRepositoryCommand( + const gitDependencies = directory + ? await nestedPatchGitDependencies(repository, dependencies) + : dependencies; + const output = await gitDependencies.runRepositoryCommand( "git", ["--literal-pathspecs", "diff", "--name-only", "-z", tree, head], join(repository, directory), @@ -7108,9 +7109,26 @@ async function snapshotGitPatchState( const trees = new Map(); const visit = async (directory: string): Promise => { const checkout = join(repository, directory); - const tree = await snapshotPatchTree(checkout, dependencies); + if (directory) { + if ( + isOutsidePath( + relative(await realpath(repository), await realpath(checkout)), + ) + ) { + throw new CodexSecurityError( + "Nested Git checkout is outside the selected repository.", + ); + } + await enclosingGitWorktreeRoot(checkout, undefined, { + requireIfPresent: true, + }); + } + const gitDependencies = directory + ? await nestedPatchGitDependencies(repository, dependencies) + : dependencies; + const tree = await snapshotPatchTree(checkout, gitDependencies); trees.set(directory, tree); - const entries = await dependencies.runRepositoryCommand( + const entries = await gitDependencies.runRepositoryCommand( "git", ["ls-tree", "-r", "-z", tree], checkout, @@ -7127,6 +7145,32 @@ async function snapshotGitPatchState( return { trees }; } +async function nestedPatchGitDependencies( + repository: string, + dependencies: CliDependencies, +): Promise { + const root = + (await gitMarkerRoot(repository, undefined, "outermost")) ?? repository; + return { + ...dependencies, + runRepositoryCommand: (command, args, checkout, options) => + dependencies.runRepositoryCommand( + command, + ["-C", checkout, ...args], + root, + { + ...options, + environment: { + ...Object.fromEntries( + [...GIT_REPOSITORY_ENVIRONMENT].map((name) => [name, undefined]), + ), + ...options?.environment, + }, + }, + ), + }; +} + // Literal patch inputs also work in directories without Git metadata. async function snapshotPatchDirectory( repository: string, diff --git a/sdk/typescript/src/targets.ts b/sdk/typescript/src/targets.ts index 1d1392d5d5..b408338804 100644 --- a/sdk/typescript/src/targets.ts +++ b/sdk/typescript/src/targets.ts @@ -30,7 +30,7 @@ const UNSUPPORTED_GIT_ENVIRONMENT = new Set([ "GIT_COMMON_DIR", "GIT_REPLACE_REF_BASE", ]); -const GIT_REPOSITORY_ENVIRONMENT = new Set([ +export const GIT_REPOSITORY_ENVIRONMENT = new Set([ ...UNSUPPORTED_GIT_ENVIRONMENT, "GIT_CEILING_DIRECTORIES", "GIT_DISCOVERY_ACROSS_FILESYSTEM", diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 04061011fa..206f343bb5 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -5,13 +5,29 @@ import { parse as parseToml } from "smol-toml"; import { afterEach, describe, expect, test, mock } from "bun:test"; import { execFileSync } from "node:child_process"; import { hash } from "node:crypto"; -import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import { + chmod, + copyFile, + mkdir, + readFile, + rm, + symlink, + writeFile, +} from "node:fs/promises"; import { tmpdir } from "node:os"; -import { dirname, join, relative, resolve } from "node:path"; +import { + basename, + delimiter, + dirname, + join, + relative, + resolve, +} from "node:path"; import { Writable } from "node:stream"; import { stripVTControlCharacters } from "node:util"; import type { Finding, JsonObject, SeverityLevel } from "../src/index.js"; import { main } from "../src/cli.js"; +import { resolveTrustedExecutable } from "../src/trusted-executable.js"; import type { LinearClientFactory } from "../src/linear.js"; import { capture, dependencies, fakeResult } from "./cli-fixtures.js"; import { @@ -2851,8 +2867,259 @@ describe("patch publication integrity", () => { }); }); - test.each(["unborn", "nested"])( - "detects local patches in %s Git repositories", + test.each([ + "unborn", + "nested", + "nested environment", + "nested objects", + "nested common", + ])("detects local patches in %s Git repositories", async (kind) => { + const directory = await fixtures.create("patch-git-state-"); + const git = repositoryGit(directory); + git("init", "--initial-branch=main"); + git("config", "user.name", "Synthetic User"); + git("config", "user.email", "synthetic@example.test"); + await writeFile(join(directory, "app.ts"), "unsafe\n"); + let path = "app.ts"; + if (kind.startsWith("nested")) { + git("add", "."); + git("commit", "-m", "Synthetic baseline"); + const nested = join(directory, "nested"); + await mkdir(nested); + const inner = repositoryGit(nested); + inner("init", "--initial-branch=main"); + inner("config", "user.name", "Synthetic User"); + inner("config", "user.email", "synthetic@example.test"); + await writeFile(join(nested, "app.ts"), "unsafe\n"); + inner("add", "."); + inner("commit", "-m", "Synthetic nested baseline"); + path = "nested/app.ts"; + } + const gitEnvironment = + kind === "nested environment" + ? { GIT_DIR: ".git", GIT_WORK_TREE: directory } + : kind === "nested objects" + ? { GIT_OBJECT_DIRECTORY: join(directory, ".git", "objects") } + : kind === "nested common" + ? { GIT_COMMON_DIR: join(directory, ".git") } + : {}; + const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { + currentDirectory: directory, + onRepositoryCommand: (command, args, cwd, options) => + runGitRepositoryCommand(command, args, cwd, { + ...options, + environment: { ...gitEnvironment, ...options?.environment }, + }), + onCodex: async (_args, output) => { + await writeFile(join(directory, path), "fixed\n"); + output?.stdout.write("Fixed and checked."); + return 0; + }, + }); + expect(outcome.exitCode, outcome.stderr).toBe(0); + expect(JSON.parse(outcome.stdout)).toMatchObject({ + applied: true, + files: [path], + }); + }); + test("keeps the outer executable boundary for nested patch snapshots", async () => { + const repository = await fixtures.create("patch-nested-executable-"); + const nested = join(repository, "dependency"); + await mkdir(nested); + const git = repositoryGit(repository); + const inner = repositoryGit(nested); + for (const run of [git, inner]) { + run("init", "--initial-branch=main"); + run("config", "user.name", "Synthetic User"); + run("config", "user.email", "synthetic@example.test"); + } + await writeFile(join(nested, "app.ts"), "original\n"); + inner("add", "."); + inner("commit", "-m", "Synthetic nested baseline"); + git("add", "."); + git("commit", "-m", "Synthetic parent baseline"); + const trusted = await resolveTrustedExecutable( + "git", + process.env, + repository, + ); + expect(trusted).not.toBeNull(); + const bin = join(repository, "bin"); + await mkdir(bin); + const repositoryGitPath = join(bin, basename(trusted!.executable)); + await copyFile(trusted!.executable, repositoryGitPath); + await chmod(repositoryGitPath, 0o755); + const environment = { + ...process.env, + PATH: [bin, process.env["PATH"]].join(delimiter), + }; + let nestedCommands = 0; + const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { + currentDirectory: repository, + onRepositoryCommand: async (command, args, cwd, options) => { + // Exercise the same resolver used by the default command dependency. + const selected = await resolveTrustedExecutable( + command, + environment, + cwd, + ); + expect(selected?.executable).not.toBe(repositoryGitPath); + if (cwd === nested || args.includes(nested)) nestedCommands++; + return runGitRepositoryCommand(command, args, cwd, options); + }, + onCodex: async (_args, output) => { + await writeFile(join(nested, "app.ts"), "fixed\n"); + output?.stdout.write("Fixed and checked."); + return 0; + }, + }); + expect(outcome.exitCode, outcome.stderr).toBe(0); + expect(nestedCommands).toBeGreaterThan(0); + expect(JSON.parse(outcome.stdout)).toMatchObject({ + applied: true, + files: ["dependency/app.ts"], + }); + }); + + test("refuses a sparse gitlink redirected outside the patch checkout", async () => { + const directory = await fixtures.create("patch-sparse-gitlink-"); + const repository = join(directory, "repository"); + const nested = join(repository, "dependency"); + const external = join(directory, "external"); + await mkdir(nested, { recursive: true }); + await mkdir(external); + const git = repositoryGit(repository); + const inner = repositoryGit(nested); + const outside = repositoryGit(external); + for (const run of [git, inner, outside]) { + run("init", "--initial-branch=main"); + run("config", "user.name", "Synthetic User"); + run("config", "user.email", "synthetic@example.test"); + } + for (const [path, run] of [ + [nested, inner], + [external, outside], + ] as const) { + await writeFile(join(path, "app.ts"), "original\n"); + run("add", "."); + run("commit", "-m", "Synthetic baseline"); + } + git("add", "."); + git("commit", "-m", "Synthetic parent baseline"); + git("config", "core.sparseCheckout", "true"); + await writeFile( + join(repository, ".git", "info", "sparse-checkout"), + "/*\n!/dependency\n", + ); + git("update-index", "--skip-worktree", "dependency"); + await rm(nested, { recursive: true }); + await symlink( + external, + nested, + process.platform === "win32" ? "junction" : "dir", + ); + const outsideFile = join(external, "uncommitted.txt"); + await writeFile( + outsideFile, + "Synthetic bytes outside the selected checkout\n", + ); + const blob = outside("hash-object", outsideFile); + const index = outside("write-tree"); + expect(() => outside("cat-file", "-e", blob)).toThrow(); + let modelCalls = 0; + const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { + currentDirectory: repository, + onRepositoryCommand: runGitRepositoryCommand, + onCodex: async () => { + modelCalls++; + return 0; + }, + }); + expect(() => outside("cat-file", "-e", blob)).toThrow(); + expect(outside("write-tree")).toBe(index); + expect(modelCalls).toBe(0); + expect(outcome.exitCode).toBe(2); + expect(outcome.stderr).toContain("outside"); + expect(await readFile(outsideFile, "utf8")).toBe( + "Synthetic bytes outside the selected checkout\n", + ); + }); + + test.each(["worktree", "metadata"])( + "refuses a nested checkout rebound to external %s before writing objects", + async (binding) => { + const directory = await fixtures.create("patch-nested-binding-"); + const checkout = join(directory, "checkout"); + const nested = join(checkout, "dependency"); + const external = join(directory, "external"); + await mkdir(nested, { recursive: true }); + await mkdir(external); + const git = repositoryGit(checkout); + const inner = repositoryGit(nested); + for (const run of [git, inner]) { + run("init", "--initial-branch=main"); + run("config", "user.name", "Synthetic User"); + run("config", "user.email", "synthetic@example.test"); + } + await writeFile(join(nested, "app.ts"), "original\n"); + inner("add", "."); + inner("commit", "-m", "Synthetic nested baseline"); + git("add", "."); + git("commit", "-m", "Synthetic parent baseline"); + const outsideFile = join(external, "outside.txt"); + await writeFile( + outsideFile, + "Synthetic bytes outside the selected checkout\n", + ); + const blob = git("hash-object", outsideFile); + expect(() => inner("cat-file", "-e", blob)).toThrow(); + let objects = inner; + if (binding === "worktree") { + inner("config", "core.worktree", external); + } else { + const outside = repositoryGit(external); + outside("init", "--initial-branch=main"); + outside("config", "user.name", "Synthetic User"); + outside("config", "user.email", "synthetic@example.test"); + outside("add", "."); + outside("commit", "-m", "Synthetic external baseline"); + await rm(join(nested, ".git"), { recursive: true }); + await writeFile( + join(nested, ".git"), + `gitdir: ${join(external, ".git")}\n`, + ); + await writeFile( + join(nested, "app.ts"), + "Synthetic unpublished target bytes\n", + ); + objects = outside; + } + const snapshotBlob = git("hash-object", join(nested, "app.ts")); + if (binding === "metadata") + expect(() => objects("cat-file", "-e", snapshotBlob)).toThrow(); + let modelCalls = 0; + const outcome = await runWorkflow( + ["patch", "Synthetic issue", "--json"], + { + currentDirectory: checkout, + onRepositoryCommand: runGitRepositoryCommand, + onCodex: async () => { + modelCalls++; + return 0; + }, + }, + ); + if (binding === "worktree") + expect(() => inner("cat-file", "-e", blob)).toThrow(); + else expect(() => objects("cat-file", "-e", snapshotBlob)).toThrow(); + expect(modelCalls).toBe(0); + expect(outcome.exitCode).toBe(2); + expect(outcome.stderr).toMatch(/worktree|metadata/u); + }, + ); + + test.each(["nested"])( + "retains changed files when %s Git patch assessment cannot produce an outer patch", async (kind) => { const directory = await fixtures.create("patch-git-state-"); const git = repositoryGit(directory); @@ -2876,7 +3143,7 @@ describe("patch publication integrity", () => { path = "nested/app.ts"; } const outcome = await runWorkflow( - ["patch", "Synthetic issue", "--json"], + ["patch", "Synthetic issue", "--assess-patch-risk", "--json"], { currentDirectory: directory, onRepositoryCommand: runGitRepositoryCommand, @@ -2887,7 +3154,8 @@ describe("patch publication integrity", () => { }, }, ); - expect(outcome.exitCode, outcome.stderr).toBe(0); + expect(outcome.exitCode, outcome.stderr).toBe(2); + expect(outcome.stderr).toContain("No completed patch changes to assess."); expect(JSON.parse(outcome.stdout)).toMatchObject({ applied: true, files: [path], From 78308b7e93167d1d65b7f2afcf68f389a40a56f8 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 14:37:20 +0000 Subject: [PATCH 02/28] fix(patch): stage submodule updates through parent paths --- sdk/typescript/src/cli.ts | 17 +++-- sdk/typescript/tests-ts/cli-patch.test.ts | 79 +++++++++++++++++++++++ 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 43e09c2553..bd5dd09f10 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -5348,10 +5348,15 @@ export async function main( ) : undefined; if (publication) { + const publicationFiles = await changedPatchFiles( + repository, + patchGitBase!, + dependencies, + ); await createPatchPullRequest( repository, publication, - files, + publicationFiles.length > 0 ? publicationFiles : files, errorOutput, dependencies, patchRisk?.summary, @@ -7050,7 +7055,7 @@ interface GitPatchState { async function changedPatchFiles( repository: string, - base: GitPatchState | Map, + base: string | GitPatchState | Map, dependencies: CliDependencies, ): Promise { if (base instanceof Map) { @@ -7059,9 +7064,13 @@ async function changedPatchFiles( .filter((path) => base.get(path) !== head.get(path)) .sort(); } - const heads = (await snapshotGitPatchState(repository, dependencies)).trees; + const bases = typeof base === "string" ? new Map([["", base]]) : base.trees; + const heads = + typeof base === "string" + ? new Map([["", await snapshotPatchTree(repository, dependencies)]]) + : (await snapshotGitPatchState(repository, dependencies)).trees; const files = new Set(); - for (const [directory, tree] of base.trees) { + for (const [directory, tree] of bases) { const head = heads.get(directory); if (head === undefined) continue; const gitDependencies = directory diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 206f343bb5..7dc1b39766 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -2730,6 +2730,85 @@ describe("patch publication integrity", () => { }, ); + test.each(["committed", "uncommitted"])( + "preserves supplied-issue submodule publication with %s changes", + async (state) => { + const directory = await fixtures.create("patch-submodule-publication-"); + const checkout = join(directory, "checkout"); + const nested = join(checkout, "dependency"); + await mkdir(nested, { recursive: true }); + const git = repositoryGit(checkout); + const inner = repositoryGit(nested); + for (const run of [git, inner]) { + run("init", "--initial-branch=main"); + run("config", "user.name", "Synthetic User"); + run("config", "user.email", "synthetic@example.test"); + run("config", "commit.gpgsign", "false"); + } + await writeFile(join(nested, "app.ts"), "original\n"); + inner("add", "."); + inner("commit", "-m", "Synthetic nested baseline"); + const original = inner("rev-parse", "HEAD"); + await writeFile(join(nested, "app.ts"), "fixed\n"); + inner("add", "."); + inner("commit", "-m", "Synthetic nested fix"); + const fixed = inner("rev-parse", "HEAD"); + inner("checkout", original); + git("add", "."); + git("commit", "-m", "Synthetic parent baseline"); + const head = git("rev-parse", "HEAD"); + const index = git("write-tree"); + const remote = join(directory, "remote.git"); + git("init", "--bare", remote); + git("remote", "add", "origin", remote); + let published = false; + const outcome = await runWorkflow( + ["patch", "Synthetic issue", "--create-pr", "--json"], + { + currentDirectory: checkout, + onRepositoryCommand: (command, args, cwd, options) => { + if (command === "git") + return runGitRepositoryCommand(command, args, cwd, options); + if (args[1] === "list") return ""; + published = true; + return "https://github.example.test/example/repository/pull/1"; + }, + onCodex: async () => { + if (state === "committed") inner("checkout", fixed); + else await writeFile(join(nested, "app.ts"), "fixed\n"); + return 0; + }, + }, + ); + expect(outcome.exitCode, outcome.stderr).toBe( + state === "committed" ? 0 : 2, + ); + expect(JSON.parse(outcome.stdout)).toMatchObject({ + applied: true, + files: + state === "committed" + ? ["dependency", "dependency/app.ts"] + : ["dependency/app.ts"], + }); + expect(published).toBe(state === "committed"); + if (state === "committed") { + expect(git("show", "--format=", "--name-only", "HEAD")).toBe( + "dependency", + ); + expect(git("rev-parse", "HEAD:dependency")).toBe(fixed); + expect(git("ls-remote", "origin")).toContain(git("rev-parse", "HEAD")); + } else { + expect(outcome.stderr).toContain("submodule"); + expect(git("branch", "--show-current")).toBe("main"); + expect(git("rev-parse", "HEAD")).toBe(head); + expect(git("write-tree")).toBe(index); + expect(git("branch", "--format=%(refname)")).toBe("refs/heads/main"); + expect(git("ls-remote", "origin")).toBe(""); + expect(await readFile(join(nested, "app.ts"), "utf8")).toBe("fixed\n"); + } + }, + ); + test("keeps the original branch when a verified file belongs to a nested repository", async () => { const directory = await fixtures.create("patch-nested-publication-"); const git = repositoryGit(directory); From 32d35eea3b2c82aa79a144994c24001fddbe3887 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 16:53:25 +0000 Subject: [PATCH 03/28] fix(cli): capture publication paths before assessment --- sdk/typescript/src/cli.ts | 14 ++-- sdk/typescript/tests-ts/cli-patch.test.ts | 79 +++++++++++++++++++++++ 2 files changed, 87 insertions(+), 6 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 6f4b38b3a5..3bfd1d2a50 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -5401,6 +5401,14 @@ export async function main( ); } errorOutput.write(`Patch applied. Files changed: ${files.length}.\n`); + const publicationFiles = publication + ? await changedPatchFiles( + gitRepository, + patchGitBase!, + gitDependencies, + true, + ) + : []; const patchRisk = options.assessPatchRisk ? await runPatchRiskAssessment( { @@ -5423,12 +5431,6 @@ export async function main( ) : undefined; if (publication) { - const publicationFiles = await changedPatchFiles( - gitRepository, - patchGitBase!, - gitDependencies, - true, - ); await createPatchPullRequest( gitRepository, publication, diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index b70c9291c6..72febd08de 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -3926,3 +3926,82 @@ const runGitRepositoryCommand: NonNullable< }); return options?.trim === false ? result : result.trim(); }; + +test.each(["untracked", "tracked"])( + "keeps unrelated %s edits made during assessment out of publication", + async (kind) => { + const root = await temporaryDirectory("patch-assessment-publication-"); + const repository = join(root, "repository"); + const remote = join(root, "remote.git"); + const git = repositoryGit(repository); + try { + await mkdir(repository); + git("init", "--initial-branch=main"); + git("config", "user.name", "Synthetic User"); + git("config", "user.email", "synthetic@example.test"); + git("config", "commit.gpgsign", "false"); + await writeFile(join(repository, "app.ts"), "original\n"); + if (kind === "tracked") + await writeFile(join(repository, "user-notes.txt"), "original notes\n"); + git("add", "."); + git("commit", "-m", "Synthetic initial commit"); + git("init", "--bare", remote); + git("remote", "add", "origin", remote); + const outcome = await runWorkflow( + [ + "patch", + "Synthetic issue", + "--assess-patch-risk", + "--create-pr", + "--json", + ], + { + currentDirectory: repository, + onRepositoryCommand: (command, args, directory, options) => + command === "git" + ? runGitRepositoryCommand(command, args, directory, options) + : args[1] === "create" + ? "https://github.example.test/example/repository/pull/17" + : "", + onCodex: async (_args, output) => { + if ( + output?.appServer?.prompt.includes( + "$codex-security:assess-patch-risk", + ) + ) { + const artifact = JSON.parse( + output.appServer.prompt + .split("\n") + .find((line) => line.startsWith('{"path":'))!, + ); + expect(artifact.changedFiles).toEqual(["app.ts"]); + await writeFile( + join(repository, "user-notes.txt"), + "unrelated notes from concurrent work\n", + ); + output.stdout.write(patchRiskAssessment().report); + } else { + await writeFile(join(repository, "app.ts"), "fixed\n"); + output?.stdout.write("Patch complete."); + } + return 0; + }, + }, + ); + expect(outcome.exitCode, outcome.stderr).toBe(0); + expect(JSON.parse(outcome.stdout).files).toEqual(["app.ts"]); + expect(git("show", "--format=", "--name-only", "HEAD", "--")).toBe( + "app.ts", + ); + expect(git("rev-parse", "HEAD")).toBe(git("rev-parse", "@{upstream}")); + expect(git("status", "--porcelain")).toBe( + `${kind === "tracked" ? "M" : "??"} user-notes.txt`, + ); + expect(await readFile(join(repository, "user-notes.txt"), "utf8")).toBe( + "unrelated notes from concurrent work\n", + ); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); From b81102de5f6e9dda9608c12a0832373aa3c3fe13 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 17:10:22 +0000 Subject: [PATCH 04/28] fix(patch): reuse captured paths for publication --- sdk/typescript/src/cli.ts | 54 ++++++------- sdk/typescript/tests-ts/cli-patch.test.ts | 93 +++++++++++++++++++++++ 2 files changed, 118 insertions(+), 29 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 5a0bf9bd74..bf751e0b21 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -5188,7 +5188,7 @@ export async function main( ); const { patches } = patchRun; exitCode = patchRun.exitCode; - const files = await changedPatchFiles( + const { files } = await changedPatchFiles( selected.repository, patchBase, dependencies, @@ -5369,12 +5369,13 @@ export async function main( if (directory !== commandContext.directory) commandDirectory = gitRepository; } - const files = await changedPatchFiles( - gitRepository, - patchBase, - gitDependencies, - options.assessPatchRisk || options.createPr, - ); + const { files, rootFiles: publicationFiles } = + await changedPatchFiles( + gitRepository, + patchBase, + gitDependencies, + options.assessPatchRisk || options.createPr, + ); patchResult = { repository, applied: files.length > 0, @@ -5401,14 +5402,6 @@ export async function main( ); } errorOutput.write(`Patch applied. Files changed: ${files.length}.\n`); - const publicationFiles = publication - ? await changedPatchFiles( - gitRepository, - patchGitBase!, - gitDependencies, - true, - ) - : []; const patchRisk = options.assessPatchRisk ? await runPatchRiskAssessment( { @@ -7189,24 +7182,25 @@ interface GitPatchState { async function changedPatchFiles( repository: string, - base: string | GitPatchState | Map, + base: GitPatchState | Map, dependencies: CliDependencies, rootRelative = false, -): Promise { +): Promise<{ files: string[]; rootFiles: string[] }> { if (base instanceof Map) { const head = await snapshotPatchDirectory(repository); - return [...new Set([...base.keys(), ...head.keys()])] + const files = [...new Set([...base.keys(), ...head.keys()])] .filter((path) => base.get(path) !== head.get(path)) .sort(); + return { files, rootFiles: files }; } - const bases = typeof base === "string" ? new Map([["", base]]) : base.trees; - const heads = - typeof base === "string" - ? new Map([["", await snapshotPatchTree(repository, dependencies)]]) - : (await snapshotGitPatchState(repository, dependencies, rootRelative)) - .trees; + const { trees: heads } = await snapshotGitPatchState( + repository, + dependencies, + rootRelative, + ); const files = new Set(); - for (const [directory, tree] of bases) { + const rootFiles: string[] = []; + for (const [directory, tree] of base.trees) { const head = heads.get(directory); if (head === undefined) continue; const gitDependencies = directory @@ -7226,10 +7220,12 @@ async function changedPatchFiles( join(repository, directory), { trim: false }, ); - for (const path of output.split("\0").filter(Boolean)) + for (const path of output.split("\0").filter(Boolean)) { files.add(directory ? `${directory}/${path}` : path); + if (!directory) rootFiles.push(path); + } } - return [...files].sort(); + return { files: [...files].sort(), rootFiles: rootFiles.sort() }; } async function snapshotPatchState( @@ -7603,11 +7599,11 @@ async function runFindingPatches( onEvent: progress.observe.bind(progress), }, ); - changedFiles = await changedPatchFiles( + ({ files: changedFiles } = await changedPatchFiles( selected.repository, base, dependencies, - ); + )); } finally { progress.stop(); } diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 33ecbbaa1c..f73fcbe3de 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -4146,3 +4146,96 @@ test.each(["untracked", "tracked"])( } }, ); + +test.each(["untracked", "tracked"])( + "keeps unrelated %s edits made after patch capture out of publication", + async (kind) => { + const root = await temporaryDirectory("patch-assessment-publication-"); + const repository = join(root, "repository"); + const remote = join(root, "remote.git"); + const git = repositoryGit(repository); + try { + await mkdir(repository); + git("init", "--initial-branch=main"); + git("config", "user.name", "Synthetic User"); + git("config", "user.email", "synthetic@example.test"); + git("config", "commit.gpgsign", "false"); + await writeFile(join(repository, "app.ts"), "original\n"); + if (kind === "tracked") + await writeFile(join(repository, "user-notes.txt"), "original notes\n"); + git("add", "."); + git("commit", "-m", "Synthetic initial commit"); + git("init", "--bare", remote); + git("remote", "add", "origin", remote); + let patched = false; + let injected = false; + const outcome = await runWorkflow( + [ + "patch", + "Synthetic issue", + "--assess-patch-risk", + "--create-pr", + "--json", + ], + { + currentDirectory: repository, + onRepositoryCommand: async (command, args, directory, options) => { + if (command !== "git") + return args[1] === "create" + ? "https://github.example.test/example/repository/pull/17" + : ""; + const result = await runGitRepositoryCommand( + command, + args, + directory, + options, + ); + if (patched && !injected && args.includes("--name-only")) { + injected = true; + await writeFile( + join(repository, "user-notes.txt"), + "unrelated notes from concurrent work\n", + ); + } + return result; + }, + onCodex: async (_args, output) => { + if ( + output?.appServer?.prompt.includes( + "$codex-security:assess-patch-risk", + ) + ) { + const artifact = JSON.parse( + output.appServer.prompt + .split("\n") + .find((line) => line.startsWith('{"path":'))!, + ); + expect(artifact.changedFiles).toEqual(["app.ts"]); + + output.stdout.write(patchRiskAssessment().report); + } else { + await writeFile(join(repository, "app.ts"), "fixed\n"); + patched = true; + output?.stdout.write("Patch complete."); + } + return 0; + }, + }, + ); + expect(outcome.exitCode, outcome.stderr).toBe(0); + expect(JSON.parse(outcome.stdout).files).toEqual(["app.ts"]); + expect(git("show", "--format=", "--name-only", "HEAD", "--")).toBe( + "app.ts", + ); + expect(git("rev-parse", "HEAD")).toBe(git("rev-parse", "@{upstream}")); + expect(git("status", "--porcelain")).toBe( + `${kind === "tracked" ? "M" : "??"} user-notes.txt`, + ); + expect(await readFile(join(repository, "user-notes.txt"), "utf8")).toBe( + "unrelated notes from concurrent work\n", + ); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); From be97862a3d2a39cfc0fc53d82df5567ad61084f2 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 19:58:57 +0000 Subject: [PATCH 05/28] test(patch): use Git path spelling for alias fixtures --- .../tests-ts/support/cli-patch-context.mts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/sdk/typescript/tests-ts/support/cli-patch-context.mts b/sdk/typescript/tests-ts/support/cli-patch-context.mts index 7d8d352a53..0e55cb493c 100644 --- a/sdk/typescript/tests-ts/support/cli-patch-context.mts +++ b/sdk/typescript/tests-ts/support/cli-patch-context.mts @@ -1,6 +1,6 @@ import { execFileSync } from "node:child_process"; import { mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises"; -import { join, relative, resolve } from "node:path"; +import { join, relative, resolve, sep } from "node:path"; import { main } from "../../src/cli.js"; import { capture, dependencies } from "../cli-fixtures.js"; @@ -42,9 +42,13 @@ for (const kind of ["absolute Git alias", "relative Git alias", "gh", "glab"]) { alias, process.platform === "win32" ? "junction" : "dir", ); - environment["GIT_DIR"] = kind.startsWith("relative") - ? relative(repository, join(alias, ".git")) - : join(alias, ".git"); + environment["GIT_DIR"] = ( + kind.startsWith("relative") + ? relative(repository, join(alias, ".git")) + : join(alias, ".git") + ) + .split(sep) + .join("/"); await writeFile( environment["GIT_CONFIG_GLOBAL"]!, '[includeIf "gitdir:**/alias/.git"]\npath = identity\n', @@ -55,6 +59,12 @@ for (const kind of ["absolute Git alias", "relative Git alias", "gh", "glab"]) { ); git(["config", "--local", "--unset", "user.name"]); git(["config", "--local", "--unset", "user.email"]); + if ( + !git(["var", "GIT_AUTHOR_IDENT"]).startsWith( + "Synthetic Alias ", + ) + ) + throw new Error("Git alias identity was not selected before patching"); } else { await mkdir(join(directory, "elsewhere", "target"), { recursive: true }); await symlink( From b007335f939f501d79c0209a74219d9e7daa4832 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 20:29:13 +0000 Subject: [PATCH 06/28] fix(patch): retain nested Git replacement settings --- plugins/codex-security/tests/test_workbench_db.py | 2 +- sdk/typescript/src/cli.ts | 4 +++- sdk/typescript/tests-ts/cli-patch.test.ts | 13 ++++++++++++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/plugins/codex-security/tests/test_workbench_db.py b/plugins/codex-security/tests/test_workbench_db.py index b4520e13d8..7628fd912e 100644 --- a/plugins/codex-security/tests/test_workbench_db.py +++ b/plugins/codex-security/tests/test_workbench_db.py @@ -4248,7 +4248,7 @@ def test_saved_scan_remains_readable_with_large_hunk_counts(tmp_path: Path, padd remediation = saved["findings"][0]["remediationState"] assert remediation["state"] == "generated" if padded: - assert remediation["patch"] == patch + assert remediation["patch"] == patch_path.read_bytes().decode() assert remediation["patchStats"]["additions"] == 1 assert remediation["patchStats"]["deletions"] == 1 else: diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 65ccbfd77b..6d4fdc075e 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7337,7 +7337,9 @@ async function nestedPatchGitDependencies( ...options, environment: { ...Object.fromEntries( - [...GIT_REPOSITORY_ENVIRONMENT].map((name) => [name, undefined]), + [...GIT_REPOSITORY_ENVIRONMENT] + .filter((name) => name !== "GIT_NO_REPLACE_OBJECTS") + .map((name) => [name, undefined]), ), ...options?.environment, }, diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index aee3a7ebf2..848694d86d 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -3824,6 +3824,7 @@ describe("patch publication integrity", () => { "nested environment", "nested objects", "nested common", + "nested replacements", ])("detects local patches in %s Git repositories", async (kind) => { const directory = await fixtures.create("patch-git-state-"); const git = repositoryGit(directory); @@ -3844,6 +3845,14 @@ describe("patch publication integrity", () => { await writeFile(join(nested, "app.ts"), "unsafe\n"); inner("add", "."); inner("commit", "-m", "Synthetic nested baseline"); + if (kind === "nested replacements") { + const original = inner("rev-parse", "HEAD^{tree}"); + await writeFile(join(nested, "app.ts"), "fixed\n"); + inner("add", "."); + const replacement = inner("write-tree"); + inner("reset", "--hard", "HEAD"); + inner("replace", original, replacement); + } path = "nested/app.ts"; } const gitEnvironment = @@ -3853,7 +3862,9 @@ describe("patch publication integrity", () => { ? { GIT_OBJECT_DIRECTORY: join(directory, ".git", "objects") } : kind === "nested common" ? { GIT_COMMON_DIR: join(directory, ".git") } - : {}; + : kind === "nested replacements" + ? { GIT_NO_REPLACE_OBJECTS: "1" } + : {}; const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { currentDirectory: directory, onRepositoryCommand: (command, args, cwd, options) => From a1ce0b834280baf0590ff09be13796bea4b08880 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 20:52:36 +0000 Subject: [PATCH 07/28] fix(patch): preserve nested replacement namespaces --- sdk/typescript/src/cli.ts | 6 +++++- sdk/typescript/tests-ts/cli-patch.test.ts | 7 +++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index cdea6781ad..f5b6fe0ea9 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7394,7 +7394,11 @@ async function nestedPatchGitDependencies( environment: { ...Object.fromEntries( [...GIT_REPOSITORY_ENVIRONMENT] - .filter((name) => name !== "GIT_NO_REPLACE_OBJECTS") + .filter( + (name) => + name !== "GIT_NO_REPLACE_OBJECTS" && + name !== "GIT_REPLACE_REF_BASE", + ) .map((name) => [name, undefined]), ), ...options?.environment, diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index c278e78864..128ed128d8 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -3849,6 +3849,7 @@ describe("patch publication integrity", () => { "nested objects", "nested common", "nested replacements", + "nested replacement namespace", ])("detects local patches in %s Git repositories", async (kind) => { const directory = await fixtures.create("patch-git-state-"); const git = repositoryGit(directory); @@ -3869,7 +3870,7 @@ describe("patch publication integrity", () => { await writeFile(join(nested, "app.ts"), "unsafe\n"); inner("add", "."); inner("commit", "-m", "Synthetic nested baseline"); - if (kind === "nested replacements") { + if (kind.startsWith("nested replacement")) { const original = inner("rev-parse", "HEAD^{tree}"); await writeFile(join(nested, "app.ts"), "fixed\n"); inner("add", "."); @@ -3888,7 +3889,9 @@ describe("patch publication integrity", () => { ? { GIT_COMMON_DIR: join(directory, ".git") } : kind === "nested replacements" ? { GIT_NO_REPLACE_OBJECTS: "1" } - : {}; + : kind === "nested replacement namespace" + ? { GIT_REPLACE_REF_BASE: "refs/synthetic-replacements/" } + : {}; const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { currentDirectory: directory, onRepositoryCommand: (command, args, cwd, options) => From 26b75dba7030fa8fa234ddd731ea53ac5470c287 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 21:48:51 +0000 Subject: [PATCH 08/28] fix(patch): preserve shared alternate objects for nested snapshots --- sdk/typescript/src/cli.ts | 1 + sdk/typescript/tests-ts/cli-patch.test.ts | 22 +++++++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 660f538306..475d4bd352 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7387,6 +7387,7 @@ async function nestedPatchGitDependencies( [...GIT_REPOSITORY_ENVIRONMENT] .filter( (name) => + name !== "GIT_ALTERNATE_OBJECT_DIRECTORIES" && name !== "GIT_NO_REPLACE_OBJECTS" && name !== "GIT_REPLACE_REF_BASE", ) diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 128ed128d8..6d86bb4b1f 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -11,6 +11,7 @@ import { mkdir, mkdtemp, readFile, + rename, rm, symlink, writeFile, @@ -3848,6 +3849,7 @@ describe("patch publication integrity", () => { "nested environment", "nested objects", "nested common", + "nested alternates", "nested replacements", "nested replacement namespace", ])("detects local patches in %s Git repositories", async (kind) => { @@ -3858,6 +3860,7 @@ describe("patch publication integrity", () => { git("config", "user.email", "synthetic@example.test"); await writeFile(join(directory, "app.ts"), "unsafe\n"); let path = "app.ts"; + let alternateObjects: string | undefined; if (kind.startsWith("nested")) { git("add", "."); git("commit", "-m", "Synthetic baseline"); @@ -3870,6 +3873,13 @@ describe("patch publication integrity", () => { await writeFile(join(nested, "app.ts"), "unsafe\n"); inner("add", "."); inner("commit", "-m", "Synthetic nested baseline"); + if (kind === "nested alternates") { + const pool = await fixtures.create("patch-shared-objects-"); + alternateObjects = join(pool, "objects"); + await rename(join(nested, ".git", "objects"), alternateObjects); + await mkdir(join(nested, ".git", "objects")); + expect(() => inner("rev-parse", "HEAD^{tree}")).toThrow(); + } if (kind.startsWith("nested replacement")) { const original = inner("rev-parse", "HEAD^{tree}"); await writeFile(join(nested, "app.ts"), "fixed\n"); @@ -3887,11 +3897,13 @@ describe("patch publication integrity", () => { ? { GIT_OBJECT_DIRECTORY: join(directory, ".git", "objects") } : kind === "nested common" ? { GIT_COMMON_DIR: join(directory, ".git") } - : kind === "nested replacements" - ? { GIT_NO_REPLACE_OBJECTS: "1" } - : kind === "nested replacement namespace" - ? { GIT_REPLACE_REF_BASE: "refs/synthetic-replacements/" } - : {}; + : kind === "nested alternates" + ? { GIT_ALTERNATE_OBJECT_DIRECTORIES: alternateObjects } + : kind === "nested replacements" + ? { GIT_NO_REPLACE_OBJECTS: "1" } + : kind === "nested replacement namespace" + ? { GIT_REPLACE_REF_BASE: "refs/synthetic-replacements/" } + : {}; const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { currentDirectory: directory, onRepositoryCommand: (command, args, cwd, options) => From a0c2f337409a6055b59f29e785220dd605481622 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 22:30:19 +0000 Subject: [PATCH 09/28] fix(cli): preserve relative nested object lookup context --- sdk/typescript/src/cli.ts | 12 ++++++- sdk/typescript/tests-ts/cli-patch.test.ts | 42 ++++++++++++++++++----- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 475d4bd352..2a38cfa604 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7373,12 +7373,22 @@ async function nestedPatchGitDependencies( ): Promise { const root = (await gitMarkerRoot(repository, undefined, "outermost")) ?? repository; + // Keep relative alternate-object paths in the selected repository's Git context. + const directory = await patchRepositoryRoot(repository, dependencies); return { ...dependencies, runRepositoryCommand: (command, args, checkout, options) => dependencies.runRepositoryCommand( command, - ["-C", checkout, ...args], + [ + "-C", + directory, + "--git-dir", + join(checkout, ".git"), + "--work-tree", + checkout, + ...args, + ], root, { ...options, diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 6d86bb4b1f..48c0aacac2 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -3850,10 +3850,18 @@ describe("patch publication integrity", () => { "nested objects", "nested common", "nested alternates", + "nested relative alternates", + "nested component relative alternates", + "nested quoted relative alternates", + "nested gitfile relative alternates", "nested replacements", "nested replacement namespace", ])("detects local patches in %s Git repositories", async (kind) => { const directory = await fixtures.create("patch-git-state-"); + const selectedDirectory = kind.includes("component") + ? join(directory, "component") + : directory; + if (selectedDirectory !== directory) await mkdir(selectedDirectory); const git = repositoryGit(directory); git("init", "--initial-branch=main"); git("config", "user.name", "Synthetic User"); @@ -3864,21 +3872,37 @@ describe("patch publication integrity", () => { if (kind.startsWith("nested")) { git("add", "."); git("commit", "-m", "Synthetic baseline"); - const nested = join(directory, "nested"); + const nested = join(selectedDirectory, "nested"); await mkdir(nested); const inner = repositoryGit(nested); - inner("init", "--initial-branch=main"); + inner( + "init", + "--initial-branch=main", + ...(kind.includes("gitfile") + ? ["--separate-git-dir", join(directory, ".git", "nested-metadata")] + : []), + ); + if (kind.includes("gitfile")) inner("config", "core.worktree", nested); inner("config", "user.name", "Synthetic User"); inner("config", "user.email", "synthetic@example.test"); await writeFile(join(nested, "app.ts"), "unsafe\n"); inner("add", "."); inner("commit", "-m", "Synthetic nested baseline"); - if (kind === "nested alternates") { - const pool = await fixtures.create("patch-shared-objects-"); + if (kind.includes("alternates")) { + const pool = await fixtures.create( + kind.includes("quoted") + ? `patch-shared${delimiter}objects-` + : "patch-shared-objects-", + ); alternateObjects = join(pool, "objects"); - await rename(join(nested, ".git", "objects"), alternateObjects); - await mkdir(join(nested, ".git", "objects")); + const gitDirectory = inner("rev-parse", "--absolute-git-dir"); + await rename(join(gitDirectory, "objects"), alternateObjects); + await mkdir(join(gitDirectory, "objects")); expect(() => inner("rev-parse", "HEAD^{tree}")).toThrow(); + if (kind.includes("relative")) + alternateObjects = relative(directory, alternateObjects); + if (kind.includes("quoted")) + alternateObjects = JSON.stringify(alternateObjects); } if (kind.startsWith("nested replacement")) { const original = inner("rev-parse", "HEAD^{tree}"); @@ -3897,7 +3921,7 @@ describe("patch publication integrity", () => { ? { GIT_OBJECT_DIRECTORY: join(directory, ".git", "objects") } : kind === "nested common" ? { GIT_COMMON_DIR: join(directory, ".git") } - : kind === "nested alternates" + : kind.includes("alternates") ? { GIT_ALTERNATE_OBJECT_DIRECTORIES: alternateObjects } : kind === "nested replacements" ? { GIT_NO_REPLACE_OBJECTS: "1" } @@ -3905,14 +3929,14 @@ describe("patch publication integrity", () => { ? { GIT_REPLACE_REF_BASE: "refs/synthetic-replacements/" } : {}; const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { - currentDirectory: directory, + currentDirectory: selectedDirectory, onRepositoryCommand: (command, args, cwd, options) => runGitRepositoryCommand(command, args, cwd, { ...options, environment: { ...gitEnvironment, ...options?.environment }, }), onCodex: async (_args, output) => { - await writeFile(join(directory, path), "fixed\n"); + await writeFile(join(selectedDirectory, path), "fixed\n"); output?.stdout.write("Fixed and checked."); return 0; }, From 7d381beb990bed0510e620139407d487abd83de8 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 5 Oct 2026 22:29:00 +0000 Subject: [PATCH 10/28] test(plugin): support isolated helper checks on Python 3.10 --- plugins/codex-security/tests/test_cli_dry_runs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/codex-security/tests/test_cli_dry_runs.py b/plugins/codex-security/tests/test_cli_dry_runs.py index 1a4cce6ccb..0af345e7a1 100644 --- a/plugins/codex-security/tests/test_cli_dry_runs.py +++ b/plugins/codex-security/tests/test_cli_dry_runs.py @@ -30,7 +30,7 @@ def test_all_scripts_support_help() -> None: @pytest.mark.parametrize("name", ("workbench_publication.py", "workbench_severity.py")) def test_workbench_helpers_support_help_with_safe_path(name: str) -> None: result = subprocess.run( - [sys.executable, "-P", str(SCRIPT_DIR / name), "--help"], + [sys.executable, "-I", str(SCRIPT_DIR / name), "--help"], check=False, capture_output=True, text=True, From 67d5949e8cd0014273b36da5a9ba076f3cb886b5 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 22:52:58 +0000 Subject: [PATCH 11/28] fix(patch): preserve nested alternate object stores --- sdk/typescript/src/cli.ts | 1 + sdk/typescript/tests-ts/cli-patch.test.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index f5b6fe0ea9..b425bef592 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7396,6 +7396,7 @@ async function nestedPatchGitDependencies( [...GIT_REPOSITORY_ENVIRONMENT] .filter( (name) => + name !== "GIT_ALTERNATE_OBJECT_DIRECTORIES" && name !== "GIT_NO_REPLACE_OBJECTS" && name !== "GIT_REPLACE_REF_BASE", ) diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 128ed128d8..bb3e42e882 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -11,6 +11,7 @@ import { mkdir, mkdtemp, readFile, + rename, rm, symlink, writeFile, @@ -3847,6 +3848,7 @@ describe("patch publication integrity", () => { "nested", "nested environment", "nested objects", + "nested alternate objects", "nested common", "nested replacements", "nested replacement namespace", @@ -3858,6 +3860,7 @@ describe("patch publication integrity", () => { git("config", "user.email", "synthetic@example.test"); await writeFile(join(directory, "app.ts"), "unsafe\n"); let path = "app.ts"; + let alternateObjects: string | undefined; if (kind.startsWith("nested")) { git("add", "."); git("commit", "-m", "Synthetic baseline"); @@ -3870,6 +3873,14 @@ describe("patch publication integrity", () => { await writeFile(join(nested, "app.ts"), "unsafe\n"); inner("add", "."); inner("commit", "-m", "Synthetic nested baseline"); + if (kind === "nested alternate objects") { + alternateObjects = join( + await fixtures.create("patch-alternate-objects-"), + "objects", + ); + await rename(join(nested, ".git", "objects"), alternateObjects); + await mkdir(join(nested, ".git", "objects")); + } if (kind.startsWith("nested replacement")) { const original = inner("rev-parse", "HEAD^{tree}"); await writeFile(join(nested, "app.ts"), "fixed\n"); @@ -3891,7 +3902,9 @@ describe("patch publication integrity", () => { ? { GIT_NO_REPLACE_OBJECTS: "1" } : kind === "nested replacement namespace" ? { GIT_REPLACE_REF_BASE: "refs/synthetic-replacements/" } - : {}; + : kind === "nested alternate objects" + ? { GIT_ALTERNATE_OBJECT_DIRECTORIES: alternateObjects } + : {}; const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { currentDirectory: directory, onRepositoryCommand: (command, args, cwd, options) => From 8abde75c883eb17d86653a54da2ccce344fa55fb Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Mon, 5 Oct 2026 23:34:37 +0000 Subject: [PATCH 12/28] style(test): format combined Git alternate controls --- sdk/typescript/tests-ts/cli-patch.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 282d00f7be..3b3d2b8da1 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -3997,7 +3997,7 @@ describe("patch publication integrity", () => { await writeFile(join(nested, "app.ts"), "unsafe\n"); inner("add", "."); inner("commit", "-m", "Synthetic nested baseline"); - if ((kind.includes("alternates") || kind === "nested alternate objects")) { + if (kind.includes("alternates") || kind === "nested alternate objects") { const pool = await fixtures.create( kind.includes("quoted") ? `patch-shared${delimiter}objects-` @@ -4030,7 +4030,7 @@ describe("patch publication integrity", () => { ? { GIT_OBJECT_DIRECTORY: join(directory, ".git", "objects") } : kind === "nested common" ? { GIT_COMMON_DIR: join(directory, ".git") } - : (kind.includes("alternates") || kind === "nested alternate objects") + : kind.includes("alternates") || kind === "nested alternate objects" ? { GIT_ALTERNATE_OBJECT_DIRECTORIES: alternateObjects } : kind === "nested replacements" ? { GIT_NO_REPLACE_OBJECTS: "1" } From 44782e4db80d013a06230376e1c28b864032ab19 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 01:46:22 +0000 Subject: [PATCH 13/28] Keep configured object pools readable in nested patch snapshots --- sdk/typescript/src/cli.ts | 38 ++++++++++ sdk/typescript/tests-ts/cli-patch.test.ts | 91 +++++++++++++++++------ 2 files changed, 108 insertions(+), 21 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 4d9ca45e58..0668c94d70 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -31,6 +31,7 @@ import { import { homedir, tmpdir } from "node:os"; import { basename, + delimiter, dirname, isAbsolute, join, @@ -7377,6 +7378,40 @@ async function nestedPatchGitDependencies( (await gitMarkerRoot(repository, undefined, "outermost")) ?? repository; // Keep relative alternate-object paths in the selected repository's Git context. const directory = await patchRepositoryRoot(repository, dependencies); + const objectDirectory = + environmentValue(dependencies.environment, "GIT_OBJECT_DIRECTORY") === + undefined + ? undefined + : ( + await dependencies.runRepositoryCommand( + "git", + ["rev-parse", "--path-format=absolute", "--git-path", "objects"], + repository, + { trim: false }, + ) + ).replace(/\n$/u, ""); + const alternateObjects = + objectDirectory === undefined + ? undefined + : [ + // Git's alternate list accepts C-quoted paths, including its delimiter. + `"${objectDirectory.replace( + /[\\"\u0000-\u001f\u007f]/gu, + (character) => + `\\${character.charCodeAt(0).toString(8).padStart(3, "0")}`, + )}"`, + ...(environmentValue( + dependencies.environment, + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + ) === undefined + ? [] + : [ + environmentValue( + dependencies.environment, + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + )!, + ]), + ].join(delimiter); return { ...dependencies, runRepositoryCommand: (command, args, checkout, options) => @@ -7405,6 +7440,9 @@ async function nestedPatchGitDependencies( ) .map((name) => [name, undefined]), ), + ...(alternateObjects === undefined + ? {} + : { GIT_ALTERNATE_OBJECT_DIRECTORIES: alternateObjects }), ...options?.environment, }, }, diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index b7ed32ff65..5a211e5656 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -8,6 +8,7 @@ import { hash } from "node:crypto"; import { chmod, copyFile, + cp, mkdir, mkdtemp, readFile, @@ -4088,6 +4089,9 @@ describe("patch publication integrity", () => { "nested", "nested environment", "nested objects", + "nested shared primary objects", + "nested relative primary objects", + "nested quoted primary objects", "nested alternate objects", "nested common", "nested relative alternates", @@ -4109,6 +4113,9 @@ describe("patch publication integrity", () => { await writeFile(join(directory, "app.ts"), "unsafe\n"); let path = "app.ts"; let alternateObjects: string | undefined; + let primaryObjects: string | undefined; + let parentIndex: Buffer | undefined; + let nestedIndex: Buffer | undefined; if (kind.startsWith("nested")) { git("add", "."); git("commit", "-m", "Synthetic baseline"); @@ -4144,6 +4151,29 @@ describe("patch publication integrity", () => { if (kind.includes("quoted")) alternateObjects = JSON.stringify(alternateObjects); } + if (kind.includes("primary objects")) { + const pool = await fixtures.create( + kind.includes("quoted") + ? `patch-shared${delimiter}primary-` + : "patch-shared-primary-", + ); + primaryObjects = join(pool, "objects"); + const gitDirectory = inner("rev-parse", "--absolute-git-dir"); + await rename(join(gitDirectory, "objects"), primaryObjects); + await mkdir(join(gitDirectory, "objects")); + await cp(join(directory, ".git", "objects"), primaryObjects, { + recursive: true, + force: false, + }); + await rm(join(directory, ".git", "objects"), { recursive: true }); + await mkdir(join(directory, ".git", "objects")); + expect(() => git("rev-parse", "HEAD^{tree}")).toThrow(); + expect(() => inner("rev-parse", "HEAD^{tree}")).toThrow(); + parentIndex = await readFile(join(directory, ".git", "index")); + nestedIndex = await readFile(join(gitDirectory, "index")); + if (kind.includes("relative")) + primaryObjects = relative(directory, primaryObjects); + } if (kind.startsWith("nested replacement")) { const original = inner("rev-parse", "HEAD^{tree}"); await writeFile(join(nested, "app.ts"), "fixed\n"); @@ -4159,28 +4189,47 @@ describe("patch publication integrity", () => { ? { GIT_DIR: ".git", GIT_WORK_TREE: directory } : kind === "nested objects" ? { GIT_OBJECT_DIRECTORY: join(directory, ".git", "objects") } - : kind === "nested common" - ? { GIT_COMMON_DIR: join(directory, ".git") } - : kind.includes("alternates") || kind === "nested alternate objects" - ? { GIT_ALTERNATE_OBJECT_DIRECTORIES: alternateObjects } - : kind === "nested replacements" - ? { GIT_NO_REPLACE_OBJECTS: "1" } - : kind === "nested replacement namespace" - ? { GIT_REPLACE_REF_BASE: "refs/synthetic-replacements/" } - : {}; - const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { - currentDirectory: selectedDirectory, - onRepositoryCommand: (command, args, cwd, options) => - runGitRepositoryCommand(command, args, cwd, { - ...options, - environment: { ...gitEnvironment, ...options?.environment }, - }), - onCodex: async (_args, output) => { - await writeFile(join(selectedDirectory, path), "fixed\n"); - output?.stdout.write("Fixed and checked."); - return 0; + : kind.includes("primary objects") + ? { GIT_OBJECT_DIRECTORY: primaryObjects } + : kind === "nested common" + ? { GIT_COMMON_DIR: join(directory, ".git") } + : kind.includes("alternates") || + kind === "nested alternate objects" + ? { GIT_ALTERNATE_OBJECT_DIRECTORIES: alternateObjects } + : kind === "nested replacements" + ? { GIT_NO_REPLACE_OBJECTS: "1" } + : kind === "nested replacement namespace" + ? { GIT_REPLACE_REF_BASE: "refs/synthetic-replacements/" } + : {}; + const outcome = await runWorkflow( + ["patch", "Synthetic issue", "--json"], + { + currentDirectory: selectedDirectory, + onRepositoryCommand: (command, args, cwd, options) => + runGitRepositoryCommand(command, args, cwd, { + ...options, + environment: { ...gitEnvironment, ...options?.environment }, + }), + onCodex: async (_args, output) => { + await writeFile(join(selectedDirectory, path), "fixed\n"); + output?.stdout.write("Fixed and checked."); + return 0; + }, }, - }); + { + configure: (current) => { + current.environment = { ...current.environment, ...gitEnvironment }; + }, + }, + ); + if (primaryObjects !== undefined) { + expect(await readFile(join(directory, ".git", "index"))).toEqual( + parentIndex, + ); + expect( + await readFile(join(selectedDirectory, "nested", ".git", "index")), + ).toEqual(nestedIndex); + } expect(outcome.exitCode, outcome.stderr).toBe(0); expect(JSON.parse(outcome.stdout)).toMatchObject({ applied: true, From de5d45916983713c947ce4cc262881b0dc311dec Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 01:50:22 +0000 Subject: [PATCH 14/28] Compare preserved Git index bytes with stable hashes --- sdk/typescript/tests-ts/cli-patch.test.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 9ce7d06199..a523c466f5 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -4128,8 +4128,8 @@ describe("patch publication integrity", () => { let path = "app.ts"; let alternateObjects: string | undefined; let primaryObjects: string | undefined; - let parentIndex: Buffer | undefined; - let nestedIndex: Buffer | undefined; + let parentIndex: string | undefined; + let nestedIndex: string | undefined; if (kind.startsWith("nested")) { git("add", "."); git("commit", "-m", "Synthetic baseline"); @@ -4183,8 +4183,8 @@ describe("patch publication integrity", () => { await mkdir(join(directory, ".git", "objects")); expect(() => git("rev-parse", "HEAD^{tree}")).toThrow(); expect(() => inner("rev-parse", "HEAD^{tree}")).toThrow(); - parentIndex = await readFile(join(directory, ".git", "index")); - nestedIndex = await readFile(join(gitDirectory, "index")); + parentIndex = hash(await readFile(join(directory, ".git", "index"))); + nestedIndex = hash(await readFile(join(gitDirectory, "index"))); if (kind.includes("relative")) primaryObjects = relative(directory, primaryObjects); } @@ -4237,12 +4237,14 @@ describe("patch publication integrity", () => { }, ); if (primaryObjects !== undefined) { - expect(await readFile(join(directory, ".git", "index"))).toEqual( + expect(hash(await readFile(join(directory, ".git", "index")))).toBe( parentIndex, ); expect( - await readFile(join(selectedDirectory, "nested", ".git", "index")), - ).toEqual(nestedIndex); + hash( + await readFile(join(selectedDirectory, "nested", ".git", "index")), + ), + ).toBe(nestedIndex); } expect(outcome.exitCode, outcome.stderr).toBe(0); expect(JSON.parse(outcome.stdout)).toMatchObject({ From 012a9c74fbc266d3fed18c60c34c52b8aee791c6 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 01:54:20 +0000 Subject: [PATCH 15/28] Correct primary object pool index assertions --- sdk/typescript/tests-ts/cli-patch.test.ts | 24 ++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index a523c466f5..15986939f0 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -4128,8 +4128,8 @@ describe("patch publication integrity", () => { let path = "app.ts"; let alternateObjects: string | undefined; let primaryObjects: string | undefined; - let parentIndex: string | undefined; - let nestedIndex: string | undefined; + let parentIndex = ""; + let nestedIndex = ""; if (kind.startsWith("nested")) { git("add", "."); git("commit", "-m", "Synthetic baseline"); @@ -4183,8 +4183,16 @@ describe("patch publication integrity", () => { await mkdir(join(directory, ".git", "objects")); expect(() => git("rev-parse", "HEAD^{tree}")).toThrow(); expect(() => inner("rev-parse", "HEAD^{tree}")).toThrow(); - parentIndex = hash(await readFile(join(directory, ".git", "index"))); - nestedIndex = hash(await readFile(join(gitDirectory, "index"))); + parentIndex = hash( + "sha256", + await readFile(join(directory, ".git", "index")), + "hex", + ); + nestedIndex = hash( + "sha256", + await readFile(join(gitDirectory, "index")), + "hex", + ); if (kind.includes("relative")) primaryObjects = relative(directory, primaryObjects); } @@ -4237,12 +4245,14 @@ describe("patch publication integrity", () => { }, ); if (primaryObjects !== undefined) { - expect(hash(await readFile(join(directory, ".git", "index")))).toBe( - parentIndex, - ); + expect( + hash("sha256", await readFile(join(directory, ".git", "index")), "hex"), + ).toBe(parentIndex); expect( hash( + "sha256", await readFile(join(selectedDirectory, "nested", ".git", "index")), + "hex", ), ).toBe(nestedIndex); } From d9a9a8c3f4013b7fe03cc8e782a105962ca04482 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 02:42:33 +0000 Subject: [PATCH 16/28] Preserve configured storage for nested checkouts without local objects --- sdk/typescript/src/cli.ts | 64 +++++++++++++++++------ sdk/typescript/src/targets.ts | 39 ++++++++++++-- sdk/typescript/tests-ts/cli-patch.test.ts | 10 +++- 3 files changed, 92 insertions(+), 21 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 386c3e6846..2f9bb6fb2c 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7287,7 +7287,11 @@ async function changedPatchFiles( const head = heads.get(directory); if (head === undefined) continue; const gitDependencies = directory - ? await nestedPatchGitDependencies(repository, dependencies) + ? await nestedPatchGitDependencies( + repository, + dependencies, + join(repository, directory), + ) : dependencies; const output = await gitDependencies.runRepositoryCommand( "git", @@ -7355,10 +7359,14 @@ async function snapshotGitPatchState( } await enclosingGitWorktreeRoot(checkout, undefined, { requireIfPresent: true, + objectDirectory: await configuredPatchObjectDirectory( + repository, + dependencies, + ), }); } const gitDependencies = directory - ? await nestedPatchGitDependencies(repository, dependencies) + ? await nestedPatchGitDependencies(repository, dependencies, checkout) : dependencies; const tree = await snapshotPatchTree(checkout, gitDependencies); trees.set(directory, tree); @@ -7379,26 +7387,36 @@ async function snapshotGitPatchState( return { trees }; } +async function configuredPatchObjectDirectory( + repository: string, + dependencies: CliDependencies, +): Promise { + return environmentValue(dependencies.environment, "GIT_OBJECT_DIRECTORY") === + undefined + ? undefined + : ( + await dependencies.runRepositoryCommand( + "git", + ["rev-parse", "--path-format=absolute", "--git-path", "objects"], + repository, + { trim: false }, + ) + ).replace(/\n$/u, ""); +} + async function nestedPatchGitDependencies( repository: string, dependencies: CliDependencies, + checkout: string, ): Promise { const root = (await gitMarkerRoot(repository, undefined, "outermost")) ?? repository; // Keep relative alternate-object paths in the selected repository's Git context. const directory = await patchRepositoryRoot(repository, dependencies); - const objectDirectory = - environmentValue(dependencies.environment, "GIT_OBJECT_DIRECTORY") === - undefined - ? undefined - : ( - await dependencies.runRepositoryCommand( - "git", - ["rev-parse", "--path-format=absolute", "--git-path", "objects"], - repository, - { trim: false }, - ) - ).replace(/\n$/u, ""); + const objectDirectory = await configuredPatchObjectDirectory( + repository, + dependencies, + ); const alternateObjects = objectDirectory === undefined ? undefined @@ -7421,7 +7439,8 @@ async function nestedPatchGitDependencies( )!, ]), ].join(delimiter); - return { + let nestedObjectDirectory: string | undefined; + const nested: CliDependencies = { ...dependencies, runRepositoryCommand: (command, args, checkout, options) => dependencies.runRepositoryCommand( @@ -7449,6 +7468,7 @@ async function nestedPatchGitDependencies( ) .map((name) => [name, undefined]), ), + GIT_OBJECT_DIRECTORY: nestedObjectDirectory, ...(alternateObjects === undefined ? {} : { GIT_ALTERNATE_OBJECT_DIRECTORIES: alternateObjects }), @@ -7457,6 +7477,20 @@ async function nestedPatchGitDependencies( }, ), }; + if (objectDirectory !== undefined) { + const commonDirectory = ( + await nested.runRepositoryCommand( + "git", + ["rev-parse", "--path-format=absolute", "--git-common-dir"], + checkout, + { trim: false, environment: { GIT_OBJECT_DIRECTORY: objectDirectory } }, + ) + ).replace(/\n$/u, ""); + // Git accepts configured primary storage even without a local objects directory. + if (!existsSync(join(commonDirectory, "objects"))) + nestedObjectDirectory = objectDirectory; + } + return nested; } // Literal patch inputs also work in directories without Git metadata. diff --git a/sdk/typescript/src/targets.ts b/sdk/typescript/src/targets.ts index b408338804..53b33cec48 100644 --- a/sdk/typescript/src/targets.ts +++ b/sdk/typescript/src/targets.ts @@ -154,9 +154,13 @@ function requirePortableWindowsRepositoryPath(path: string): void { export async function enclosingGitWorktreeRoot( repository: string, signal?: AbortSignal, - options: { requireIfPresent?: boolean } = {}, + options: { requireIfPresent?: boolean; objectDirectory?: string } = {}, ): Promise { const strict = options.requireIfPresent === true; + const objectEnvironment = + options.objectDirectory === undefined + ? {} + : { GIT_OBJECT_DIRECTORY: options.objectDirectory }; const markerRoot = strict ? await gitMarkerRoot(repository, signal, "nearest") : null; @@ -168,6 +172,7 @@ export async function enclosingGitWorktreeRoot( repository, ["rev-parse", "--is-inside-git-dir"], signal, + objectEnvironment, )) === "true" ) { throw new InvalidTargetError( @@ -180,6 +185,7 @@ export async function enclosingGitWorktreeRoot( repository, ["rev-parse", "--show-toplevel"], signal, + objectEnvironment, ); canonicalRoot = await abortable(() => realpath(root), signal); } catch (error) { @@ -205,7 +211,11 @@ export async function enclosingGitWorktreeRoot( ); } if (markerRoot !== null) - await requireGitWorktreeBinding(canonicalRoot, signal); + await requireGitWorktreeBinding( + canonicalRoot, + signal, + options.objectDirectory, + ); return canonicalRoot; } @@ -299,11 +309,25 @@ export async function isGitMetadataDirectory( export async function gitMetadataDirectories( repository: string, signal?: AbortSignal, - options: { includeLocalObjects?: boolean } = {}, + options: { includeLocalObjects?: boolean; objectDirectory?: string } = {}, ): Promise<[string, string, ...string[]]> { + const environment = + options.objectDirectory === undefined + ? {} + : { GIT_OBJECT_DIRECTORY: options.objectDirectory }; const [directory, commonDirectory] = await Promise.all([ - gitOutput(repository, ["rev-parse", "--absolute-git-dir"], signal), - gitOutput(repository, ["rev-parse", "--git-common-dir"], signal), + gitOutput( + repository, + ["rev-parse", "--absolute-git-dir"], + signal, + environment, + ), + gitOutput( + repository, + ["rev-parse", "--git-common-dir"], + signal, + environment, + ), ]); const roots = await Promise.all([ abortable(() => realpath(resolve(repository, directory)), signal), @@ -413,12 +437,14 @@ export async function gitObjectDirectories( async function requireGitWorktreeBinding( repository: string, signal?: AbortSignal, + objectDirectory?: string, ): Promise { let cause: unknown; try { const [directory, commonDirectory] = await gitMetadataDirectories( repository, signal, + { objectDirectory }, ); if ( [directory, commonDirectory].every( @@ -433,6 +459,9 @@ async function requireGitWorktreeBinding( repository, ["config", "--get", "core.worktree"], signal, + objectDirectory === undefined + ? {} + : { GIT_OBJECT_DIRECTORY: objectDirectory }, ) ) return; diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 15986939f0..909a824d0d 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -5,6 +5,7 @@ import { parse as parseToml } from "smol-toml"; import { afterEach, describe, expect, test, mock } from "bun:test"; import { execFile, execFileSync } from "node:child_process"; import { hash } from "node:crypto"; +import { existsSync } from "node:fs"; import { chmod, copyFile, @@ -4106,6 +4107,8 @@ describe("patch publication integrity", () => { "nested shared primary objects", "nested relative primary objects", "nested quoted primary objects", + "nested missing local primary objects", + "nested missing local relative primary objects", "nested alternate objects", "nested common", "nested relative alternates", @@ -4174,7 +4177,8 @@ describe("patch publication integrity", () => { primaryObjects = join(pool, "objects"); const gitDirectory = inner("rev-parse", "--absolute-git-dir"); await rename(join(gitDirectory, "objects"), primaryObjects); - await mkdir(join(gitDirectory, "objects")); + if (!kind.includes("missing local")) + await mkdir(join(gitDirectory, "objects")); await cp(join(directory, ".git", "objects"), primaryObjects, { recursive: true, force: false, @@ -4244,6 +4248,10 @@ describe("patch publication integrity", () => { }, }, ); + if (kind.includes("missing local")) + expect( + existsSync(join(selectedDirectory, "nested", ".git", "objects")), + ).toBe(false); if (primaryObjects !== undefined) { expect( hash("sha256", await readFile(join(directory, ".git", "index")), "hex"), From b0f61663069cf44dfe61ddb895268e18e74eeccf Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 04:15:03 +0000 Subject: [PATCH 17/28] fix(sdk): retain nested Git object storage and invocation context --- sdk/typescript/src/cli.ts | 63 ++++++++------- sdk/typescript/tests-ts/cli-patch.test.ts | 95 +++++++++++++++++++++++ 2 files changed, 129 insertions(+), 29 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 2f9bb6fb2c..0b387d9edd 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7411,35 +7411,20 @@ async function nestedPatchGitDependencies( ): Promise { const root = (await gitMarkerRoot(repository, undefined, "outermost")) ?? repository; - // Keep relative alternate-object paths in the selected repository's Git context. - const directory = await patchRepositoryRoot(repository, dependencies); + // Resolve "." in Git's actual setup directory, including an outside invocation. + const directory = ( + await dependencies.runRepositoryCommand( + "git", + ["rev-parse", "--path-format=absolute", "--git-path", "objects"], + repository, + { trim: false, environment: { GIT_OBJECT_DIRECTORY: "." } }, + ) + ).replace(/\n$/u, ""); const objectDirectory = await configuredPatchObjectDirectory( repository, dependencies, ); - const alternateObjects = - objectDirectory === undefined - ? undefined - : [ - // Git's alternate list accepts C-quoted paths, including its delimiter. - `"${objectDirectory.replace( - /[\\"\u0000-\u001f\u007f]/gu, - (character) => - `\\${character.charCodeAt(0).toString(8).padStart(3, "0")}`, - )}"`, - ...(environmentValue( - dependencies.environment, - "GIT_ALTERNATE_OBJECT_DIRECTORIES", - ) === undefined - ? [] - : [ - environmentValue( - dependencies.environment, - "GIT_ALTERNATE_OBJECT_DIRECTORIES", - )!, - ]), - ].join(delimiter); - let nestedObjectDirectory: string | undefined; + let alternateObjects: string | undefined; const nested: CliDependencies = { ...dependencies, runRepositoryCommand: (command, args, checkout, options) => @@ -7468,7 +7453,7 @@ async function nestedPatchGitDependencies( ) .map((name) => [name, undefined]), ), - GIT_OBJECT_DIRECTORY: nestedObjectDirectory, + GIT_OBJECT_DIRECTORY: objectDirectory, ...(alternateObjects === undefined ? {} : { GIT_ALTERNATE_OBJECT_DIRECTORIES: alternateObjects }), @@ -7486,9 +7471,29 @@ async function nestedPatchGitDependencies( { trim: false, environment: { GIT_OBJECT_DIRECTORY: objectDirectory } }, ) ).replace(/\n$/u, ""); - // Git accepts configured primary storage even without a local objects directory. - if (!existsSync(join(commonDirectory, "objects"))) - nestedObjectDirectory = objectDirectory; + const localObjects = join(commonDirectory, "objects"); + if (existsSync(localObjects)) { + // Retain local reads while writing to the caller's configured primary pool. + // Git's alternate list accepts C-quoted paths, including its delimiter. + alternateObjects = [ + `"${localObjects.replace( + /[\\"\u0000-\u001f\u007f]/gu, + (character) => + `\\${character.charCodeAt(0).toString(8).padStart(3, "0")}`, + )}"`, + ...(environmentValue( + dependencies.environment, + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + ) === undefined + ? [] + : [ + environmentValue( + dependencies.environment, + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + )!, + ]), + ].join(delimiter); + } } return nested; } diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 909a824d0d..78c112a284 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -4105,6 +4105,7 @@ describe("patch publication integrity", () => { "nested environment", "nested objects", "nested shared primary objects", + "nested read-only local primary objects", "nested relative primary objects", "nested quoted primary objects", "nested missing local primary objects", @@ -4179,6 +4180,8 @@ describe("patch publication integrity", () => { await rename(join(gitDirectory, "objects"), primaryObjects); if (!kind.includes("missing local")) await mkdir(join(gitDirectory, "objects")); + if (kind.includes("read-only local")) + await chmod(join(gitDirectory, "objects"), 0o555); await cp(join(directory, ".git", "objects"), primaryObjects, { recursive: true, force: false, @@ -4706,3 +4709,95 @@ test.each(["untracked", "tracked"])( } }, ); + +test.each(["absolute", "relative"])( + "preserves %s nested alternates from an outside invocation", + async (kind) => { + const root = await temporaryDirectory("patch-outside-alternate-"); + const repository = join(root, "repository"); + const invocation = join(root, "invocation"); + const nested = join(repository, "nested"); + const pool = join(invocation, "pool", "objects"); + try { + await mkdir(repository); + await mkdir(invocation); + const git = repositoryGit(repository); + git("init", "--initial-branch=main"); + git("config", "user.name", "Synthetic User"); + git("config", "user.email", "synthetic@example.test"); + await writeFile(join(repository, "app.ts"), "original\n"); + git("add", "."); + git("commit", "-m", "Synthetic parent baseline"); + await mkdir(nested); + const inner = repositoryGit(nested); + inner("init", "--initial-branch=main"); + inner("config", "user.name", "Synthetic User"); + inner("config", "user.email", "synthetic@example.test"); + await writeFile(join(nested, "app.ts"), "original\n"); + inner("add", "."); + inner("commit", "-m", "Synthetic nested baseline"); + await mkdir(dirname(pool)); + await rename(join(nested, ".git", "objects"), pool); + await mkdir(join(nested, ".git", "objects")); + expect(() => inner("rev-parse", "HEAD^{tree}")).toThrow(); + const gitEnvironment = { + GIT_DIR: join(repository, ".git"), + GIT_WORK_TREE: repository, + GIT_ALTERNATE_OBJECT_DIRECTORIES: + kind === "relative" ? relative(invocation, pool) : pool, + }; + expect( + gitText( + [ + "--git-dir", + join(nested, ".git"), + "--work-tree", + nested, + "rev-parse", + "HEAD^{tree}", + ], + { cwd: invocation, env: { ...process.env, ...gitEnvironment } }, + ).trim(), + ).toMatch(/^[0-9a-f]+$/); + let calls = 0; + const outcome = await runWorkflow( + ["patch", "Synthetic issue", "--assess-patch-risk", "--json"], + { + currentDirectory: invocation, + environment: { ...process.env, ...gitEnvironment }, + onRepositoryCommand: (command, args, cwd, options) => + runGitRepositoryCommand(command, args, cwd, { + ...options, + environment: { ...gitEnvironment, ...options?.environment }, + }), + onCodex: async (_args, output) => { + calls++; + if ( + output?.appServer?.prompt.includes( + "$codex-security:assess-patch-risk", + ) + ) { + output.stdout.write(patchRiskAssessment().report); + } else { + await writeFile(join(repository, "app.ts"), "fixed\n"); + await writeFile(join(nested, "app.ts"), "fixed\n"); + output?.stdout.write("Fixed and checked."); + } + return 0; + }, + }, + ); + expect(outcome.exitCode, outcome.stderr).toBe(0); + expect(calls).toBe(2); + expect(JSON.parse(outcome.stdout)).toMatchObject({ + applied: true, + files: [ + relative(invocation, join(repository, "app.ts")).split(sep).join("/"), + relative(invocation, join(nested, "app.ts")).split(sep).join("/"), + ], + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); From 898f48eece0b91a4088a66ebc168c2638275a6bf Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 05:46:26 +0000 Subject: [PATCH 18/28] Capture alternate object paths before patch changes cwd --- sdk/typescript/src/cli.ts | 23 ++++++ sdk/typescript/tests-ts/cli-patch.test.ts | 97 +++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 8ca66ced30..6da7d242a6 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -5377,6 +5377,10 @@ export async function main( ) commandDirectory = gitRepository; commandEnvironment = commandContext.environment; + gitDependencies.environment = { + ...dependencies.environment, + ...commandEnvironment, + }; } const { files, rootFiles: publicationFiles } = await changedPatchFiles( @@ -6918,6 +6922,25 @@ async function patchCommandContext( if (dependencies.environment[name] !== undefined) environment[name] = await gitPath(["--path-format=absolute", ...args]); } + if ( + dependencies.environment["GIT_ALTERNATE_OBJECT_DIRECTORIES"] !== undefined + ) { + const objects = await dependencies.runRepositoryCommand( + "git", + ["count-objects", "-v"], + repository, + { trim: false }, + ); + // Git resolves and C-quotes alternate paths in the original command context. + environment["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = objects + .split("\n") + .filter((line) => line.startsWith("alternate: ")) + .map((line) => { + const path = line.slice("alternate: ".length); + return path.startsWith('"') ? path : JSON.stringify(path); + }) + .join(delimiter); + } for (const name of ["GH_CONFIG_DIR", "GLAB_CONFIG_DIR"]) { const value = dependencies.environment[name]; if (value === undefined) continue; diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 54d55c7f34..559827a562 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -4861,3 +4861,100 @@ test.skipIf(process.platform !== "win32")( } }, ); + +test.each(["absolute", "relative", "quoted relative"])( + "preserves %s nested alternates after removing an outside invocation", + async (kind) => { + const root = await temporaryDirectory("patch-outside-alternate-"); + const repository = join(root, "repository"); + const invocation = join(root, "temporary", "invocation"); + const nested = join(repository, "nested"); + const pool = join(root, "pool with space", "objects"); + try { + await mkdir(repository); + await mkdir(invocation, { recursive: true }); + const git = repositoryGit(repository); + git("init", "--initial-branch=main"); + git("config", "user.name", "Synthetic User"); + git("config", "user.email", "synthetic@example.test"); + await writeFile(join(repository, "app.ts"), "original\n"); + git("add", "."); + git("commit", "-m", "Synthetic parent baseline"); + await mkdir(nested); + const inner = repositoryGit(nested); + inner("init", "--initial-branch=main"); + inner("config", "user.name", "Synthetic User"); + inner("config", "user.email", "synthetic@example.test"); + await writeFile(join(nested, "app.ts"), "original\n"); + inner("add", "."); + inner("commit", "-m", "Synthetic nested baseline"); + await mkdir(dirname(pool)); + await rename(join(nested, ".git", "objects"), pool); + await mkdir(join(nested, ".git", "objects")); + expect(() => inner("rev-parse", "HEAD^{tree}")).toThrow(); + const gitEnvironment = { + GIT_DIR: join(repository, ".git"), + GIT_WORK_TREE: repository, + GIT_ALTERNATE_OBJECT_DIRECTORIES: + kind === "absolute" + ? pool + : kind === "relative" + ? relative(invocation, pool) + : JSON.stringify(relative(invocation, pool)), + }; + expect( + gitText( + [ + "--git-dir", + join(nested, ".git"), + "--work-tree", + nested, + "rev-parse", + "HEAD^{tree}", + ], + { cwd: invocation, env: { ...process.env, ...gitEnvironment } }, + ).trim(), + ).toMatch(/^[0-9a-f]+$/); + let calls = 0; + const outcome = await runWorkflow( + ["patch", "Synthetic issue", "--assess-patch-risk", "--json"], + { + currentDirectory: invocation, + environment: { ...process.env, ...gitEnvironment }, + onRepositoryCommand: (command, args, cwd, options) => + runGitRepositoryCommand(command, args, cwd, { + ...options, + environment: { ...gitEnvironment, ...options?.environment }, + }), + onCodex: async (_args, output) => { + calls++; + if ( + output?.appServer?.prompt.includes( + "$codex-security:assess-patch-risk", + ) + ) { + output.stdout.write(patchRiskAssessment().report); + } else { + await writeFile(join(repository, "app.ts"), "fixed\n"); + await writeFile(join(nested, "app.ts"), "fixed\n"); + await rm(invocation, { recursive: true }); + output?.stdout.write("Fixed and checked."); + } + return 0; + }, + }, + ); + expect(outcome.exitCode, outcome.stderr).toBe(0); + expect(calls).toBe(2); + expect(JSON.parse(outcome.stdout)).toMatchObject({ + applied: true, + files: [ + relative(invocation, join(repository, "app.ts")).split(sep).join("/"), + relative(invocation, join(nested, "app.ts")).split(sep).join("/"), + ], + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); From 3c2917b867ff430256a54c74cc5aa0bcc1235c35 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 05:09:00 +0000 Subject: [PATCH 19/28] fix(ci): preserve the existing Markdown formatting scope --- sdk/typescript/scripts/check-markdown.mjs | 21 +++++++++--- .../tests-ts/check-markdown.test.ts | 34 ++++++++++++++----- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/sdk/typescript/scripts/check-markdown.mjs b/sdk/typescript/scripts/check-markdown.mjs index 1ef2ff028b..b06f20fda8 100644 --- a/sdk/typescript/scripts/check-markdown.mjs +++ b/sdk/typescript/scripts/check-markdown.mjs @@ -4,11 +4,22 @@ import { join } from "node:path"; import { fileURLToPath } from "node:url"; const root = fileURLToPath(new URL("../../../", import.meta.url)); -const files = execFileSync("git", ["ls-files", "-z", "--", "*.md"], { - cwd: root, - encoding: "utf8", - maxBuffer: Infinity, -}) +const files = execFileSync( + "git", + [ + "ls-files", + "-z", + "--", + ":(glob)sdk/typescript/**/*.md", + ":(glob)examples/custom-validation/*.md", + ":(glob)plugins/codex-security/native/*.md", + ], + { + cwd: root, + encoding: "utf8", + maxBuffer: Infinity, + }, +) .split("\0") .filter( (path) => diff --git a/sdk/typescript/tests-ts/check-markdown.test.ts b/sdk/typescript/tests-ts/check-markdown.test.ts index 71d93f9212..16311efa50 100644 --- a/sdk/typescript/tests-ts/check-markdown.test.ts +++ b/sdk/typescript/tests-ts/check-markdown.test.ts @@ -16,7 +16,7 @@ test("checks tracked Markdown including leading-dash paths and skips deleted, un const root = await mkdtemp(join(tmpdir(), "markdown checks ")); const sdk = join(root, "sdk", "typescript"); const script = join(sdk, "scripts", "check-markdown.mjs"); - const guide = join(root, "guide with # spaces.md"); + const guide = join(sdk, "guide with # spaces.md"); const invalid = "# Title\n\n\n"; try { await mkdir(join(sdk, "scripts"), { recursive: true }); @@ -30,23 +30,36 @@ test("checks tracked Markdown including leading-dash paths and skips deleted, un "junction", ); await writeFile(join(root, ".gitignore"), "node_modules/\n"); - await writeFile(join(root, "README.md"), "# Readme\n"); - await writeFile(join(root, "--plugin=fixture.md"), "# Markdown fixture\n"); + await writeFile(join(sdk, "README.md"), "# Readme\n"); + // The previous format command checks these folders, not all repository Markdown. + await mkdir(join(root, "github-action")); + await writeFile(join(root, "github-action", "README.md"), invalid); + const outsideGuides = [ + join(root, "examples", "custom-validation", "guide.md"), + join(root, "plugins", "codex-security", "native", "guide.md"), + ]; + for (const path of outsideGuides) { + await mkdir(join(path, ".."), { recursive: true }); + await writeFile(path, invalid); + await mkdir(join(path, "..", "nested")); + await writeFile(join(path, "..", "nested", "outside.md"), invalid); + } + await writeFile(join(sdk, "--plugin=fixture.md"), "# Markdown fixture\n"); // The combined paths exceed the Windows process command-line limit. for (let index = 0; index < 400; index++) { await writeFile( - join(root, `guide-${index}-${"x".repeat(80)}.md`), + join(sdk, `guide-${index}-${"x".repeat(80)}.md`), "# Guide\n", ); } await writeFile(guide, invalid); - await writeFile(join(root, "deleted.md"), invalid); + await writeFile(join(sdk, "deleted.md"), invalid); if (process.platform !== "win32") - await symlink("untracked.md", join(root, "linked.md")); + await symlink("untracked.md", join(sdk, "linked.md")); gitText(["init", "--quiet"], { cwd: root }); gitText(["add", "--", "."], { cwd: root }); - await rm(join(root, "deleted.md")); - await writeFile(join(root, "untracked.md"), invalid); + await rm(join(sdk, "deleted.md")); + await writeFile(join(sdk, "untracked.md"), invalid); const failing = await runCommand("node", [script], { cwd: sdk, @@ -54,8 +67,13 @@ test("checks tracked Markdown including leading-dash paths and skips deleted, un }); expect(failing.status).toBe(1); expect(failing.stdout + failing.stderr).toContain("guide with # spaces.md"); + for (const path of outsideGuides) + expect(failing.stdout + failing.stderr).toContain( + path.replaceAll("\\", "/").slice(root.length + 1), + ); await writeFile(guide, "# Title\n"); + for (const path of outsideGuides) await writeFile(path, "# Guide\n"); const passing = await runCommand("node", [script], { cwd: sdk, timeout: 30_000, From 48c8f719365357a159d9288f4429d736b00905f3 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 05:50:15 +0000 Subject: [PATCH 20/28] Use existing environment lookup for alternate paths --- sdk/typescript/src/cli.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index b953da57bf..3ff1e903b2 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -6925,7 +6925,10 @@ async function patchCommandContext( environment[name] = await gitPath(["--path-format=absolute", ...args]); } if ( - dependencies.environment["GIT_ALTERNATE_OBJECT_DIRECTORIES"] !== undefined + environmentValue( + dependencies.environment, + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + ) !== undefined ) { const objects = await dependencies.runRepositoryCommand( "git", From 3af9af79e3ef7db6bca3056db429c20628d92a82 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 07:14:51 +0000 Subject: [PATCH 21/28] fix(ci): retain current main Markdown QA scope --- sdk/typescript/scripts/check-markdown.mjs | 21 +++--------- .../tests-ts/check-markdown.test.ts | 34 +++++-------------- 2 files changed, 13 insertions(+), 42 deletions(-) diff --git a/sdk/typescript/scripts/check-markdown.mjs b/sdk/typescript/scripts/check-markdown.mjs index b06f20fda8..1ef2ff028b 100644 --- a/sdk/typescript/scripts/check-markdown.mjs +++ b/sdk/typescript/scripts/check-markdown.mjs @@ -4,22 +4,11 @@ import { join } from "node:path"; import { fileURLToPath } from "node:url"; const root = fileURLToPath(new URL("../../../", import.meta.url)); -const files = execFileSync( - "git", - [ - "ls-files", - "-z", - "--", - ":(glob)sdk/typescript/**/*.md", - ":(glob)examples/custom-validation/*.md", - ":(glob)plugins/codex-security/native/*.md", - ], - { - cwd: root, - encoding: "utf8", - maxBuffer: Infinity, - }, -) +const files = execFileSync("git", ["ls-files", "-z", "--", "*.md"], { + cwd: root, + encoding: "utf8", + maxBuffer: Infinity, +}) .split("\0") .filter( (path) => diff --git a/sdk/typescript/tests-ts/check-markdown.test.ts b/sdk/typescript/tests-ts/check-markdown.test.ts index 16311efa50..71d93f9212 100644 --- a/sdk/typescript/tests-ts/check-markdown.test.ts +++ b/sdk/typescript/tests-ts/check-markdown.test.ts @@ -16,7 +16,7 @@ test("checks tracked Markdown including leading-dash paths and skips deleted, un const root = await mkdtemp(join(tmpdir(), "markdown checks ")); const sdk = join(root, "sdk", "typescript"); const script = join(sdk, "scripts", "check-markdown.mjs"); - const guide = join(sdk, "guide with # spaces.md"); + const guide = join(root, "guide with # spaces.md"); const invalid = "# Title\n\n\n"; try { await mkdir(join(sdk, "scripts"), { recursive: true }); @@ -30,36 +30,23 @@ test("checks tracked Markdown including leading-dash paths and skips deleted, un "junction", ); await writeFile(join(root, ".gitignore"), "node_modules/\n"); - await writeFile(join(sdk, "README.md"), "# Readme\n"); - // The previous format command checks these folders, not all repository Markdown. - await mkdir(join(root, "github-action")); - await writeFile(join(root, "github-action", "README.md"), invalid); - const outsideGuides = [ - join(root, "examples", "custom-validation", "guide.md"), - join(root, "plugins", "codex-security", "native", "guide.md"), - ]; - for (const path of outsideGuides) { - await mkdir(join(path, ".."), { recursive: true }); - await writeFile(path, invalid); - await mkdir(join(path, "..", "nested")); - await writeFile(join(path, "..", "nested", "outside.md"), invalid); - } - await writeFile(join(sdk, "--plugin=fixture.md"), "# Markdown fixture\n"); + await writeFile(join(root, "README.md"), "# Readme\n"); + await writeFile(join(root, "--plugin=fixture.md"), "# Markdown fixture\n"); // The combined paths exceed the Windows process command-line limit. for (let index = 0; index < 400; index++) { await writeFile( - join(sdk, `guide-${index}-${"x".repeat(80)}.md`), + join(root, `guide-${index}-${"x".repeat(80)}.md`), "# Guide\n", ); } await writeFile(guide, invalid); - await writeFile(join(sdk, "deleted.md"), invalid); + await writeFile(join(root, "deleted.md"), invalid); if (process.platform !== "win32") - await symlink("untracked.md", join(sdk, "linked.md")); + await symlink("untracked.md", join(root, "linked.md")); gitText(["init", "--quiet"], { cwd: root }); gitText(["add", "--", "."], { cwd: root }); - await rm(join(sdk, "deleted.md")); - await writeFile(join(sdk, "untracked.md"), invalid); + await rm(join(root, "deleted.md")); + await writeFile(join(root, "untracked.md"), invalid); const failing = await runCommand("node", [script], { cwd: sdk, @@ -67,13 +54,8 @@ test("checks tracked Markdown including leading-dash paths and skips deleted, un }); expect(failing.status).toBe(1); expect(failing.stdout + failing.stderr).toContain("guide with # spaces.md"); - for (const path of outsideGuides) - expect(failing.stdout + failing.stderr).toContain( - path.replaceAll("\\", "/").slice(root.length + 1), - ); await writeFile(guide, "# Title\n"); - for (const path of outsideGuides) await writeFile(path, "# Guide\n"); const passing = await runCommand("node", [script], { cwd: sdk, timeout: 30_000, From 8f71dfab5e1a18b2441c546ac4a5bd32cf653570 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 07:17:09 +0000 Subject: [PATCH 22/28] fix(patch): account for newly initialized submodule edits --- sdk/typescript/src/cli.ts | 11 ++- sdk/typescript/tests-ts/cli-patch.test.ts | 95 +++++++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 475d4bd352..62b01a4044 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7273,7 +7273,11 @@ async function changedPatchFiles( const rootFiles: string[] = []; for (const [directory, tree] of base.trees) { const head = heads.get(directory); - if (head === undefined) continue; + if ( + head === undefined || + (directory && !existsSync(join(repository, directory, ".git"))) + ) + continue; const gitDependencies = directory ? await nestedPatchGitDependencies(repository, dependencies) : dependencies; @@ -7359,8 +7363,9 @@ async function snapshotGitPatchState( for (const entry of entries.split("\0")) { if (!entry.startsWith("160000 ")) continue; const path = entry.slice(entry.indexOf("\t") + 1); - if (existsSync(join(checkout, path, ".git"))) - await visit(directory ? `${directory}/${path}` : path); + const child = directory ? `${directory}/${path}` : path; + if (existsSync(join(checkout, path, ".git"))) await visit(child); + else trees.set(child, entry.slice(0, entry.indexOf("\t")).split(" ")[2]!); } }; await visit(""); diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 13aa7f1d01..5763c225a8 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -3924,6 +3924,101 @@ describe("patch publication integrity", () => { files: [path], }); }); + test.each([ + "initialized during patching", + "initialized without edits", + "left uninitialized", + "already initialized with local edits", + ])("tracks patch changes when a registered child is %s", async (state) => { + const root = await fixtures.create("patch-submodule-initialization-"); + const repository = join(root, "repository"); + const source = join(root, "child-source"); + const child = join(repository, "dependency"); + await mkdir(repository); + await mkdir(source); + const git = repositoryGit(repository); + const sourceGit = repositoryGit(source); + for (const run of [git, sourceGit]) { + run("init", "--initial-branch=main"); + run("config", "user.name", "Synthetic User"); + run("config", "user.email", "synthetic@example.test"); + } + await writeFile(join(source, "app.ts"), "unsafe\noriginal\n"); + sourceGit("add", "."); + sourceGit("commit", "-m", "Synthetic child baseline"); + git( + "-c", + "protocol.file.allow=always", + "submodule", + "add", + source, + "dependency", + ); + git("add", "."); + git("commit", "-m", "Synthetic parent baseline"); + const retained = state === "already initialized with local edits"; + const changed = retained || state === "initialized during patching"; + if (retained) + await writeFile( + join(child, "app.ts"), + (await readFile(join(child, "app.ts"), "utf8")) + "local edit\n", + ); + else git("submodule", "deinit", "-f", "--", "dependency"); + const originalIndex = git("write-tree"); + const originalGitlink = git("ls-tree", "HEAD", "dependency"); + let modelCalls = 0; + let expectedContents: string | undefined; + const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { + currentDirectory: repository, + onRepositoryCommand: runGitRepositoryCommand, + onCodex: async (_args, output) => { + modelCalls++; + expect( + git("submodule", "status", "--", "dependency").startsWith("-"), + ).toBe(!retained); + if (!retained && state !== "left uninitialized") + git( + "-c", + "protocol.file.allow=always", + "submodule", + "update", + "--init", + "--", + "dependency", + ); + if (state !== "left uninitialized") { + expectedContents = await readFile(join(child, "app.ts"), "utf8"); + if (changed) { + expectedContents = expectedContents.replace("unsafe", "fixed"); + await writeFile(join(child, "app.ts"), expectedContents); + } + } + output?.stdout.write("Checked the synthetic patch."); + return 0; + }, + }); + expect(modelCalls).toBe(1); + expect(outcome.exitCode, outcome.stderr).toBe(changed ? 0 : 2); + expect(JSON.parse(outcome.stdout)).toMatchObject({ + applied: changed, + files: changed ? ["dependency/app.ts"] : [], + ...(changed ? {} : { error: { code: "NO_PATCH_APPLIED" } }), + }); + expect(git("write-tree")).toBe(originalIndex); + expect(git("ls-tree", "HEAD", "dependency")).toBe(originalGitlink); + if (expectedContents === undefined) + expect(git("submodule", "status", "--", "dependency")).toStartWith("-"); + else { + expect(await readFile(join(child, "app.ts"), "utf8")).toBe( + expectedContents, + ); + expect(repositoryGit(child)("diff", "--name-only")).toBe( + changed ? "app.ts" : "", + ); + if (retained) expect(expectedContents).toContain("local edit\n"); + } + }); + test("keeps the outer executable boundary for nested patch snapshots", async () => { const repository = await fixtures.create("patch-nested-executable-"); const nested = join(repository, "dependency"); From fb08e12d5246bc8b9a61582d996ba68d35de8009 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 08:18:36 +0000 Subject: [PATCH 23/28] fix(patch): compare newly initialized children with their HEAD --- sdk/typescript/src/cli.ts | 2 +- sdk/typescript/tests-ts/cli-patch.test.ts | 42 +++++++++++++++++++---- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 5d691d62fb..90be63c487 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7500,7 +7500,7 @@ async function snapshotGitPatchState( const path = entry.slice(entry.indexOf("\t") + 1); const child = directory ? `${directory}/${path}` : path; if (existsSync(join(checkout, path, ".git"))) await visit(child); - else trees.set(child, entry.slice(0, entry.indexOf("\t")).split(" ")[2]!); + else trees.set(child, "HEAD"); } }; await visit(""); diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 033cb14d7e..cf188b894d 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -4555,6 +4555,8 @@ describe("patch publication integrity", () => { "initialized without edits", "left uninitialized", "already initialized with local edits", + "shallow initialized without edits", + "shallow initialized with edits", ])("tracks patch changes when a registered child is %s", async (state) => { const root = await fixtures.create("patch-submodule-initialization-"); const repository = join(root, "repository"); @@ -4572,24 +4574,37 @@ describe("patch publication integrity", () => { await writeFile(join(source, "app.ts"), "unsafe\noriginal\n"); sourceGit("add", "."); sourceGit("commit", "-m", "Synthetic child baseline"); + const originalChildHead = sourceGit("rev-parse", "HEAD"); git( "-c", "protocol.file.allow=always", "submodule", "add", - source, + pathToFileURL(source).href, "dependency", ); git("add", "."); git("commit", "-m", "Synthetic parent baseline"); const retained = state === "already initialized with local edits"; - const changed = retained || state === "initialized during patching"; + const shallow = state.startsWith("shallow initialized"); + const changed = + retained || + state === "initialized during patching" || + state === "shallow initialized with edits"; if (retained) await writeFile( join(child, "app.ts"), (await readFile(join(child, "app.ts"), "utf8")) + "local edit\n", ); else git("submodule", "deinit", "-f", "--", "dependency"); + if (shallow) { + await rm(join(repository, ".git", "modules", "dependency"), { + recursive: true, + }); + await writeFile(join(source, "upstream.ts"), "new upstream file\n"); + sourceGit("add", "."); + sourceGit("commit", "-m", "Synthetic upstream change"); + } const originalIndex = git("write-tree"); const originalGitlink = git("ls-tree", "HEAD", "dependency"); let modelCalls = 0; @@ -4609,9 +4624,20 @@ describe("patch publication integrity", () => { "submodule", "update", "--init", + ...(shallow ? ["--remote", "--depth=1"] : []), "--", "dependency", ); + if (shallow) { + const childGit = repositoryGit(child); + expect(childGit("rev-parse", "--is-shallow-repository")).toBe("true"); + expect(childGit("rev-parse", "HEAD")).toBe( + sourceGit("rev-parse", "HEAD"), + ); + expect(() => + childGit("cat-file", "-e", `${originalChildHead}^{commit}`), + ).toThrow(); + } if (state !== "left uninitialized") { expectedContents = await readFile(join(child, "app.ts"), "utf8"); if (changed) { @@ -4624,11 +4650,15 @@ describe("patch publication integrity", () => { }, }); expect(modelCalls).toBe(1); - expect(outcome.exitCode, outcome.stderr).toBe(changed ? 0 : 2); + const applied = changed || shallow; + expect(outcome.exitCode, outcome.stderr).toBe(applied ? 0 : 2); expect(JSON.parse(outcome.stdout)).toMatchObject({ - applied: changed, - files: changed ? ["dependency/app.ts"] : [], - ...(changed ? {} : { error: { code: "NO_PATCH_APPLIED" } }), + applied, + files: [ + ...(shallow ? ["dependency"] : []), + ...(changed ? ["dependency/app.ts"] : []), + ], + ...(applied ? {} : { error: { code: "NO_PATCH_APPLIED" } }), }); expect(git("write-tree")).toBe(originalIndex); expect(git("ls-tree", "HEAD", "dependency")).toBe(originalGitlink); From 7dd65bedb3d0564e1af61df5fdc92bbe8e181e46 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 08:30:51 +0000 Subject: [PATCH 24/28] fix(patch): separate snapshot revisions from file paths --- sdk/typescript/src/cli.ts | 1 + sdk/typescript/tests-ts/cli-patch.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 61e49c8b50..c22461788c 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7444,6 +7444,7 @@ async function changedPatchFiles( "-z", tree, head, + "--", ], join(repository, directory), { trim: false }, diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 9d9c9aaa17..41d28b4ff5 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -4615,6 +4615,7 @@ describe("patch publication integrity", () => { run("config", "user.email", "synthetic@example.test"); } await writeFile(join(source, "app.ts"), "unsafe\noriginal\n"); + await writeFile(join(source, "HEAD"), "ordinary tracked file\n"); sourceGit("add", "."); sourceGit("commit", "-m", "Synthetic child baseline"); const originalChildHead = sourceGit("rev-parse", "HEAD"); From d7333c0fbd217fb36bdaaf2aa0b5ca3a0203be34 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 08:52:10 +0000 Subject: [PATCH 25/28] fix(patch): retain outer executable boundary for nested validation --- sdk/typescript/src/cli.ts | 1 + sdk/typescript/src/targets.ts | 37 +++++++--- sdk/typescript/tests-ts/targets.test.ts | 90 ++++++++++++++++--------- 3 files changed, 89 insertions(+), 39 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index c22461788c..4b36f383c6 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7501,6 +7501,7 @@ async function snapshotGitPatchState( } await enclosingGitWorktreeRoot(checkout, undefined, { requireIfPresent: true, + protectedRoot: repository, }); } const gitDependencies = directory diff --git a/sdk/typescript/src/targets.ts b/sdk/typescript/src/targets.ts index b408338804..5ab0dfb9f0 100644 --- a/sdk/typescript/src/targets.ts +++ b/sdk/typescript/src/targets.ts @@ -154,9 +154,10 @@ function requirePortableWindowsRepositoryPath(path: string): void { export async function enclosingGitWorktreeRoot( repository: string, signal?: AbortSignal, - options: { requireIfPresent?: boolean } = {}, + options: { requireIfPresent?: boolean; protectedRoot?: string } = {}, ): Promise { const strict = options.requireIfPresent === true; + const protectedRoot = options.protectedRoot ?? repository; const markerRoot = strict ? await gitMarkerRoot(repository, signal, "nearest") : null; @@ -165,9 +166,11 @@ export async function enclosingGitWorktreeRoot( if (strict) { if ( (await gitOutput( - repository, + protectedRoot, ["rev-parse", "--is-inside-git-dir"], signal, + {}, + repository, )) === "true" ) { throw new InvalidTargetError( @@ -177,9 +180,11 @@ export async function enclosingGitWorktreeRoot( if (markerRoot === null) return null; } const root = await gitOutput( - repository, + protectedRoot, ["rev-parse", "--show-toplevel"], signal, + {}, + repository, ); canonicalRoot = await abortable(() => realpath(root), signal); } catch (error) { @@ -205,7 +210,7 @@ export async function enclosingGitWorktreeRoot( ); } if (markerRoot !== null) - await requireGitWorktreeBinding(canonicalRoot, signal); + await requireGitWorktreeBinding(canonicalRoot, signal, protectedRoot); return canonicalRoot; } @@ -299,11 +304,23 @@ export async function isGitMetadataDirectory( export async function gitMetadataDirectories( repository: string, signal?: AbortSignal, - options: { includeLocalObjects?: boolean } = {}, + options: { includeLocalObjects?: boolean; protectedRoot?: string } = {}, ): Promise<[string, string, ...string[]]> { const [directory, commonDirectory] = await Promise.all([ - gitOutput(repository, ["rev-parse", "--absolute-git-dir"], signal), - gitOutput(repository, ["rev-parse", "--git-common-dir"], signal), + gitOutput( + options.protectedRoot ?? repository, + ["rev-parse", "--absolute-git-dir"], + signal, + {}, + repository, + ), + gitOutput( + options.protectedRoot ?? repository, + ["rev-parse", "--git-common-dir"], + signal, + {}, + repository, + ), ]); const roots = await Promise.all([ abortable(() => realpath(resolve(repository, directory)), signal), @@ -413,12 +430,14 @@ export async function gitObjectDirectories( async function requireGitWorktreeBinding( repository: string, signal?: AbortSignal, + protectedRoot = repository, ): Promise { let cause: unknown; try { const [directory, commonDirectory] = await gitMetadataDirectories( repository, signal, + { protectedRoot }, ); if ( [directory, commonDirectory].every( @@ -430,9 +449,11 @@ async function requireGitWorktreeBinding( // The toplevel check already verified the configured worktree path. if ( await gitOutput( - repository, + protectedRoot, ["config", "--get", "core.worktree"], signal, + {}, + repository, ) ) return; diff --git a/sdk/typescript/tests-ts/targets.test.ts b/sdk/typescript/tests-ts/targets.test.ts index a475f67bfa..ad27aef413 100644 --- a/sdk/typescript/tests-ts/targets.test.ts +++ b/sdk/typescript/tests-ts/targets.test.ts @@ -6,6 +6,7 @@ import { mkdir, readFile, realpath, + rename, rm, symlink, utimes, @@ -528,40 +529,67 @@ describe("scan target normalization", () => { expect(existsSync(marker)).toBe(false); }); - test("does not execute worktree-local Git shims when scanning a subdirectory", async () => { - const repo = await repository(); - const target = join(repo, "src"); - const unsafeBin = join(repo, "node_modules", ".bin"); - const marker = join(repo, "git-executed"); - const revision = git(repo, "rev-parse", "HEAD"); - await createRepositoryGitShim(unsafeBin, marker); + test.each(["ordinary", "nested"])( + "does not execute worktree-local Git shims when scanning a subdirectory (%s)", + async (layout) => { + const nested = layout === "nested"; + const repo = await repository(); + const target = join(repo, "src"); + const unsafeBin = join(repo, "node_modules", ".bin"); + const marker = join(repo, "git-executed"); + const revision = git(repo, "rev-parse", "HEAD"); + const outerMetadata = join(repo, "..", "outer-git"); + if (nested) { + git( + target, + "init", + "--separate-git-dir", + join(repo, "..", "child-git"), + "-b", + "main", + ); + git(target, "config", "user.email", "test@example.com"); + git(target, "config", "user.name", "Test"); + git(target, "config", "core.worktree", target); + git(target, "add", "."); + git(target, "commit", "-m", "initial child"); + await rename(join(repo, ".git"), outerMetadata); + } + await createRepositoryGitShim(unsafeBin, marker); - const script = ` + const script = ` const { enclosingGitWorktreeRoot, repositoryRevision } = await import(process.argv[1]); - console.log(await enclosingGitWorktreeRoot(process.argv[2])); - console.log(await repositoryRevision(process.argv[2])); + const nested = process.argv[4] === "true"; + console.log(await enclosingGitWorktreeRoot(process.argv[2], undefined, + nested ? { requireIfPresent: true, protectedRoot: process.argv[3] } : undefined)); + if (!nested) console.log(await repositoryRevision(process.argv[2])); `; - const result = spawnSync( - process.execPath, - [ - "-e", - script, - fileURLToPath(new URL("../src/targets.ts", import.meta.url)), - target, - ], - { - cwd: target, - encoding: "utf8", - env: environmentWithPath([unsafeBin]), - }, - ); - expect(result.status).toBe(0); - expect(result.stdout.trim().split(/\r?\n/u)).toEqual([ - await realpath(repo), - revision, - ]); - expect(existsSync(marker)).toBe(false); - }); + const result = spawnSync( + process.execPath, + [ + "-e", + script, + fileURLToPath(new URL("../src/targets.ts", import.meta.url)), + target, + repo, + String(nested), + ], + { + cwd: target, + encoding: "utf8", + env: { + ...environmentWithPath([unsafeBin]), + ...(nested ? { GIT_DIR: outerMetadata, GIT_WORK_TREE: repo } : {}), + }, + }, + ); + expect(result.status, result.stderr || result.error?.message).toBe(0); + expect(result.stdout.trim().split(/\r?\n/u)).toEqual( + nested ? [await realpath(target)] : [await realpath(repo), revision], + ); + expect(existsSync(marker)).toBe(false); + }, + ); test("keeps the requested base and head when refs diverge", async () => { const repo = await repository(); From 41aac82972b39dd1782dbc59a52eebe45715bfb3 Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 09:09:26 +0000 Subject: [PATCH 26/28] fix(patch): compare replacement children against their current baseline --- sdk/typescript/src/cli.ts | 22 ++- sdk/typescript/tests-ts/cli-patch.test.ts | 171 ++++++++++++++++------ 2 files changed, 145 insertions(+), 48 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 4b36f383c6..50981449ed 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7400,7 +7400,7 @@ async function changedPatchFiles( ); const files = new Set(); const rootFiles: string[] = []; - for (const [directory, tree] of base.trees) { + for (const [directory, originalTree] of base.trees) { const head = heads.get(directory); if ( head === undefined || @@ -7410,6 +7410,26 @@ async function changedPatchFiles( const gitDependencies = directory ? await nestedPatchGitDependencies(repository, dependencies) : dependencies; + let tree = originalTree; + if (directory && tree !== "HEAD") { + try { + await gitDependencies.runRepositoryCommand( + "git", + ["rev-parse", "--verify", "--quiet", `${tree}^{tree}`], + join(repository, directory), + ); + } catch (error) { + if ( + !(error instanceof Error) || + !("code" in error) || + error.code !== 1 || + !("stderr" in error) || + error.stderr !== "" + ) + throw error; + tree = "HEAD"; + } + } if (dirtyFiles?.size) { const renamed = ( await gitDependencies.runRepositoryCommand( diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 41d28b4ff5..ef81b439fe 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -4600,6 +4600,9 @@ describe("patch publication integrity", () => { "already initialized with local edits", "shallow initialized without edits", "shallow initialized with edits", + "replaced without edits", + "replaced with edits", + "moved with unchanged local edits", ])("tracks patch changes when a registered child is %s", async (state) => { const root = await fixtures.create("patch-submodule-initialization-"); const repository = join(root, "repository"); @@ -4629,18 +4632,22 @@ describe("patch publication integrity", () => { ); git("add", "."); git("commit", "-m", "Synthetic parent baseline"); - const retained = state === "already initialized with local edits"; + const moved = state === "moved with unchanged local edits"; + const replaced = state.startsWith("replaced"); + const retained = state === "already initialized with local edits" || moved; + const initialized = retained || replaced; const shallow = state.startsWith("shallow initialized"); const changed = - retained || + (retained && !moved) || state === "initialized during patching" || - state === "shallow initialized with edits"; + state === "shallow initialized with edits" || + state === "replaced with edits"; if (retained) await writeFile( join(child, "app.ts"), (await readFile(join(child, "app.ts"), "utf8")) + "local edit\n", ); - else git("submodule", "deinit", "-f", "--", "dependency"); + else if (!initialized) git("submodule", "deinit", "-f", "--", "dependency"); if (shallow) { await rm(join(repository, ".git", "modules", "dependency"), { recursive: true, @@ -4649,62 +4656,132 @@ describe("patch publication integrity", () => { sourceGit("add", "."); sourceGit("commit", "-m", "Synthetic upstream change"); } - const originalIndex = git("write-tree"); + let expectedIndex = git("write-tree"); const originalGitlink = git("ls-tree", "HEAD", "dependency"); let modelCalls = 0; + let assessments = 0; let expectedContents: string | undefined; - const outcome = await runWorkflow(["patch", "Synthetic issue", "--json"], { - currentDirectory: repository, - onRepositoryCommand: runGitRepositoryCommand, - onCodex: async (_args, output) => { - modelCalls++; - expect( - git("submodule", "status", "--", "dependency").startsWith("-"), - ).toBe(!retained); - if (!retained && state !== "left uninitialized") - git( - "-c", - "protocol.file.allow=always", - "submodule", - "update", - "--init", - ...(shallow ? ["--remote", "--depth=1"] : []), - "--", - "dependency", - ); - if (shallow) { - const childGit = repositoryGit(child); - expect(childGit("rev-parse", "--is-shallow-repository")).toBe("true"); - expect(childGit("rev-parse", "HEAD")).toBe( - sourceGit("rev-parse", "HEAD"), - ); - expect(() => - childGit("cat-file", "-e", `${originalChildHead}^{commit}`), - ).toThrow(); - } - if (state !== "left uninitialized") { - expectedContents = await readFile(join(child, "app.ts"), "utf8"); - if (changed) { - expectedContents = expectedContents.replace("unsafe", "fixed"); - await writeFile(join(child, "app.ts"), expectedContents); + const outcome = await runWorkflow( + ["patch", "Synthetic issue", "--assess-patch-risk", "--json"], + { + currentDirectory: repository, + onRepositoryCommand: async (command, args, cwd, options) => { + expect(command).toBe("git"); + const { stdout } = await promisify(execFile)(command, [...args], { + cwd: options?.directory ?? cwd, + env: { ...process.env, ...options?.environment }, + maxBuffer: options?.maxBuffer, + }); + return options?.trim === false ? stdout : stdout.trim(); + }, + onCodex: async (_args, output) => { + modelCalls++; + expect( + git("submodule", "status", "--", "dependency").startsWith("-"), + ).toBe(!initialized); + if (!initialized && state !== "left uninitialized") + git( + "-c", + "protocol.file.allow=always", + "submodule", + "update", + "--init", + ...(shallow ? ["--remote", "--depth=1"] : []), + "--", + "dependency", + ); + if (replaced) { + const replacement = join(root, "replacement-source"); + await mkdir(replacement); + const replacementGit = repositoryGit(replacement); + replacementGit("init", "--initial-branch=main"); + replacementGit("config", "user.name", "Synthetic User"); + replacementGit("config", "user.email", "synthetic@example.test"); + await writeFile( + join(replacement, "app.ts"), + "unsafe\nreplacement\n", + ); + replacementGit("add", "."); + replacementGit("commit", "-m", "Synthetic replacement child"); + git("rm", "-f", "--", "dependency"); + git( + "-c", + "protocol.file.allow=always", + "submodule", + "add", + "--name", + "replacement", + pathToFileURL(replacement).href, + "dependency", + ); + expect(() => + repositoryGit(child)( + "cat-file", + "-e", + `${originalChildHead}^{commit}`, + ), + ).toThrow(); } - } - output?.stdout.write("Checked the synthetic patch."); - return 0; + if (moved) { + const metadata = join( + repository, + ".git", + "modules", + "moved-dependency", + ); + await rename( + join(repository, ".git", "modules", "dependency"), + metadata, + ); + await writeFile(join(child, ".git"), `gitdir: ${metadata}\n`); + } + if (shallow) { + const childGit = repositoryGit(child); + expect(childGit("rev-parse", "--is-shallow-repository")).toBe( + "true", + ); + expect(childGit("rev-parse", "HEAD")).toBe( + sourceGit("rev-parse", "HEAD"), + ); + expect(() => + childGit("cat-file", "-e", `${originalChildHead}^{commit}`), + ).toThrow(); + } + if (state !== "left uninitialized") { + expectedContents = await readFile(join(child, "app.ts"), "utf8"); + if (changed) { + expectedContents = expectedContents.replace("unsafe", "fixed"); + await writeFile(join(child, "app.ts"), expectedContents); + } + } + expectedIndex = git("write-tree"); + output?.stdout.write("Checked the synthetic patch."); + return 0; + }, }, - }); + { + configure: (current) => { + current.assessPatchRisk = async () => { + assessments++; + return patchRiskAssessment(); + }; + }, + }, + ); expect(modelCalls).toBe(1); - const applied = changed || shallow; + const applied = changed || shallow || replaced; + expect(assessments).toBe(applied ? 1 : 0); expect(outcome.exitCode, outcome.stderr).toBe(applied ? 0 : 2); expect(JSON.parse(outcome.stdout)).toMatchObject({ applied, files: [ - ...(shallow ? ["dependency"] : []), + ...(replaced ? [".gitmodules"] : []), + ...(shallow || replaced ? ["dependency"] : []), ...(changed ? ["dependency/app.ts"] : []), ], ...(applied ? {} : { error: { code: "NO_PATCH_APPLIED" } }), }); - expect(git("write-tree")).toBe(originalIndex); + expect(git("write-tree")).toBe(expectedIndex); expect(git("ls-tree", "HEAD", "dependency")).toBe(originalGitlink); if (expectedContents === undefined) expect(git("submodule", "status", "--", "dependency")).toStartWith("-"); @@ -4713,7 +4790,7 @@ describe("patch publication integrity", () => { expectedContents, ); expect(repositoryGit(child)("diff", "--name-only")).toBe( - changed ? "app.ts" : "", + changed || retained ? "app.ts" : "", ); if (retained) expect(expectedContents).toContain("local edit\n"); } From 0aeec2bf99a628232da51dc9b737262c864d527a Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 20:49:37 +0000 Subject: [PATCH 27/28] fix(patch): preserve alternate paths when invocation directory changes --- sdk/typescript/src/cli.ts | 22 +++ sdk/typescript/tests-ts/cli-patch.test.ts | 187 ++++++++++++++++++++++ 2 files changed, 209 insertions(+) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index 4958c8d219..cdc8a21689 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7167,6 +7167,28 @@ async function patchCommandContext( if (dependencies.environment[name] !== undefined) environment[name] = await gitPath(["--path-format=absolute", ...args]); } + if ( + environmentValue( + dependencies.environment, + "GIT_ALTERNATE_OBJECT_DIRECTORIES", + ) !== undefined + ) { + const objects = await dependencies.runRepositoryCommand( + "git", + ["count-objects", "-v"], + repository, + { trim: false, directory }, + ); + // Git resolves and C-quotes alternate paths in the original command context. + environment["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = objects + .split("\n") + .filter((line) => line.startsWith("alternate: ")) + .map((line) => { + const path = line.slice("alternate: ".length); + return path.startsWith('"') ? path : JSON.stringify(path); + }) + .join(delimiter); + } const physicalDirectory = await realpath(directory); for (const name of ["GH_CONFIG_DIR", "GLAB_CONFIG_DIR"]) { const value = dependencies.environment[name]; diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index 7feb267c72..f5518c9f12 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -8784,3 +8784,190 @@ describe("patch worktree root identity", () => { }, ); }); + +test.each(["absolute", "relative"])( + "preserves %s nested alternates from an outside invocation", + async (kind) => { + const root = await temporaryDirectory("patch-outside-alternate-"); + const repository = join(root, "repository"); + const invocation = join(root, "invocation"); + const nested = join(repository, "nested"); + const pool = join(invocation, "pool", "objects"); + try { + await mkdir(repository); + await mkdir(invocation); + const git = repositoryGit(repository); + git("init", "--initial-branch=main"); + git("config", "user.name", "Synthetic User"); + git("config", "user.email", "synthetic@example.test"); + await writeFile(join(repository, "app.ts"), "original\n"); + git("add", "."); + git("commit", "-m", "Synthetic parent baseline"); + await mkdir(nested); + const inner = repositoryGit(nested); + inner("init", "--initial-branch=main"); + inner("config", "user.name", "Synthetic User"); + inner("config", "user.email", "synthetic@example.test"); + await writeFile(join(nested, "app.ts"), "original\n"); + inner("add", "."); + inner("commit", "-m", "Synthetic nested baseline"); + await mkdir(dirname(pool)); + await rename(join(nested, ".git", "objects"), pool); + await mkdir(join(nested, ".git", "objects")); + expect(() => inner("rev-parse", "HEAD^{tree}")).toThrow(); + const gitEnvironment = { + GIT_DIR: join(repository, ".git"), + GIT_WORK_TREE: repository, + GIT_ALTERNATE_OBJECT_DIRECTORIES: + kind === "relative" ? relative(invocation, pool) : pool, + }; + expect( + gitText( + [ + "--git-dir", + join(nested, ".git"), + "--work-tree", + nested, + "rev-parse", + "HEAD^{tree}", + ], + { cwd: invocation, env: { ...process.env, ...gitEnvironment } }, + ).trim(), + ).toMatch(/^[0-9a-f]+$/); + let calls = 0; + const outcome = await runWorkflow( + ["patch", "Synthetic issue", "--assess-patch-risk", "--json"], + { + currentDirectory: invocation, + environment: { ...process.env, ...gitEnvironment }, + onRepositoryCommand: (command, args, cwd, options) => + runGitRepositoryCommand(command, args, cwd, { + ...options, + environment: { ...gitEnvironment, ...options?.environment }, + }), + onCodex: async (_args, output) => { + calls++; + if ( + output?.appServer?.prompt.includes( + "$codex-security:assess-patch-risk", + ) + ) { + output.stdout.write(patchRiskAssessment().report); + } else { + await writeFile(join(repository, "app.ts"), "fixed\n"); + await writeFile(join(nested, "app.ts"), "fixed\n"); + output?.stdout.write("Fixed and checked."); + } + return 0; + }, + }, + ); + expect(outcome.exitCode, outcome.stderr).toBe(0); + expect(calls).toBe(2); + expect(JSON.parse(outcome.stdout)).toMatchObject({ + applied: true, + files: ["app.ts", "nested/app.ts"].map((file) => + relative(invocation, join(repository, file)).split(sep).join("/"), + ), + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); + +test.each(["absolute", "relative", "quoted relative"])( + "preserves %s nested alternates after removing an outside invocation", + async (kind) => { + const root = await temporaryDirectory("patch-outside-alternate-"); + const repository = join(root, "repository"); + const invocation = join(root, "temporary", "invocation"); + const nested = join(repository, "nested"); + const pool = join(root, "pool with space", "objects"); + try { + await mkdir(repository); + await mkdir(invocation, { recursive: true }); + const git = repositoryGit(repository); + git("init", "--initial-branch=main"); + git("config", "user.name", "Synthetic User"); + git("config", "user.email", "synthetic@example.test"); + await writeFile(join(repository, "app.ts"), "original\n"); + git("add", "."); + git("commit", "-m", "Synthetic parent baseline"); + await mkdir(nested); + const inner = repositoryGit(nested); + inner("init", "--initial-branch=main"); + inner("config", "user.name", "Synthetic User"); + inner("config", "user.email", "synthetic@example.test"); + await writeFile(join(nested, "app.ts"), "original\n"); + inner("add", "."); + inner("commit", "-m", "Synthetic nested baseline"); + await mkdir(dirname(pool)); + await rename(join(nested, ".git", "objects"), pool); + await mkdir(join(nested, ".git", "objects")); + expect(() => inner("rev-parse", "HEAD^{tree}")).toThrow(); + const gitEnvironment = { + GIT_DIR: join(repository, ".git"), + GIT_WORK_TREE: repository, + GIT_ALTERNATE_OBJECT_DIRECTORIES: + kind === "absolute" + ? pool + : kind === "relative" + ? relative(invocation, pool) + : JSON.stringify(relative(invocation, pool)), + }; + expect( + gitText( + [ + "--git-dir", + join(nested, ".git"), + "--work-tree", + nested, + "rev-parse", + "HEAD^{tree}", + ], + { cwd: invocation, env: { ...process.env, ...gitEnvironment } }, + ).trim(), + ).toMatch(/^[0-9a-f]+$/); + let calls = 0; + const outcome = await runWorkflow( + ["patch", "Synthetic issue", "--assess-patch-risk", "--json"], + { + currentDirectory: invocation, + environment: { ...process.env, ...gitEnvironment }, + onRepositoryCommand: (command, args, cwd, options) => + runGitRepositoryCommand(command, args, cwd, { + ...options, + environment: { ...gitEnvironment, ...options?.environment }, + }), + onCodex: async (_args, output) => { + calls++; + if ( + output?.appServer?.prompt.includes( + "$codex-security:assess-patch-risk", + ) + ) { + output.stdout.write(patchRiskAssessment().report); + } else { + await writeFile(join(repository, "app.ts"), "fixed\n"); + await writeFile(join(nested, "app.ts"), "fixed\n"); + await rm(invocation, { recursive: true }); + output?.stdout.write("Fixed and checked."); + } + return 0; + }, + }, + ); + expect(outcome.exitCode, outcome.stderr).toBe(0); + expect(calls).toBe(2); + expect(JSON.parse(outcome.stdout)).toMatchObject({ + applied: true, + files: ["app.ts", "nested/app.ts"].map((file) => + relative(invocation, join(repository, file)).split(sep).join("/"), + ), + }); + } finally { + await rm(root, { recursive: true, force: true }); + } + }, +); From c8bb82efcc3551758a65a77969f8545358ff8ffe Mon Sep 17 00:00:00 2001 From: Michael D'Angelo Date: Tue, 6 Oct 2026 21:13:31 +0000 Subject: [PATCH 28/28] Preserve configured parent object alternates for nested snapshots --- sdk/typescript/src/cli.ts | 41 +++++++++++++++-------- sdk/typescript/tests-ts/cli-patch.test.ts | 19 ++++++++--- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/sdk/typescript/src/cli.ts b/sdk/typescript/src/cli.ts index cdc8a21689..7cfe588166 100644 --- a/sdk/typescript/src/cli.ts +++ b/sdk/typescript/src/cli.ts @@ -7173,22 +7173,35 @@ async function patchCommandContext( "GIT_ALTERNATE_OBJECT_DIRECTORIES", ) !== undefined ) { - const objects = await dependencies.runRepositoryCommand( - "git", - ["count-objects", "-v"], - repository, - { trim: false, directory }, + const emptyObjects = await mkdtemp( + join(tmpdir(), "codex-security-alternates-"), ); - // Git resolves and C-quotes alternate paths in the original command context. - environment["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = objects - .split("\n") - .filter((line) => line.startsWith("alternate: ")) - .map((line) => { - const path = line.slice("alternate: ".length); - return path.startsWith('"') ? path : JSON.stringify(path); - }) - .join(delimiter); + try { + const objects = await dependencies.runRepositoryCommand( + "git", + ["count-objects", "-v"], + repository, + { + trim: false, + directory, + environment: { GIT_OBJECT_DIRECTORY: emptyObjects }, + }, + ); + // An empty primary keeps Git from omitting alternates needed by nested checkouts. + // Git resolves and C-quotes them in the original command context. + environment["GIT_ALTERNATE_OBJECT_DIRECTORIES"] = objects + .split("\n") + .filter((line) => line.startsWith("alternate: ")) + .map((line) => { + const path = line.slice("alternate: ".length); + return path.startsWith('"') ? path : JSON.stringify(path); + }) + .join(delimiter); + } finally { + await rm(emptyObjects, { recursive: true, force: true }); + } } + const physicalDirectory = await realpath(directory); for (const name of ["GH_CONFIG_DIR", "GLAB_CONFIG_DIR"]) { const value = dependencies.environment[name]; diff --git a/sdk/typescript/tests-ts/cli-patch.test.ts b/sdk/typescript/tests-ts/cli-patch.test.ts index f5518c9f12..1bf127ba72 100644 --- a/sdk/typescript/tests-ts/cli-patch.test.ts +++ b/sdk/typescript/tests-ts/cli-patch.test.ts @@ -8785,14 +8785,17 @@ describe("patch worktree root identity", () => { ); }); -test.each(["absolute", "relative"])( +test.each(["absolute", "relative", "parent primary pool"])( "preserves %s nested alternates from an outside invocation", async (kind) => { const root = await temporaryDirectory("patch-outside-alternate-"); const repository = join(root, "repository"); const invocation = join(root, "invocation"); const nested = join(repository, "nested"); - const pool = join(invocation, "pool", "objects"); + const pool = + kind === "parent primary pool" + ? join(repository, ".git", "objects") + : join(invocation, "pool", "objects"); try { await mkdir(repository); await mkdir(invocation); @@ -8811,8 +8814,16 @@ test.each(["absolute", "relative"])( await writeFile(join(nested, "app.ts"), "original\n"); inner("add", "."); inner("commit", "-m", "Synthetic nested baseline"); - await mkdir(dirname(pool)); - await rename(join(nested, ".git", "objects"), pool); + if (kind === "parent primary pool") { + await cp(join(nested, ".git", "objects"), pool, { + recursive: true, + force: false, + }); + await rm(join(nested, ".git", "objects"), { recursive: true }); + } else { + await mkdir(dirname(pool)); + await rename(join(nested, ".git", "objects"), pool); + } await mkdir(join(nested, ".git", "objects")); expect(() => inner("rev-parse", "HEAD^{tree}")).toThrow(); const gitEnvironment = {