diff --git a/packages/container/src/core/templates-entrypoint/github-remotes.ts b/packages/container/src/core/templates-entrypoint/github-remotes.ts new file mode 100644 index 00000000..8f69af47 --- /dev/null +++ b/packages/container/src/core/templates-entrypoint/github-remotes.ts @@ -0,0 +1,53 @@ +const githubRemoteHelpersTemplate = String.raw`docker_git_github_repo_from_remote_url() { + local remote_url="$1" + local repo_path="" + local owner="" + local repo="" + + case "$remote_url" in + https://github.com/*) + repo_path="${"${"}remote_url#https://github.com/}" + ;; + https://*@github.com/*) + repo_path="${"${"}remote_url#https://*@github.com/}" + ;; + ssh://git@github.com/*) + repo_path="${"${"}remote_url#ssh://git@github.com/}" + ;; + git@github.com:*) + repo_path="${"${"}remote_url#git@github.com:}" + ;; + *) + return 1 + ;; + esac + + repo_path="${"${"}repo_path%%\?*}" + repo_path="${"${"}repo_path%%#*}" + repo_path="${"${"}repo_path%/}" + repo_path="${"${"}repo_path%.git}" + owner="${"${"}repo_path%%/*}" + repo="${"${"}repo_path#*/}" + repo="${"${"}repo%%/*}" + repo="${"${"}repo%.git}" + + if [[ -z "$owner" || -z "$repo" || "$owner" == "$repo_path" ]]; then + return 1 + fi + + printf "%s/%s\n" "$owner" "$repo" +} + +docker_git_github_repo_from_remote() { + local remote="$1" + local remote_url="" + + remote_url="$(git remote get-url "$remote" 2>/dev/null || true)" + if [[ -z "$remote_url" ]]; then + return 1 + fi + + docker_git_github_repo_from_remote_url "$remote_url" +}` + +export const renderGitHubRemoteHelpers = (): string => githubRemoteHelpersTemplate diff --git a/packages/container/src/core/templates-entrypoint/plan-to-git.ts b/packages/container/src/core/templates-entrypoint/plan-to-git.ts index 950040a1..a7ffd805 100644 --- a/packages/container/src/core/templates-entrypoint/plan-to-git.ts +++ b/packages/container/src/core/templates-entrypoint/plan-to-git.ts @@ -1,9 +1,28 @@ +import { renderGitHubRemoteHelpers } from "./github-remotes.js" + const planToGitHookPathsTemplate = `PLAN_TO_GIT_SYNC_HELPER="$HOOKS_DIR/plan-to-git-sync" PLAN_TO_GIT_CODEX_HOOK="$HOOKS_DIR/plan-to-git-codex-hook" PLAN_TO_GIT_CLAUDE_HOOK="$HOOKS_DIR/plan-to-git-claude-hook" CODEX_REQUIREMENTS_FILE="/etc/codex/requirements.toml" CLAUDE_PLAN_TO_GIT_SETTINGS_FILE="$CLAUDE_CONFIG_DIR/settings.json"` +const planToGitRunnerTemplate = `${renderGitHubRemoteHelpers()} + +docker_git_plan_to_git_run() { + local base_repo="" + + if ! base_repo="$(docker_git_github_repo_from_remote upstream)"; then + base_repo="$(docker_git_github_repo_from_remote origin || true)" + fi + + if [[ -z "$base_repo" ]]; then + plan-to-git "$@" + return $? + fi + + PLAN_TO_GIT_REPO="$base_repo" plan-to-git "$@" +}` + const planToGitSyncHelperInstallTemplate = String.raw`cat <<'EOF' > "$PLAN_TO_GIT_SYNC_HELPER" #!/usr/bin/env bash set -euo pipefail @@ -19,8 +38,10 @@ fi export PLAN_TO_GIT_STATE_DIR="${"${"}PLAN_TO_GIT_STATE_DIR:-/tmp/plan-to-git}" +${planToGitRunnerTemplate} + docker_git_plan_to_git_explicit_pr_supported() { - plan-to-git sync --help 2>/dev/null | grep -q -- "--pr " + docker_git_plan_to_git_run sync --help 2>/dev/null | grep -q -- "--pr " } docker_git_plan_to_git_resolve_pr_number() { @@ -61,12 +82,12 @@ docker_git_plan_to_git_sync() { if [[ -n "$pr_number" ]] && docker_git_plan_to_git_explicit_pr_supported; then echo "[plan-to-git] Syncing queued agent plans to PR #$pr_number" - plan-to-git sync --pr "$pr_number" + docker_git_plan_to_git_run sync --pr "$pr_number" return 0 fi echo "[plan-to-git] Syncing queued agent plans via current branch discovery" - plan-to-git sync + docker_git_plan_to_git_run sync } docker_git_plan_to_git_sync @@ -83,14 +104,15 @@ if [ "\${DOCKER_GIT_SKIP_PLAN_TO_GIT:-}" != "1" ]; then echo "[plan-to-git] Error: plan-to-git not found" >&2 exit 1 fi - plan-to-git import-codex --no-sync - plan-to-git import-claude --no-sync + ${planToGitRunnerTemplate} + docker_git_plan_to_git_run import-codex --no-sync + docker_git_plan_to_git_run import-claude --no-sync PLAN_TO_GIT_SYNC_HELPER="\${DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER:-/opt/docker-git/hooks/plan-to-git-sync}" if [[ -x "$PLAN_TO_GIT_SYNC_HELPER" ]]; then "$PLAN_TO_GIT_SYNC_HELPER" else echo "[plan-to-git] Sync helper not found; falling back to current branch discovery" >&2 - plan-to-git sync + docker_git_plan_to_git_run sync fi fi` @@ -108,7 +130,8 @@ if ! command -v plan-to-git >/dev/null 2>&1; then fi export PLAN_TO_GIT_STATE_DIR="${"${"}PLAN_TO_GIT_STATE_DIR:-/tmp/plan-to-git}" -plan-to-git hook --source codex +${planToGitRunnerTemplate} +docker_git_plan_to_git_run hook --source codex PLAN_TO_GIT_SYNC_HELPER="${"${"}DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER:-/opt/docker-git/hooks/plan-to-git-sync}" "$PLAN_TO_GIT_SYNC_HELPER" >&2 || true EOF @@ -128,7 +151,8 @@ if ! command -v plan-to-git >/dev/null 2>&1; then fi export PLAN_TO_GIT_STATE_DIR="${"${"}PLAN_TO_GIT_STATE_DIR:-/tmp/plan-to-git}" -plan-to-git hook --source claude +${planToGitRunnerTemplate} +docker_git_plan_to_git_run hook --source claude PLAN_TO_GIT_SYNC_HELPER="${"${"}DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER:-/opt/docker-git/hooks/plan-to-git-sync}" "$PLAN_TO_GIT_SYNC_HELPER" >&2 || true EOF diff --git a/packages/container/src/core/templates-entrypoint/post-push-pr.ts b/packages/container/src/core/templates-entrypoint/post-push-pr.ts index 5c83114e..caf23391 100644 --- a/packages/container/src/core/templates-entrypoint/post-push-pr.ts +++ b/packages/container/src/core/templates-entrypoint/post-push-pr.ts @@ -1,64 +1,10 @@ -const postPushPrEnsureTemplate = String - .raw`# CHANGE: ensure an open GitHub PR exists for the pushed branch before PR-bound post-push tools run. +import { renderGitHubRemoteHelpers } from "./github-remotes.js" + +const postPushPrEnsureTemplate = + `# CHANGE: ensure an open GitHub PR exists for the pushed branch before PR-bound post-push tools run. # WHY: issue #375 requires every successful git push to leave the branch with an open PR; plan sync and session backup both target PR discussion. # REF: issue-375 -docker_git_github_repo_from_remote_url() { - local remote_url="$1" - local repo_path="" - local owner="" - local repo="" - - case "$remote_url" in - https://github.com/*) - repo_path="${"${"}remote_url#https://github.com/}" - ;; - https://github.com/*) - repo_path="${"${"}remote_url#https://github.com/}" - ;; - https://*@github.com/*) - repo_path="${"${"}remote_url#https://*@github.com/}" - ;; - https://*@github.com/*) - repo_path="${"${"}remote_url#https://*@github.com/}" - ;; - ssh://git@github.com/*) - repo_path="${"${"}remote_url#ssh://git@github.com/}" - ;; - git@github.com:*) - repo_path="${"${"}remote_url#git@github.com:}" - ;; - *) - return 1 - ;; - esac - - repo_path="${"${"}repo_path%%\?*}" - repo_path="${"${"}repo_path%%#*}" - repo_path="${"${"}repo_path%/}" - repo_path="${"${"}repo_path%.git}" - owner="${"${"}repo_path%%/*}" - repo="${"${"}repo_path#*/}" - repo="${"${"}repo%%/*}" - repo="${"${"}repo%.git}" - - if [[ -z "$owner" || -z "$repo" || "$owner" == "$repo_path" ]]; then - return 1 - fi - - printf "%s/%s\n" "$owner" "$repo" -} - -docker_git_github_repo_from_remote() { - local remote="$1" - local remote_url="" - - remote_url="$(git remote get-url "$remote" 2>/dev/null || true)" - if [[ -z "$remote_url" ]]; then - return 1 - fi - - docker_git_github_repo_from_remote_url "$remote_url" -} +${renderGitHubRemoteHelpers()} docker_git_ensure_open_pr() { local branch="" @@ -100,8 +46,8 @@ docker_git_ensure_open_pr() { if [[ "$head_repo" == "$base_repo" ]]; then head_arg="$branch" else - head_owner="${"${"}head_repo%%/*}" - head_arg="${"${"}head_owner}:${"${"}branch}" + head_owner="\${head_repo%%/*}" + head_arg="\${head_owner}:\${branch}" fi if ! pr_url="$(gh pr list --repo "$base_repo" --state open --head "$head_arg" --json url --jq '.[0].url // ""' 2>/dev/null)"; then diff --git a/packages/container/src/core/templates/dockerfile-prelude.ts b/packages/container/src/core/templates/dockerfile-prelude.ts index 0f4ab2f6..b82e9afc 100644 --- a/packages/container/src/core/templates/dockerfile-prelude.ts +++ b/packages/container/src/core/templates/dockerfile-prelude.ts @@ -83,13 +83,13 @@ RUN cargo install --git https://github.com/ProverCoderAI/rust-browser-connection RUN printf "%s\\n" "ALL ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/zz-all \ && chmod 0440 /etc/sudoers.d/zz-all` -const planToGitRevision = "f60fbe71131854be4c6c1d9fb79abafd2dd6949b" +const planToGitRevision = "4e58e315d3a06db3f9e75682455be315cd29d7c8" // CHANGE: install plan-to-git in generated project containers. // WHY: issue #397 requires multi-agent plan capture, Claude Code hooks, temp-backed state, and explicit PR sync. // QUOTE(ТЗ): "подключение новое версии plan-to-git и настройки hooks для claude code и настройки что бы всё уходило на гитхаб автоматически" // REF: issue-397 -// SOURCE: https://github.com/ProverCoderAI/plan-to-git/tree/f60fbe71131854be4c6c1d9fb79abafd2dd6949b +// SOURCE: https://github.com/ProverCoderAI/plan-to-git/tree/4e58e315d3a06db3f9e75682455be315cd29d7c8 // FORMAT THEOREM: image_build_success -> executable(/usr/local/bin/plan-to-git) // PURITY: SHELL // EFFECT: Docker build downloads and installs a pinned Rust CLI from GitHub. @@ -99,6 +99,7 @@ const renderDockerfilePlanToGit = (): string => `# Install plan-to-git for multi-agent plan capture and explicit PR sync (issue #397) RUN cargo install --git https://github.com/ProverCoderAI/plan-to-git --rev ${planToGitRevision} --locked --bins --root /usr/local \ && /usr/local/bin/plan-to-git --help >/dev/null \ + && /usr/local/bin/plan-to-git --help | grep -q -- "--repo" \ && /usr/local/bin/plan-to-git hook --help | grep -q -- "claude" \ && /usr/local/bin/plan-to-git sync --help | grep -q -- "--pr "` diff --git a/packages/container/tests/core/git-post-push-wrapper.test.ts b/packages/container/tests/core/git-post-push-wrapper.test.ts index 0b752a54..4bb6d587 100644 --- a/packages/container/tests/core/git-post-push-wrapper.test.ts +++ b/packages/container/tests/core/git-post-push-wrapper.test.ts @@ -21,6 +21,7 @@ type WrapperHarness = { readonly externalDir: string readonly binDir: string readonly wrapperPath: string + readonly planToGitSyncHelperPath: string readonly gitLogPath: string readonly nodeCwdLogPath: string readonly nodeRepoRootLogPath: string @@ -29,6 +30,13 @@ type WrapperHarness = { readonly ghLogPath: string } +const expectedPlanToGitRuns = ( + repoDir: string, + commands: ReadonlyArray, + targetRepo = "org/repo" +): ReadonlyArray => + commands.flatMap((command) => [`${repoDir}\ttarget-repo:${targetRepo}`, `${repoDir}\t${command}`]) + const fakeGitScript = `#!/usr/bin/env bash set -euo pipefail @@ -167,18 +175,33 @@ exit 0 const fakePlanToGitScript = `#!/usr/bin/env bash set -euo pipefail +if [[ "\${1:-}" == "sync" && "\${2:-}" == "--help" ]]; then + printf '%s\\n' "--pr " + exit 0 +fi + if [[ -n "\${FAKE_PLAN_TO_GIT_LOG_PATH:-}" ]]; then + if [[ -n "\${PLAN_TO_GIT_REPO:-}" ]]; then + printf '%s\\ttarget-repo:%s\\n' "$PWD" "$PLAN_TO_GIT_REPO" >> "$FAKE_PLAN_TO_GIT_LOG_PATH" + fi printf '%s\\t%s\\n' "$PWD" "$*" >> "$FAKE_PLAN_TO_GIT_LOG_PATH" fi -if [[ "\${1:-}" != "import-codex" && "\${1:-}" != "import-claude" && "\${1:-}" != "sync" ]]; then +if [[ "\${1:-}" != "import-codex" && "\${1:-}" != "import-claude" && "\${1:-}" != "sync" && "\${1:-}" != "hook" ]]; then if [[ -n "\${FAKE_PLAN_TO_GIT_LOG_PATH:-}" ]]; then printf '%s\\tunexpected-command:%s\\n' "$PWD" "\${1:-}" >> "$FAKE_PLAN_TO_GIT_LOG_PATH" fi - echo "fakePlanToGit: expected import-codex, import-claude, or sync command, got: \${1:-}" >&2 + echo "fakePlanToGit: expected import-codex, import-claude, hook, or sync command, got: \${1:-}" >&2 exit 127 fi +if [[ "\${1:-}" == "sync" && "\${2:-}" == "--pr" && -n "\${FAKE_PLAN_TO_GIT_EXPECT_TARGET_REPO:-}" ]]; then + if [[ "\${PLAN_TO_GIT_REPO:-}" != "$FAKE_PLAN_TO_GIT_EXPECT_TARGET_REPO" ]]; then + echo "fakePlanToGit: expected target repo $FAKE_PLAN_TO_GIT_EXPECT_TARGET_REPO, got \${PLAN_TO_GIT_REPO:-}" >&2 + exit 44 + fi +fi + if [[ -n "\${FAKE_PLAN_TO_GIT_EXIT_CODE:-}" ]]; then exit "$FAKE_PLAN_TO_GIT_EXIT_CODE" fi @@ -282,6 +305,7 @@ const makeHarnessEnv = ( FAKE_NODE_SCRIPT_LOG_PATH: harness.nodeScriptLogPath, FAKE_PLAN_TO_GIT_LOG_PATH: harness.planToGitLogPath, FAKE_GH_LOG_PATH: harness.ghLogPath, + DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER: harness.planToGitSyncHelperPath, ...overrides }) @@ -341,6 +365,10 @@ const withHarness = ( const postPushPath = path.join(hooksDir, "post-push") yield* _(writeExecutable(postPushPath, postPushScript)) + const planToGitSyncHelperScript = extractEmbeddedScript(renderEntrypointGitHooks(), "$PLAN_TO_GIT_SYNC_HELPER") + const planToGitSyncHelperPath = path.join(hooksDir, "plan-to-git-sync") + yield* _(writeExecutable(planToGitSyncHelperPath, planToGitSyncHelperScript)) + const wrapperTemplate = extractEmbeddedScript( renderEntrypointGitPostPushWrapperInstall(), "$GIT_WRAPPER_BIN" @@ -357,6 +385,7 @@ const withHarness = ( externalDir, binDir, wrapperPath, + planToGitSyncHelperPath, gitLogPath, nodeCwdLogPath, nodeRepoRootLogPath, @@ -383,11 +412,9 @@ describe("git post-push wrapper", () => { expect(nodeCwd).toEqual([harness.repoDir]) expect(nodeRepoRoot).toEqual([harness.repoDir]) expect(nodeScript).toEqual(["backup --verbose --background --require-comment"]) - expect(planToGit).toEqual([ - `${harness.repoDir}\timport-codex --no-sync`, - `${harness.repoDir}\timport-claude --no-sync`, - `${harness.repoDir}\tsync` - ]) + expect(planToGit).toEqual( + expectedPlanToGitRuns(harness.repoDir, ["import-codex --no-sync", "import-claude --no-sync", "sync"]) + ) expect(gh).toContain(`${harness.repoDir}\tpr create --repo org/repo --base main --head issue-375 --fill`) }) ).pipe(Effect.provide(NodeContext.layer))) @@ -407,11 +434,9 @@ describe("git post-push wrapper", () => { expect(nodeCwd).toEqual([harness.repoDir]) expect(nodeRepoRoot).toEqual([harness.repoDir]) expect(nodeScript).toEqual(["backup --verbose --background --require-comment"]) - expect(planToGit).toEqual([ - `${harness.repoDir}\timport-codex --no-sync`, - `${harness.repoDir}\timport-claude --no-sync`, - `${harness.repoDir}\tsync` - ]) + expect(planToGit).toEqual( + expectedPlanToGitRuns(harness.repoDir, ["import-codex --no-sync", "import-claude --no-sync", "sync"]) + ) expect(gh).toContain(`${harness.repoDir}\tpr create --repo org/repo --base main --head issue-375 --fill`) expect(gitLog.some((line) => line.startsWith(`${harness.externalDir}\t-C ${harness.repoDir} push`))).toBe(true) }) @@ -500,7 +525,7 @@ describe("git post-push wrapper", () => { const gh = yield* _(readLogLines(harness.ghLogPath)) expect(nodeScript).toEqual([]) - expect(planToGit).toEqual([`${harness.repoDir}\timport-codex --no-sync`]) + expect(planToGit).toEqual(expectedPlanToGitRuns(harness.repoDir, ["import-codex --no-sync"])) expect(gh).toContain(`${harness.repoDir}\tpr create --repo org/repo --base main --head issue-375 --fill`) }) ).pipe(Effect.provide(NodeContext.layer))) @@ -519,11 +544,9 @@ describe("git post-push wrapper", () => { const planToGit = yield* _(readLogLines(harness.planToGitLogPath)) expect(nodeScript).toEqual(["backup --verbose --background --require-comment"]) - expect(planToGit).toEqual([ - `${harness.repoDir}\timport-codex --no-sync`, - `${harness.repoDir}\timport-claude --no-sync`, - `${harness.repoDir}\tsync` - ]) + expect(planToGit).toEqual( + expectedPlanToGitRuns(harness.repoDir, ["import-codex --no-sync", "import-claude --no-sync", "sync"]) + ) }) ).pipe(Effect.provide(NodeContext.layer))) @@ -541,11 +564,9 @@ describe("git post-push wrapper", () => { const gh = yield* _(readLogLines(harness.ghLogPath)) expect(nodeScript).toEqual(["backup --verbose --background --require-comment"]) - expect(planToGit).toEqual([ - `${harness.repoDir}\timport-codex --no-sync`, - `${harness.repoDir}\timport-claude --no-sync`, - `${harness.repoDir}\tsync` - ]) + expect(planToGit).toEqual( + expectedPlanToGitRuns(harness.repoDir, ["import-codex --no-sync", "import-claude --no-sync", "sync"]) + ) expect(gh).toContain(`${harness.repoDir}\tpr list --repo org/repo --state open --head issue-375 --json url --jq .[0].url // ""`) expect(gh.some((line) => line.includes("pr create"))).toBe(false) }) @@ -572,6 +593,34 @@ describe("git post-push wrapper", () => { }) ).pipe(Effect.provide(NodeContext.layer))) + it.effect("syncs explicit PR plans against upstream target repo when origin is a fork", () => + withHarness((harness) => + Effect.gen(function*(_) { + yield* _( + runWrapper(harness, harness.repoDir, ["push", "origin", "HEAD"], { + env: { + DOCKER_GIT_PR_NUMBER: "375", + FAKE_GH_OPEN_PR_URL: "https://github.com/org/repo/pull/375", + FAKE_GIT_ORIGIN_URL: "https://github.com/me/repo.git", + FAKE_GIT_UPSTREAM_URL: "https://github.com/org/repo.git", + FAKE_PLAN_TO_GIT_EXPECT_TARGET_REPO: "org/repo" + } + }) + ) + + const nodeScript = yield* _(readLogLines(harness.nodeScriptLogPath)) + const planToGit = yield* _(readLogLines(harness.planToGitLogPath)) + const gh = yield* _(readLogLines(harness.ghLogPath)) + + expect(nodeScript).toEqual(["backup --verbose --background --require-comment"]) + expect(planToGit).toEqual( + expectedPlanToGitRuns(harness.repoDir, ["import-codex --no-sync", "import-claude --no-sync", "sync --pr 375"]) + ) + expect(gh).toContain(`${harness.repoDir}\tpr list --repo org/repo --state open --head me:issue-375 --json url --jq .[0].url // ""`) + expect(gh.some((line) => line.includes("pr create"))).toBe(false) + }) + ).pipe(Effect.provide(NodeContext.layer))) + it.effect("propagates PR creation failures before plan sync and session backup", () => withHarness((harness) => Effect.gen(function*(_) { diff --git a/packages/container/tests/core/templates.test.ts b/packages/container/tests/core/templates.test.ts index 4fb97ef2..276c687c 100644 --- a/packages/container/tests/core/templates.test.ts +++ b/packages/container/tests/core/templates.test.ts @@ -208,8 +208,9 @@ describe("renderDockerfile", () => { "rtk --version", "rtk gain >/dev/null 2>&1 || true", "# Install plan-to-git for multi-agent plan capture and explicit PR sync (issue #397)", - "cargo install --git https://github.com/ProverCoderAI/plan-to-git --rev f60fbe71131854be4c6c1d9fb79abafd2dd6949b --locked --bins --root /usr/local", + "cargo install --git https://github.com/ProverCoderAI/plan-to-git --rev 4e58e315d3a06db3f9e75682455be315cd29d7c8 --locked --bins --root /usr/local", "/usr/local/bin/plan-to-git --help >/dev/null", + '/usr/local/bin/plan-to-git --help | grep -q -- "--repo"', '/usr/local/bin/plan-to-git hook --help | grep -q -- "claude"', '/usr/local/bin/plan-to-git sync --help | grep -q -- "--pr "', 'ARG DOCKER_GIT_SESSION_SYNC_PACKAGE="@prover-coder-ai/docker-git-session-sync@latest"', @@ -526,17 +527,20 @@ describe("renderEntrypointGitHooks", () => { expect(hooks).toContain("[post-push-pr] Error: cannot create PR from detached HEAD") expect(hooks).toContain("[post-push-pr] Error: failed to list open PRs") expect(hooks).toContain("DOCKER_GIT_SKIP_PLAN_TO_GIT") - expect(hooks).toContain("plan-to-git import-codex --no-sync") - expect(hooks).toContain("plan-to-git import-claude --no-sync") + expect(hooks).toContain("docker_git_plan_to_git_run") + expect(hooks).toContain('base_repo="$(docker_git_github_repo_from_remote origin || true)"') + expect(hooks).toContain('PLAN_TO_GIT_REPO="$base_repo" plan-to-git "$@"') + expect(hooks).toContain("docker_git_plan_to_git_run import-codex --no-sync") + expect(hooks).toContain("docker_git_plan_to_git_run import-claude --no-sync") expect(hooks).toContain("docker_git_plan_to_git_explicit_pr_supported") expect(hooks).toContain("docker_git_plan_to_git_resolve_pr_number") expect(hooks).toContain("DOCKER_GIT_PR_NUMBER PR_NUMBER GITHUB_PR_NUMBER") expect(hooks).toContain('candidate="${REPO_REF:-}"') - expect(hooks).toContain('plan-to-git sync --pr "$pr_number"') - expect(hooks).toContain("plan-to-git sync") + expect(hooks).toContain('docker_git_plan_to_git_run sync --pr "$pr_number"') + expect(hooks).toContain("docker_git_plan_to_git_run sync") expect(hooks).toContain('[plan-to-git] Syncing queued agent plans to PR #$pr_number') - expect(hooks).toContain("plan-to-git hook --source codex") - expect(hooks).toContain("plan-to-git hook --source claude") + expect(hooks).toContain("docker_git_plan_to_git_run hook --source codex") + expect(hooks).toContain("docker_git_plan_to_git_run hook --source claude") expect(hooks).toContain('export PLAN_TO_GIT_STATE_DIR="${PLAN_TO_GIT_STATE_DIR:-/tmp/plan-to-git}"') expect(hooks).toContain('"$PLAN_TO_GIT_SYNC_HELPER" >&2 || true') expect(hooks).toContain("docker_git_install_claude_plan_to_git_hooks") @@ -570,8 +574,8 @@ describe("renderEntrypointGitHooks", () => { const cdIndex = hooks.indexOf('cd "$REPO_ROOT"') const ensurePrIndex = hooks.indexOf("docker_git_ensure_open_pr\n\n# CHANGE: backfill agent session plans") - const planImportIndex = hooks.indexOf("plan-to-git import-codex --no-sync") - const claudeImportIndex = hooks.indexOf("plan-to-git import-claude --no-sync") + const planImportIndex = hooks.indexOf("docker_git_plan_to_git_run import-codex --no-sync") + const claudeImportIndex = hooks.indexOf("docker_git_plan_to_git_run import-claude --no-sync") const planSyncIndex = hooks.indexOf('"$PLAN_TO_GIT_SYNC_HELPER"', claudeImportIndex) const sessionBackupIndex = hooks.indexOf("docker-git-session-sync backup --verbose --background --require-comment")