#!/usr/bin/env bash
# run-plan-lib.sh — deterministic plumbing for the run-plan controller.
#
# WHY THIS EXISTS: the run-plan engine cannot exec shell (Workflow sandbox = no fs/child_process),
# so every repo touch is dispatched to a subagent that has Bash. Deterministic steps were prose the
# subagent re-derived each run — and subagents REFORMULATE commands (a `ccr start &` became a blocking
# foreground pipe and hung a run 46 min; a `reset --hard <task-branch>` nearly became `reset --hard
# <integration>` and would have nuked committed fixes). This lib makes those steps TESTED code the
# agent merely invokes by path, collapsing the reformulation surface to one fixed command.
#
# CONTRACT: each subcommand prints ONE line of JSON on stdout matching the engine's agent() schema
# EXACTLY (the agent relays stdout verbatim as its structured output). Diagnostics go to stderr.
# Exit 0 always on a completed check (a "violation"/"not ok" is DATA in the JSON, not a shell error);
# exit non-zero ONLY on a usage/environment fault the agent must surface.
#
# Subcommands: metagate  (setup | lease | commit added in later waves)
set -uo pipefail   # NOT -e: greps that find nothing return 1 by design; we guard explicitly.

# ── JSON helpers ───────────────────────────────────────────────────────────
# Escape a string for embedding inside a JSON double-quoted value. Details are controlled,
# single-line, and NEVER contain a captured secret (we print the file, not the match).
jstr() { local s=${1//\\/\\\\}; s=${s//\"/\\\"}; s=${s//[$'\x01'-$'\x1f']/ }; printf '%s' "$s"; }

# ── tmp root resolution — NEVER /tmp (a full/tmpfs /tmp crashed the desktop before) ──────────
# Prefers <repoRoot>/tmp (writable, git-repo-local); falls back to ~/tmp only if that fails.
resolve_tmp_root() {
  local root=${1:-$PWD} dir
  dir="$root/tmp"
  if mkdir -p "$dir" 2>/dev/null && [[ -w "$dir" ]]; then
    printf '%s\n' "$dir"; return 0
  fi
  dir="$HOME/tmp"
  mkdir -p "$dir" 2>/dev/null
  printf '%s\n' "$dir"
}

# ── run-state beacons (~/.claude/workflows/.run, gitignored) ──────────────────
# Per-run files the SessionStart resume hook reads to tell a LIVE run from an orphaned one:
#   <slug>.owner  — owning session id, stamped ONCE at launch by the main thread (ownerstamp).
#   <slug>.pid    — "<host> <cli-pid> <starttime>": the owning CLI PROCESS. Same-host, the hook checks
#                   it with /proc — a deterministic live/dead answer with NO staleness window (the
#                   primary cross-session guard against double-driving a live run).
#   <slug>.live   — touched by the deterministic task seam (lease/commit/reconcile) → "workflow made
#                   progress". Only a CROSS-HOST fallback now (a transcript/beat can go cold while a bg
#                   run is healthy, so mtime alone is not trusted where the pid can be checked).
# RUN_DIR is resolved from THIS file's location so it is machine-independent (env-overridable for tests).
RUN_DIR="${RUN_PLAN_RUN_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." 2>/dev/null && pwd)/.run}"

# Beat the liveness file. Called from the deterministic seam, NEVER a dispatch prompt (an LLM skips
# boilerplate, and a missed beat makes a live run look dead). Fail-soft: never breaks the JSON contract.
_hb() { local slug=${1:-}; [[ -n "$slug" ]] || return 0; mkdir -p "$RUN_DIR" 2>/dev/null && touch "$RUN_DIR/$slug.live" 2>/dev/null; return 0; }

# The owning CLI process id: walk up from this shell to the `claude` process (the long-lived session
# process — a Bash tool call is its direct child). Falls back to PPID if not found (best effort). The
# resume hook checks this pid with /proc on the same host to tell a live owner from a dead one WITHOUT
# a staleness window — the deterministic close for the false-orphan→double-driver hole.
_cli_pid() {
  local p=${PPID:-} comm i
  for i in 1 2 3 4 5 6; do
    [[ -z "$p" || "$p" == "1" ]] && break
    comm=$(ps -o comm= -p "$p" 2>/dev/null | tr -d '[:space:]')
    [[ "$comm" == "claude" ]] && { printf '%s' "$p"; return 0; }
    p=$(ps -o ppid= -p "$p" 2>/dev/null | tr -d '[:space:]')
  done
  printf '%s' "${PPID:-0}"
}
# Start-time (jiffies since boot, /proc stat field 22) for a pid — the pid-REUSE guard. The comm field
# is parenthesized and may contain spaces, so split AFTER the last ')'. Empty if /proc is unavailable.
_proc_start() {
  local pid=${1:-} s
  [[ -r "/proc/$pid/stat" ]] && s=$(cat "/proc/$pid/stat" 2>/dev/null) || { printf ''; return 0; }
  s=${s##*) }
  awk '{print $20}' <<< "$s"   # field 22 overall = field 20 after dropping "pid (comm) "
}

# ── metagate: mechanical meta-gates on diff base..head. "Pure checks, no judgment." ─────────
# Args: <wt> <base> <head>
# Emits: {"pass":bool,"violations":[{"gate":string,"detail":string}]}
metagate() {
  local wt=$1 base=$2 head=$3
  [[ -d "$wt/.git" || -f "$wt/.git" ]] || { echo "metagate: not a git worktree: $wt" >&2; return 3; }
  local G=() D=()                      # parallel violation arrays: gate, detail
  add() { G+=("$1"); D+=("$2"); }

  local range="$base..$head"

  # no-amend: >=1 new commit AND base is an ancestor of head (no rebase/amend orphaned base).
  local n_commits
  n_commits=$(git -C "$wt" rev-list --count "$range" 2>/dev/null || echo 0)
  if [[ "$n_commits" -lt 1 ]]; then
    add no-amend "no new commits on $range (count=$n_commits)"
  elif ! git -C "$wt" merge-base --is-ancestor "$base" "$head" 2>/dev/null; then
    add no-amend "base $base is not an ancestor of head — rebase/amend of base detected"
  fi

  # no-coauthor: commit messages in range must not carry a co-author / generated-by trailer.
  local bodies
  bodies=$(git -C "$wt" log "$range" --format=%B 2>/dev/null || true)
  if printf '%s' "$bodies" | grep -qiE 'Co-Authored-By|Generated with'; then
    add no-coauthor "commit message in $range contains Co-Authored-By / Generated-with trailer"
  fi

  # Per-file added-line scans (stub / conflict-marker / secret). Added line = diff '+' not '+++'.
  local files f added
  files=$(git -C "$wt" diff --name-only "$range" 2>/dev/null || true)
  while IFS= read -r f; do
    [[ -z "$f" ]] && continue
    added=$(git -C "$wt" diff "$range" -- "$f" 2>/dev/null | grep '^+' | grep -v '^+++' || true)
    [[ -z "$added" ]] && continue

    # no-conflict-marker: any file. An added line that IS a conflict marker.
    if printf '%s\n' "$added" | grep -qE '^\+(<<<<<<<|=======|>>>>>>>)'; then
      add no-conflict-marker "conflict marker added in $f"
    fi

    # no-stub: code files only. Exclude md / specs / plans / test+fixture files.
    if ! printf '%s' "$f" | grep -qE '\.md$|(^|/)(specs?|plans?)/|(^|/)__?fixtures?__?/|\.(test|spec)\.'; then
      if printf '%s\n' "$added" | grep -qE 'TODO|FIXME|not[ _]?implemented|NotImplemented|placeholder|^\+[[:space:]]*\.\.\.[[:space:]]*$'; then
        add no-stub "stub/placeholder marker added in $f"
      fi
    fi

    # no-secret: code files only. Exclude md / example / lock / snap / test+fixture files.
    if ! printf '%s' "$f" | grep -qE '\.(md|example|lock|snap)$|(^|/)__?fixtures?__?/|\.(test|spec)\.'; then
      local hits
      hits=$(printf '%s\n' "$added" | grep -aE \
        '\-{5}BEGIN [A-Z ]*PRIVATE KEY\-{5}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9]{36}|xox[baprs]-[A-Za-z0-9-]{10,}' \
        || true)
      # keyword = high-entropy literal assignment (case-insensitive)
      local kw
      kw=$(printf '%s\n' "$added" | grep -aiE \
        "(secret|api[_-]?key|password|passwd|credential|auth[_-]?token|access[_-]?token)[[:space:]]*[:=][[:space:]]*['\"\`][A-Za-z0-9+/=_-]{16,}['\"\`]" \
        || true)
      # allowlist: drop kw lines that reference an env/var or an obvious placeholder literal.
      if [[ -n "$kw" ]]; then
        kw=$(printf '%s\n' "$kw" | grep -avE \
          'process\.env\.|import\.meta\.env\.|\$[A-Za-z_]|\{\{|example|dummy|placeholder|redacted|changeme|your[_-]|xxxx|<.+>' \
          || true)
      fi
      if [[ -n "$hits" || -n "$kw" ]]; then
        add no-secret "committed credential literal in $f"   # file only — NEVER echo the secret
      fi
    fi
  done <<< "$files"

  # no-warn: DETERMINISTIC, baseline-free, allowlist-justified — NO subagent benign judgment.
  # gate0 writes the verbatim warning lines to $wt/.run-warns (always — empty file = zero warns).
  # $wt/.warnignore (committed in the TARGET repo) lists benign patterns. A warning blocks UNLESS a
  # pattern matches it — but a pattern is EFFECTIVE only if (a) a '#' justification line sits directly
  # above it AND (b) it is not a catch-all (does not match a bare warning keyword). These two structural
  # checks stop an agent rubber-stamping fixable warnings: every suppression states a reason and targets
  # one specific message. The judgment half — "is this reason honest / is the warning truly unfixable" —
  # lives in the review step (reviewHigh/reviewLow scrutinize .warnignore additions in base..head).
  #   • .run-warns MISSING         → gate0 handoff failed              → fail-closed block.
  #   • warning, no effective rule  → nothing justified                → block.
  #   • pattern w/o '#' above it     → unjustified suppression          → ignored (warning still blocks).
  #   • catch-all pattern            → would mute future real warnings  → rejected + block.
  #   • .warnignore regex error      → cannot prove benign              → fail-closed block.
  # Self-documenting: the halt detail IS the doc — no agent should read this lib to resolve a block.
  local warn_howto="To resolve: FIX the warning if it is fixable (your own code, or a bumpable dep) — suppression is ONLY for genuinely unfixable upstream/third-party warnings. To suppress one, add to <repo-root>/.warnignore a grep -E pattern matching ONLY that exact message, with a '#' line directly ABOVE it stating why it cannot be fixed; commit it on the task branch (review will scrutinise the new entry). A pattern with no '#' above it, or a catch-all matching a bare 'warning'/'deprecated' keyword, is rejected. Fail-closed: anything fixable must be FIXED, not allowlisted."
  local warnfile="$wt/.run-warns" ignorefile="$wt/.warnignore"
  if [[ ! -f "$warnfile" ]]; then
    add no-warn "gate0 did not write $warnfile (it must ALWAYS write it — empty file if zero warnings). This is a gate0/handoff failure, NOT a .warnignore task: re-run gate0 so it emits the file. Fail-closed."
  else
    # normalize: strip the (per-run, randomized) worktree path prefix so patterns are stable; drop blanks.
    local warns
    warns=$(sed "s#${wt}/##g" "$warnfile" | grep -vE '^[[:space:]]*$' || true)
    if [[ -n "$warns" ]]; then
      # Effective allowlist = patterns that are BOTH justified (a '#' line directly above) AND specific
      # (not a catch-all). A bare keyword like "warning"/"deprecated" is a canary: any pattern matching
      # it would mute future real warnings → rejected.
      local justified='' effective='' broad='' residual rc=0
      if [[ -f "$ignorefile" ]]; then
        justified=$(awk '
          /^[[:space:]]*#/ { armed=1; next }     # comment arms justification for the NEXT pattern only
          /^[[:space:]]*$/ { next }              # blank: keep armed state
          { if (armed) print; armed=0 }' "$ignorefile")
      fi
      if [[ -n "$justified" ]]; then
        local canary=$'warning\nWARN\nwarn\nWarning\ndeprecated\nDEPRECATED\nwarning: deprecated' p
        while IFS= read -r p; do
          [[ -z "$p" ]] && continue
          if printf '%s\n' "$canary" | grep -qE -- "$p" 2>/dev/null; then
            broad+="$p"$'\n'
          else
            effective+="$p"$'\n'
          fi
        done <<< "$justified"
      fi
      broad=$(printf '%s' "$broad" | grep -vE '^[[:space:]]*$' || true)
      effective=$(printf '%s' "$effective" | grep -vE '^[[:space:]]*$' || true)
      if [[ -n "$broad" ]]; then
        add no-warn "over-broad .warnignore pattern(s) rejected — each matches a bare warning keyword and would mute future real warnings: $(printf '%s' "$broad" | awk 'NR<=3{if(NR>1)printf" | ";printf"%s",$0}'). Make each pattern specific to the exact benign message."
      fi
      if [[ -n "$effective" ]]; then
        residual=$(printf '%s\n' "$warns" | grep -vEf <(printf '%s\n' "$effective"))
        rc=$?
        if [[ $rc -gt 1 ]]; then
          add no-warn "$ignorefile contains an invalid grep -E pattern (grep rc=$rc). Fix the regex so the file parses; fail-closed until it does. $warn_howto"
          residual=''
        fi
      else
        residual="$warns"
      fi
      if [[ -n "$residual" ]]; then
        local n sample
        n=$(printf '%s\n' "$residual" | grep -c . || true)
        sample=$(printf '%s\n' "$residual" | awk 'NR<=3{if(NR>1)printf" | ";printf"%s",$0}')
        add no-warn "${n} build warning(s) not justified by .warnignore. Triage each: real → FIX it; genuinely unfixable upstream → allowlist it. $warn_howto  WARNINGS: ${sample}"
      fi
    fi
  fi

  # emit
  local pass=true; [[ ${#G[@]} -gt 0 ]] && pass=false
  local out='{"pass":'"$pass"',"violations":[' i
  for ((i=0; i<${#G[@]}; i++)); do
    [[ $i -gt 0 ]] && out+=','
    out+='{"gate":"'"$(jstr "${G[$i]}")"'","detail":"'"$(jstr "${D[$i]}")"'"}'
  done
  out+=']}'
  printf '%s\n' "$out"
}

# ── financegate: per-file MEASURED finance detector over diff base..head ───────
# Invokes security-gate's finance detector (gpt-5.4/low via codex backend, k=1 production config) on each changed code
# file, then aggregates its emitted prevent/contract finding JSON into S_REVIEW. Pure plumbing:
# the per-file gate.py invocation + the JSON aggregation are deterministic — a subagent must NOT
# re-derive the command or the field mapping. Single source + regression suite live here.
#
# Args:  <wt> <base> <head> <slug> <taskId>
# Emits: {"clean":bool,"findings":[{"file":string,"issue":string,"severity":string}]}
#        finding.file = the finding's file; .issue = the finding message; .severity = its level.
#
# FAIL-CLOSED / no-false-clean (stricter than the old prose by design — grounded in gate.py:
#   --emit is written UNCONDITIONALLY at the end of main(), so:):
#   • emit PRESENT + valid JSON + findings:[]  → detector ran, found nothing / file out of finance
#                                                 scope → legitimately CLEAN for that file.
#   • emit ABSENT or invalid JSON              → detector did NOT complete (crash/early-exit) →
#                                                 a coverage-failure finding, NEVER silent-clean.
#   • emit status=="degraded"                  → detector's own coverage-incomplete signal →
#                                                 surfaced as a coverage finding (no-false-clean).
# The gate.py command is overridable via $GATE_CMD (whitespace-split) for hermetic testing;
# the real run uses the production path below.
financegate() {
  local wt=$1 base=$2 head=$3 slug=$4 taskId=$5
  [[ -d "$wt/.git" || -f "$wt/.git" ]] || { echo "financegate: not a git worktree: $wt" >&2; return 3; }
  local gate="/home/user/Projects/security-gate/orchestrator/gate.py"
  local -a gate_cmd
  if [[ -n "${GATE_CMD:-}" ]]; then read -ra gate_cmd <<< "$GATE_CMD"; else gate_cmd=(python3 "$gate"); fi
  [[ -e "${gate_cmd[-1]}" ]] || { echo "financegate: detector not found: ${gate_cmd[-1]}" >&2; return 3; }

  local range="$base..$head" files f
  files=$(git -C "$wt" diff --name-only "$range" 2>/dev/null | grep -E '\.(ts|tsx|js|mjs|astro)$' || true)

  local tmpd; tmpd=$(mktemp -d) || { echo "financegate: mktemp failed" >&2; return 3; }
  trap 'rm -rf "$tmpd"' RETURN
  : > "$tmpd/manifest.tsv"

  local n=0
  while IFS= read -r f; do
    [[ -z "$f" ]] && continue
    # skip test / spec / fixture files (md already excluded by the extension grep above)
    printf '%s' "$f" | grep -qE '\.(test|spec)\.|(^|/)__?fixtures?__?/' && continue
    # skip DELETIONS: diff --name-only lists removed files too; nothing to scan, and running the
    # detector on a vanished path would crash → a spurious coverage failure under absent=failure.
    [[ -f "$wt/$f" ]] || continue
    SG_LLM_BACKEND=codex "${gate_cmd[@]}" "$wt/$f" --detector finance --k 1 --model gpt-5.4 --effort low --emit "$tmpd/$n.json" >/dev/null 2>&1
    printf '%s\t%s\t%s\n' "$n" "$?" "$f" >> "$tmpd/manifest.tsv"
    n=$((n+1))
  done <<< "$files"

  # Aggregate every emit into the S_REVIEW shape. One python pass: robust against absent/invalid
  # emits and honours the detector's degraded status. Prints exactly one JSON line on stdout.
  python3 - "$tmpd" <<'PY'
import sys, os, json
tmpd = sys.argv[1]
findings, clean = [], True
manifest = os.path.join(tmpd, 'manifest.tsv')
rows = []
try:
    with open(manifest, encoding='utf-8') as m:
        for line in m:
            line = line.rstrip('\n')
            if line:
                rows.append(line.split('\t', 2))
except FileNotFoundError:
    pass
for idx, rc, fpath in rows:
    emit = os.path.join(tmpd, f'{idx}.json')
    if not os.path.exists(emit) or os.path.getsize(emit) == 0:
        findings.append({"file": fpath, "issue": f"finance detector did not complete (rc={rc}, no emit) — coverage failure, not clean", "severity": "error"})
        clean = False; continue
    try:
        with open(emit, encoding='utf-8') as fh:
            d = json.load(fh)
    except Exception as e:
        findings.append({"file": fpath, "issue": f"finance emit unparseable (rc={rc}): {e} — coverage failure, not clean", "severity": "error"})
        clean = False; continue
    for fd in (d.get('findings') or []):
        findings.append({"file": str(fd.get('file') or fpath),
                         "issue": str(fd.get('message', '(no message)')),
                         "severity": str(fd.get('level', 'error'))})
        clean = False
    if d.get('status') == 'degraded':
        unres = (d.get('coverage') or {}).get('unresolved') or []
        findings.append({"file": fpath, "issue": "finance coverage degraded: " + ("; ".join(map(str, unres)) if unres else "unresolved"), "severity": "warn"})
        clean = False
print(json.dumps({"clean": clean, "findings": findings}, separators=(',', ':'), ensure_ascii=False))
PY
}

# ── S_IMPLEMENT / S_DONE JSON emitters ───────────────────────────────────────
# emit_implement <ok> <baseSha> <headSha> <resumed|""> <detail>   (resumed "" => field omitted)
emit_implement() {
  local out='{"ok":'"$1"',"baseSha":"'"$(jstr "$2")"'","headSha":"'"$(jstr "$3")"'"'
  [[ -n "$4" ]] && out+=',"resumed":'"$4"
  out+=',"detail":"'"$(jstr "$5")"'"}'
  printf '%s\n' "$out"
}
# emit_done <committed> <detail>
emit_done() { printf '{"committed":%s,"detail":"%s"}\n' "$1" "$(jstr "$2")"; }

# is the path a worktree REGISTERED to this repo? (never delete a path we didn't create)
is_registered_wt() { git -C "$1" worktree list --porcelain 2>/dev/null | grep -qxF "worktree $2"; }

# jsonl_is_status <path> <taskId> <status> → exit 0 if that task's status == <status>, else 1.
jsonl_is_status() {
  python3 - "$1" "$2" "$3" <<'PY'
import sys, json
path, taskId, want = sys.argv[1:4]
try:
    with open(path, encoding='utf-8') as f:
        for ln in f:
            s = ln.strip()
            if not s: continue
            try: o = json.loads(s)
            except Exception: continue
            if isinstance(o, dict) and o.get('type') == 'task' and o.get('id') == taskId:
                sys.exit(0 if o.get('status') == want else 1)
except Exception:
    sys.exit(1)
sys.exit(1)
PY
}

# ── jsonl_set_task: atomic, single-target, self-verifying status/lease rewrite ─
# Touches ONLY the one type:task line whose id matches; every other line byte-identical; temp+os.replace.
# jsonl_set_task <path> <taskId> <newStatus> <lease_ts_epoch | DROP>
jsonl_set_task() {
  python3 - "$1" "$2" "$3" "$4" <<'PY'
import sys, json, os, tempfile
path, taskId, status, lease = sys.argv[1:5]
with open(path, 'r', encoding='utf-8') as f:
    lines = f.readlines()
out, hits = [], 0
for ln in lines:
    s = ln.rstrip('\n')
    if not s.strip():
        out.append(ln); continue
    try:
        obj = json.loads(s)
    except Exception:
        out.append(ln); continue          # never touch an unparseable line
    if isinstance(obj, dict) and obj.get('type') == 'task' and obj.get('id') == taskId:
        hits += 1
        obj['status'] = status
        if lease == 'DROP': obj.pop('lease_ts', None)
        else: obj['lease_ts'] = int(lease)
        out.append(json.dumps(obj, separators=(',', ':'), ensure_ascii=False) + ('\n' if ln.endswith('\n') else ''))
    else:
        out.append(ln)                     # verbatim — byte-identical
if hits != 1:
    sys.stderr.write(f'expected exactly 1 task id={taskId}, found {hits}\n'); sys.exit(1)
if len(out) != len(lines):
    sys.stderr.write('line count changed\n'); sys.exit(1)
d = os.path.dirname(os.path.abspath(path))
fd, tmp = tempfile.mkstemp(dir=d)
try:
    with os.fdopen(fd, 'w', encoding='utf-8') as g: g.writelines(out)
    os.replace(tmp, path)
except Exception as e:
    try: os.unlink(tmp)
    except OSError: pass
    sys.stderr.write(f'write failed: {e}\n'); sys.exit(1)
PY
}

# ── setup: resolve base, create integration branch + throwaway worktree ───────
# Non-destructive: only CREATES; refuses to delete a path it didn't register. Fail-closed.
# Args: <repoRoot> <integration> <base_branch|""> <slug>   Emits S_IMPLEMENT (detail = intWt abs path).
setup() {
  local repoRoot=$1 integration=$2 base_decl=$3 slug=$4
  [[ -e "$repoRoot/.git" ]] || { emit_implement false "" "" "" "not a git repo: $repoRoot"; return 0; }
  git -C "$repoRoot" fetch --quiet origin 2>/dev/null || true   # best-effort; offline is fine
  local BASE=""
  if [[ -n "$base_decl" ]]; then
    BASE=$base_decl
  else
    BASE=$(git -C "$repoRoot" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null || true)
    if [[ -z "$BASE" ]]; then
      local c; for c in origin/main origin/master main master; do
        git -C "$repoRoot" rev-parse --verify "$c^{commit}" >/dev/null 2>&1 && { BASE=$c; break; }
      done
    fi
  fi
  [[ -n "$BASE" ]] || { emit_implement false "" "" "" "base unresolved (no declared base, no origin/HEAD, no main/master)"; return 0; }
  git -C "$repoRoot" rev-parse --verify "$BASE^{commit}" >/dev/null 2>&1 \
    || { emit_implement false "" "" "" "base unresolved: $BASE"; return 0; }   # NEVER fall back to checkout HEAD
  if ! git -C "$repoRoot" show-ref --verify --quiet "refs/heads/$integration"; then
    git -C "$repoRoot" branch "$integration" "$BASE" 2>/dev/null \
      || { emit_implement false "" "" "" "cannot create integration $integration from $BASE"; return 0; }
  fi
  local intWt="$repoRoot/.wt-$slug-int"
  if is_registered_wt "$repoRoot" "$intWt"; then
    git -C "$intWt" reset --hard "$integration" >/dev/null 2>&1 \
      || { emit_implement false "" "" "" "cannot reset existing intWt to $integration"; return 0; }
  elif [[ -e "$intWt" ]]; then
    emit_implement false "" "" "" "intWt exists but is NOT a registered worktree: $intWt (refusing to delete)"; return 0
  else
    git -C "$repoRoot" worktree add "$intWt" "$integration" >/dev/null 2>&1 \
      || { emit_implement false "" "" "" "worktree add failed: $intWt"; return 0; }
  fi
  local tip; tip=$(git -C "$repoRoot" rev-parse "$integration" 2>/dev/null || echo "")
  emit_implement true "$BASE" "$tip" "" "$intWt"   # baseSha carries the RESOLVED base ref (codex integrate review threads it as --base); headSha=tip; caller uses ok+detail+baseSha
}

# ── lease: task branch + worktree, RESUME-SAFE (preserve committed work) ───────
# SAFETY INVARIANT: when the task branch EXISTS, reset target is the TASK BRANCH ONLY — never integration.
# Args: <repoRoot> <integration> <slug> <taskId> <jsonlPath|"">   Emits S_IMPLEMENT (detail = wt abs path).
lease() {
  local repoRoot=$1 integration=$2 slug=$3 taskId=$4 jsonlPath=$5
  _hb "$slug"   # task start → liveness beat
  local taskBranch="plan/$slug--$taskId" wt; wt="$(resolve_tmp_root "$repoRoot")/wt-$slug-$taskId"
  [[ -e "$repoRoot/.git" ]] || { emit_implement false "" "" false "not a git repo: $repoRoot"; return 0; }
  git -C "$repoRoot" rev-parse --verify "refs/heads/$integration" >/dev/null 2>&1 \
    || { emit_implement false "" "" false "integration branch missing: $integration"; return 0; }

  if git -C "$repoRoot" show-ref --verify --quiet "refs/heads/$taskBranch"; then
    # EXISTS — a prior run leased it. Reuse AT ITS OWN HEAD. reset target = taskBranch (NEVER integration).
    if is_registered_wt "$repoRoot" "$wt"; then
      git -C "$wt" checkout -f "$taskBranch" >/dev/null 2>&1 \
        && git -C "$wt" reset --hard "$taskBranch" >/dev/null 2>&1 \
        || { emit_implement false "" "" false "reuse of existing task worktree failed"; return 0; }
    elif [[ -e "$wt" ]]; then
      emit_implement false "" "" false "wt exists but is NOT a registered worktree: $wt (refusing to delete)"; return 0
    else
      git -C "$repoRoot" worktree add "$wt" "$taskBranch" >/dev/null 2>&1 \
        || { emit_implement false "" "" false "worktree add (existing branch) failed"; return 0; }
    fi
  else
    # ABSENT — fresh off integration (no commits exist to lose, so reset-to-integration is safe HERE only).
    if [[ -e "$wt" ]] && ! is_registered_wt "$repoRoot" "$wt"; then
      emit_implement false "" "" false "wt exists but is NOT a registered worktree: $wt (refusing to delete)"; return 0
    fi
    if is_registered_wt "$repoRoot" "$wt"; then
      git -C "$wt" checkout -fB "$taskBranch" "$integration" >/dev/null 2>&1 \
        && git -C "$wt" reset --hard "$integration" >/dev/null 2>&1 \
        || { emit_implement false "" "" false "fresh re-point of stray worktree failed"; return 0; }
    else
      git -C "$repoRoot" worktree add -b "$taskBranch" "$wt" "$integration" >/dev/null 2>&1 \
        || { emit_implement false "" "" false "worktree add -b failed"; return 0; }
    fi
  fi

  local baseSha headSha resumed=false
  baseSha=$(git -C "$wt" merge-base "$integration" HEAD 2>/dev/null || echo "")
  headSha=$(git -C "$wt" rev-parse HEAD 2>/dev/null || echo "")
  [[ -n "$baseSha" && -n "$headSha" ]] \
    || { emit_implement false "$baseSha" "$headSha" false "could not resolve base/head in $wt"; return 0; }
  [[ "$headSha" != "$baseSha" ]] && resumed=true
  if [[ -n "$jsonlPath" && -f "$jsonlPath" ]]; then
    jsonl_set_task "$jsonlPath" "$taskId" WORKING "$(date +%s)" \
      || { emit_implement false "$baseSha" "$headSha" "$resumed" "jsonl update failed for $taskId"; return 0; }
  fi
  # WS2 guard: task mutation MUST run in a LINKED worktree, never the primary main checkout.
  # Primary: --git-dir == --git-common-dir. Linked worktree: --git-dir is <common>/worktrees/<id>.
  if [[ "$(git -C "$wt" rev-parse --git-dir 2>/dev/null)" == "$(git -C "$wt" rev-parse --git-common-dir 2>/dev/null)" ]]; then
    emit_implement false "$baseSha" "$headSha" "$resumed" "refusing: task worktree $wt is the PRIMARY checkout, not a linked worktree"; return 0
  fi
  emit_implement true "$baseSha" "$headSha" "$resumed" "$wt"
}

# ── commit: merge task branch into integration, mark COMMITTED, then clean up ──
# Deletes the task branch/worktree ONLY after a VERIFIED-INCLUDED merge AND a successful jsonl mark.
# On conflict or any verify failure: abort, delete NOTHING (resume-safe). NEVER push, NEVER touch main.
# Args: <repoRoot> <intWt> <integration> <slug> <taskId> <jsonlPath|"">   Emits S_DONE.
commit() {
  local repoRoot=$1 intWt=$2 integration=$3 slug=$4 taskId=$5 jsonlPath=$6
  _hb "$slug"   # task end → liveness beat
  local taskBranch="plan/$slug--$taskId" wt; wt="$(resolve_tmp_root "$repoRoot")/wt-$slug-$taskId"
  [[ -d "$intWt" ]] || { emit_done false "intWt missing: $intWt"; return 0; }
  local cur; cur=$(git -C "$intWt" symbolic-ref --short HEAD 2>/dev/null || echo "")
  [[ "$cur" == "$integration" ]] || { emit_done false "intWt is on '$cur', expected integration '$integration' — refusing to merge"; return 0; }
  # idempotent re-run guard: a prior tier may have completed the merge + COMMITTED mark but lost its relay
  # (agent death/fabrication), so the chain re-runs commit on the next tier — by then the task branch is
  # already deleted. COMMITTED is the durable mark, set only after a verified-included merge → this is a no-op.
  if [[ -n "$jsonlPath" && -f "$jsonlPath" ]] && jsonl_is_status "$jsonlPath" "$taskId" COMMITTED; then
    emit_done true "already COMMITTED ($taskId) — idempotent re-run"; return 0
  fi
  git -C "$intWt" rev-parse --verify "$taskBranch" >/dev/null 2>&1 \
    || { emit_done false "task branch missing: $taskBranch"; return 0; }

  if ! git -C "$intWt" merge --no-ff --no-edit "$taskBranch" >/dev/null 2>&1; then
    git -C "$intWt" merge --abort >/dev/null 2>&1 || true
    emit_done false "merge conflict integrating $taskBranch — aborted, nothing deleted"; return 0
  fi
  # verify the merge truly included the task branch before any deletion
  if ! git -C "$intWt" merge-base --is-ancestor "$taskBranch" HEAD 2>/dev/null; then
    emit_done false "post-merge verify failed: $taskBranch not ancestor of integration HEAD — NOT deleting"; return 0
  fi
  if [[ -n "$jsonlPath" && -f "$jsonlPath" ]]; then
    jsonl_set_task "$jsonlPath" "$taskId" COMMITTED DROP \
      || { emit_done false "merge ok but jsonl update failed for $taskId — NOT deleting branch (resume-safe)"; return 0; }
  fi
  git -C "$repoRoot" worktree remove "$wt" --force >/dev/null 2>&1 || true
  git -C "$repoRoot" branch -D "$taskBranch" >/dev/null 2>&1 || true
  emit_done true "merged $taskBranch into $integration; marked COMMITTED"
}

# ── reconcile: deterministic status recovery vs git, PERSISTED to the jsonl ────
# Runs AFTER setup, so "done" = a commit for the task id is reachable from the integration branch
# (work pre-existing on base, or merged by a prior run). Scans EVERY task incl. PENDING:
#   • not-done status + commit reachable      → COMMITTED   (the pre-done-task recovery)
#   • COMMITTED      + NO commit reachable    → PENDING     (never trust a phantom COMMITTED)
# Every change is written through jsonl_set_task, so the recovery survives a later HALT instead of
# living only in the controller's memory — the gap that forced repeated manual JSONL fixes.
# Match is EXACT (no substring id collisions): an id extracted from a feat(<id>): conventional scope
# OR from a merge of plan/<slug>--<id> (the controller's own --no-ff merge subject), compared == taskId.
# Args:  <repoRoot> <integration> <slug> <jsonlPath>
# Emits: {"tasks":[{"id":string,"status":string,"note":string}]}   (ONLY changed tasks)
reconcile() {
  local repoRoot=$1 integration=$2 slug=$3 jsonlPath=$4
  _hb "$slug"   # wave boundary → liveness beat
  [[ -e "$repoRoot/.git" ]] || { echo "reconcile: not a git repo: $repoRoot" >&2; return 3; }
  [[ -f "$jsonlPath" ]] || { echo "reconcile: jsonl not found: $jsonlPath" >&2; return 3; }
  # integration branch absent ⇒ fresh run, nothing merged ⇒ no changes (not an error).
  if ! git -C "$repoRoot" rev-parse --verify "refs/heads/$integration" >/dev/null 2>&1; then
    printf '{"tasks":[]}\n'; return 0
  fi
  local tmpd; tmpd=$(mktemp -d) || { echo "reconcile: mktemp failed" >&2; return 3; }
  trap 'rm -rf "$tmpd"' RETURN
  git -C "$repoRoot" log "$integration" --format='%s' > "$tmpd/subjects.txt" 2>/dev/null || true

  # python derives the change list (id<TAB>newStatus<TAB>note) — pure, no writes here.
  # landed = subject-scope match (feat(id):/merge marker) OR the task's own recorded `commit`
  # SHA is an ancestor of integration — covers commits predating the scope-tag convention
  # (e.g. a hand-written "chore: ..." commit) that subject-scanning can never match.
  local changes
  changes=$(python3 - "$repoRoot" "$integration" "$jsonlPath" "$slug" "$tmpd/subjects.txt" <<'PY'
import sys, json, re, subprocess
repoRoot, integration, jsonlPath, slug, subjectsPath = sys.argv[1:6]
done = set()
scope_re = re.compile(r'\b\w+\(([^)]+)\):')                       # feat(<id>): / fix(<id>): …
merge_re = re.compile(re.escape('plan/' + slug + '--') + r'([A-Za-z0-9._-]+)')  # merge of task branch
skip_subject_re = re.compile(r'^(?:docs\(|specs:|feat\(specs\):|fix\(specs\):|merge\(specs\):)', re.I)
token_re = re.compile(r'[A-Za-z0-9._-]+')
with open(subjectsPath, encoding='utf-8') as fh:
    for s in fh:
        for m in scope_re.finditer(s): done.add(m.group(1).strip())
        for m in merge_re.finditer(s): done.add(m.group(1))
subject_tokens = []
with open(subjectsPath, encoding='utf-8') as fh:
    for s in fh:
        if skip_subject_re.search(s):
            continue
        subject_tokens.append(set(token_re.findall(s)))

def is_ancestor(sha):
    if not sha: return False
    r = subprocess.run(['git', '-C', repoRoot, 'merge-base', '--is-ancestor', sha, integration],
                        stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    return r.returncode == 0

changes = []
with open(jsonlPath, encoding='utf-8') as fh:
    for line in fh:
        line = line.strip()
        if not line: continue
        try: o = json.loads(line)
        except Exception: continue
        if not (isinstance(o, dict) and o.get('type') == 'task'): continue
        tid, st = o.get('id'), o.get('status')
        if not tid: continue
        landed = tid in done or any(tid in toks for toks in subject_tokens) or is_ancestor(o.get('commit'))
        if landed and st not in ('COMMITTED', 'REVIEWED'):
            changes.append((tid, 'COMMITTED', 'git: commit for %s reachable from integration' % tid))
        elif st == 'COMMITTED' and not landed:
            changes.append((tid, 'PENDING', 'git: no commit for %s on integration — downgraded' % tid))
for tid, st, note in changes:
    print('\t'.join((tid, st, note)))
PY
) || { echo "reconcile: change computation failed" >&2; return 3; }

  # persist each change through the atomic primitive, then emit the changed set.
  local G_id=() G_st=() G_note=() tid st note
  while IFS=$'\t' read -r tid st note; do
    [[ -z "$tid" ]] && continue
    jsonl_set_task "$jsonlPath" "$tid" "$st" DROP \
      || { echo "reconcile: jsonl write failed for $tid" >&2; return 3; }
    G_id+=("$tid"); G_st+=("$st"); G_note+=("$note")
  done <<< "$changes"
  local out='{"tasks":[' i
  for ((i=0; i<${#G_id[@]}; i++)); do
    [[ $i -gt 0 ]] && out+=','
    out+='{"id":"'"$(jstr "${G_id[$i]}")"'","status":"'"$(jstr "${G_st[$i]}")"'","note":"'"$(jstr "${G_note[$i]}")"'"}'
  done
  out+=']}'
  printf '%s\n' "$out"
}

# ── runstart: capture the run baseline (integration HEAD + wall-clock epoch) ───
# Called ONCE after reconcile, BEFORE any task executes. The sha anchors "added THIS session"
# (warnreport diffs .warnignore against it); the ts anchors elapsed run-time (runstats).
# Args: <intWt>   Emits: {"ok":bool,"sha":string,"ts":int}
runstart() {
  local intWt=$1 sha=''
  [[ -d "$intWt/.git" || -f "$intWt/.git" ]] || { printf '{"ok":false,"sha":"","ts":%s}\n' "$(date +%s)"; return 0; }
  sha=$(git -C "$intWt" rev-parse HEAD 2>/dev/null || echo '')
  [[ -n "$sha" ]] && printf '{"ok":true,"sha":"%s","ts":%s}\n' "$sha" "$(date +%s)" \
                  || printf '{"ok":false,"sha":"","ts":%s}\n' "$(date +%s)"
}

# ── warnreport: end-of-run human-review surface for warning suppressions ───────
# The deterministic gate + the review step stop LAZY .warnignore abuse, but a determined agent can
# still fabricate a plausible "unfixable upstream" justification — only a human closes that. So every
# run ends by surfacing what to eyeball: the FULL standing pile (accumulated) and, called out, only
# what THIS session added (the review-now set). Informational — never blocks; fail-soft to empty.
# Args:  <intWt> <startSha|"">
# Emits: {"accumulated":[{"pat":s,"why":s}],"session":[{"pat":s,"why":s}],
#         "counts":{"accumulated":int,"session":int}}
#   pat = the grep -E suppression pattern; why = the '#' justification line(s) directly above it.
warnreport() {
  local intWt=$1 startSha=${2:-}
  local diff=''
  if [[ -n "$startSha" && ( -d "$intWt/.git" || -f "$intWt/.git" ) ]]; then
    diff=$(git -C "$intWt" diff "$startSha"..HEAD -- .warnignore 2>/dev/null || true)
  fi
  WR_DIFF="$diff" python3 - "$intWt/.warnignore" <<'PY'
import sys, os, json
path = sys.argv[1]
def parse(lines):
    """[(pattern, justification)] — justification = '#' comment line(s) directly above the pattern."""
    out, pend = [], []
    for raw in lines:
        s = raw.rstrip('\n')
        t = s.strip()
        if not t:
            pend = []                       # blank breaks the comment→pattern adjacency
        elif t.startswith('#'):
            pend.append(t.lstrip('#').strip())
        else:
            out.append((t, ' '.join(pend)))
            pend = []
    return out
acc = []
if os.path.exists(path):
    with open(path, encoding='utf-8') as f:
        acc = parse(f.readlines())
why_of = {p: w for p, w in acc}
# session-added = '+' lines (not '+++') of the .warnignore diff vs the run-start sha
added = [l[1:] for l in os.environ.get('WR_DIFF', '').splitlines()
         if l.startswith('+') and not l.startswith('+++')]
sess = [(t, why_of.get(t, '')) for t in (a.strip() for a in added)
        if t and not t.startswith('#')]
def j(rows): return [{"pat": p, "why": w} for p, w in rows]
print(json.dumps({"accumulated": j(acc), "session": j(sess),
                  "counts": {"accumulated": len(acc), "session": len(sess)}},
                 separators=(',', ':'), ensure_ascii=False))
PY
}

# ── gate0 baseline-ratchet: deterministic red-baseline test gate ──────────────
# WHY: strict gate0 demands absolute green, so a red-baseline REMEDIATION plan (suite starts red, goes
# green over many waves) halts at wave 1 — its first task cannot fix failures a later wave owns. The
# baseline-ratchet mode instead captures the failing TEST-ID set at run start and gates each task on
# (absolute build/typecheck green) AND (no NEW test regression vs that baseline).
#   identity   = "<file>::<fullName>"  (fullName ALONE collides cross-file — measured 2/2379 in multideal)
#   suite error= a failed/errored suite with zero failed assertions (import/collection break) emits a
#                synthetic "<file>::<SUITE_ERROR>" id so a broken file cannot masquerade as zero-fails.
#   ratchet    = the baseline shrinks ONLY for ids now EXPLICITLY PASSING — never merely absent — so a
#                test "fixed" by skipping or deleting cannot launder rot into the protected set.
#   runner     = vitest JSON reporter only; no report / crash ⇒ fail-closed RED (never a false green).
# Baseline lives in the repo's SHARED git dir (survives worktree churn AND a driver death across resume),
# captured ONCE; resume reads it back; absent mid-run ⇒ HALT — never re-capture a now-partially-fixed
# suite (that silently resets the ratchet and hides every later re-break).

_g0_baseline_path() {              # <repoRoot> <slug> -> abs path on stdout (caller mkdir -p its dir)
  local repo=$1 slug=$2 gd
  gd=$(git -C "$repo" rev-parse --git-common-dir 2>/dev/null) || return 1
  case "$gd" in /*) ;; *) gd="$repo/$gd" ;; esac     # --git-common-dir may print relative
  printf '%s/run-plan/%s/gate0-baseline.json' "$gd" "$slug"
}

# Run <test_cmd> in <wt> (env scoped to the subshell), parse the vitest JSON it writes to $GATE0_JSON
# into pass/fail/skip id buckets. Emits {"ok":bool,"pass":[],"fail":[],"skip":[]}; ok=false ⇒ no report.
gate0_collect() {                 # <worktree> <test_cmd>
  local wt=$1 test_cmd=$2 jf
  [[ -d "$wt" ]] || { echo "gate0-collect: no worktree $wt" >&2; return 3; }
  jf=$(mktemp "$(dirname "$wt")/gate0-XXXXXX.json"); rm -f "$jf"   # dirname($wt) IS the resolved tmp root — $wt lives at <tmp-root>/wt-*
  ( cd "$wt" && GATE0_JSON="$jf" bash -c "$test_cmd" ) >/dev/null 2>&1 || true
  if [[ ! -s "$jf" ]]; then rm -f "$jf"; printf '{"ok":false,"pass":[],"fail":[],"skip":[]}\n'; return 0; fi
  G0_JSON="$jf" G0_WT="$wt" python3 - <<'PY'
import os, json
try:
    with open(os.environ["G0_JSON"], encoding="utf-8") as f: j = json.load(f)
except Exception:
    print(json.dumps({"ok": False, "pass": [], "fail": [], "skip": []}, separators=(',',':'))); raise SystemExit
wt = os.environ["G0_WT"]
def rel(name):
    try: return os.path.relpath(name or "", wt)
    except Exception: return name or ""
P=set(); F=set(); S=set()
for tf in j.get("testResults", []) or []:
    file = rel(tf.get("name",""))
    fails_here = 0
    for a in (tf.get("assertionResults", []) or []):
        ident = file + "::" + (a.get("fullName") or a.get("title") or "")
        st = a.get("status")
        if st == "passed": P.add(ident)
        elif st == "failed": F.add(ident); fails_here += 1
        elif st in ("skipped","pending","todo","disabled"): S.add(ident)
    if tf.get("status") in ("failed","error") and fails_here == 0:
        F.add(file + "::<SUITE_ERROR>")      # suite import/collection error: never invisible
print(json.dumps({"ok": True, "pass": sorted(P), "fail": sorted(F), "skip": sorted(S)},
                 separators=(',',':'), ensure_ascii=False))
PY
  rm -f "$jf"
}

# Run gate0_collect into a TEMP FILE and echo its path. The buckets JSON is large at real scale (~250KB
# for a 2k-test suite) — too big for an env var / argv (ARG_MAX), and a heredoc'd python claims stdin, so
# downstream consumers read the buckets from THIS file, never a pipe/env. Caller rm -f's the path.
_g0_collect_file() {              # <worktree> <test_cmd> -> temp file path on stdout
  local f; f=$(mktemp "$(dirname "$1")/g0c-XXXXXX.json")   # dirname($1) IS the resolved tmp root — $1 (worktree) lives at <tmp-root>/wt-*
  gate0_collect "$1" "$2" > "$f"
  printf '%s' "$f"
}

# Capture the run baseline ONCE. Persists BOTH the excused-fail set AND the protected-pass set (the
# latter detects a once-green test later disabled by skip — a laundered regression invisible to a
# fail-only diff). Resume (file present) reads it back, never re-captures. First-run collect failure ⇒
# ok=false (caller HALTs — cannot establish the excused set, fail-closed). Atomic write (temp+rename).
gate0_baseline_capture() {        # <repoRoot> <slug> <worktree> <test_cmd>
  local repo=$1 slug=$2 wt=$3 test_cmd=$4 bp c ok n
  bp=$(_g0_baseline_path "$repo" "$slug") || { echo "gate0-baseline: not a git repo $repo" >&2; return 3; }
  if [[ -s "$bp" ]]; then
    n=$(python3 -c 'import json,sys;print(len(json.load(open(sys.argv[1])).get("fail",[])))' "$bp" 2>/dev/null || echo 0)
    printf '{"ok":true,"captured":false,"count":%s}\n' "$n"; return 0
  fi
  local cf; cf=$(_g0_collect_file "$wt" "$test_cmd")
  mkdir -p "$(dirname "$bp")"
  n=$(G0_CF="$cf" G0_BP="$bp" python3 - <<'PY'
import os, json
d = json.load(open(os.environ["G0_CF"])); bp = os.environ["G0_BP"]
if not d["ok"]:
    print("ERR"); raise SystemExit
tmp = bp + ".tmp"
json.dump({"fail": d["fail"], "pass": d["pass"]}, open(tmp, "w")); os.replace(tmp, bp)
print(len(d["fail"]))
PY
)
  rm -f "$cf"
  if [[ "$n" == "ERR" || -z "$n" ]]; then printf '{"ok":false,"captured":false,"count":0}\n'; return 0; fi
  printf '{"ok":true,"captured":true,"count":%s}\n' "$n"
}

# Per-task / integration gate in baseline-ratchet mode. Emits S_GATE0 ({"green",...}) so the controller
# ladder + fixer loop are UNCHANGED — the lib, not the agent, computes green. green = absolute checks pass
# AND no regression, where regression = (a failure NOT excused at baseline) OR (a once-green test now
# SKIPPED — disabled break) OR (a once-green test DELETED/RENAMED — coverage erosion, minus G0_ALLOWDEL
# waiver) OR (zero tests collected vs a non-empty baseline). Absent/corrupt baseline ⇒ green=false.
# G0_ALLOWDEL = JSON-array env of explicitly-waived deleted ids. NO_COLOR so ANSI can't break the JSON.
gate0_ratchet() {                 # <repoRoot> <slug> <worktree> <absolute_cmd> <test_cmd>
  local repo=$1 slug=$2 wt=$3 abs_cmd=$4 test_cmd=$5 bp ao ar c
  bp=$(_g0_baseline_path "$repo" "$slug") || { echo "gate0-ratchet: not a git repo $repo" >&2; return 3; }
  [[ -s "$bp" ]] || { printf '{"green":false,"output":"gate0 baseline missing — run halted; never re-capture mid-run","new_fails":[]}\n'; return 0; }
  ao=$( cd "$wt" && NO_COLOR=1 FORCE_COLOR=0 bash -c "$abs_cmd" 2>&1 ); ar=$?
  if [[ $ar -ne 0 ]]; then
    printf '{"green":false,"output":"%s","new_fails":[]}\n' "$(jstr "absolute checks RED (exit $ar): $(printf '%s' "$ao" | tail -c 500)")"; return 0
  fi
  c=$(_g0_collect_file "$wt" "$test_cmd")
  G0_CF="$c" G0_BP="$bp" G0_ALLOWDEL="${G0_ALLOWDEL:-[]}" python3 - <<'PY'
import os, json
c = json.load(open(os.environ["G0_CF"]))
try:
    b = json.load(open(os.environ["G0_BP"])); base_fail = set(b.get("fail", [])); base_pass = set(b.get("pass", []))
except Exception:
    print(json.dumps({"green": False, "output": "gate0 baseline unreadable (corrupt) — fail-closed", "new_fails": []}, separators=(',',':'))); raise SystemExit
if not c["ok"]:
    print(json.dumps({"green": False, "output": "test runner crashed / no JSON report (fail-closed)", "new_fails": []}, separators=(',',':'))); raise SystemExit
if not (c["pass"] or c["fail"] or c["skip"]) and (base_fail or base_pass):
    print(json.dumps({"green": False, "output": "test collection returned ZERO tests but baseline knows %d ids — suite vanished/misconfigured (glob/dir/config), not legitimately green (fail-closed)" % (len(base_fail) + len(base_pass)), "new_fails": []}, separators=(',',':'))); raise SystemExit
try: allow_del = set(json.loads(os.environ.get("G0_ALLOWDEL") or "[]"))
except Exception: allow_del = set()
cur_fail = set(c["fail"]); cur_skip = set(c["skip"]); cur_pass = set(c["pass"])
seen = cur_pass | cur_fail | cur_skip
new_fail = cur_fail - base_fail                       # a failure not excused at baseline
disabled = base_pass & cur_skip                       # a once-green test now skipped = a hidden break
vanished = (base_pass - seen) - allow_del             # a once-green test deleted/renamed = coverage erosion
reg = sorted(new_fail | disabled | vanished)
newly = sorted(base_fail & cur_pass)
green = len(reg) == 0
if green:
    out = "no regressions (%d excused fails, %d now passing)" % (len(cur_fail & base_fail), len(newly))
else:
    bits = (["%d new fail" % len(new_fail)] if new_fail else []) \
         + (["%d disabled-once-green" % len(disabled)] if disabled else []) \
         + (["%d deleted-once-green" % len(vanished)] if vanished else [])
    out = "REGRESSIONS (%s): %s" % (", ".join(bits), ", ".join(reg[:20]))
print(json.dumps({"green": green, "output": out, "new_fails": reg, "newly_passing": newly},
                 separators=(',',':'), ensure_ascii=False))
PY
  rm -f "$c"
}

# Ratchet after a green task (sequential) / wave (parallel): a test now EXPLICITLY passing leaves the
# excused-fail set AND joins the protected-pass set (so re-breaking OR disabling it later is a regression).
# Shrink is on explicit pass ONLY — skip/delete never count as fixed. Atomic write; collect failure = no-op.
gate0_ratchet_mark() {            # <repoRoot> <slug> <worktree> <test_cmd>
  local repo=$1 slug=$2 wt=$3 test_cmd=$4 bp c
  bp=$(_g0_baseline_path "$repo" "$slug") || { echo "gate0-ratchet-mark: not a git repo $repo" >&2; return 3; }
  [[ -s "$bp" ]] || { printf '{"ok":false,"removed":0,"remaining":0}\n'; return 0; }
  c=$(_g0_collect_file "$wt" "$test_cmd")
  G0_CF="$c" G0_BP="$bp" python3 - <<'PY'
import os, json
c = json.load(open(os.environ["G0_CF"])); bp = os.environ["G0_BP"]
try:
    b = json.load(open(bp)); base_fail = list(b.get("fail", [])); base_pass = set(b.get("pass", []))
except Exception:
    print(json.dumps({"ok": False, "removed": 0, "remaining": 0}, separators=(',',':'))); raise SystemExit
if not c["ok"]:
    print(json.dumps({"ok": False, "removed": 0, "remaining": len(base_fail)}, separators=(',',':'))); raise SystemExit
if not (c["pass"] or c["fail"] or c["skip"]) and (base_fail or base_pass):
    print(json.dumps({"ok": False, "removed": 0, "remaining": len(base_fail)}, separators=(',',':'))); raise SystemExit
passing = set(c["pass"])
new_fail = [x for x in base_fail if x not in passing]       # shrink ONLY on explicit pass (BLOCKER A)
new_pass = sorted(base_pass | (set(base_fail) & passing))   # fixed tests JOIN the protected set
tmp = bp + ".tmp"
json.dump({"fail": new_fail, "pass": new_pass}, open(tmp, "w")); os.replace(tmp, bp)
print(json.dumps({"ok": True, "removed": len(base_fail) - len(new_fail), "remaining": len(new_fail)}, separators=(',',':')))
PY
  rm -f "$c"
}

# Residual excused-failures still standing — the convergence assertion. A remediation plan's WHOLE POINT
# is to reach green; the per-task ratchet only proves "no new regression", NOT "the rot got fixed". At
# convergence the controller HALTs if this is non-empty (minus an explicit meta.gate0_allow_residual).
gate0_residual() {                # <repoRoot> <slug>
  local repo=$1 slug=$2 bp
  bp=$(_g0_baseline_path "$repo" "$slug") || { echo "gate0-residual: not a git repo $repo" >&2; return 3; }
  [[ -s "$bp" ]] || { printf '{"ok":true,"remaining":0,"fails":[]}\n'; return 0; }
  G0_BP="$bp" python3 - <<'PY'
import os, json
try:
    f = json.load(open(os.environ["G0_BP"])).get("fail", [])
except Exception:
    print(json.dumps({"ok": False, "remaining": 0, "fails": []}, separators=(',',':'))); raise SystemExit
print(json.dumps({"ok": True, "remaining": len(f), "fails": sorted(f)}, separators=(',',':'), ensure_ascii=False))
PY
}

# ── ownership beacons: stamp the owning session at launch / clear on clean land ──
# ownerstamp runs on the MAIN thread at launch (CLAUDE_CODE_SESSION_ID is the real session there; a
# dispatched subagent would record a child id). Atomic temp+rename so the hook never reads a torn file.
# Empty session id ⇒ owner="" ⇒ the hook treats the run as legacy and flags fail-closed.
ownerstamp() {
  local slug=${1:-} sid=${CLAUDE_CODE_SESSION_ID:-}
  [[ -n "$slug" ]] || { echo "ownerstamp: slug required" >&2; return 2; }
  mkdir -p "$RUN_DIR" || { echo "ownerstamp: cannot mkdir $RUN_DIR" >&2; return 3; }
  local tmp="$RUN_DIR/.$slug.owner.$$"
  printf '%s\n' "$sid" > "$tmp" && mv -f "$tmp" "$RUN_DIR/$slug.owner"
  # owner-process beacon: host + CLI pid + start-time (atomic temp+rename, like .owner).
  local cli host start ptmp
  cli=$(_cli_pid); host=$(hostname 2>/dev/null || echo unknown); start=$(_proc_start "$cli")
  ptmp="$RUN_DIR/.$slug.pid.$$"
  printf '%s %s %s\n' "$host" "$cli" "$start" > "$ptmp" && mv -f "$ptmp" "$RUN_DIR/$slug.pid"
  touch "$RUN_DIR/$slug.live" 2>/dev/null || true
  printf '{"ok":true,"slug":"%s","owner":"%s","host":"%s","pid":"%s"}\n' \
    "$(jstr "$slug")" "$(jstr "$sid")" "$(jstr "$host")" "$(jstr "$cli")"
}

# ownerclear: drop both beacons on a clean land. Hygiene only (a landed run's integration branch is
# gone, so the hook can't flag it regardless). Fail-soft.
ownerclear() {
  local slug=${1:-}; [[ -n "$slug" ]] || { echo "ownerclear: slug required" >&2; return 2; }
  rm -f "$RUN_DIR/$slug.owner" "$RUN_DIR/$slug.live" "$RUN_DIR/$slug.pid" 2>/dev/null || true
  printf '{"ok":true,"slug":"%s"}\n' "$(jstr "$slug")"
}

# ── load: deterministic plan manifest (replaces the LLM "load" seat) ──────────
# Finds the plan jsonl for <slug>, resolves repoRoot (nearest ancestor whose .git is a DIRECTORY, never
# a gitlink worktree file), parses meta+tasks+anchors+gated, and emits the S_MANIFEST JSON the engine
# consumes. $0 model reasoning + zero parse flakiness; a haiku driver only RELAYS this output verbatim.
# On ANY failure it still prints a SCHEMA-VALID empty manifest (repoRoot:"") so the driver's schema check
# passes and the engine HALTs on the empty repoRoot. Args: <slug>   Emits: S_MANIFEST
_empty_manifest() { printf '{"repoRoot":"","jsonlPath":"","planPath":"","specPath":"","meta":{"slug":"%s","exec_mode":""},"tasks":[]}\n' "$(jstr "${1:-}")"; }
load() {
  local slug=${1:-}
  [[ -n "$slug" ]] || { _empty_manifest ""; echo "load: slug required" >&2; return 2; }
  local projects="${RUN_PLAN_PROJECTS:-$HOME/Projects}" jsonl
  jsonl=$(find "$projects" -maxdepth 5 -path "*/docs/plans/*-${slug}.jsonl" \
            -not -path "*/.worktrees/*" -not -path "*/.claude/worktrees/*" \
            \( -not -path "*/.wt-*" -o -path "*/.wt-${slug}-int/*" \) 2>/dev/null | sort | tail -1)
  [[ -n "$jsonl" ]] || { _empty_manifest "$slug"; echo "load: no jsonl for slug $slug under $projects" >&2; return 3; }
  local d repoRoot=""
  d=$(cd "$(dirname "$jsonl")" && pwd)
  while [[ -n "$d" && "$d" != "/" ]]; do
    [[ -d "$d/.git" ]] && { repoRoot="$d"; break; }
    d=$(dirname "$d")
  done
  [[ -n "$repoRoot" ]] || { _empty_manifest "$slug"; echo "load: no .git-dir ancestor of $jsonl" >&2; return 3; }
  RPL_JSONL="$jsonl" RPL_ROOT="$repoRoot" RPL_SLUG="$slug" python3 - <<'PY'
import os, json, sys
jsonl=os.environ['RPL_JSONL']; root=os.environ['RPL_ROOT']; slug=os.environ['RPL_SLUG']
def empty(): print(json.dumps({"repoRoot":"","jsonlPath":"","planPath":"","specPath":"","meta":{"slug":slug,"exec_mode":""},"tasks":[]},separators=(',',':')))
def absify(p): return "" if not p else (p if os.path.isabs(p) else os.path.normpath(os.path.join(root,p)))
meta=None; tasks=[]; gated=[]; anchors={}
with open(jsonl, encoding='utf-8') as fh:
    for line in fh:
        line=line.strip()
        if not line: continue
        try: o=json.loads(line)
        except Exception: continue
        if not isinstance(o,dict): continue
        t=o.get('type')
        if t=='meta': meta=o
        elif t=='task':
            tasks.append({'id':o.get('id'),'wave':o.get('wave'),'phase':o.get('phase'),
                'desc':o.get('desc',''),'status':o.get('status',''),'deps':o.get('deps',[]) or [],
                'blocker':o.get('blocker'),'requires_decision':o.get('requires_decision')})
        elif t=='anchor': anchors[o.get('what','')]=o.get('path','')
        elif t=='gated':
            gated.append({k:o.get(k) for k in ('id','category','needs','why','blast_radius','options','default','status','answer','resolved_by','source','binds_meta')})
if meta is None: empty(); sys.stderr.write('load: no meta line\n'); sys.exit(3)
m={k:v for k,v in meta.items() if k!='type'}; m.setdefault('slug',slug); m.setdefault('exec_mode',m.get('exec_mode',''))
out={'repoRoot':root,'jsonlPath':jsonl,'planPath':absify(anchors.get('plan','')),'specPath':absify(anchors.get('spec','')),'meta':m,'tasks':tasks}
if gated: out['gated']=gated
print(json.dumps(out, separators=(',',':'), ensure_ascii=False))
PY
}

# ── routecache: persist the classify result so a resume skips the LLM classifier ──
# Writes meta.preflight.route (id->executor) + route_hash into the meta line. The engine reuses the cached
# route iff route_hash == the current meta.preflight.task_graph_hash (graph unchanged). Atomic temp+rename.
# Args: <jsonlPath> <task_graph_hash> <routeJSON>   Emits: {"ok":bool}
routecache() {
  local jsonl=$1 hash=$2 routeJson=$3
  [[ -f "$jsonl" ]] || { echo '{"ok":false}'; echo "routecache: jsonl not found: $jsonl" >&2; return 2; }
  RPL_J="$jsonl" RPL_H="$hash" RPL_R="$routeJson" python3 - <<'PY'
import os, json, tempfile, sys
jsonl=os.environ['RPL_J']; h=os.environ['RPL_H']
try: route=json.loads(os.environ['RPL_R'])
except Exception: route={}
if not isinstance(route,dict): route={}
with open(jsonl, encoding='utf-8') as f: lines=f.readlines()
out=[]; hits=0
for ln in lines:
    s=ln.rstrip('\n')
    if not s.strip(): out.append(ln); continue
    try: o=json.loads(s)
    except Exception: out.append(ln); continue
    if isinstance(o,dict) and o.get('type')=='meta':
        hits+=1
        pf=o.get('preflight') if isinstance(o.get('preflight'),dict) else {}
        pf['route']=route; pf['route_hash']=h; o['preflight']=pf
        out.append(json.dumps(o,separators=(',',':'),ensure_ascii=False)+('\n' if ln.endswith('\n') else ''))
    else: out.append(ln)
if hits!=1:
    sys.stderr.write(f'routecache: expected 1 meta line, found {hits}\n'); print('{"ok":false}'); sys.exit(1)
d=os.path.dirname(os.path.abspath(jsonl)); fd,tmp=tempfile.mkstemp(dir=d)
try:
    with os.fdopen(fd,'w',encoding='utf-8') as g: g.writelines(out)
    os.replace(tmp,jsonl)
except Exception as e:
    try: os.unlink(tmp)
    except OSError: pass
    sys.stderr.write(f'routecache: write failed: {e}\n'); print('{"ok":false}'); sys.exit(1)
print('{"ok":true}')
PY
}

# ── runid beacon: persist the Workflow runId for SAME-SESSION journal resume ───
# The skill stamps the runId the Workflow tool returns at launch; on an in-session re-run it reads it back
# and passes resumeFromRunId so the whole agent prefix replays from the journal cache. Same-session ONLY —
# a cross-session resume cannot use it (journal gone), so the skill pairs runidread with the owner check.
runidstamp() {
  local slug=${1:-} rid=${2:-}
  [[ -n "$slug" ]] || { echo "runidstamp: slug required" >&2; return 2; }
  mkdir -p "$RUN_DIR" || { echo "runidstamp: cannot mkdir $RUN_DIR" >&2; return 3; }
  local tmp="$RUN_DIR/.$slug.runid.$$"
  printf '%s\n' "$rid" > "$tmp" && mv -f "$tmp" "$RUN_DIR/$slug.runid"
  printf '{"ok":true,"slug":"%s","runid":"%s"}\n' "$(jstr "$slug")" "$(jstr "$rid")"
}
runidread() {
  local slug=${1:-}; [[ -n "$slug" ]] || { echo "runidread: slug required" >&2; return 2; }
  [[ -f "$RUN_DIR/$slug.runid" ]] && head -1 "$RUN_DIR/$slug.runid" || echo ""
}

# ── dispatch (only when executed, not when sourced by the test harness) ───────
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  cmd=${1:-}; shift || true
  case "$cmd" in
    load)        load "$@" ;;
    routecache)  routecache "$@" ;;
    runidstamp)  runidstamp "$@" ;;
    runidread)   runidread "$@" ;;
    metagate)    metagate "$@" ;;
    financegate) financegate "$@" ;;
    setup)       setup "$@" ;;
    lease)       lease "$@" ;;
    commit)      commit "$@" ;;
    reconcile)   reconcile "$@" ;;
    runstart)    runstart "$@" ;;
    ownerstamp)  ownerstamp "$@" ;;
    ownerclear)  ownerclear "$@" ;;
    warnreport)  warnreport "$@" ;;
    gate0-collect)         gate0_collect "$@" ;;
    gate0-baseline)        gate0_baseline_capture "$@" ;;
    gate0-ratchet)         gate0_ratchet "$@" ;;
    gate0-ratchet-mark)    gate0_ratchet_mark "$@" ;;
    gate0-residual)        gate0_residual "$@" ;;
    *) echo "run-plan-lib.sh: unknown subcommand '$cmd' (have: load routecache runidstamp runidread metagate financegate setup lease commit reconcile runstart ownerstamp ownerclear warnreport gate0-collect gate0-baseline gate0-ratchet gate0-ratchet-mark gate0-residual)" >&2; exit 2 ;;
  esac
fi
