#!/bin/bash
# ds-autofix —— launchd → headless agent 的 A 档自动修入口
#
# owner 2026-09-07 拍 `open-after-checks`（docs/decision-queue.md Q7）：
# 放行清单全绿后 agent 自行放开自动修，⛔ 缺任一条证据不放开。
# 本脚本 = 那份清单里第 1/3/4 条的**被测对象**。
#
# 🔴 设计要点：**纪律不靠 agent 自觉**。
#    清单第 3/4 条判的是流程纪律，如果纪律只写在 prompt 里，「验过一次」= 验过一次 LLM 的自觉，
#    ⛔ 不可复跑。所以：
#      · `git pull --ff-only` 前置由**本脚本**做（agent 无从跳过）
#      · `add -A` / `--no-verify` / `force` / `merge` / `rebase` 由 **PreToolUse hook 机械拦**
#        （bin/ds-autofix-guard.mjs，自证 23 必拦 + 17 必放）
#      · push 只走 fast-forward；非 ff ⇒ **停手**并以专用退出码报告，⛔ 不 force 不 merge
#      · agent 说「修好了」不算 —— 本脚本**独立复跑采集器**并独立查 git 状态与 guard 日志
#
# 退出码（⛔ 刻意分档，装置故障与被测对象缺陷不得共用一个信号，同 ds-sweep）：
#   0 = 全清（无事可做，或修完并提交/推送成功）
#   1 = 要人看（C 档已登记进队列 / 收尾发现残留 / agent 自陈 needsHuman）
#   2 = 采集器自称装置故障（exit 2）⇒ ⛔ 不得读作「DS 没问题」
#   3 = 本脚本前置不成立（找不到 claude / 仓 / guard / prompt）—— fail-closed
#   4 = **并行提交冲突 ⇒ 已停手**（⛔ 未 force · 未 merge）
#   5 = **提交纪律违例**（收尾核查抓到：受保护文件被提交等）

set -u

LAB="${LAB:-$HOME/Documents/AICoding/VS_Code/ai-ds-lab}"
DS="${DS_ROOT:-$HOME/Documents/AICoding/VS_Code/tvu-design-system}"
STATE_DIR="${STATE_DIR:-$HOME/.claude/ds-autofix}"
QUEUE_FILE="${AUTOFIX_QUEUE:-$LAB/docs/decision-queue.md}"
PUSH_ENABLED="${DS_AUTOFIX_PUSH:-1}"
MAX_TURNS="${AUTOFIX_MAX_TURNS:-80}"

GUARD="$LAB/bin/ds-autofix-guard.mjs"
PROMPT_TPL="$LAB/bin/ds-autofix-prompt.md"

mkdir -p "$STATE_DIR" || { echo "FATAL 建不了 state dir $STATE_DIR" >&2; exit 3; }

TS="$(date +%Y-%m-%dT%H:%M:%S%z)"
STAMP="$(date +%Y-%m-%dT%H%M%S)"
RUN_DIR="$STATE_DIR/run-$STAMP"
mkdir -p "$RUN_DIR" || { echo "FATAL 建不了 run dir $RUN_DIR" >&2; exit 3; }
LOG="$STATE_DIR/ds-autofix.log"
REPORT="$RUN_DIR/run-report.json"

log() { echo "$*" | tee -a "$LOG"; }
jstr() { node -e 'process.stdout.write(JSON.stringify(process.argv[1]??null))' "${1-}"; }
jfile() { if [ -f "${1-}" ]; then node -e 'const fs=require("fs");process.stdout.write(JSON.stringify(fs.readFileSync(process.argv[1],"utf8")))' "$1"; else echo null; fi; }

# 报告先写一份「装置前置」骨架，任何提前 exit 都留得下痕迹
write_report() {
  cat > "$REPORT" <<EOF
{
  "startedAt": $(jstr "$TS"),
  "finishedAt": $(jstr "$(date +%Y-%m-%dT%H:%M:%S%z)"),
  "phase": $(jstr "${PHASE:-preflight}"),
  "exitCode": ${1:-3},
  "verdict": $(jstr "${VERDICT:-未定}"),
  "invokedBy": $(jstr "${AUTOFIX_INVOKED_BY:-manual}"),
  "env": {
    "shell": $(jstr "$0"),
    "node": $(jstr "$(command -v node 2>/dev/null) $(node -v 2>/dev/null)"),
    "pnpm": $(jstr "$(command -v pnpm 2>/dev/null) $(pnpm -v 2>/dev/null)"),
    "claudeBin": $(jstr "${CLAUDE_BIN:-?}"),
    "claudeVersion": $(jstr "${CLAUDE_V:-?}"),
    "dsRoot": $(jstr "$DS"),
    "queueFile": $(jstr "$QUEUE_FILE"),
    "pushEnabled": $(jstr "$PUSH_ENABLED")
  },
  "git": {
    "branch": $(jstr "${BRANCH:-?}"),
    "headBefore": $(jstr "${HEAD_BEFORE:-?}"),
    "headAfterPull": $(jstr "${HEAD_AFTER_PULL:-?}"),
    "headAfterAgent": $(jstr "${HEAD_AFTER:-?}"),
    "protectedPaths": $(node -e 'const s=process.argv[1]||"";process.stdout.write(JSON.stringify(s.split(":").filter(Boolean)))' "${PROTECTED:-}"),
    "newCommits": $(node -e 'const s=process.argv[1]||"";process.stdout.write(JSON.stringify(s.split("\n").filter(Boolean)))' "${NEW_COMMITS:-}"),
    "pushed": $(jstr "${PUSHED:-no}")
  },
  "sweep": {
    "beforeExit": ${SWEEP_RC_BEFORE:-null},
    "afterExit": ${SWEEP_RC_AFTER:-null},
    "artifactBefore": $(jstr "${SWEEP_JSON_BEFORE:-}"),
    "artifactAfter": $(jstr "${SWEEP_JSON_AFTER:-}")
  },
  "agent": {
    "ran": $(jstr "${AGENT_RAN:-no}"),
    "exitCode": ${AGENT_RC:-null},
    "stdoutFile": $(jstr "${AGENT_OUT:-}"),
    "reportBlock": $(jfile "${AGENT_REPORT_BLOCK:-/nonexistent}")
  },
  "guard": {
    "logFile": $(jstr "${GUARD_LOG:-}"),
    "blockedCount": ${GUARD_BLOCKED:-0},
    "allowedCount": ${GUARD_ALLOWED:-0}
  },
  "queue": {
    "file": $(jstr "$QUEUE_FILE"),
    "touched": $(jstr "${QUEUE_TOUCHED:-unknown}"),
    "hashBefore": $(jstr "${QUEUE_HASH_BEFORE:-}"),
    "hashAfter": $(jstr "${QUEUE_HASH_AFTER:-}"),
    "repoHeadBefore": $(jstr "${QUEUE_HEAD_BEFORE:-}"),
    "repoHeadAfter": $(jstr "${QUEUE_HEAD_AFTER:-}")
  },
  "postChecks": $(node -e 'const s=process.argv[1]||"";process.stdout.write(JSON.stringify(s.split("\n").filter(Boolean)))' "${POST_CHECKS:-}")
}
EOF
}

die() { # die <exit> <msg...>
  local rc="$1"; shift
  log "[$TS] $*"
  write_report "$rc"
  exit "$rc"
}

PHASE="preflight"
POST_CHECKS=""

# ── 前置：装置在不在（⛔ 缺了要喊，绝不静默 exit 0）─────────────────────────
command -v node >/dev/null 2>&1 || die 3 "FATAL node 不在 PATH（launchd 不继承交互 shell 的 PATH）。PATH=$PATH"
command -v pnpm >/dev/null 2>&1 || die 3 "FATAL pnpm 不在 PATH —— 闸一条都跑不了。PATH=$PATH"
[ -f "$GUARD" ]      || die 3 "FATAL 找不到 guard $GUARD —— ⛔ 没有机械纪律层就不许唤起有写权的 agent"
[ -f "$PROMPT_TPL" ] || die 3 "FATAL 找不到 prompt 模板 $PROMPT_TPL"
[ -f "$QUEUE_FILE" ] || die 3 "FATAL 找不到 C 档落点 $QUEUE_FILE —— ⛔ C 档无处可落时不许开工"
[ -d "$DS/.git" ]    || die 3 "FATAL 找不到 DS 仓 $DS"
[ -f "$LAB/metrics/ds-sweep.mjs" ] || die 3 "FATAL 找不到采集器 $LAB/metrics/ds-sweep.mjs"

# guard 自证必须当场过 —— ⛔ 一个坏了的 guard 比没有 guard 更危险（它会让人以为有护栏）
if ! node "$GUARD" --self-test >"$RUN_DIR/guard-self-test.txt" 2>&1; then
  die 3 "FATAL guard 自证不过 ⇒ ⛔ 不唤起 agent。明细 $RUN_DIR/guard-self-test.txt"
fi

# ⚠️ launchd 的 claude 与交互 shell 的可能是**两个不同的二进制**（2026-09-07 亲验：
#    bash -l → ServBay 2.1.89 · 交互 → nvm 2.1.251）⇒ 版本必须写进报告，⛔ 别假设。
CLAUDE_BIN="${AUTOFIX_CLAUDE_BIN:-$(command -v claude 2>/dev/null)}"
if [ -z "$CLAUDE_BIN" ] || [ ! -x "$CLAUDE_BIN" ]; then
  die 3 "FATAL 找不到可执行的 claude（AUTOFIX_CLAUDE_BIN='${AUTOFIX_CLAUDE_BIN:-未设}' · PATH 里也没有）⇒ headless agent 起不来"
fi
CLAUDE_V="$("$CLAUDE_BIN" --version 2>&1 | head -1)"
[ -n "$CLAUDE_V" ] || die 3 "FATAL $CLAUDE_BIN --version 没输出 ⇒ 不确定这是不是 claude，⛔ 不冒险唤起"

BRANCH="$(git -C "$DS" rev-parse --abbrev-ref HEAD 2>/dev/null || echo '?')"
HEAD_BEFORE="$(git -C "$DS" rev-parse --short HEAD 2>/dev/null || echo '?')"

# 开工前就脏的文件 = **不属于本次修复**的文件。注入给 guard，机械拦住「顺手提交」。
PROTECTED="$(git -C "$DS" status --porcelain | sed 's/^...//' | tr '\n' ':' | sed 's/:$//')"
export AUTOFIX_PROTECTED_PATHS="$PROTECTED"
# 队列文件所在仓的起手状态 —— POST-4 靠它判「C 档有没有真落地」。
# ⚠️ ⛔ 别用 `git diff` 判：agent 可能把队列改动**提交**了，那时 diff 是空的
#    ⇒ 2026-09-07 R2 真踩到（我的判据把一次通过的 run 误读成不通过）。
QUEUE_REPO="$(git -C "$(dirname "$QUEUE_FILE")" rev-parse --show-toplevel 2>/dev/null || echo '')"
QUEUE_HEAD_BEFORE="$([ -n "$QUEUE_REPO" ] && git -C "$QUEUE_REPO" rev-parse --short HEAD 2>/dev/null || echo '')"
QUEUE_HASH_BEFORE="$(git hash-object "$QUEUE_FILE" 2>/dev/null || echo '')"

GUARD_LOG="$RUN_DIR/guard.jsonl"
export AUTOFIX_GUARD_LOG="$GUARD_LOG"
: > "$GUARD_LOG"

log "[$TS] ▶ ds-autofix 起手 @$DS ($BRANCH @$HEAD_BEFORE) · claude=$CLAUDE_V @ $CLAUDE_BIN"
[ -n "$PROTECTED" ] && log "         开工前就脏（⛔ 不碰）：$PROTECTED"

# ── 第 1 步：pull --ff-only 前置（⛔ 由本脚本做，agent 无从跳过）────────────
PHASE="pull"
if git -C "$DS" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
  # 🔴 fetch 失败 ≠ 分叉。⛔ 别把「拉不到上游」报成「并行冲突」——
  #    那是两种完全不同的处置（前者是装置/网络前置不成立，后者要人裁定冲突）。
  #    DS 的 origin 是自建 Gitea over http ⇒ 不在内网时 fetch 会失败，这条会真触发。
  #    拉不到上游 ⇒ **不知道**有没有并行改动 ⇒ 无人值守时不动手（同「canCommit: null 不得读作能」）。
  if ! git -C "$DS" fetch --quiet 2>>"$LOG"; then
    VERDICT="拉不到上游（fetch 失败）⇒ 无法判断有没有并行改动 ⇒ ⛔ 不唤起有写权的 agent。这是前置不成立，⛔ 不是并行冲突"
    HEAD_AFTER_PULL="$HEAD_BEFORE"
    log "[$TS] FATAL $VERDICT"
    log "         上游 = $(git -C "$DS" rev-parse --abbrev-ref --symbolic-full-name '@{u}' 2>/dev/null || echo '?') —— 明细见 $LOG"
    write_report 3
    exit 3
  fi
  if ! git -C "$DS" pull --ff-only --quiet >>"$LOG" 2>&1; then
    VERDICT="并行冲突：起手 pull 非 fast-forward ⇒ 已停手（⛔ 未 force · 未 merge · 未唤起 agent）"
    HEAD_AFTER_PULL="$HEAD_BEFORE"
    log "[$TS] 🔴 HALT $VERDICT"
    log "         本地 $HEAD_BEFORE · 上游 $(git -C "$DS" rev-parse --short '@{u}' 2>/dev/null || echo '?')"
    log "         ⛔ 冲突处置需要判断 ⇒ 无人值守时正确动作是停手。人来处置后再跑。"
    write_report 4
    exit 4
  fi
  HEAD_AFTER_PULL="$(git -C "$DS" rev-parse --short HEAD)"
  [ "$HEAD_AFTER_PULL" != "$HEAD_BEFORE" ] && log "         pull 前置：$HEAD_BEFORE → $HEAD_AFTER_PULL（fast-forward）"
else
  HEAD_AFTER_PULL="$HEAD_BEFORE"
  log "         ⚠️ 当前分支没有 upstream ⇒ 跳过 pull 前置（如实登记，⛔ 不当它「拉过了」）"
fi

# ── 第 2 步：取数（⛔ 不带管道，管道会吞退出码 lab:N47）────────────────────
PHASE="sweep-before"
SWEEP_JSON_BEFORE="$RUN_DIR/sweep-before.json"
SWEEP_TXT_BEFORE="$RUN_DIR/sweep-before.txt"
node "$LAB/metrics/ds-sweep.mjs" --ds "$DS" --out "$SWEEP_JSON_BEFORE" >"$SWEEP_TXT_BEFORE" 2>&1
SWEEP_RC_BEFORE=$?

case $SWEEP_RC_BEFORE in
  0)
    VERDICT="巡检全清（扫描面内）⇒ 无 A 档可修，⛔ 不唤起 agent"
    log "[$TS] OK   $VERDICT"
    write_report 0
    exit 0
    ;;
  1) log "         巡检报「要人看」⇒ 唤起 agent 分档" ;;
  2)
    VERDICT="采集器自称装置前置不成立（exit 2）⇒ ⛔ 不得读作「DS 没问题」，也⛔ 不在读数不可信时唤起有写权的 agent"
    log "[$TS] BROKEN $VERDICT —— 明细 $SWEEP_TXT_BEFORE"
    write_report 2
    exit 2
    ;;
  *)
    VERDICT="采集器退出码 $SWEEP_RC_BEFORE 不在 0/1/2 之内 ⇒ 当装置故障处置"
    log "[$TS] UNEXPECTED $VERDICT"
    write_report 2
    exit 2
    ;;
esac

# ── 第 3 步：唤起 headless agent ───────────────────────────────────────────
PHASE="agent"
SETTINGS="$RUN_DIR/claude-settings.json"
cat > "$SETTINGS" <<EOF
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "node $GUARD" }]
      }
    ]
  }
}
EOF

PROMPT_FILE="$RUN_DIR/prompt.md"
node -e '
const fs = require("fs")
const [tpl, out, ...pairs] = process.argv.slice(1)
let s = fs.readFileSync(tpl, "utf8")
for (let i = 0; i < pairs.length; i += 2) s = s.split(`{{${pairs[i]}}}`).join(pairs[i + 1])
const left = s.match(/\{\{[A-Z_]+\}\}/g)
if (left) { console.error("✗ prompt 模板还有没替换的占位符：" + left.join(", ")); process.exit(1) }
fs.writeFileSync(out, s)
' "$PROMPT_TPL" "$PROMPT_FILE" \
  DS_ROOT "$DS" \
  DS_HEAD "$HEAD_AFTER_PULL" \
  SWEEP_ARTIFACT "$SWEEP_JSON_BEFORE" \
  SWEEP_TEXT "$SWEEP_TXT_BEFORE" \
  QUEUE_FILE "$QUEUE_FILE" \
  PROTECTED "${PROTECTED:-（无）}" \
  || die 3 "FATAL prompt 模板占位符没替换干净 ⇒ ⛔ 不拿半成品 prompt 唤起 agent"

AGENT_OUT="$RUN_DIR/agent-stdout.json"
AGENT_ERR="$RUN_DIR/agent-stderr.txt"
AGENT_RAN="yes"

( cd "$DS" && "$CLAUDE_BIN" -p "$(cat "$PROMPT_FILE")" \
    --output-format json \
    --permission-mode bypassPermissions \
    --settings "$SETTINGS" \
    --add-dir "$(dirname "$(dirname "$QUEUE_FILE")")" \
    --max-turns "$MAX_TURNS" \
    >"$AGENT_OUT" 2>"$AGENT_ERR" )
AGENT_RC=$?

# agent 的自然语言产出抠出来（⛔ 它只是叙述，证据看下面的收尾核查）
AGENT_TEXT="$RUN_DIR/agent-text.txt"
node -e '
const fs = require("fs")
let t = ""
try {
  const o = JSON.parse(fs.readFileSync(process.argv[1], "utf8"))
  t = typeof o === "string" ? o : (o.result ?? o.text ?? JSON.stringify(o, null, 2))
} catch { t = fs.existsSync(process.argv[1]) ? fs.readFileSync(process.argv[1], "utf8") : "" }
fs.writeFileSync(process.argv[2], String(t))
' "$AGENT_OUT" "$AGENT_TEXT" 2>/dev/null || true

AGENT_REPORT_BLOCK="$RUN_DIR/agent-report-block.txt"
awk '/^=== AUTOFIX REPORT ===/,/^=== END REPORT ===/' "$AGENT_TEXT" > "$AGENT_REPORT_BLOCK" 2>/dev/null || true

# ⚠️ ⛔ 别写成 `$(grep -c … || echo 0)` —— `grep -c` 零匹配时**已经印了 `0` 且退出码 1**，
#    `||` 会再补一个 ⇒ 变量成了 `0\n0`：报告 JSON 非法，`[ -gt ]` 当场报
#    「integer expression expected」。2026-09-07 R2 真踩到，读数留在 run-2026-09-07T163018。
GUARD_BLOCKED="$(grep -c '"decision":"BLOCK"' "$GUARD_LOG" 2>/dev/null | head -1)"
GUARD_ALLOWED="$(grep -c '"decision":"ALLOW"' "$GUARD_LOG" 2>/dev/null | head -1)"
GUARD_BLOCKED="${GUARD_BLOCKED:-0}"
GUARD_ALLOWED="${GUARD_ALLOWED:-0}"

log "         agent 退出码 $AGENT_RC · guard 拦下 $GUARD_BLOCKED 条 / 放行 $GUARD_ALLOWED 条"
if [ "${GUARD_BLOCKED:-0}" -gt 0 ]; then
  log "         ⚠️ agent 试过被拦的命令 —— 明细 $GUARD_LOG（这不是失败，但要留痕）"
fi

# ── 第 4 步：收尾核查（⛔ 不看 agent 怎么说，只看仓库状态）──────────────────
PHASE="post-checks"
HEAD_AFTER="$(git -C "$DS" rev-parse --short HEAD 2>/dev/null || echo '?')"
NEW_COMMITS="$(git -C "$DS" log --format='%h %s' "$HEAD_AFTER_PULL..HEAD" 2>/dev/null)"

DISCIPLINE_BREACH=0

# POST-1 受保护文件⛔ 不得出现在任何新 commit 里（= 没 add -A 的硬证据）
if [ -n "$PROTECTED" ] && [ -n "$NEW_COMMITS" ]; then
  CHANGED="$(git -C "$DS" diff --name-only "$HEAD_AFTER_PULL..HEAD" 2>/dev/null)"
  OLDIFS="$IFS"; IFS=':'
  for p in $PROTECTED; do
    if echo "$CHANGED" | grep -Fxq "$p"; then
      POST_CHECKS="$POST_CHECKS
POST-1 ✗ 受保护文件被提交：$p ⇒ 提交纪律违例（疑似 add -A）"
      DISCIPLINE_BREACH=1
    fi
  done
  IFS="$OLDIFS"
fi
[ "$DISCIPLINE_BREACH" -eq 0 ] && POST_CHECKS="$POST_CHECKS
POST-1 ✓ 新 commit 里没有受保护文件（逐条比对 $(echo "$PROTECTED" | tr ':' '\n' | grep -c . 2>/dev/null | head -1) 个）"

# POST-2 工作树残留：除开工前就脏的那些之外，⛔ 不许留下未提交改动
DIRTY_NOW="$(git -C "$DS" status --porcelain | sed 's/^...//' | sort)"
DIRTY_BEFORE="$(echo "$PROTECTED" | tr ':' '\n' | grep -v '^$' | sort)"
LEFTOVER="$(comm -23 <(echo "$DIRTY_NOW" | grep -v '^$') <(echo "$DIRTY_BEFORE") 2>/dev/null)"
if [ -n "$LEFTOVER" ]; then
  POST_CHECKS="$POST_CHECKS
POST-2 ✗ 工作树有新残留（agent 改了但没提交）：$(echo "$LEFTOVER" | tr '\n' ' ')"
else
  POST_CHECKS="$POST_CHECKS
POST-2 ✓ 无新残留（工作树回到开工前的脏文件集合）"
fi

# POST-3 独立复跑采集器 —— ⛔ agent 说「修好了」不算证据，这一跑才算
PHASE="sweep-after"
SWEEP_JSON_AFTER="$RUN_DIR/sweep-after.json"
SWEEP_TXT_AFTER="$RUN_DIR/sweep-after.txt"
node "$LAB/metrics/ds-sweep.mjs" --ds "$DS" --out "$SWEEP_JSON_AFTER" >"$SWEEP_TXT_AFTER" 2>&1
SWEEP_RC_AFTER=$?
POST_CHECKS="$POST_CHECKS
POST-3 独立复跑采集器：EXIT $SWEEP_RC_BEFORE → $SWEEP_RC_AFTER"

# POST-4 C 档是否真落进队列（agent 只在输出里说一句⛔ 不算）
# ⚠️ 判据 = **文件内容哈希变了**（涵盖「改了没提交」与「改了并提交」两种），
#    ⛔ 不是 `git diff` —— 后者在 agent 提交过之后是空的（R2 实证）。
QUEUE_HASH_AFTER="$(git hash-object "$QUEUE_FILE" 2>/dev/null || echo '')"
QUEUE_HEAD_AFTER="$([ -n "$QUEUE_REPO" ] && git -C "$QUEUE_REPO" rev-parse --short HEAD 2>/dev/null || echo '')"
if [ -n "$QUEUE_HASH_BEFORE" ] && [ "$QUEUE_HASH_AFTER" != "$QUEUE_HASH_BEFORE" ]; then
  QUEUE_TOUCHED="yes"
  POST_CHECKS="$POST_CHECKS
POST-4 ✓ 队列文件内容变了（$QUEUE_HASH_BEFORE → $QUEUE_HASH_AFTER · 队列仓 HEAD $QUEUE_HEAD_BEFORE → ${QUEUE_HEAD_AFTER:-n/a}）"
else
  QUEUE_TOUCHED="no"
  POST_CHECKS="$POST_CHECKS
POST-4 · 队列文件内容未变（若本轮判出 C 档，这是失败信号）"
fi

if [ "$DISCIPLINE_BREACH" -eq 1 ]; then
  VERDICT="提交纪律违例 —— 见 postChecks"
  log "[$TS] 🔴 BREACH $VERDICT"
  write_report 5
  exit 5
fi

# ── 第 5 步：push（只走 fast-forward；非 ff ⇒ 停手，⛔ 不 force 不 merge）───
PHASE="push"
PUSHED="no"
if [ -z "$NEW_COMMITS" ]; then
  PUSHED="n/a（没有新 commit）"
elif [ "$PUSH_ENABLED" != "1" ]; then
  PUSHED="skipped（DS_AUTOFIX_PUSH=$PUSH_ENABLED）"
  log "         ⚠️ push 被关掉 ⇒ 新 commit 只在本地（如实登记）"
else
  # ⚠️ 只为造故障存在的注入点（Q7 第 4 条要验「run 中途 origin 分叉」）。
  #    生产环境不设此变量 ⇒ 这一行是 no-op。
  [ -n "${AUTOFIX_FAULT_PRE_PUSH:-}" ] && { log "         ⚠️ 故障注入：$AUTOFIX_FAULT_PRE_PUSH"; eval "$AUTOFIX_FAULT_PRE_PUSH" >>"$LOG" 2>&1; }
  if git -C "$DS" push --quiet 2>>"$LOG"; then
    PUSHED="yes"
    log "         push ✓ $HEAD_AFTER_PULL..$HEAD_AFTER"
  else
    PUSHED="rejected"
    VERDICT="并行冲突：push 非 fast-forward ⇒ 已停手（⛔ 未 force · 未 merge · 未 rebase）"
    log "[$TS] 🔴 HALT $VERDICT"
    log "         本地新 commit 已在本地（$HEAD_AFTER），⛔ 未做任何历史改写。人来处置："
    log "         先 git fetch 看清上游改了什么，再决定 rebase / merge —— 那是判断，不是机械动作。"
    write_report 4
    exit 4
  fi
fi

# ── 判读 ──────────────────────────────────────────────────────────────────
PHASE="verdict"
NEEDS_HUMAN="$(grep -iE '^needsHuman:' "$AGENT_REPORT_BLOCK" 2>/dev/null | head -1)"
RC=0
if [ "$SWEEP_RC_AFTER" -eq 2 ]; then
  VERDICT="修完复跑时采集器自称装置故障 ⇒ 读数不可信"
  RC=2
elif [ -n "$LEFTOVER" ]; then
  VERDICT="工作树有新残留 ⇒ 要人看（⛔ wrapper 不代 agent 提交）"
  RC=1
elif echo "$NEEDS_HUMAN" | grep -qi 'yes'; then
  VERDICT="agent 自陈 needsHuman: yes（多半是 C 档已登记进队列）⇒ 要人看"
  RC=1
elif [ "$SWEEP_RC_AFTER" -eq 1 ]; then
  VERDICT="修完后巡检仍报「要人看」⇒ 剩下的不是本轮能自动解掉的"
  RC=1
else
  VERDICT="A 档已修并提交${PUSHED:+（push: $PUSHED）}，复跑巡检全清"
  RC=0
fi

log "[$TS] ⇒ EXIT $RC · $VERDICT"
log "         报告 $REPORT"
write_report "$RC"
exit "$RC"
