@@ -21,6 +21,7 @@ type WrapperHarness = {
2121 readonly externalDir : string
2222 readonly binDir : string
2323 readonly wrapperPath : string
24+ readonly planToGitSyncHelperPath : string
2425 readonly gitLogPath : string
2526 readonly nodeCwdLogPath : string
2627 readonly nodeRepoRootLogPath : string
@@ -167,18 +168,70 @@ exit 0
167168const fakePlanToGitScript = `#!/usr/bin/env bash
168169set -euo pipefail
169170
171+ fake_effective_origin_url() {
172+ local origin_url="\${FAKE_GIT_ORIGIN_URL:-https://github.com/org/repo.git}"
173+ local config_index=0
174+ local key_var=""
175+ local value_var=""
176+ local key=""
177+ local value=""
178+ local base_url=""
179+ local suffix=""
180+
181+ if [[ -n "\${GIT_CONFIG_COUNT:-}" && "\${GIT_CONFIG_COUNT}" =~ ^[0-9]+$ ]]; then
182+ while [[ "$config_index" -lt "$GIT_CONFIG_COUNT" ]]; do
183+ key_var="GIT_CONFIG_KEY_\${config_index}"
184+ value_var="GIT_CONFIG_VALUE_\${config_index}"
185+ key="\${!key_var:-}"
186+ value="\${!value_var:-}"
187+ if [[ "$key" == url.*.insteadOf && -n "$value" && "$origin_url" == "$value"* ]]; then
188+ base_url="\${key#url.}"
189+ base_url="\${base_url%.insteadOf}"
190+ suffix="\${origin_url#"$value"}"
191+ printf '%s\\n' "\${base_url}\${suffix}"
192+ return 0
193+ fi
194+ config_index=$((config_index + 1))
195+ done
196+ fi
197+
198+ printf '%s\\n' "$origin_url"
199+ }
200+
201+ if [[ "\${1:-}" == "sync" && "\${2:-}" == "--help" ]]; then
202+ printf '%s\\n' "--pr <PR>"
203+ exit 0
204+ fi
205+
170206if [[ -n "\${FAKE_PLAN_TO_GIT_LOG_PATH:-}" ]]; then
207+ if [[ -n "\${GIT_CONFIG_COUNT:-}" && "\${GIT_CONFIG_COUNT}" =~ ^[0-9]+$ ]]; then
208+ config_index=0
209+ while [[ "$config_index" -lt "$GIT_CONFIG_COUNT" ]]; do
210+ key_var="GIT_CONFIG_KEY_\${config_index}"
211+ value_var="GIT_CONFIG_VALUE_\${config_index}"
212+ printf '%s\\tgit-config:%s=%s\\n' "$PWD" "\${!key_var:-}" "\${!value_var:-}" >> "$FAKE_PLAN_TO_GIT_LOG_PATH"
213+ config_index=$((config_index + 1))
214+ done
215+ fi
171216 printf '%s\\t%s\\n' "$PWD" "$*" >> "$FAKE_PLAN_TO_GIT_LOG_PATH"
172217fi
173218
174- if [[ "\${1:-}" != "import-codex" && "\${1:-}" != "import-claude" && "\${1:-}" != "sync" ]]; then
219+ if [[ "\${1:-}" != "import-codex" && "\${1:-}" != "import-claude" && "\${1:-}" != "sync" && "\${1:-}" != "hook" ]]; then
175220 if [[ -n "\${FAKE_PLAN_TO_GIT_LOG_PATH:-}" ]]; then
176221 printf '%s\\tunexpected-command:%s\\n' "$PWD" "\${1:-<empty>}" >> "$FAKE_PLAN_TO_GIT_LOG_PATH"
177222 fi
178- echo "fakePlanToGit: expected import-codex, import-claude, or sync command, got: \${1:-<empty>}" >&2
223+ echo "fakePlanToGit: expected import-codex, import-claude, hook, or sync command, got: \${1:-<empty>}" >&2
179224 exit 127
180225fi
181226
227+ if [[ "\${1:-}" == "sync" && "\${2:-}" == "--pr" && -n "\${FAKE_PLAN_TO_GIT_EXPECT_EFFECTIVE_ORIGIN_URL:-}" ]]; then
228+ effective_origin_url="$(fake_effective_origin_url)"
229+ if [[ "$effective_origin_url" != "$FAKE_PLAN_TO_GIT_EXPECT_EFFECTIVE_ORIGIN_URL" ]]; then
230+ echo "fakePlanToGit: expected effective origin $FAKE_PLAN_TO_GIT_EXPECT_EFFECTIVE_ORIGIN_URL, got $effective_origin_url" >&2
231+ exit 44
232+ fi
233+ fi
234+
182235if [[ -n "\${FAKE_PLAN_TO_GIT_EXIT_CODE:-}" ]]; then
183236 exit "$FAKE_PLAN_TO_GIT_EXIT_CODE"
184237fi
@@ -282,6 +335,7 @@ const makeHarnessEnv = (
282335 FAKE_NODE_SCRIPT_LOG_PATH : harness . nodeScriptLogPath ,
283336 FAKE_PLAN_TO_GIT_LOG_PATH : harness . planToGitLogPath ,
284337 FAKE_GH_LOG_PATH : harness . ghLogPath ,
338+ DOCKER_GIT_PLAN_TO_GIT_SYNC_HELPER : harness . planToGitSyncHelperPath ,
285339 ...overrides
286340} )
287341
@@ -341,6 +395,10 @@ const withHarness = <A, E, R>(
341395 const postPushPath = path . join ( hooksDir , "post-push" )
342396 yield * _ ( writeExecutable ( postPushPath , postPushScript ) )
343397
398+ const planToGitSyncHelperScript = extractEmbeddedScript ( renderEntrypointGitHooks ( ) , "$PLAN_TO_GIT_SYNC_HELPER" )
399+ const planToGitSyncHelperPath = path . join ( hooksDir , "plan-to-git-sync" )
400+ yield * _ ( writeExecutable ( planToGitSyncHelperPath , planToGitSyncHelperScript ) )
401+
344402 const wrapperTemplate = extractEmbeddedScript (
345403 renderEntrypointGitPostPushWrapperInstall ( ) ,
346404 "$GIT_WRAPPER_BIN"
@@ -357,6 +415,7 @@ const withHarness = <A, E, R>(
357415 externalDir,
358416 binDir,
359417 wrapperPath,
418+ planToGitSyncHelperPath,
360419 gitLogPath,
361420 nodeCwdLogPath,
362421 nodeRepoRootLogPath,
@@ -572,6 +631,40 @@ describe("git post-push wrapper", () => {
572631 } )
573632 ) . pipe ( Effect . provide ( NodeContext . layer ) ) )
574633
634+ it . effect ( "syncs explicit PR plans against upstream when origin is a fork" , ( ) =>
635+ withHarness ( ( harness ) =>
636+ Effect . gen ( function * ( _ ) {
637+ yield * _ (
638+ runWrapper ( harness , harness . repoDir , [ "push" , "origin" , "HEAD" ] , {
639+ env : {
640+ DOCKER_GIT_PR_NUMBER : "375" ,
641+ FAKE_GH_OPEN_PR_URL : "https://github.com/org/repo/pull/375" ,
642+ FAKE_GIT_ORIGIN_URL : "https://github.com/me/repo.git" ,
643+ FAKE_GIT_UPSTREAM_URL : "https://github.com/org/repo.git" ,
644+ FAKE_PLAN_TO_GIT_EXPECT_EFFECTIVE_ORIGIN_URL : "https://github.com/org/repo.git"
645+ }
646+ } )
647+ )
648+
649+ const nodeScript = yield * _ ( readLogLines ( harness . nodeScriptLogPath ) )
650+ const planToGit = yield * _ ( readLogLines ( harness . planToGitLogPath ) )
651+ const gh = yield * _ ( readLogLines ( harness . ghLogPath ) )
652+ const scopedRepoConfig = `${ harness . repoDir } \tgit-config:url.https://github.com/org/repo.git.insteadOf=https://github.com/me/repo.git`
653+
654+ expect ( nodeScript ) . toEqual ( [ "backup --verbose --background --require-comment" ] )
655+ expect ( planToGit ) . toEqual ( [
656+ scopedRepoConfig ,
657+ `${ harness . repoDir } \timport-codex --no-sync` ,
658+ scopedRepoConfig ,
659+ `${ harness . repoDir } \timport-claude --no-sync` ,
660+ scopedRepoConfig ,
661+ `${ harness . repoDir } \tsync --pr 375`
662+ ] )
663+ expect ( gh ) . toContain ( `${ harness . repoDir } \tpr list --repo org/repo --state open --head me:issue-375 --json url --jq .[0].url // ""` )
664+ expect ( gh . some ( ( line ) => line . includes ( "pr create" ) ) ) . toBe ( false )
665+ } )
666+ ) . pipe ( Effect . provide ( NodeContext . layer ) ) )
667+
575668 it . effect ( "propagates PR creation failures before plan sync and session backup" , ( ) =>
576669 withHarness ( ( harness ) =>
577670 Effect . gen ( function * ( _ ) {
0 commit comments