sync: BOWEI_US 2026-09-02

This commit is contained in:
Myfro9
2026-09-02 15:35:47 -05:00
parent e39e852853
commit 10f436cfde
4 changed files with 428 additions and 2 deletions
@@ -17,6 +17,27 @@ CODEX_SKILLS_DIR = C:\Users\<user>\.codex\skills
凭据说明:首次 clone 成功后运行 `git config --global credential.helper manager`, 凭据说明:首次 clone 成功后运行 `git config --global credential.helper manager`,
让 Windows 凭据管理器接管,此后 `SKILLS_REPO_PASS` 可以清空。 让 Windows 凭据管理器接管,此后 `SKILLS_REPO_PASS` 可以清空。
### gitea 服务端(tools-sync 的 gitea_doctor 用;不自建 gitea 就整节留空)
```
GITEA_SSH_HOST = <gitea 宿主机地址,同网段/VPN 内的直连地址优先>
GITEA_SSH_PORT = 22
GITEA_SSH_HOST_ALT = <走公网跳板时的地址,没有就留空>
GITEA_SSH_PORT_ALT = <跳板端口,没有就留空>
GITEA_SSH_USER = <登录用户>
GITEA_HOST_KEY_FP = <SHA256:... 主机公钥指纹,用于确认连对了机器>
GITEA_CONTAINER = gitea
GITEA_DOCKER_BIN = /usr/local/bin/docker
GITEA_DATA_DIR = <宿主机上 bind 到容器 /data 的目录>
GITEA_COMPOSE_FILE = <docker-compose.yml 路径,没用 compose 就留空>
GITEA_HTTP_LOCAL = http://localhost:3000
```
填写要点:脚本要免密登录,先把本机公钥加进服务端的 `authorized_keys` 并实测一次。
`GITEA_DOCKER_BIN` 写绝对路径——**ssh 非交互登录通常不加载登录 shell 的 PATH,
`docker` 会找不到**。`GITEA_HOST_KEY_FP` 不是可选项:有跳板时它是你唯一能确认
"连上的确实是那台机器"的凭据。
## Python / Conda ## Python / Conda
``` ```
+165
View File
@@ -0,0 +1,165 @@
# tools-sync: verify the gitea transport from this machine, and when it is broken, log
# into the server and repair it.
#
# Why this exists: on 2026-08-25 the server started handing out empty packs. push kept
# working, `ls-remote` kept working, only the pack plane was dead - so every sync run
# "succeeded" while three commits sat stranded on one machine for eight days. Checking
# the transport is cheap; discovering the outage a week late is not.
#
# .\gitea_doctor.ps1 # check only, report what is wrong
# .\gitea_doctor.ps1 -Fix # check, then repair on the server, then re-verify
#
# Exit: 0 = transport healthy at the end of the run, 1 = still broken.
param(
[string]$EnvFile = "$env:USERPROFILE\skills_env.md",
[string]$RepoUrl,
# Log in and repair, instead of only reporting.
[switch]$Fix,
# Also allow repairs that can lose data (container recreate, repo surgery). The
# server-side script asks for this separately; nothing here needs it by default.
[switch]$AllowDestructive,
[int]$SshTimeout = 15
)
$ErrorActionPreference = "Stop"
function Fail([string]$Msg) { Write-Host "FAIL: $Msg"; exit 1 }
# ---------- environment ----------
if (-not (Test-Path $EnvFile)) { Fail "$EnvFile not found. Copy references\skills_env_template.md there first." }
$envText = Get-Content $EnvFile -Raw
function Get-EnvKey([string]$Key) {
$m = [regex]::Match($envText, "(?m)^\s*$([regex]::Escape($Key))\s*=\s*(.+?)\s*$")
if (-not $m.Success) { return "" }
$v = $m.Groups[1].Value.Trim()
if ($v -match "^<.*>$") { return "" } # unfilled placeholder
return $v
}
if (-not $RepoUrl) { $RepoUrl = Get-EnvKey "SKILLS_REPO_URL" }
if (-not $RepoUrl) { Fail "SKILLS_REPO_URL missing from $EnvFile and no -RepoUrl given." }
$sshHost = Get-EnvKey "GITEA_SSH_HOST"
$sshPort = Get-EnvKey "GITEA_SSH_PORT"
$sshHostA = Get-EnvKey "GITEA_SSH_HOST_ALT"
$sshPortA = Get-EnvKey "GITEA_SSH_PORT_ALT"
$sshUser = Get-EnvKey "GITEA_SSH_USER"
$container = Get-EnvKey "GITEA_CONTAINER"
$dockerBin = Get-EnvKey "GITEA_DOCKER_BIN"
if (-not $sshPort) { $sshPort = "22" }
if (-not $container) { $container = "gitea" }
if (-not $dockerBin) { $dockerBin = "/usr/local/bin/docker" }
# ---------- client-side transport check ----------
# Two planes, because they fail independently and the difference is the whole diagnosis:
# refs = ls-remote / info-refs (kept working right through the 2026-09 outage)
# pack = an actual fetch (this is what was dead)
function Test-Transport {
$result = [ordered]@{ Refs = $false; Pack = $false; Sha = ""; Detail = "" }
$prev = $ErrorActionPreference; $ErrorActionPreference = "Continue"
$refs = & git ls-remote $RepoUrl "refs/heads/*" 2>&1 | ForEach-Object { "$_" }
$refsCode = $LASTEXITCODE
$ErrorActionPreference = $prev
if ($refsCode -eq 0 -and $refs) {
$result.Refs = $true
$result.Sha = (($refs -join "`n") -split "\s+")[0]
} else {
$result.Detail = ($refs -join " ")
return $result
}
# tree:0 + depth 1 keeps this to a few KB; the outage failed even at that size, so a
# tiny probe is a valid detector and costs nothing to run on every sync.
$tmp = Join-Path ([IO.Path]::GetTempPath()) ("gitea-doctor-" + [guid]::NewGuid().ToString("N"))
$prev = $ErrorActionPreference; $ErrorActionPreference = "Continue"
$out = & git -c core.askPass=true clone --quiet --bare --depth 1 --filter=tree:0 $RepoUrl $tmp 2>&1 | ForEach-Object { "$_" }
$code = $LASTEXITCODE
$ErrorActionPreference = $prev
if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue }
if ($code -eq 0) { $result.Pack = $true } else { $result.Detail = ($out -join " ") }
return $result
}
Write-Host "=== gitea transport check: $RepoUrl ==="
$t = Test-Transport
if ($t.Refs) { Write-Host " refs plane: OK (main-ish head $($t.Sha))" } else { Write-Host " refs plane: BROKEN - $($t.Detail)" }
if ($t.Pack) { Write-Host " pack plane: OK" } else { Write-Host " pack plane: BROKEN - $($t.Detail)" }
if ($t.Refs -and $t.Pack) { Write-Host "transport healthy"; exit 0 }
# A dead refs plane too means the host or gitea is down/unreachable, not the empty-pack
# fault - still worth logging in, the triage script starts by checking the container.
if (-not $Fix) {
Write-Host ""
Write-Host "Transport is broken. Re-run with -Fix to log into $sshUser@$sshHost and repair,"
Write-Host "or see the skill's Phase 1.5 to do it by hand."
exit 1
}
# ---------- reach the server ----------
if (-not $sshHost -or -not $sshUser) { Fail "GITEA_SSH_HOST / GITEA_SSH_USER missing from $EnvFile - cannot reach the server." }
function Invoke-Remote([string]$HostName, [string]$Port, [string]$LocalScript, [string[]]$ScriptArgs) {
# Copy the script over rather than piping it into `sh -s`. Piping looks simpler but
# PS 5.1 encodes native-process stdin its own way and prepends a UTF-8 BOM, which
# lands on the shebang and makes sh report "#!/bin/sh: No such file or directory".
# scp moves bytes verbatim, so what runs there is exactly the file that is here.
$remote = "/tmp/tools-sync-triage.$PID.sh"
$prev = $ErrorActionPreference; $ErrorActionPreference = "Continue"
$common = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=$SshTimeout", "-o", "StrictHostKeyChecking=accept-new")
$cp = & scp @common "-P" $Port $LocalScript "${sshUser}@${HostName}:$remote" 2>&1 | ForEach-Object { "$_" }
if ($LASTEXITCODE -ne 0) {
$ErrorActionPreference = $prev
return @{ Code = 255; Out = ($cp -join "`n") }
}
$cmd = "sh $remote " + ($ScriptArgs -join " ") + "; rc=`$?; rm -f $remote; exit `$rc"
$out = & ssh @common "-p" $Port "$sshUser@$HostName" $cmd 2>&1 | ForEach-Object { "$_" }
$code = $LASTEXITCODE
$ErrorActionPreference = $prev
return @{ Code = $code; Out = ($out -join "`n") }
}
$triage = Join-Path $PSScriptRoot "gitea_triage.sh"
if (-not (Test-Path $triage)) { Fail "gitea_triage.sh missing next to this script." }
# Normalise to LF and no BOM before shipping it: a CRLF or BOM on the shebang line makes
# sh fail with a "No such file or directory" that names the interpreter, not the cause.
$triageStaged = Join-Path ([IO.Path]::GetTempPath()) "tools-sync-triage-$PID.sh"
$triageText = ((Get-Content $triage -Raw) -replace "`r`n", "`n") -replace "^", ""
[IO.File]::WriteAllText($triageStaged, $triageText, (New-Object System.Text.UTF8Encoding($false)))
$args = @("--container", $container, "--docker", $dockerBin, "--fix")
if ($AllowDestructive) { $args += "--allow-destructive" }
$targets = @(@{ H = $sshHost; P = $sshPort; Label = "direct" })
if ($sshHostA -and $sshPortA) { $targets += @{ H = $sshHostA; P = $sshPortA; Label = "fallback" } }
$r = $null
foreach ($tg in $targets) {
Write-Host ""
Write-Host "=== server triage via $($tg.Label): $sshUser@$($tg.H):$($tg.P) ==="
$r = Invoke-Remote $tg.H $tg.P $triageStaged $args
if ($r.Out -notmatch "Connection (timed out|refused)|Could not resolve|Permission denied|No route to host|lost connection") { break }
Write-Host " unreachable: $(($r.Out -split "`n")[0])"
$r = $null
}
Remove-Item $triageStaged -Force -ErrorAction SilentlyContinue
if (-not $r) { Fail "could not reach the gitea host on any configured path (see GITEA_SSH_HOST / _ALT)." }
Write-Host $r.Out
# ---------- re-verify from here ----------
# The server saying "repaired" is a claim; the client completing a fetch is the evidence.
Write-Host ""
Write-Host "=== re-verifying transport from this machine ==="
$t2 = Test-Transport
if ($t2.Refs -and $t2.Pack) {
Write-Host "transport healthy again - re-run the sync."
exit 0
}
if ($t2.Refs) { Write-Host " refs plane: OK" } else { Write-Host " refs plane: BROKEN - $($t2.Detail)" }
if ($t2.Pack) { Write-Host " pack plane: OK" } else { Write-Host " pack plane: BROKEN - $($t2.Detail)" }
Fail "server triage ran but the transport is still broken. Read the RESULT line above; if it says still-broken, the fault is outside the known repairs - escalate to the skill's Phase 1.5 ladder."
+203
View File
@@ -0,0 +1,203 @@
#!/bin/sh
# tools-sync: gitea server-side triage + repair. Runs ON the gitea host (POSIX sh).
#
# Driven by gitea_doctor.ps1, which pipes this in over ssh. Safe to run by hand too.
#
# Every check prints "PASS <id> <detail>" or "FAIL <id> <detail>" so the caller can
# parse the outcome without re-deriving it. Repairs only happen with --fix, and only
# the reversible ones; anything that could lose data needs --allow-destructive.
#
# Exit: 0 = healthy (after any repairs), 1 = still broken, 2 = usage/precondition error.
CONTAINER=gitea
DOCKER=/usr/local/bin/docker
FIX=0
DESTRUCTIVE=0
REPO=""
while [ $# -gt 0 ]; do
case "$1" in
--container) CONTAINER="$2"; shift 2 ;;
--docker) DOCKER="$2"; shift 2 ;;
--repo) REPO="$2"; shift 2 ;;
--fix) FIX=1; shift ;;
--allow-destructive) DESTRUCTIVE=1; shift ;;
*) echo "FAIL usage unknown argument: $1"; exit 2 ;;
esac
done
[ -x "$DOCKER" ] || { echo "FAIL docker $DOCKER not executable (ssh PATH is not the login PATH)"; exit 2; }
D="$DOCKER"
TS=$(date +%Y%m%d-%H%M%S)
BROKEN=0
REPAIRED=""
note() { echo "PASS $1 $2"; }
bad() { echo "FAIL $1 $2"; BROKEN=1; }
fixed() { echo "FIXED $1 $2"; REPAIRED="$REPAIRED $1"; }
dex() { $D exec "$CONTAINER" sh -c "$1" 2>&1; }
echo "=== gitea triage $TS (container=$CONTAINER fix=$FIX destructive=$DESTRUCTIVE) ==="
# ---------- A. container is up ----------
STATUS=$($D inspect "$CONTAINER" --format '{{.State.Status}}' 2>/dev/null)
if [ -z "$STATUS" ]; then
bad container "no such container: $CONTAINER"
echo "--- containers present ---"; $D ps -a --format '{{.Names}} {{.Image}} {{.Status}}'
exit 1
fi
if [ "$STATUS" != "running" ]; then
bad container "status=$STATUS"
if [ "$FIX" = 1 ]; then
$D start "$CONTAINER" >/dev/null 2>&1 && sleep 8
STATUS=$($D inspect "$CONTAINER" --format '{{.State.Status}}' 2>/dev/null)
[ "$STATUS" = "running" ] && fixed container "started" || { echo "FAIL container could not start"; exit 1; }
else
exit 1
fi
else
note container "running ($($D inspect "$CONTAINER" --format '{{.Config.Image}}'))"
fi
# Where gitea's own HOME (and therefore its global gitconfig) lives. It is NOT /data/git.
GHOME=$(dex 'getent passwd git 2>/dev/null | cut -d: -f6')
[ -n "$GHOME" ] || GHOME=/data/git
GCFG=$(dex 'ls /data/gitea/home/.gitconfig >/dev/null 2>&1 && echo /data/gitea/home/.gitconfig || echo '"$GHOME"'/.gitconfig')
GCFG=$(echo "$GCFG" | tr -d '\r')
echo "INFO gitconfig $GCFG"
# ---------- B. disk ----------
FREE=$(dex 'df -P /data | tail -1 | awk "{print \$4}"')
FREE=$(echo "$FREE" | tr -d '\r ')
if [ -n "$FREE" ] && [ "$FREE" -lt 1048576 ] 2>/dev/null; then
bad disk "only ${FREE}KB free on /data"
else
note disk "${FREE}KB free on /data"
fi
# ---------- C. global gitconfig sanity ----------
# The 2026-09-02 outage: [uploadpack] packObjectsHook replaced git pack-objects with a
# script that produced no pack, so every clone got an empty one. Any hook-ish config
# here is remote code execution by design, so treat its mere presence as a failure.
CFG_SIZE=$(dex "wc -c < $GCFG 2>/dev/null" | tr -d '\r ')
[ -n "$CFG_SIZE" ] || CFG_SIZE=0
HOOKCFG=$(dex "grep -icE 'packObjectsHook|core\\.hooksPath|sshCommand|credential\\.helper *= *!' $GCFG 2>/dev/null" | tr -d '\r ')
[ -n "$HOOKCFG" ] || HOOKCFG=0
if [ "$HOOKCFG" != 0 ] || [ "$CFG_SIZE" -gt 65536 ] 2>/dev/null; then
bad gitconfig "size=${CFG_SIZE}B suspicious-directives=${HOOKCFG} (expected a few hundred B, 0)"
dex "grep -inE 'packObjectsHook|core\\.hooksPath|sshCommand' $GCFG | head -5"
if [ "$FIX" = 1 ]; then
# gitea rewrites a clean global gitconfig at startup, so quarantine + restart is
# both the repair and the proof: hand-editing it would only paper over the entry.
dex "cp $GCFG /data/gitea/home/.gitconfig.quarantine.$TS 2>/dev/null || cp $GCFG $GCFG.quarantine.$TS"
dex "rm -f $GCFG"
$D restart "$CONTAINER" >/dev/null 2>&1
sleep 12
NEW=$(dex "wc -c < $GCFG 2>/dev/null" | tr -d '\r ')
NEWHOOK=$(dex "grep -icE 'packObjectsHook|core\\.hooksPath' $GCFG 2>/dev/null" | tr -d '\r ')
[ -n "$NEWHOOK" ] || NEWHOOK=0
if [ "$NEWHOOK" = 0 ] && [ -n "$NEW" ]; then
fixed gitconfig "quarantined to .gitconfig.quarantine.$TS, gitea rebuilt a clean ${NEW}B one"
else
echo "FAIL gitconfig rebuild did not produce a clean file (size=$NEW hooks=$NEWHOOK)"
fi
fi
else
note gitconfig "${CFG_SIZE}B, no hook directives"
fi
# ---------- D. tampered repo hooks ----------
TAMPER=$(dex "grep -rlE '/tmp/[A-Za-z0-9_]+' /data/git/repositories 2>/dev/null | head -20")
TAMPER=$(echo "$TAMPER" | tr -d '\r' | sed '/^$/d')
if [ -n "$TAMPER" ]; then
bad hooks "hook files referencing /tmp:"
echo "$TAMPER" | sed 's/^/ /'
if [ "$FIX" = 1 ]; then
for h in $TAMPER; do
base=$(basename "$h")
tpl="/usr/share/git-core/templates/hooks/$base"
if dex "[ -f $tpl ]" >/dev/null 2>&1; then
dex "cp $tpl $h && chmod 755 $h"
else
dex "grep -v '/tmp/' $h > $h.clean && mv $h.clean $h && chmod 755 $h"
fi
done
LEFT=$(dex "grep -rlE '/tmp/[A-Za-z0-9_]+' /data/git/repositories 2>/dev/null | head -5" | tr -d '\r' | sed '/^$/d')
[ -z "$LEFT" ] && fixed hooks "restored from git templates" || echo "FAIL hooks still tampered: $LEFT"
fi
else
note hooks "no hook references /tmp"
fi
# ---------- E. dropped payloads ----------
PAYLOAD=$(dex "ls /tmp/giteahook* /tmp/col_* 2>/dev/null | head -20" | tr -d '\r' | sed '/^$/d')
if [ -n "$PAYLOAD" ]; then
bad payload "executable drops in /tmp:"
echo "$PAYLOAD" | sed 's/^/ /'
[ "$FIX" = 1 ] && { dex "rm -f /tmp/giteahook* /tmp/col_*" >/dev/null 2>&1; fixed payload "removed"; }
else
note payload "none"
fi
# ---------- F. git binary not shimmed ----------
# A leftover diagnostic wrapper (git.real present) silently changes every git call.
if dex "[ -e /usr/bin/git.real ]" >/dev/null 2>&1; then
bad gitbin "/usr/bin/git.real exists - /usr/bin/git is a wrapper, not the real binary"
if [ "$FIX" = 1 ]; then
dex "cp /usr/bin/git.real /usr/bin/git && rm -f /usr/bin/git.real" >/dev/null 2>&1
fixed gitbin "real git restored"
fi
else
note gitbin "$(dex 'git --version' | tr -d '\r')"
fi
# ---------- G. pack generation (the ladder's decisive rung) ----------
# Must use --stateless-rpc: that is the code path HTTP takes. A `clone file://` proves
# nothing here, which is exactly how this fault stayed misdiagnosed for a week.
if [ -z "$REPO" ]; then
REPO=$(dex 'ls -d /data/git/repositories/*/*.git 2>/dev/null | head -1' | tr -d '\r')
fi
if [ -n "$REPO" ]; then
SHA=$(dex "git --git-dir=$REPO rev-parse HEAD 2>/dev/null" | tr -d '\r')
if [ -n "$SHA" ]; then
BYTES=$(dex "printf '0032want $SHA\n00000009done\n' | git upload-pack --stateless-rpc $REPO 2>/dev/null | wc -c" | tr -d '\r ')
[ -n "$BYTES" ] || BYTES=0
if [ "$BYTES" -lt 512 ] 2>/dev/null; then
bad packgen "upload-pack --stateless-rpc produced only ${BYTES}B for $REPO (a real pack is KBs+)"
echo " -> git itself is being told not to work; check the gitconfig directives above"
else
note packgen "${BYTES}B pack from $REPO"
fi
else
bad packgen "cannot resolve HEAD of $REPO"
fi
else
bad packgen "no repository found under /data/git/repositories"
fi
# ---------- H. end-to-end clone inside the container ----------
# Bypasses frp, any reverse proxy and docker port forwarding: a failure here is gitea's.
PUB=$(dex 'ls -d /data/git/repositories/*/*.git 2>/dev/null | head -5' | tr -d '\r')
CLONE_OK=0
CLONE_MSG="no public repo reachable anonymously"
for r in $PUB; do
owner=$(basename "$(dirname "$r")")
name=$(basename "$r" .git)
out=$(dex "rm -rf /tmp/tsdoc; GIT_TERMINAL_PROMPT=0 git clone -q --filter=tree:0 --depth 1 http://localhost:3000/$owner/$name.git /tmp/tsdoc 2>&1; ls /tmp/tsdoc/.git >/dev/null 2>&1 && echo TSDOC_OK; rm -rf /tmp/tsdoc")
case "$out" in
*TSDOC_OK*) CLONE_OK=1; CLONE_MSG="$owner/$name"; break ;;
*"could not read Username"*|*"Authentication"*) continue ;;
*) CLONE_MSG="$owner/$name: $(echo "$out" | tr -d '\r' | tr '\n' ' ' | cut -c1-160)" ;;
esac
done
[ "$CLONE_OK" = 1 ] && note httpclone "$CLONE_MSG" || bad httpclone "$CLONE_MSG"
# ---------- verdict ----------
echo "=== verdict ==="
if [ "$BROKEN" = 0 ]; then
echo "RESULT healthy"; exit 0
fi
if [ -n "$REPAIRED" ]; then
echo "RESULT repaired:$REPAIRED (caller must re-verify from the client side)"; exit 0
fi
echo "RESULT still-broken (run with --fix to apply the reversible repairs)"; exit 1
+39 -2
View File
@@ -15,7 +15,12 @@ param(
# linking; -Force deletes it and links (caller must have rescued unique content first). # linking; -Force deletes it and links (caller must have rescued unique content first).
[switch]$Force, [switch]$Force,
# Report what would happen without committing/pushing/linking. # Report what would happen without committing/pushing/linking.
[switch]$DryRun [switch]$DryRun,
# A transport failure normally aborts the run and leaves commits stranded here. By
# default we instead log into the gitea host, repair it, and retry once. -NoDoctor
# keeps the old abort-and-report behaviour.
[switch]$NoDoctor,
[switch]$AllowDestructive
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -34,6 +39,28 @@ function Invoke-Git {
function Fail([string]$Msg) { Write-Host "FAIL: $Msg"; exit 1 } function Fail([string]$Msg) { Write-Host "FAIL: $Msg"; exit 1 }
# git reports a server-side transport fault in the vocabulary of the pack protocol, not
# of the server. These are the shapes seen when gitea is up (auth fine, refs fine) but
# the pack plane is dead - the case gitea_doctor knows how to repair.
function Test-TransportFault([string]$GitOutput) {
return ($GitOutput -match "bad pack header|early EOF|invalid index-pack output|RPC failed|unexpected disconnect|remote end hung up|Could not read from remote repository|Connection (reset|refused|timed out)")
}
function Invoke-Doctor([string]$Why) {
if ($NoDoctor) { Write-Host "(-NoDoctor set, skipping server repair)"; return $false }
$doctor = Join-Path $PSScriptRoot "gitea_doctor.ps1"
if (-not (Test-Path $doctor)) { Write-Host "gitea_doctor.ps1 not found; cannot repair the server."; return $false }
Write-Host ""
Write-Host "=== transport fault ($Why) - running gitea_doctor -Fix ==="
$dargs = @("-RepoUrl", $RepoUrl, "-Fix")
if ($AllowDestructive) { $dargs += "-AllowDestructive" }
& $doctor @dargs
$ok = ($LASTEXITCODE -eq 0)
Write-Host "=== doctor $(if ($ok) { 'reports the transport healthy' } else { 'could NOT restore the transport' }) ==="
Write-Host ""
return $ok
}
# ---------- Phase 1: primary checkout (~\.claude\skills) ---------- # ---------- Phase 1: primary checkout (~\.claude\skills) ----------
Write-Host "=== [1/2] primary git sync: $ClaudeSkills ===" Write-Host "=== [1/2] primary git sync: $ClaudeSkills ==="
if (-not (Test-Path (Join-Path $ClaudeSkills ".git"))) { if (-not (Test-Path (Join-Path $ClaudeSkills ".git"))) {
@@ -94,16 +121,26 @@ if ($DryRun) {
Assert-RemoteInSync $ClaudeSkills $Branch Assert-RemoteInSync $ClaudeSkills $Branch
} else { } else {
$r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch) $r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch)
if ($r.Code -ne 0 -and (Test-TransportFault $r.Out)) {
# A rebase may have been left half-started before the transport died.
Invoke-Git $ClaudeSkills @("rebase", "--abort") | Out-Null
if (Invoke-Doctor "pull --rebase") {
$r = Invoke-Git $ClaudeSkills @("pull", "--rebase", "origin", $Branch)
}
}
if ($r.Code -ne 0) { if ($r.Code -ne 0) {
Write-Host $r.Out Write-Host $r.Out
$stranded = Invoke-Git $ClaudeSkills @("rev-list", "--count", "origin/$Branch..$Branch") $stranded = Invoke-Git $ClaudeSkills @("rev-list", "--count", "origin/$Branch..$Branch")
if ($stranded.Code -eq 0 -and [int]$stranded.Out -gt 0) { if ($stranded.Code -eq 0 -and [int]$stranded.Out -gt 0) {
Write-Host "NOTE: $($stranded.Out) local commit(s) cannot reach the server until this is fixed." Write-Host "NOTE: $($stranded.Out) local commit(s) cannot reach the server until this is fixed."
} }
Fail "pull --rebase failed on primary. Resolve conflicts (or 'git rebase --abort') then re-run. If the transport itself is broken (see skill Phase 1), fix the server -- do not leave commits stranded." Fail "pull --rebase failed on primary. Conflicts are yours to resolve (or 'git rebase --abort'); a transport fault the doctor could not repair is in the skill's Phase 1.5 ladder. Either way: do not leave commits stranded."
} }
Write-Host $r.Out Write-Host $r.Out
$r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch) $r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch)
if ($r.Code -ne 0 -and (Test-TransportFault $r.Out)) {
if (Invoke-Doctor "push") { $r = Invoke-Git $ClaudeSkills @("push", "origin", $Branch) }
}
if ($r.Code -ne 0) { Write-Host $r.Out; Fail "push failed (auth? see skill Phase 1)." } if ($r.Code -ne 0) { Write-Host $r.Out; Fail "push failed (auth? see skill Phase 1)." }
Assert-RemoteInSync $ClaudeSkills $Branch Assert-RemoteInSync $ClaudeSkills $Branch
} }