#!/bin/bash
# Unified Harness Memory helper CLI.
#
# Usage:
#   scripts/harness-mem setup [--project <path>] [--platform <all|codex|opencode|claude|cursor|antigravity|comma-list>] [--skip-start] [--skip-smoke] [--skip-quality]
#   scripts/harness-mem doctor [--project <path>] [--platform <all|codex|opencode|claude|cursor|antigravity|comma-list>] [--fix]
#   scripts/harness-mem smoke [--project <path>]
#   scripts/harness-mem uninstall [--project <path>] [--platform <all|codex|opencode|claude|cursor|antigravity|comma-list>] [--purge-db]
#   scripts/harness-mem versions [--project <path>]
#   scripts/harness-mem update
#   scripts/harness-mem import-claude-mem --source <claude_mem.db> [--dry-run] [--import-project <name>]
#   scripts/harness-mem ingest-hermes-state [--source ~/.hermes/state.db] [--project-key <name>] [--dry-run|--execute] [--limit <n>] [--since <iso>]
#   scripts/harness-mem admin-repair-sqlite-vec-map --model <model> [--dimension <n>] [--limit <n>] [--dry-run|--execute]
#   scripts/harness-mem admin-vector-backfill start|status|stop [--compact-batch-size <n>] [--reindex-batch-size <n>] [--interval-ms <n>] [--target-coverage <0..1>] [--model <model>] [--dimension <n>] [--reset]
#   scripts/harness-mem verify-import --job <job_id>
#   scripts/harness-mem cutover-claude-mem --job <job_id> --stop-now
#   scripts/harness-mem cleanup-stale-mcp [--dry-run] [--execute --older-than <duration>] [--json]
#   scripts/harness-mem telemetry status|export [--limit <n>] [--json]
#   scripts/harness-mem mcp-gateway {start|stop|status} [--foreground] [--json]
#   scripts/harness-mem mcp-config [--transport <stdio|http>] [--client <claude,codex,cursor,hermes|all>] [--write]
#   scripts/harness-mem adr new --title <title> --status <status> --options <text> --consequences <text> --supersedes <text> --source-plans "Plans.md §NNN" [--write]
#   scripts/harness-mem model use-adaptive
#   scripts/harness-mem work import-plans [Plans.md] [--project <path>] [--dry-run|--write]
#   scripts/harness-mem work ready --project <path>
#   scripts/harness-mem work sync-plans [--project <path>|--all-projects|--root <dir>] [--dry-run|--write]
#   scripts/harness-mem work export-plans --project <path>

set -euo pipefail
IFS=$'\n\t'

SCRIPT_SOURCE="${BASH_SOURCE[0]}"
while [ -L "$SCRIPT_SOURCE" ]; do
  SCRIPT_SOURCE_DIR="$(cd -P "$(dirname "$SCRIPT_SOURCE")" && pwd)"
  SCRIPT_TARGET="$(readlink "$SCRIPT_SOURCE")"
  if [[ "$SCRIPT_TARGET" != /* ]]; then
    SCRIPT_SOURCE="${SCRIPT_SOURCE_DIR}/${SCRIPT_TARGET}"
  else
    SCRIPT_SOURCE="$SCRIPT_TARGET"
  fi
done
SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_SOURCE")" && pwd)"
SOURCE_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
HARNESS_ROOT="$SOURCE_ROOT"

TARGET_DIR="${HARNESS_MEM_TARGET_DIR:-$PWD}"
PLATFORM="all"
PLATFORM_EXPLICIT=0
BACKEND_MODE=""
UI_LANG="${HARNESS_MEM_LANG:-ja}"
SETUP_INTERACTIVE_PROMPT_USED=0
SETUP_IMPORT_CLAUDE_MEM=0
SETUP_INSTALL_CODEX_SKILL=0
SETUP_STOP_CLAUDE_MEM_AFTER_IMPORT=0
SETUP_AUTO_UPDATE_OPT_IN=-1
UPDATE_AUTO_UPDATE_OPT_IN=-1
FIX_MODE=0
READ_ONLY_MODE=0
STRICT_EXIT=0
DOCTOR_PROCESSES=0
DOCTOR_MCP_TRANSPORT=""
MCP_CONFIG_TRANSPORT="stdio"
MCP_TRANSPORT_EXPLICIT=0
MCP_HTTP_CONFIG_DETECTED=0
FIX_PLAN=0
JSON_OUTPUT=0
MCP_CLEANUP_DRY_RUN=0
MCP_CLEANUP_EXECUTE=0
MCP_CLEANUP_OLDER_THAN=""
MCP_GATEWAY_FOREGROUND=0
PURGE_DB=0
SKIP_START=0
SKIP_SMOKE=0
SKIP_QUALITY=0
INLINE_PLUGIN=0
QUIET=0
IMPORT_SOURCE=""
IMPORT_JOB_ID=""
IMPORT_PROJECT=""
IMPORT_DRY_RUN=0
HERMES_STATE_SOURCE="${HOME}/.hermes/state.db"
HERMES_STATE_PROJECT=""
HERMES_STATE_EXECUTE=0
HERMES_STATE_LIMIT=""
HERMES_STATE_SINCE=""
HERMES_STATE_AFTER_MESSAGE_ID=""
HERMES_STATE_BATCH_SIZE="${HARNESS_MEM_HERMES_BACKFILL_BATCH_SIZE:-100}"
HERMES_STATE_MAX_CONTENT_CHARS=""
HERMES_STATE_INCLUDE_TOOL_CONTENT=0
REPAIR_SQLITE_VEC_MODEL=""
REPAIR_SQLITE_VEC_DIMENSION=""
REPAIR_SQLITE_VEC_LIMIT=""
REPAIR_SQLITE_VEC_EXECUTE=0
REPAIR_SQLITE_VEC_REBUILD_EXISTING=0
VECTOR_BACKFILL_COMPACT_BATCH_SIZE=""
VECTOR_BACKFILL_REINDEX_BATCH_SIZE=""
VECTOR_BACKFILL_INTERVAL_MS=""
VECTOR_BACKFILL_TARGET_COVERAGE=""
VECTOR_BACKFILL_MODEL=""
VECTOR_BACKFILL_DIMENSION=""
VECTOR_BACKFILL_RESET=0
STOP_NOW=0
BACKUP_DEST_DIR=""
BACKUP_EVIDENCE_PATH=""
BACKUP_EVIDENCE_SHA256=""
BACKUP_EVIDENCE_TTL_SECONDS=""
BACKUP_EVIDENCE_CANDIDATE_IDS=()
FORGET_MAINTENANCE_FORCE=0
FORGET_MAINTENANCE_REASON=""
VERSION_CHECK_IN_SETUP=1
AUTO_UPDATE_PACKAGE="${HARNESS_MEM_NPM_PACKAGE:-@chachamaru127/harness-mem}"
AUTO_UPDATE_CHANNEL="${HARNESS_MEM_AUTO_UPDATE_CHANNEL:-latest}"
AUTO_UPDATE_CHECK_INTERVAL_SEC="${HARNESS_MEM_AUTO_UPDATE_INTERVAL_SEC:-86400}"
COMPANION_CONTRACT_VERSION="${HARNESS_MEM_COMPANION_CONTRACT_VERSION:-claude-harness-companion.v1}"

MEM_HOST="${HARNESS_MEM_HOST:-127.0.0.1}"
MEM_PORT="${HARNESS_MEM_PORT:-37888}"
STATE_DIR="${HARNESS_MEM_HOME:-$HOME/.harness-mem}"
DB_PATH="${HARNESS_MEM_DB_PATH:-$STATE_DIR/harness-mem.db}"
VERSIONS_DIR="${STATE_DIR}/versions"
VERSIONS_SNAPSHOT_PATH="${VERSIONS_DIR}/tool-versions.json"
VERSIONS_HISTORY_PATH="${VERSIONS_DIR}/tool-versions-history.jsonl"
AUTO_UPDATE_STATE_PATH="${STATE_DIR}/runtime/auto-update-state.json"
MCP_GATEWAY_ADDR="${HARNESS_MEM_MCP_ADDR:-127.0.0.1:37889}"
MCP_GATEWAY_ENDPOINT="/mcp"
MCP_GATEWAY_PID_FILE="${STATE_DIR}/mcp-gateway.pid"
MCP_GATEWAY_LOG_FILE="${STATE_DIR}/mcp-gateway.log"
MCP_GATEWAY_TOKEN_FILE="${HARNESS_MEM_MCP_TOKEN_FILE:-$STATE_DIR/mcp-gateway.token}"
MCP_GATEWAY_ENV_FILE="${HARNESS_MEM_MCP_ENV_FILE:-$STATE_DIR/mcp-gateway.env}"
MCP_GATEWAY_START_TIMEOUT_SEC="${HARNESS_MEM_MCP_GATEWAY_START_TIMEOUT_SEC:-10}"
MCP_GATEWAY_STOP_TIMEOUT_SEC="${HARNESS_MEM_MCP_GATEWAY_STOP_TIMEOUT_SEC:-5}"
MCP_GATEWAY_LAUNCHD_LABEL="${HARNESS_MEM_MCP_GATEWAY_LAUNCHD_LABEL:-com.harness-mem.mcp-gateway}"
CURSOR_MCP_SERVER_ID="harness-mem"
CURSOR_LEGACY_MCP_SERVER_ID="harness"

BEGIN_CODEX_NOTIFY="# >>> harness-mem codex notify"
END_CODEX_NOTIFY="# <<< harness-mem codex notify"
BEGIN_CODEX_MCP="# >>> harness-mem codex mcp"
END_CODEX_MCP="# <<< harness-mem codex mcp"
BEGIN_CODEX_FEATURES="# >>> harness-mem codex features"
END_CODEX_FEATURES="# <<< harness-mem codex features"

usage() {
  cat <<'EOF'
Unified Harness Memory helper

Commands:
  setup      Configure Codex/OpenCode/Cursor/Claude wiring, start daemon, run smoke and quality checks.
  doctor     Validate wiring and daemon health (optionally repair with --fix).
  recall     Manage contextual recall mode (on|quiet|off|status) or explain recall results.
  versions   Snapshot local/upstream versions for Codex/OpenCode/Cursor/Claude/Antigravity.
  update     Update global harness-mem package and prompt auto-update opt-in (interactive).
  smoke      Run isolated daemon smoke test against record/search privacy flow.
  uninstall  Remove memory wiring and optionally purge local memory DB.
  backup                     Create a WAL-safe snapshot of the memory DB via VACUUM INTO.
  backup-evidence            Preverify a backup file and return a hard-purge evidence token.
  forget-maintenance         Run the archive-first forget maintenance planner/trigger.
  forget status              Show autonomous forgetting status, counts, last run, restore window and risk.
  import-claude-mem          One-shot import from Claude-mem SQLite into harness-mem.
  ingest-hermes-state        Backfill Hermes ~/.hermes/state.db into harness-mem (dry-run by default).
  admin-repair-sqlite-vec-map
                             Repair sqlite-vec map/index rows from existing mem_vectors (dry-run by default).
  admin-vector-backfill start|status|stop
                             Manage the out-of-request vector compact/reindex backfill worker.
  verify-import              Verify imported data quality/privacy checks by job id.
  cutover-claude-mem         Stop Claude-mem only after verify passed.
  migrate-from-claude-mem    1-command migration: import -> verify -> cutover from Claude-mem.
  rollback-claude-mem        Rollback: restart Claude-mem and re-enable LaunchAgent.
  promote                    Promote backend mode: local → hybrid → managed.
  rollback                   Rollback backend mode to local.
  cleanup-stale-mcp          Inspect or terminate stale stdio harness-mcp-* children.
  telemetry status|export     Inspect local telemetry spans and metric summaries without an external collector.
  mcp-gateway start|stop|status
                              Manage the local Streamable HTTP MCP gateway.
  mcp-config                  Print or write Claude/Codex/Cursor/Hermes MCP config snippets.
  adr new                     Render a BEADS-shaped ADR template. Dry-run by default; --write is explicit.
  model list                 List available models in the catalog.
  model pull <id> [--yes]    Download a model (~size confirmation required).
  model use <id>             Set active local embedding model in config.
  model use-adaptive         Set adaptive local routing (ruri-v3-30m + multilingual-e5).
  model status [<id>]        Show install status of a model (or all installed).
  work import-plans          Import Plans.md into the WorkGraph model. Dry-run by default; --write is explicit.
  work ready                 Show ready WorkGraph items from Plans.md and active work leases.
  work sync-plans            Safely reimport one or more project Plans.md files into WorkGraph.
  work export-plans          Print a generated Markdown view from WorkGraph DB rows.

Common options:
  --project <path>      Target project path (default: current directory)
  --platform <value>    all|codex|opencode|claude|cursor|antigravity or comma list (default: all)
                        examples: codex,cursor  /  opencode,cursor
  --dest-dir <path>     Destination directory for backup (default: same dir as DB)
  --backup-path <path>  Backup file to preverify with backup-evidence
  --backup-sha256 <hex> SHA-256 for --backup-path
  --candidate-id <id>   Candidate observation ID covered by backup-evidence (repeatable)
  --ttl-seconds <n>     Lifetime for backup-evidence token (default: 300)
  --reason <s>          Reason label for forget-maintenance
  --force               Force forget-maintenance even when thresholds are not exceeded
  --source <path>       Source SQLite path for import-claude-mem
                        For ingest-hermes-state, defaults to ~/.hermes/state.db
  --job <job_id>        Import job id for verify/cutover
  --import-project <s>  Override project name on import
  --project-key <s>     Override harness-mem project key for Hermes Backfill
  --model <model>       Model for admin-repair-sqlite-vec-map or admin-vector-backfill start
  --dimension <n>       Vector dimension for admin-repair-sqlite-vec-map or admin-vector-backfill start
  --dry-run             Plan import without writing events
                        For admin-repair-sqlite-vec-map, this is the default.
  --execute             Execute Hermes Backfill or sqlite-vec map repair operations that are dry-run by default
  --rebuild-existing    Re-upsert existing sqlite-vec rows with the current compact vector payload format
  --limit <n>           Limit Hermes messages read from state.db, or rows repaired by admin-repair-sqlite-vec-map
  --compact-batch-size <n>
                        Compact sqlite-vec rows per vector backfill worker tick
  --reindex-batch-size <n>
                        Observations reindexed per vector backfill worker tick
  --interval-ms <n>     Delay between vector backfill worker ticks
  --target-coverage <n> Stop vector backfill worker once coverage reaches this ratio
  --reset               Reset vector backfill worker cursor/state before start
  --after-message-id <n>
                        Continue Hermes Backfill after this message id
  --batch-size <n>      Execute full Hermes Backfill in batches (default: 100)
  --since <iso|unix>    Import Hermes messages at or after this timestamp
  --max-content-chars <n>
                        Truncate each imported Hermes message content at this many chars
  --include-tool-content
                        Include Hermes tool result bodies. Default is metadata-only for tool results.
  --stop-now            Required by cutover-claude-mem to actually stop Claude-mem
  --skip-version-check  Skip automatic version snapshot at end of setup/doctor
  --auto-update enable|disable
                        Non-interactively set auto-update opt-in
  --quiet               Reduce logs

Command options:
  setup:
    --skip-start        Do not start daemon
    --skip-smoke        Do not run smoke test
    --skip-quality      Do not run search quality test
    --mcp-transport http|stdio
                        New Claude/Codex setup defaults to http; existing stdio wiring is preserved unless explicit.
    (no --platform)     Interactive setup prompt:
                        1) language (Japanese/English)
                        2) target tools (multi-select)
                        3) import from Claude-mem (yes/no)
                        4) stop Claude-mem after import (yes/no)
                        5) enable auto-update opt-in (yes/no)
    doctor:
      --fix               Attempt to repair missing wiring
      --fix --plan        Print a repair plan without applying changes
      --read-only         Do not create or repair local files during checks
      --strict-exit       Return non-zero when JSON doctor reports failures
      --processes         Include a non-fatal local harness-mcp-* process inventory
      --mcp-transport http|stdio
                          Check a specific MCP transport. Without this, doctor infers HTTP checks from HTTP client config.
      --json              Output structured JSON result
  cleanup-stale-mcp:
    --dry-run             Default. List stale stdio MCP children without killing them
    --execute             Kill only eligible stale stdio MCP children
    --older-than <dur>    Required with --execute. Examples: 30s, 10m, 2h, 1d
    --json                Output structured JSON result
  telemetry:
    status                 Show local telemetry exporter status and recent span/metric summary
    export                 Print sanitized recent spans and metric summaries as JSON
    --limit <n>            Max recent spans to include in export (default: daemon default)
    --json                 Output structured JSON for status
  mcp-gateway:
    start                  Start the singleton local Streamable HTTP gateway
    stop                   Stop the pidfile-managed gateway
    status                 Show pid, endpoint, auth mode, gateway probe, and daemon health
    --foreground           Run gateway in the foreground instead of writing a pidfile
    --addr <host:port>     Override HARNESS_MEM_MCP_ADDR (default: 127.0.0.1:37889)
    --json                 Output structured JSON status
  mcp-config:
    --client <list>         claude,codex,hermes or all. all means claude,codex; Hermes is explicit only
    --transport stdio|http  Default: stdio for direct config generation. New setup defaults Claude/Codex to http.
    --url <url>             Override HTTP endpoint (default: http://127.0.0.1:37889/mcp)
    --token-env-var <name>  HTTP bearer token env var (default: HARNESS_MEM_MCP_TOKEN)
    --write                 Write managed snippets where safe; otherwise preview only
    --json                  Output structured JSON
  uninstall:
    --purge-db          Remove ~/.harness-mem/harness-mem.db after stop
  versions:
    no additional options
  update:
    (interactive tty)  Prompt auto-update opt-in (yes/no), then run npm install -g @chachamaru127/harness-mem@latest
  recall:
    on                Enable full contextual recall (rerank threshold 0.6 / fallback top 3)
    quiet             Enable quiet contextual recall (default, rerank threshold 0.8 / fallback top 1)
    off               Disable contextual recall injection
    status            Show current contextual recall mode
    explain           Query /v1/recall and print compact explanation-only result JSON
      --query <text>    Required recall query
      --project <name>  Project scope; use with --session for narrower scope
      --session <id>    Session scope
      --limit <n>       Result limit (default: 5)
      --url <url>       Override endpoint (default: http://127.0.0.1:${HARNESS_MEM_PORT:-37888}/v1/recall)
  adr new:
    --title <title>       ADR title (also used for slug unless --slug is provided)
    --status <status>     Proposed|Accepted|Superseded|Deprecated|Rejected
    --options <text>      Required; repeatable or semicolon-separated
    --consequences <text> Required; repeatable or semicolon-separated
    --supersedes <text>   Required; use "None" when there is no prior ADR
    --source-plans <ref>  Required; must include a Plans.md §NNN reference
    --write               Create docs/adr/ADR-NNN-*.md. Default is dry-run.

Embedding note:
  HARNESS_MEM_EMBEDDING_PROVIDER accepts auto|adaptive|fallback|openai|ollama|local.
  adaptive automatically routes Japanese-heavy queries to the Japanese model and mixed/code-heavy queries to the general model.
EOF
}

work_impl() {
  if ! command -v bun >/dev/null 2>&1; then
    fail "bun is required for harness-mem work commands"
  fi
  bun "${HARNESS_ROOT}/memory-server/src/workgraph/work-cli.ts" "$@"
}

adr_impl() {
  if ! command -v bun >/dev/null 2>&1; then
    fail "bun is required for harness-mem adr commands"
  fi
  bun "${HARNESS_ROOT}/memory-server/src/adr/adr-cli.ts" "$@"
}

log() {
  if [ "$QUIET" -eq 1 ]; then
    return
  fi
  echo "[harness-mem] $*"
}

warn() {
  if [ "$QUIET" -eq 1 ]; then
    return
  fi
  echo "[harness-mem][warn] $*" >&2
}

fail() {
  echo "[harness-mem][error] $*" >&2
  exit 1
}

is_uint() {
  case "${1:-}" in
    ''|*[!0-9]*) return 1 ;;
    *) return 0 ;;
  esac
}

# --- Backend config management ---
CONFIG_PATH="${STATE_DIR}/config.json"

ensure_config() {
  mkdir -p "$STATE_DIR"
  if [ ! -f "$CONFIG_PATH" ]; then
    cat > "$CONFIG_PATH" <<'CONF'
{
  "backend_mode": "local",
  "recall": {
    "mode": "quiet"
  },
  "embedding_provider": "auto",
  "embedding_model": "multilingual-e5",
  "managed": {
    "endpoint": "",
    "api_key": ""
  },
  "auto_update": {
    "enabled": false,
    "package_name": "@chachamaru127/harness-mem",
    "channel": "latest",
    "repair_platforms": []
  }
}
CONF
  else
    local tmp="${CONFIG_PATH}.tmp.$$"
    jq '
      .backend_mode = (.backend_mode // "local")
      | .recall = ((.recall // {}) + {
          mode: (.recall.mode // "quiet")
        })
      | .embedding_provider = (.embedding_provider // "auto")
      | .embedding_model = (.embedding_model // "multilingual-e5")
      | .managed = ((.managed // {}) + {
          endpoint: (.managed.endpoint // ""),
          api_key: (.managed.api_key // "")
        })
      | .auto_update = ((.auto_update // {}) + {
          enabled: (.auto_update.enabled // false),
          package_name: (.auto_update.package_name // "@chachamaru127/harness-mem"),
          channel: (.auto_update.channel // "latest"),
          repair_platforms: (
            (.auto_update.repair_platforms // [])
            | if type == "array" then . else [] end
            | map(select(type == "string" and . != ""))
          )
        })
    ' "$CONFIG_PATH" > "$tmp" && mv "$tmp" "$CONFIG_PATH"
  fi
}

read_backend_mode() {
  if [ "${READ_ONLY_MODE:-0}" -eq 1 ] && [ ! -f "$CONFIG_PATH" ]; then
    echo "local"
    return
  fi
  ensure_config
  local mode
  mode="$(jq -r '.backend_mode // "local"' "$CONFIG_PATH" 2>/dev/null || echo "local")"
  case "$mode" in
    local|managed|hybrid) echo "$mode" ;;
    *) echo "local" ;;
  esac
}

read_recall_mode() {
  if [ "${READ_ONLY_MODE:-0}" -eq 1 ] && [ ! -f "$CONFIG_PATH" ]; then
    echo "quiet"
    return
  fi
  ensure_config
  local mode
  mode="$(jq -r '.recall.mode // "quiet"' "$CONFIG_PATH" 2>/dev/null || echo "quiet")"
  case "$mode" in
    on|quiet|off) echo "$mode" ;;
    *) echo "quiet" ;;
  esac
}

write_recall_mode() {
  local new_mode="$1"
  ensure_config
  local tmp="${CONFIG_PATH}.tmp.$$"
  jq --arg mode "$new_mode" '.recall = ((.recall // {}) + {mode: $mode})' "$CONFIG_PATH" > "$tmp" && mv "$tmp" "$CONFIG_PATH"
}

write_backend_mode() {
  local new_mode="$1"
  ensure_config
  local tmp="${CONFIG_PATH}.tmp.$$"
  jq --arg m "$new_mode" '.backend_mode = $m' "$CONFIG_PATH" > "$tmp" && mv "$tmp" "$CONFIG_PATH"
}

read_embedding_provider() {
  ensure_config
  local provider
  provider="$(jq -r '.embedding_provider // "auto"' "$CONFIG_PATH" 2>/dev/null || echo "auto")"
  case "$provider" in
    auto|adaptive|fallback|openai|ollama|local) echo "$provider" ;;
    *) echo "auto" ;;
  esac
}

write_embedding_provider() {
  local new_provider="$1"
  ensure_config
  local tmp="${CONFIG_PATH}.tmp.$$"
  jq --arg p "$new_provider" '.embedding_provider = $p' "$CONFIG_PATH" > "$tmp" && mv "$tmp" "$CONFIG_PATH"
}

read_embedding_model() {
  ensure_config
  local model
  model="$(jq -r '.embedding_model // "multilingual-e5"' "$CONFIG_PATH" 2>/dev/null || echo "multilingual-e5")"
  if [ -n "$model" ] && [ "$model" != "null" ]; then
    echo "$model"
  else
    echo "multilingual-e5"
  fi
}

write_embedding_model() {
  local new_model="$1"
  ensure_config
  local tmp="${CONFIG_PATH}.tmp.$$"
  jq --arg m "$new_model" '.embedding_model = $m' "$CONFIG_PATH" > "$tmp" && mv "$tmp" "$CONFIG_PATH"
}

read_managed_endpoint() {
  ensure_config
  jq -r '.managed.endpoint // ""' "$CONFIG_PATH" 2>/dev/null || echo ""
}

read_managed_api_key() {
  ensure_config
  jq -r '.managed.api_key // ""' "$CONFIG_PATH" 2>/dev/null || echo ""
}

read_auto_update_enabled() {
  ensure_config
  local value
  value="$(jq -r '.auto_update.enabled // false' "$CONFIG_PATH" 2>/dev/null || echo "false")"
  case "$value" in
    true|1|yes|on) echo "1" ;;
    *) echo "0" ;;
  esac
}

write_auto_update_enabled() {
  local enabled="$1"
  local enabled_json
  if [ "$enabled" -eq 1 ]; then
    enabled_json="true"
  else
    enabled_json="false"
  fi

  ensure_config
  local tmp="${CONFIG_PATH}.tmp.$$"
  jq \
    --argjson enabled "$enabled_json" \
    --arg package_name "$AUTO_UPDATE_PACKAGE" \
    --arg channel "$AUTO_UPDATE_CHANNEL" \
    '
      .auto_update = ((.auto_update // {}) + {
        enabled: $enabled,
        package_name: $package_name,
        channel: $channel
      })
    ' "$CONFIG_PATH" > "$tmp" && mv "$tmp" "$CONFIG_PATH"
}

normalize_managed_platform_csv() {
  local raw="${1:-}"
  local normalized result=""
  local item expanded
  local -a items=()

  if [ -z "$raw" ]; then
    printf ''
    return 0
  fi

  IFS=',' read -r -a items <<<"$raw"
  for item in "${items[@]}"; do
    normalized="$(printf '%s' "$item" | xargs)"
    [ -n "$normalized" ] || continue
    case "$normalized" in
      all)
        for expanded in codex opencode claude cursor antigravity; do
          result="$(csv_append_unique "$result" "$expanded")"
        done
        ;;
      codex|opencode|claude|cursor|antigravity)
        result="$(csv_append_unique "$result" "$normalized")"
        ;;
    esac
  done

  printf '%s' "$result"
}

read_auto_update_repair_platforms() {
  ensure_config
  local csv
  csv="$(jq -r '(.auto_update.repair_platforms // []) | if type == "array" then map(select(type == "string" and . != "")) | join(",") else "" end' "$CONFIG_PATH" 2>/dev/null || echo "")"
  normalize_managed_platform_csv "$csv"
}

write_auto_update_repair_platforms() {
  local raw="${1:-}"
  local normalized
  normalized="$(normalize_managed_platform_csv "$raw")"

  ensure_config
  local tmp="${CONFIG_PATH}.tmp.$$"
  jq \
    --arg csv "$normalized" \
    '
      .auto_update = ((.auto_update // {}) + {
        repair_platforms: (
          if ($csv | length) == 0 then
            []
          else
            ($csv | split(",") | map(select(length > 0)))
          end
        )
      })
    ' "$CONFIG_PATH" > "$tmp" && mv "$tmp" "$CONFIG_PATH"
}

remember_auto_update_repair_platforms() {
  local requested="${1:-}"
  local current merged
  current="$(read_auto_update_repair_platforms)"
  merged="$(normalize_managed_platform_csv "${current:+$current,}${requested}")"
  write_auto_update_repair_platforms "$merged"
}

forget_auto_update_repair_platforms() {
  local requested="${1:-}"
  local current drop normalized result=""
  local -a current_items=()
  [ -f "$CONFIG_PATH" ] || return 0
  current="$(read_auto_update_repair_platforms)"
  drop="$(normalize_managed_platform_csv "$requested")"

  if [ -z "$current" ]; then
    return 0
  fi

  IFS=',' read -r -a current_items <<<"$current"
  for item in "${current_items[@]}"; do
    normalized="$(printf '%s' "$item" | xargs)"
    [ -n "$normalized" ] || continue
    if [ -n "$drop" ] && csv_has_value "$drop" "$normalized"; then
      continue
    fi
    result="$(csv_append_unique "$result" "$normalized")"
  done

  write_auto_update_repair_platforms "$result"
}

infer_auto_update_repair_platforms() {
  local remembered inferred=""
  remembered="$(read_auto_update_repair_platforms)"
  if [ -n "$remembered" ]; then
    printf '%s' "$remembered"
    return 0
  fi

  if [ -f "${HOME}/.codex/config.toml" ] || [ -f "${HOME}/.codex/hooks.json" ]; then
    inferred="$(csv_append_unique "$inferred" "codex")"
  fi
  if [ -f "${HOME}/.claude.json" ] || [ -f "${HOME}/.claude/settings.json" ] || [ -d "${HOME}/.claude" ]; then
    inferred="$(csv_append_unique "$inferred" "claude")"
  fi
  if [ -f "${HOME}/.cursor/hooks.json" ] || [ -f "${HOME}/.cursor/mcp.json" ]; then
    inferred="$(csv_append_unique "$inferred" "cursor")"
  fi
  if [ -f "${HOME}/.config/opencode/opencode.json" ]; then
    inferred="$(csv_append_unique "$inferred" "opencode")"
  fi
  if [ -n "${HARNESS_MEM_ANTIGRAVITY_ROOTS:-}" ]; then
    inferred="$(csv_append_unique "$inferred" "antigravity")"
  fi

  printf '%s' "$inferred"
}

run_post_update_repair() {
  local reason="${1:-update}"
  local platforms="${2:-}"
  local harness_bin

  platforms="$(normalize_managed_platform_csv "${platforms:-$(infer_auto_update_repair_platforms)}")"
  if [ -z "$platforms" ]; then
    log "Post-update repair: no managed platforms recorded; skipping"
    return 0
  fi

  harness_bin="$(command -v harness-mem || true)"
  if [ -z "$harness_bin" ]; then
    warn "Post-update repair skipped: harness-mem executable not found on PATH"
    return 0
  fi

  log "Post-update repair: doctor --fix --platform ${platforms} (${reason})"
  if HARNESS_MEM_SKIP_AUTO_UPDATE=1 HARNESS_MEM_NON_INTERACTIVE=1 \
    "$harness_bin" doctor --fix --platform "$platforms" --skip-version-check --quiet >/dev/null 2>&1; then
    log "Post-update repair completed for: ${platforms}"
  else
    warn "Post-update repair failed for: ${platforms}. Run manually: harness-mem doctor --fix --platform ${platforms}"
  fi
}

fetch() {
  local url="$1"
  local out="$2"
  curl -fsSL --compressed "$url" -o "$out"
}

abs_dir() {
  local input="$1"
  if [ -d "$input" ]; then
    (cd "$input" && pwd)
    return
  fi
  fail "Directory not found: $input"
}

parse_options() {
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --project)
        shift
        [ "$#" -gt 0 ] || fail "--project requires a path"
        TARGET_DIR="$1"
        ;;
      --platform)
        shift
        [ "$#" -gt 0 ] || fail "--platform requires a value"
        PLATFORM="$1"
        PLATFORM_EXPLICIT=1
        ;;
        --fix)
          FIX_MODE=1
          ;;
        --read-only)
          READ_ONLY_MODE=1
          FIX_MODE=0
          VERSION_CHECK_IN_SETUP=0
          ;;
        --strict-exit)
          STRICT_EXIT=1
          ;;
        --processes)
          DOCTOR_PROCESSES=1
          ;;
        --mcp-transport)
          shift
          [ "$#" -gt 0 ] || fail "--mcp-transport requires a value"
          DOCTOR_MCP_TRANSPORT="$1"
          MCP_CONFIG_TRANSPORT="$1"
          MCP_TRANSPORT_EXPLICIT=1
          ;;
        --plan)
          FIX_PLAN=1
          ;;
      --json)
        JSON_OUTPUT=1
        QUIET=1
        ;;
      --execute)
        MCP_CLEANUP_EXECUTE=1
        HERMES_STATE_EXECUTE=1
        REPAIR_SQLITE_VEC_EXECUTE=1
        ;;
      --rebuild-existing)
        REPAIR_SQLITE_VEC_REBUILD_EXISTING=1
        ;;
      --older-than)
        shift
        [ "$#" -gt 0 ] || fail "--older-than requires a duration"
        MCP_CLEANUP_OLDER_THAN="$1"
        ;;
      --addr)
        shift
        [ "$#" -gt 0 ] || fail "--addr requires host:port"
        MCP_GATEWAY_ADDR="$1"
        ;;
      --foreground)
        MCP_GATEWAY_FOREGROUND=1
        ;;
      --purge-db)
        PURGE_DB=1
        ;;
      --skip-start)
        SKIP_START=1
        ;;
      --skip-smoke)
        SKIP_SMOKE=1
        ;;
      --skip-quality)
        SKIP_QUALITY=1
        ;;
      --auto-update)
        shift
        [ "$#" -gt 0 ] || fail "--auto-update requires enable or disable"
        case "$1" in
          enable)
            SETUP_AUTO_UPDATE_OPT_IN=1
            UPDATE_AUTO_UPDATE_OPT_IN=1
            ;;
          disable)
            SETUP_AUTO_UPDATE_OPT_IN=0
            UPDATE_AUTO_UPDATE_OPT_IN=0
            ;;
          *)
            fail "--auto-update must be enable or disable"
            ;;
        esac
        ;;
      --inline-plugin)
        INLINE_PLUGIN=1
        ;;
      --quiet)
        QUIET=1
        ;;
      --skip-version-check)
        VERSION_CHECK_IN_SETUP=0
        ;;
      --dest-dir)
        shift
        [ "$#" -gt 0 ] || fail "--dest-dir requires a path"
        BACKUP_DEST_DIR="$1"
        ;;
      --backup-path)
        shift
        [ "$#" -gt 0 ] || fail "--backup-path requires a path"
        BACKUP_EVIDENCE_PATH="$1"
        ;;
      --backup-sha256)
        shift
        [ "$#" -gt 0 ] || fail "--backup-sha256 requires a sha256 hex string"
        BACKUP_EVIDENCE_SHA256="$1"
        ;;
      --candidate-id|--target-id)
        local opt="$1"
        shift
        [ "$#" -gt 0 ] || fail "$opt requires an observation id"
        BACKUP_EVIDENCE_CANDIDATE_IDS+=("$1")
        ;;
      --ttl-seconds)
        shift
        [ "$#" -gt 0 ] || fail "--ttl-seconds requires a number"
        BACKUP_EVIDENCE_TTL_SECONDS="$1"
        ;;
      --reason)
        shift
        [ "$#" -gt 0 ] || fail "--reason requires a value"
        FORGET_MAINTENANCE_REASON="$1"
        ;;
      --force)
        FORGET_MAINTENANCE_FORCE=1
        ;;
      --source)
        shift
        [ "$#" -gt 0 ] || fail "--source requires a path"
        IMPORT_SOURCE="$1"
        HERMES_STATE_SOURCE="$1"
        ;;
      --job)
        shift
        [ "$#" -gt 0 ] || fail "--job requires a value"
        IMPORT_JOB_ID="$1"
        ;;
      --import-project)
        shift
        [ "$#" -gt 0 ] || fail "--import-project requires a value"
        IMPORT_PROJECT="$1"
        ;;
      --project-key)
        shift
        [ "$#" -gt 0 ] || fail "--project-key requires a value"
        HERMES_STATE_PROJECT="$1"
        ;;
      --model)
        shift
        [ "$#" -gt 0 ] || fail "--model requires a value"
        REPAIR_SQLITE_VEC_MODEL="$1"
        VECTOR_BACKFILL_MODEL="$1"
        ;;
      --dimension)
        shift
        [ "$#" -gt 0 ] || fail "--dimension requires a number"
        REPAIR_SQLITE_VEC_DIMENSION="$1"
        VECTOR_BACKFILL_DIMENSION="$1"
        ;;
      --limit)
        shift
        [ "$#" -gt 0 ] || fail "--limit requires a number"
        HERMES_STATE_LIMIT="$1"
        REPAIR_SQLITE_VEC_LIMIT="$1"
        ;;
      --after-message-id)
        shift
        [ "$#" -gt 0 ] || fail "--after-message-id requires a number"
        HERMES_STATE_AFTER_MESSAGE_ID="$1"
        ;;
      --batch-size)
        shift
        [ "$#" -gt 0 ] || fail "--batch-size requires a number"
        HERMES_STATE_BATCH_SIZE="$1"
        ;;
      --since)
        shift
        [ "$#" -gt 0 ] || fail "--since requires a timestamp"
        HERMES_STATE_SINCE="$1"
        ;;
      --max-content-chars)
        shift
        [ "$#" -gt 0 ] || fail "--max-content-chars requires a number"
        HERMES_STATE_MAX_CONTENT_CHARS="$1"
        ;;
      --include-tool-content)
        HERMES_STATE_INCLUDE_TOOL_CONTENT=1
        ;;
      --compact-batch-size)
        shift
        [ "$#" -gt 0 ] || fail "--compact-batch-size requires a number"
        VECTOR_BACKFILL_COMPACT_BATCH_SIZE="$1"
        ;;
      --reindex-batch-size)
        shift
        [ "$#" -gt 0 ] || fail "--reindex-batch-size requires a number"
        VECTOR_BACKFILL_REINDEX_BATCH_SIZE="$1"
        ;;
      --interval-ms)
        shift
        [ "$#" -gt 0 ] || fail "--interval-ms requires a number"
        VECTOR_BACKFILL_INTERVAL_MS="$1"
        ;;
      --target-coverage)
        shift
        [ "$#" -gt 0 ] || fail "--target-coverage requires a number"
        VECTOR_BACKFILL_TARGET_COVERAGE="$1"
        ;;
      --reset)
        VECTOR_BACKFILL_RESET=1
        ;;
      --backend)
        shift
        [ "$#" -gt 0 ] || fail "--backend requires a value (local|managed|hybrid)"
        case "$1" in
          local|managed|hybrid) BACKEND_MODE="$1" ;;
          *) fail "--backend must be one of: local, managed, hybrid" ;;
        esac
        ;;
      --dry-run)
        IMPORT_DRY_RUN=1
        MCP_CLEANUP_DRY_RUN=1
        HERMES_STATE_EXECUTE=0
        REPAIR_SQLITE_VEC_EXECUTE=0
        ;;
      --stop-now)
        STOP_NOW=1
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        fail "Unknown option: $1"
        ;;
    esac
    shift
  done
}

csv_has_value() {
  local csv="$1"
  local needle="$2"
  local item
  IFS=',' read -r -a items <<<"$csv"
  for item in "${items[@]}"; do
    local normalized
    normalized="$(printf '%s' "$item" | xargs)"
    if [ "$normalized" = "$needle" ]; then
      return 0
    fi
  done
  return 1
}

csv_append_unique() {
  local csv="$1"
  local value="$2"
  if [ -z "$csv" ]; then
    printf '%s' "$value"
    return
  fi
  if csv_has_value "$csv" "$value"; then
    printf '%s' "$csv"
    return
  fi
  printf '%s,%s' "$csv" "$value"
}

normalize_ui_lang() {
  case "$UI_LANG" in
    en|EN|english|English)
      UI_LANG="en"
      ;;
    *)
      UI_LANG="ja"
      ;;
  esac
}

ui_is_en() {
  [ "$UI_LANG" = "en" ]
}

should_prompt_platform_selection() {
  local command="$1"
  if [ "$command" != "setup" ]; then
    return 1
  fi
  if [ "$PLATFORM_EXPLICIT" -eq 1 ]; then
    return 1
  fi
  if [ "${HARNESS_MEM_NON_INTERACTIVE:-0}" = "1" ]; then
    return 1
  fi
  if [ -t 0 ] || [ "${HARNESS_MEM_FORCE_PLATFORM_PROMPT:-0}" = "1" ]; then
    return 0
  fi
  return 1
}

prompt_platform_selection() {
  local input choices token selected valid
  while true; do
    if ui_is_en; then
      cat <<'EOF'
[harness-mem] Select setup targets (multiple allowed)
  1) codex     (global: ~/.codex/config.toml)
  2) cursor    (global: ~/.cursor/hooks.json + ~/.cursor/mcp.json)
  3) opencode  (global: ~/.config/opencode/opencode.json)
  4) claude    (global: ~/.claude.json mcpServers)
  5) antigravity (experimental workspace scanning)
  a) all
Example: 1,2   (Enter=1,2)
EOF
    else
      cat <<'EOF'
[harness-mem] setup 対象を選択してください（複数可）
  1) codex     (global: ~/.codex/config.toml)
  2) cursor    (global: ~/.cursor/hooks.json + ~/.cursor/mcp.json)
  3) opencode  (global: ~/.config/opencode/opencode.json)
  4) claude    (global: ~/.claude.json mcpServers)
  5) antigravity (experimental workspace scanning)
  a) all
入力例: 1,2   (Enter=1,2)
EOF
    fi
    printf "> "
    read -r input || input=""
    input="$(printf '%s' "$input" | tr -d '[:space:]')"

    if [ -z "$input" ]; then
      PLATFORM="codex,cursor"
      log "Selected platforms: $PLATFORM"
      return
    fi

    case "$input" in
      a|A|all|ALL)
        PLATFORM="all"
        log "Selected platforms: $PLATFORM"
        return
        ;;
    esac

    selected=""
    valid=1
    IFS=',' read -r -a choices <<<"$input"
    for token in "${choices[@]}"; do
      case "$token" in
        1|codex)
          selected="$(csv_append_unique "$selected" "codex")"
          ;;
        2|cursor)
          selected="$(csv_append_unique "$selected" "cursor")"
          ;;
        3|opencode)
          selected="$(csv_append_unique "$selected" "opencode")"
          ;;
        4|claude)
          selected="$(csv_append_unique "$selected" "claude")"
          ;;
        5|antigravity)
          selected="$(csv_append_unique "$selected" "antigravity")"
          ;;
        *)
          if ui_is_en; then
            warn "Invalid selection: ${token}. Use 1,2,3,4,5 or a"
          else
            warn "無効な選択: ${token}。1,2,3,4,5 または a を入力してください"
          fi
          valid=0
          ;;
      esac
    done

    if [ "$valid" -eq 1 ] && [ -n "$selected" ]; then
      PLATFORM="$selected"
      log "Selected platforms: $PLATFORM"
      return
    fi
  done
}

prompt_language_selection() {
  local input
  while true; do
    cat <<'EOF'
[harness-mem] 言語を選択してください / Select language
  1) 日本語
  2) English
入力例: 1   (Enter=1)
EOF
    printf "> "
    read -r input || input=""
    input="$(printf '%s' "$input" | tr -d '[:space:]')"
    case "$input" in
      ""|1|ja|JA|jp|JP|japanese|Japanese)
        UI_LANG="ja"
        log "言語を日本語に設定しました"
        return
        ;;
      2|en|EN|english|English)
        UI_LANG="en"
        log "Language set to English"
        return
        ;;
      *)
        warn "Invalid selection: ${input}. Use 1 or 2"
        ;;
    esac
  done
}

prompt_yes_no_default_no() {
  local question="$1"
  local answer=""
  while true; do
    printf "%s [y/N]: " "$question"
    read -r answer || answer=""
    answer="$(printf '%s' "$answer" | tr -d '[:space:]')"
    if [ -z "$answer" ]; then
      return 1
    fi
    case "$answer" in
      y|Y|yes|YES|Yes)
        return 0
        ;;
      n|N|no|NO|No)
        return 1
        ;;
      *)
        if ui_is_en; then
          warn "Invalid input: ${answer}. Use y or n"
        else
          warn "無効な入力: ${answer}。y または n を入力してください"
        fi
        ;;
    esac
  done
}

should_prompt_setup_migration_selection() {
  local command="$1"
  if [ "$command" != "setup" ]; then
    return 1
  fi
  if [ "$SETUP_INTERACTIVE_PROMPT_USED" -ne 1 ]; then
    return 1
  fi
  return 0
}

prompt_setup_migration_selection() {
  local import_question stop_question
  if ui_is_en; then
    import_question="Import existing data from Claude-mem?"
    stop_question="Stop Claude-mem after import completes?"
  else
    import_question="Claude-memから既存データをインポートしますか?"
    stop_question="インポート完了後にClaude-memを停止しますか?"
  fi

  if prompt_yes_no_default_no "$import_question"; then
    SETUP_IMPORT_CLAUDE_MEM=1
    if prompt_yes_no_default_no "$stop_question"; then
      SETUP_STOP_CLAUDE_MEM_AFTER_IMPORT=1
    fi
  fi

  log "Migration options: import_claude_mem=${SETUP_IMPORT_CLAUDE_MEM}, stop_after_import=${SETUP_STOP_CLAUDE_MEM_AFTER_IMPORT}"
}

should_prompt_setup_auto_update_selection() {
  local command="$1"
  if [ "$command" != "setup" ]; then
    return 1
  fi
  if [ "$SETUP_INTERACTIVE_PROMPT_USED" -ne 1 ]; then
    return 1
  fi
  return 0
}

should_prompt_update_auto_update_selection() {
  local command="$1"
  if [ "$command" != "update" ]; then
    return 1
  fi
  if [ "${HARNESS_MEM_NON_INTERACTIVE:-0}" = "1" ]; then
    return 1
  fi
  if [ "${HARNESS_MEM_FORCE_UPDATE_PROMPT:-0}" = "1" ]; then
    return 0
  fi
  if [ "$(read_auto_update_enabled)" -eq 1 ]; then
    return 1
  fi
  if [ -t 0 ]; then
    return 0
  fi
  return 1
}
prompt_setup_auto_update_selection() {
  local auto_update_question
  if ui_is_en; then
    auto_update_question="Enable opt-in automatic updates for harness-mem?"
  else
    auto_update_question="harness-mem の自動更新（opt-in）を有効化しますか?"
  fi

  if prompt_yes_no_default_no "$auto_update_question"; then
    SETUP_AUTO_UPDATE_OPT_IN=1
    log "Auto-update opt-in: enabled"
  else
    SETUP_AUTO_UPDATE_OPT_IN=0
    log "Auto-update opt-in: disabled"
  fi
}

should_prompt_codex_skill_install() {
  local command="$1"
  if [ "$command" != "setup" ] && [ "$command" != "update" ]; then
    return 1
  fi
  if ! is_platform_enabled "codex"; then
    return 1
  fi
  if [ "${HARNESS_MEM_NON_INTERACTIVE:-0}" = "1" ]; then
    return 1
  fi
  local skill_mem_dst="${HOME}/.codex/skills/harness-mem/SKILL.md"
  local skill_recall_dst="${HOME}/.codex/skills/harness-recall/SKILL.md"
  if [ -f "$skill_mem_dst" ] && [ -f "$skill_recall_dst" ] && check_codex_skill_bundle >/dev/null 2>&1; then
    return 1
  fi
  if [ -t 0 ] || [ "${HARNESS_MEM_FORCE_SKILL_PROMPT:-0}" = "1" ]; then
    return 0
  fi
  return 1
}

prompt_codex_skill_install() {
  local question
  if ui_is_en; then
    question="Install harness-mem Codex Agent Skill to ~/.codex/skills/?"
  else
    question="harness-mem の Codex Agent Skill を ~/.codex/skills/ にインストールしますか?"
  fi

  if prompt_yes_no_default_no "$question"; then
    SETUP_INSTALL_CODEX_SKILL=1
    log "Codex skill install: enabled"
  else
    SETUP_INSTALL_CODEX_SKILL=0
    log "Codex skill install: skipped"
  fi
}

install_codex_skill() {
  local failed=0
  local name skill_src skill_dst
  for name in harness-mem harness-recall; do
    skill_src="${HARNESS_ROOT}/codex/skills/${name}/SKILL.md"
    skill_dst="${HOME}/.codex/skills/${name}/SKILL.md"

    if [ ! -f "$skill_src" ]; then
      warn "Codex skill source not found: $skill_src"
      failed=1
      continue
    fi

    mkdir -p "$(dirname "$skill_dst")"
    cp "$skill_src" "$skill_dst"
    log "Installed Codex Agent Skill: $skill_dst"
  done
  return "$failed"
}

prompt_update_auto_update_selection() {
  local auto_update_question
  if ui_is_en; then
    auto_update_question="Enable opt-in automatic updates for harness-mem?"
  else
    auto_update_question="harness-mem の自動更新（opt-in）を有効化しますか?"
  fi

  if prompt_yes_no_default_no "$auto_update_question"; then
    UPDATE_AUTO_UPDATE_OPT_IN=1
  else
    UPDATE_AUTO_UPDATE_OPT_IN=0
  fi
}
is_platform_enabled() {
  local target="$1"
  local entry
  IFS=',' read -r -a entries <<<"$PLATFORM"
  for entry in "${entries[@]}"; do
    local normalized
    normalized="$(printf '%s' "$entry" | xargs)"
    [ -z "$normalized" ] && continue
    if [ "$normalized" = "all" ] || [ "$normalized" = "$target" ]; then
      return 0
    fi
  done
  return 1
}

validate_platform_selection() {
  local entry
  local seen=0
  IFS=',' read -r -a entries <<<"$PLATFORM"
  for entry in "${entries[@]}"; do
    local normalized
    normalized="$(printf '%s' "$entry" | xargs)"
    [ -z "$normalized" ] && continue
    case "$normalized" in
      all|codex|opencode|claude|cursor|antigravity)
        seen=1
        ;;
      *)
        fail "Invalid --platform entry: ${normalized} (allowed: all,codex,opencode,claude,cursor,antigravity)"
        ;;
    esac
  done
  [ "$seen" -eq 1 ] || fail "Invalid --platform: ${PLATFORM}"
}

require_cmd() {
  local name="$1"
  if ! command -v "$name" >/dev/null 2>&1; then
    fail "Required command not found: $name"
  fi
}

should_use_stable_runtime_root() {
  case "$SOURCE_ROOT" in
    */.npm/_npx/*)
      return 0
      ;;
  esac
  return 1
}

sync_to_stable_runtime_root() {
  local runtime_root="${STATE_DIR}/runtime/harness-mem"

  mkdir -p "${STATE_DIR}/runtime"

  if check_cmd rsync; then
    rsync -a --delete --exclude '.git' "${SOURCE_ROOT}/" "${runtime_root}/"
  else
    rm -rf "${runtime_root}"
    mkdir -p "${runtime_root}"
    cp -R "${SOURCE_ROOT}/." "${runtime_root}/"
    rm -rf "${runtime_root}/.git"
  fi

  HARNESS_ROOT="${runtime_root}"
  log "Using stable runtime root: ${HARNESS_ROOT}"
}

check_cmd() {
  local name="$1"
  command -v "$name" >/dev/null 2>&1
}

ensure_bun() {
  if check_cmd bun; then
    return 0
  fi

  warn "bun is missing"
  case "$(uname -s)" in
    Darwin*)
      log "Installing bun via official installer..."
      if curl -fsSL https://bun.sh/install | bash; then
        hash -r 2>/dev/null || true
        if check_cmd bun; then
          log "bun installed: $(command -v bun)"
          return 0
        fi
        # PATH may not yet include ~/.bun/bin in this shell session
        if [ -d "$HOME/.bun/bin" ]; then
          export PATH="$HOME/.bun/bin:$PATH"
          hash -r 2>/dev/null || true
        fi
        if check_cmd bun; then
          log "bun installed: $(command -v bun)"
          return 0
        fi
        fail "bun installation finished but bun command is still unavailable. Add \$HOME/.bun/bin to PATH and retry."
      fi
      fail "Failed to install bun automatically. Install it manually: curl -fsSL https://bun.sh/install | bash"
      ;;
    MINGW*|MSYS*|CYGWIN*)
      # On Windows (Git Bash), try the official installer which supports MINGW64
      log "Installing bun via official installer..."
      if curl -fsSL https://bun.sh/install | bash; then
        hash -r 2>/dev/null || true
        if [ -d "$HOME/.bun/bin" ]; then
          export PATH="$HOME/.bun/bin:$PATH"
          hash -r 2>/dev/null || true
        fi
        if check_cmd bun; then
          log "bun installed: $(command -v bun)"
          return 0
        fi
      fi
      fail "Required command not found: bun. Install: powershell -c 'irm bun.sh/install.ps1 | iex'  or  npm install -g bun"
      ;;
    *)
      fail "Required command not found: bun. Install it first (https://bun.sh/docs/installation)."
      ;;
  esac
}

ensure_ripgrep() {
  if check_cmd rg; then
    return 0
  fi

  warn "ripgrep (rg) is missing"
  if check_cmd brew; then
    log "Installing ripgrep via Homebrew..."
    if brew install ripgrep; then
      hash -r
      if check_cmd rg; then
        log "ripgrep installed: $(command -v rg)"
        return 0
      fi

      local rg_prefix
      rg_prefix="$(brew --prefix ripgrep 2>/dev/null || true)"
      if [ -n "$rg_prefix" ] && [ -d "$rg_prefix/bin" ]; then
        fail "ripgrep installed but rg is not on PATH. Add ${rg_prefix}/bin to PATH and retry."
      fi
      fail "ripgrep installation finished but rg command is still unavailable."
    fi
    fail "Failed to install ripgrep automatically. Install it manually: brew install ripgrep"
  fi

  case "$(uname -s)" in
    MINGW*|MSYS*|CYGWIN*)
      fail "Required command not found: rg (ripgrep). Install it first: winget install BurntSushi.ripgrep.MSVC  or  scoop install ripgrep"
      ;;
    *)
      fail "Required command not found: rg (ripgrep). Install it first (macOS: brew install ripgrep, Linux: apt install ripgrep)."
      ;;
  esac
}

get_cursor_hooks_command() {
  printf '%s' "bash ${HOME}/.cursor/hooks/memory-cursor-event.sh"
}

ensure_dependencies() {
  ensure_bun
  require_cmd curl
  require_cmd jq
  require_cmd node
  require_cmd npm
  ensure_ripgrep
}

have_python3_command() {
  if check_cmd python3; then
    return 0
  fi

  case "$(uname -s)" in
    MINGW*|MSYS*|CYGWIN*)
      check_cmd py && py -3 -c "print('ok')" >/dev/null 2>&1
      return
      ;;
  esac

  return 1
}

run_python3() {
  if check_cmd python3; then
    python3 "$@"
    return
  fi

  case "$(uname -s)" in
    MINGW*|MSYS*|CYGWIN*)
      if check_cmd py; then
        py -3 "$@"
        return
      fi
      ;;
  esac

  fail "Required command not found: python3"
}

ensure_version_dependencies() {
  require_cmd curl
  require_cmd jq
  if ! have_python3_command; then
    case "$(uname -s)" in
      MINGW*|MSYS*|CYGWIN*)
        fail "Required command not found: python3 (Windows accepts either python3 or py -3)"
        ;;
      *)
        fail "Required command not found: python3"
        ;;
    esac
  fi
}

is_repo_checkout() {
  [ -d "${SOURCE_ROOT}/.git" ]
}

is_npx_runtime_source() {
  case "$SOURCE_ROOT" in
    */.npm/_npx/*)
      return 0
      ;;
  esac
  return 1
}

read_current_harness_mem_version() {
  local pkg_json="${HARNESS_ROOT}/package.json"
  if [ ! -f "$pkg_json" ]; then
    printf ''
    return
  fi
  jq -r '.version // ""' "$pkg_json" 2>/dev/null || printf ''
}

write_auto_update_state() {
  local checked_epoch="$1"
  local installed="$2"
  local latest="$3"
  local status="$4"

  mkdir -p "$(dirname "$AUTO_UPDATE_STATE_PATH")"
  jq -nc \
    --arg checked_at_epoch "$checked_epoch" \
    --arg checked_at "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
    --arg installed "$installed" \
    --arg latest "$latest" \
    --arg status "$status" \
    '{
      last_checked_at_epoch: ($checked_at_epoch | tonumber),
      last_checked_at: $checked_at,
      installed_version: ($installed | select(. != "") // null),
      latest_version: ($latest | select(. != "") // null),
      status: $status
    }' > "$AUTO_UPDATE_STATE_PATH"
}

should_attempt_auto_update() {
  local command="$1"

  case "$command" in
    help|-h|--help|uninstall|versions|update)
      return 1
      ;;
  esac

  if [ "${HARNESS_MEM_SKIP_AUTO_UPDATE:-0}" = "1" ]; then
    return 1
  fi
  if is_repo_checkout; then
    return 1
  fi
  if is_npx_runtime_source; then
    return 1
  fi
  if [ "$(read_auto_update_enabled)" -ne 1 ]; then
    return 1
  fi
  if ! check_cmd npm; then
    return 1
  fi

  local interval="$AUTO_UPDATE_CHECK_INTERVAL_SEC"
  if ! [[ "$interval" =~ ^[0-9]+$ ]] || [ "$interval" -lt 60 ]; then
    interval=86400
  fi

  local now last_checked elapsed
  now="$(date +%s)"
  last_checked="$(jq -r '.last_checked_at_epoch // 0' "$AUTO_UPDATE_STATE_PATH" 2>/dev/null || echo "0")"
  if ! [[ "$last_checked" =~ ^[0-9]+$ ]]; then
    last_checked=0
  fi
  elapsed=$((now - last_checked))
  if [ "$elapsed" -lt "$interval" ]; then
    return 1
  fi
  return 0
}

maybe_auto_update() {
  local command="$1"
  if ! should_attempt_auto_update "$command"; then
    return 0
  fi

  local now current_version latest_version
  now="$(date +%s)"
  current_version="$(read_current_harness_mem_version)"
  latest_version="$(npm view "$AUTO_UPDATE_PACKAGE" version 2>/dev/null | tr -d '\r' | head -n 1 || true)"

  if [ -z "$latest_version" ]; then
    warn "Auto-update check failed: unable to resolve latest version for ${AUTO_UPDATE_PACKAGE}"
    write_auto_update_state "$now" "$current_version" "" "check_failed"
    return 0
  fi

  if [ -n "$current_version" ] && [ "$current_version" = "$latest_version" ]; then
    write_auto_update_state "$now" "$current_version" "$latest_version" "up_to_date"
    return 0
  fi

  log "Auto-update: installing ${AUTO_UPDATE_PACKAGE}@${AUTO_UPDATE_CHANNEL} (current=${current_version:-unknown}, latest=${latest_version})"
  if npm install -g "${AUTO_UPDATE_PACKAGE}@${AUTO_UPDATE_CHANNEL}" >/dev/null 2>&1; then
    log "Auto-update completed. Updated version will be used on the next command."
    write_auto_update_state "$now" "$current_version" "$latest_version" "updated"
    run_post_update_repair "auto-update"
  else
    warn "Auto-update failed. Run manually: npm install -g ${AUTO_UPDATE_PACKAGE}@${AUTO_UPDATE_CHANNEL}"
    write_auto_update_state "$now" "$current_version" "$latest_version" "update_failed"
  fi
}

first_line() {
  local output
  output="$("$@" 2>/dev/null | head -n 1 || true)"
  output="$(printf '%s' "$output" | tr -d '\r')"
  printf '%s' "$output"
}

detect_cli_version() {
  local cmd="$1"
  shift || true
  if ! check_cmd "$cmd"; then
    printf ''
    return
  fi
  first_line "$cmd" "$@"
}

detect_cursor_local_version() {
  local value
  value="$(detect_cli_version cursor --version)"
  if [ -n "$value" ]; then
    printf '%s' "$value"
    return
  fi

  local plist
  for plist in \
    "/Applications/Cursor.app/Contents/Info.plist" \
    "$HOME/Applications/Cursor.app/Contents/Info.plist"; do
    if [ -f "$plist" ]; then
      value="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist" 2>/dev/null || true)"
      if [ -n "$value" ]; then
        printf '%s' "$value"
        return
      fi
    fi
  done
  printf ''
}

detect_antigravity_local_version() {
  local value
  value="$(detect_cli_version antigravity --version)"
  if [ -n "$value" ]; then
    printf '%s' "$value"
    return
  fi

  local plist
  for plist in \
    "/Applications/Antigravity.app/Contents/Info.plist" \
    "$HOME/Applications/Antigravity.app/Contents/Info.plist"; do
    if [ -f "$plist" ]; then
      value="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist" 2>/dev/null || true)"
      if [ -n "$value" ]; then
        printf '%s' "$value"
        return
      fi
    fi
  done
  printf ''
}

normalize_version_hint() {
  local value="$1"
  local extracted
  extracted="$(printf '%s' "$value" | sed -nE 's/.*(rust-v|v)?([0-9]+\.[0-9]+\.[0-9]+([-.][A-Za-z0-9.]+)?).*/\2/p' | head -n 1)"
  if [ -z "$extracted" ]; then
    printf '%s' "$value" | tr '[:upper:]' '[:lower:]' | xargs
    return
  fi
  printf '%s' "$extracted" | tr '[:upper:]' '[:lower:]'
}

version_status_hint() {
  local installed="$1"
  local latest="$2"
  if [ -z "$installed" ] || [ -z "$latest" ]; then
    printf 'unknown'
    return
  fi
  local ni nl
  ni="$(normalize_version_hint "$installed")"
  nl="$(normalize_version_hint "$latest")"
  if [ "$ni" = "$nl" ]; then
    printf 'up_to_date'
    return
  fi
  if [[ "$ni" == *"$nl"* ]] || [[ "$nl" == *"$ni"* ]]; then
    printf 'up_to_date'
    return
  fi
  printf 'needs_review'
}

detect_antigravity_hooks_signal() {
  local releases_json="$1"
  local hooks_key_present="false"
  local first_hooks_key_version=""
  local signal_version=""
  local signal_excerpt=""
  local signal_type=""
  local detected="false"

  hooks_key_present="$(jq -r '
    def items:
      if type == "array" then .
      elif type == "object" and ((.versions // null) | type == "array") then .versions
      else [] end;
    [items[] | has("hooks")] | any
  ' "$releases_json" 2>/dev/null || printf 'false')"
  [ "$hooks_key_present" = "true" ] || hooks_key_present="false"

  first_hooks_key_version="$(jq -r '
    def items:
      if type == "array" then .
      elif type == "object" and ((.versions // null) | type == "array") then .versions
      else [] end;
    [items[] | select(has("hooks")) | (.version // .tag_name // .name // empty | tostring)] | .[0] // empty
  ' "$releases_json" 2>/dev/null || true)"

  if [ "$hooks_key_present" = "true" ]; then
    detected="true"
    signal_type="hooks_key"
    signal_version="$first_hooks_key_version"
  fi

  while IFS=$'\t' read -r parsed_version parsed_text; do
    [ -n "$parsed_text" ] || continue
    if printf '%s' "$parsed_text" \
      | grep -Eiq '(^|[^a-z])hooks?([^a-z]|$)|tool[._ -]?definition|chat[._ -]?message|session[._ -]?(idle|compacted)|フック'; then
      signal_version="$parsed_version"
      signal_excerpt="$parsed_text"
      signal_type="keyword"
      detected="true"
      break
    fi
  done < <(jq -r '
    def stringify:
      if . == null then ""
      elif type == "string" then .
      else tostring end;
    def items:
      if type == "array" then .
      elif type == "object" and ((.versions // null) | type == "array") then .versions
      else [] end;
    items[]
    | [
        ((.version // .tag_name // .name // "") | tostring),
        (
          [
            (.name // ""),
            (.title // ""),
            (.notes // ""),
            (.releaseNotes // ""),
            (.changelog // ""),
            (.body // ""),
            (.description // ""),
            (.summary // ""),
            (.features | stringify),
            (.changes | stringify),
            (.highlights | stringify),
            (.hooks | stringify)
          ]
          | map(select(length > 0))
          | join(" ")
        )
      ]
    | @tsv
  ' "$releases_json" 2>/dev/null || true)

  signal_excerpt="$(printf '%s' "$signal_excerpt" | sed -E 's/[[:space:]]+/ /g' | cut -c1-160)"

  jq -nc \
    --argjson detected "$detected" \
    --arg signal_version "$signal_version" \
    --arg signal_type "$signal_type" \
    --arg signal_excerpt "$signal_excerpt" \
    '{
      detected: $detected,
      signal_version: ($signal_version | select(. != "") // null),
      signal_type: ($signal_type | select(. != "") // null),
      signal_excerpt: ($signal_excerpt | select(. != "") // null)
    }'
}

versions_impl() {
  ensure_version_dependencies

  mkdir -p "$VERSIONS_DIR"
  local tmp_dir
  tmp_dir="$(mktemp -d)"
  trap 'rm -rf "${tmp_dir:-}"' RETURN

  fetch "https://raw.githubusercontent.com/anthropics/claude-code/main/CHANGELOG.md" \
    "$tmp_dir/claude-code-CHANGELOG.md" &
  fetch "https://api.github.com/repos/openai/codex/releases?per_page=50" \
    "$tmp_dir/codex-releases.json" &
  fetch "https://api.github.com/repos/anomalyco/opencode/releases?per_page=50" \
    "$tmp_dir/opencode-releases.json" &
  fetch "https://cursor.com/changelog" \
    "$tmp_dir/cursor-changelog.html" &
  fetch "https://antigravity-auto-updater-974169037036.us-central1.run.app/releases" \
    "$tmp_dir/antigravity-releases-api.json" &
  wait

  local prev_antigravity_hooks
  prev_antigravity_hooks="$(jq -r '.upstream.antigravity.hooks_detected // false' "$VERSIONS_SNAPSHOT_PATH" 2>/dev/null || printf 'false')"
  [ "$prev_antigravity_hooks" = "true" ] || prev_antigravity_hooks="false"

  local claude_latest codex_latest_stable codex_latest_any opencode_latest antigravity_latest
  local cursor_latest_date cursor_latest_title

  claude_latest="$(awk '/^## [0-9]+\.[0-9]+\.[0-9]+$/ {print $2; exit}' "$tmp_dir/claude-code-CHANGELOG.md" || true)"
  codex_latest_stable="$(jq -r 'map(select((.draft|not) and (.prerelease|not))) | sort_by(.published_at) | reverse | .[0].tag_name // empty' "$tmp_dir/codex-releases.json")"
  codex_latest_any="$(jq -r 'map(select(.draft|not)) | sort_by(.published_at) | reverse | .[0].tag_name // empty' "$tmp_dir/codex-releases.json")"
  opencode_latest="$(jq -r 'map(select((.draft|not) and (.prerelease|not))) | sort_by(.published_at) | reverse | .[0].tag_name // empty' "$tmp_dir/opencode-releases.json")"
  antigravity_latest="$(jq -r '.[0].version // empty' "$tmp_dir/antigravity-releases-api.json")"

  local cursor_parsed
  cursor_parsed="$(run_python3 - "$tmp_dir/cursor-changelog.html" <<'PY'
import re
import sys
from pathlib import Path
text = Path(sys.argv[1]).read_text(encoding="utf-8", errors="ignore")
m = re.search(r'href="(/changelog/[^"]+)"><time dateTime="([^"]+)"[^>]*>[^<]*</time>.*?<h1[^>]*>\s*<a[^>]*>\s*([^<]+)', text, flags=re.DOTALL|re.IGNORECASE)
if not m:
    print("\t")
else:
    print(f"{m.group(2)}\t{m.group(3).strip()}")
PY
)"
  cursor_latest_date="$(printf '%s' "$cursor_parsed" | awk -F'\t' '{print $1}')"
  cursor_latest_title="$(printf '%s' "$cursor_parsed" | awk -F'\t' '{print $2}')"

  local antigravity_hooks_json antigravity_hooks_detected antigravity_hooks_signal_version
  local antigravity_hooks_signal_type antigravity_hooks_signal_excerpt antigravity_hooks_introduced
  antigravity_hooks_json="$(detect_antigravity_hooks_signal "$tmp_dir/antigravity-releases-api.json")"
  antigravity_hooks_detected="$(printf '%s' "$antigravity_hooks_json" | jq -r '.detected')"
  antigravity_hooks_signal_version="$(printf '%s' "$antigravity_hooks_json" | jq -r '.signal_version // empty')"
  antigravity_hooks_signal_type="$(printf '%s' "$antigravity_hooks_json" | jq -r '.signal_type // empty')"
  antigravity_hooks_signal_excerpt="$(printf '%s' "$antigravity_hooks_json" | jq -r '.signal_excerpt // empty')"
  antigravity_hooks_introduced="false"
  if [ "$antigravity_hooks_detected" = "true" ] && [ "$prev_antigravity_hooks" != "true" ]; then
    antigravity_hooks_introduced="true"
  fi

  local codex_local claude_local opencode_local cursor_local antigravity_local
  codex_local="$(detect_cli_version codex --version)"
  claude_local="$(detect_cli_version claude --version)"
  opencode_local="$(detect_cli_version opencode --version)"
  cursor_local="$(detect_cursor_local_version)"
  antigravity_local="$(detect_antigravity_local_version)"

  local codex_status claude_status opencode_status antigravity_status cursor_status
  codex_status="$(version_status_hint "$codex_local" "$codex_latest_stable")"
  claude_status="$(version_status_hint "$claude_local" "$claude_latest")"
  opencode_status="$(version_status_hint "$opencode_local" "$opencode_latest")"
  antigravity_status="$(version_status_hint "$antigravity_local" "$antigravity_latest")"
  cursor_status="unknown"

  local snapshot
  snapshot="$(jq -nc \
    --arg generated_at "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
    --arg codex_local "$codex_local" \
    --arg claude_local "$claude_local" \
    --arg opencode_local "$opencode_local" \
    --arg cursor_local "$cursor_local" \
    --arg antigravity_local "$antigravity_local" \
    --arg claude_latest "$claude_latest" \
    --arg codex_latest_stable "$codex_latest_stable" \
    --arg codex_latest_any "$codex_latest_any" \
    --arg opencode_latest "$opencode_latest" \
    --arg cursor_latest_date "$cursor_latest_date" \
    --arg cursor_latest_title "$cursor_latest_title" \
    --arg antigravity_latest "$antigravity_latest" \
    --arg antigravity_hooks_signal_version "$antigravity_hooks_signal_version" \
    --arg antigravity_hooks_signal_type "$antigravity_hooks_signal_type" \
    --arg antigravity_hooks_signal_excerpt "$antigravity_hooks_signal_excerpt" \
    --arg codex_status "$codex_status" \
    --arg claude_status "$claude_status" \
    --arg opencode_status "$opencode_status" \
    --arg cursor_status "$cursor_status" \
    --arg antigravity_status "$antigravity_status" \
    --argjson antigravity_hooks_detected "$antigravity_hooks_detected" \
    --argjson antigravity_hooks_introduced "$antigravity_hooks_introduced" \
    '{
      generated_at: $generated_at,
      local: {
        codex: {installed: ($codex_local | select(. != "") // null)},
        claude_code: {installed: ($claude_local | select(. != "") // null)},
        opencode: {installed: ($opencode_local | select(. != "") // null)},
        cursor: {installed: ($cursor_local | select(. != "") // null)},
        antigravity: {installed: ($antigravity_local | select(. != "") // null)}
      },
      upstream: {
        claude_code: {
          latest_stable: ($claude_latest | select(. != "") // null),
          source: "https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md"
        },
        codex: {
          latest_stable: ($codex_latest_stable | select(. != "") // null),
          latest_any: ($codex_latest_any | select(. != "") // null),
          source: "https://github.com/openai/codex/releases"
        },
        opencode: {
          latest_stable: ($opencode_latest | select(. != "") // null),
          source: "https://github.com/anomalyco/opencode/releases"
        },
        cursor: {
          latest_entry_date: ($cursor_latest_date | select(. != "") // null),
          latest_entry_title: ($cursor_latest_title | select(. != "") // null),
          source: "https://cursor.com/changelog"
        },
        antigravity: {
          latest_stable: ($antigravity_latest | select(. != "") // null),
          source: "https://antigravity.google/releases",
          hooks_detected: $antigravity_hooks_detected,
          hooks_signal_version: ($antigravity_hooks_signal_version | select(. != "") // null),
          hooks_signal_type: ($antigravity_hooks_signal_type | select(. != "") // null),
          hooks_signal_excerpt: ($antigravity_hooks_signal_excerpt | select(. != "") // null)
        }
      },
      status: {
        codex: $codex_status,
        claude_code: $claude_status,
        opencode: $opencode_status,
        cursor: $cursor_status,
        antigravity: $antigravity_status
      },
      alerts: {
        antigravity_hooks_introduced: $antigravity_hooks_introduced
      }
    }')"

  printf '%s\n' "$snapshot" >"$VERSIONS_SNAPSHOT_PATH"
  printf '%s\n' "$snapshot" | jq -c . >>"$VERSIONS_HISTORY_PATH"

  log "Version snapshot saved: ${VERSIONS_SNAPSHOT_PATH}"
  log "Version history appended: ${VERSIONS_HISTORY_PATH}"
  if [ "$antigravity_hooks_introduced" = "true" ]; then
    warn "Antigravity hooks signal detected upstream (version: ${antigravity_hooks_signal_version:-unknown}). Review hook wiring requirements."
  fi

  printf '%s\n' "$snapshot" \
    | jq -r '
      "Tool versions:",
      "  codex        local=\(.local.codex.installed // "n/a") upstream=\(.upstream.codex.latest_stable // "n/a") status=\(.status.codex)",
      "  claude_code  local=\(.local.claude_code.installed // "n/a") upstream=\(.upstream.claude_code.latest_stable // "n/a") status=\(.status.claude_code)",
      "  opencode     local=\(.local.opencode.installed // "n/a") upstream=\(.upstream.opencode.latest_stable // "n/a") status=\(.status.opencode)",
      "  cursor       local=\(.local.cursor.installed // "n/a") upstream_date=\(.upstream.cursor.latest_entry_date // "n/a") status=\(.status.cursor)",
      "  antigravity  local=\(.local.antigravity.installed // "n/a") upstream=\(.upstream.antigravity.latest_stable // "n/a") status=\(.status.antigravity)",
      "  antigravity_hooks detected=\(.upstream.antigravity.hooks_detected) introduced=\(.alerts.antigravity_hooks_introduced) version=\(.upstream.antigravity.hooks_signal_version // "n/a")"
    '

  trap - RETURN
  rm -rf "$tmp_dir"
}

# Resolve Go MCP binary file name for the current OS/arch.
# Outputs the bare filename (e.g. harness-mcp-darwin-arm64 or harness-mcp-windows-amd64.exe).
# Maps Git Bash / MSYS / Cygwin to "windows" and appends .exe for that case.
_resolve_go_bin_name() {
  local os arch uname_s
  uname_s="$(uname -s)"
  case "$uname_s" in
    Darwin)                            os="darwin" ;;
    Linux)                             os="linux" ;;
    MINGW*|MSYS*|CYGWIN*|Windows_NT)   os="windows" ;;
    *)                                 os="$(printf '%s' "$uname_s" | tr '[:upper:]' '[:lower:]')" ;;
  esac
  arch="$(uname -m)"
  case "$arch" in
    x86_64|amd64)   arch="amd64" ;;
    aarch64|arm64)  arch="arm64" ;;
  esac
  if [ "$os" = "windows" ]; then
    printf '%s\n' "harness-mcp-${os}-${arch}.exe"
  else
    printf '%s\n' "harness-mcp-${os}-${arch}"
  fi
}

is_windows_shell() {
  case "$(uname -s 2>/dev/null || printf '')" in
    MINGW*|MSYS*|CYGWIN*|Windows_NT)
      return 0
      ;;
  esac
  [ "${OS:-}" = "Windows_NT" ]
}

is_windows_style_path() {
  local value="${1:-}"
  [[ "$value" =~ ^[A-Za-z]:[\\/] ]] || [[ "$value" == \\* ]]
}

file_exists_for_current_shell() {
  local value="${1:-}" posix_value=""
  [ -n "$value" ] || return 1
  if [ -f "$value" ]; then
    return 0
  fi
  if is_windows_shell && is_windows_style_path "$value"; then
    if command -v cygpath >/dev/null 2>&1; then
      posix_value="$(cygpath -u "$value" 2>/dev/null || true)"
      [ -n "$posix_value" ] && [ -f "$posix_value" ] && return 0
      return 1
    fi
    return 0
  fi
  return 1
}

parent_dir_exists_for_current_shell() {
  local value="${1:-}" dir posix_value=""
  [ -n "$value" ] || return 1
  dir="$(dirname "$value")"
  if [ -d "$dir" ]; then
    return 0
  fi
  if is_windows_shell && is_windows_style_path "$value"; then
    if command -v cygpath >/dev/null 2>&1; then
      posix_value="$(cygpath -u "$value" 2>/dev/null || true)"
      [ -n "$posix_value" ] && [ -d "$(dirname "$posix_value")" ] && return 0
      return 1
    fi
    return 0
  fi
  return 1
}

download_go_binary() {
  local bin_name bin_path release_url version
  bin_name="$(_resolve_go_bin_name)"
  bin_path="${HARNESS_ROOT}/bin/${bin_name}"

  # Already present — skip
  if [ -x "$bin_path" ]; then
    return 0
  fi

  # Ensure bin directory exists
  mkdir -p "${HARNESS_ROOT}/bin"

  # Pin to current package version to avoid version skew
  version="$(read_current_harness_mem_version 2>/dev/null || true)"

  if [ -n "$version" ]; then
    release_url="$(curl -sf --max-time 5 \
      "https://api.github.com/repos/Chachamaru127/harness-mem/releases/tags/v${version}" \
      | grep "browser_download_url.*${bin_name}\"" \
      | head -1 \
      | cut -d'"' -f4 2>/dev/null)" || true
  fi

  # Fallback: latest release (warn about possible version skew)
  if [ -z "$release_url" ]; then
    if [ -n "$version" ]; then
      log "Go MCP binary v${version} not found in Releases — falling back to latest (version skew possible)"
    fi
    release_url="$(curl -sf --max-time 5 \
      "https://api.github.com/repos/Chachamaru127/harness-mem/releases/latest" \
      | grep "browser_download_url.*${bin_name}\"" \
      | head -1 \
      | cut -d'"' -f4 2>/dev/null)" || true
  fi

  if [ -z "$release_url" ]; then
    log "Go MCP binary not available for ${bin_name} — using Node.js"
    return 1
  fi

  log "Downloading Go MCP binary: ${bin_name}"
  if curl -sfL --max-time 30 "$release_url" -o "$bin_path" && chmod +x "$bin_path"; then
    log "Go MCP binary installed: ${bin_path}"
    return 0
  else
    rm -f "$bin_path"
    log "Go MCP binary download failed — using Node.js fallback"
    return 1
  fi
}

ensure_mcp_runtime() {
  # Try to install Go MCP binary (best-effort, silent failure)
  download_go_binary 2>/dev/null || true

  local mcp_dir="${HARNESS_ROOT}/mcp-server"
  local mcp_dist="${mcp_dir}/dist/index.js"
  local mcp_src_entry="${mcp_dir}/src/index.ts"
  local mcp_sdk_local="${mcp_dir}/node_modules/@modelcontextprotocol/sdk/package.json"
  local mcp_sdk_root="${HARNESS_ROOT}/node_modules/@modelcontextprotocol/sdk/package.json"

  if [ ! -f "$mcp_dist" ]; then
    [ -f "$mcp_src_entry" ] || fail "MCP dist entry missing and source unavailable: $mcp_dist"
    log "MCP dist entry missing. Bootstrapping local MCP build."
    (
      cd "$mcp_dir"
      npm install --silent --include=dev
      npm run build --silent
    )
  fi

  [ -f "$mcp_dist" ] || fail "MCP dist build failed: $mcp_dist"

  if [ -f "$mcp_sdk_local" ] || [ -f "$mcp_sdk_root" ]; then
    return
  fi

  log "Installing MCP server dependencies (one-time)"
  (
    cd "$mcp_dir"
    npm install --silent
  )
}

remove_marked_block() {
  local file="$1"
  local begin="$2"
  local end="$3"
  local tmp
  tmp="$(mktemp)"
  awk -v begin="$begin" -v end="$end" '
    index($0, begin) { skip = 1; next }
    index($0, end) { skip = 0; next }
    skip != 1 { print $0 }
  ' "$file" >"$tmp"
  mv "$tmp" "$file"
}

render_codex_hooks_template() {
  local hooks_template="$1"
  local output_path="$2"
  local escaped_root
  escaped_root="$(printf '%s' "$HARNESS_ROOT" | sed 's/[\/&]/\\&/g')"
  sed "s/__HARNESS_ROOT__/${escaped_root}/g" "$hooks_template" >"$output_path"
}

merge_codex_hooks_json() {
  local hooks_dst="$1"
  local rendered_template="$2"
  local tmp
  tmp="$(mktemp)"

  if jq --slurpfile tpl "$rendered_template" '
    def entry_commands($entry):
      [($entry.hooks // [])[]? | .command? // empty];
    def existing_commands($entries):
      [($entries // [])[]? | (.hooks // [])[]? | .command? // empty];
    .description = ($tpl[0].description // .description)
    | .hooks = (.hooks // {})
    | reduce ["SessionStart", "UserPromptSubmit", "Stop"][] as $event (
        .;
        .hooks[$event] = (
          (.hooks[$event] // []) as $existing
          | existing_commands($existing) as $existing_cmds
          | $existing + [
              ($tpl[0].hooks[$event] // [])[]
              | select(
                  entry_commands(.) as $required_cmds
                  | ($required_cmds | length) > 0
                  and ((($required_cmds - $existing_cmds) | length) == ($required_cmds | length))
                )
            ]
        )
      )
  ' "$hooks_dst" >"$tmp" 2>/dev/null; then
    mv "$tmp" "$hooks_dst"
    return 0
  fi

  rm -f "$tmp"
  return 1
}

check_codex_hooks_wiring() {
  local hooks_dst="${HOME}/.codex/hooks.json"
  local failed=0
  local event command

  if [ ! -f "$hooks_dst" ]; then
    warn "Missing Codex hooks file: $hooks_dst"
    return 1
  fi

  if ! jq empty "$hooks_dst" >/dev/null 2>&1; then
    warn "Codex hooks.json is invalid JSON: $hooks_dst"
    return 1
  fi

  while IFS='|' read -r event command; do
    if jq -e --arg event "$event" --arg command "$command" \
      'any((.hooks[$event] // [])[]?.hooks[]?; .command == $command)' \
      "$hooks_dst" >/dev/null 2>&1; then
      log "Codex ${event} hook: OK"
    else
      warn "Codex ${event} hook is missing in: $hooks_dst"
      failed=1
    fi
  done <<EOF
SessionStart|bash ${HARNESS_ROOT}/scripts/hook-handlers/codex-session-start.sh
UserPromptSubmit|bash ${HARNESS_ROOT}/scripts/hook-handlers/codex-user-prompt.sh
Stop|bash ${HARNESS_ROOT}/scripts/hook-handlers/codex-session-stop.sh
EOF

  return "$failed"
}

normalize_codex_escaped_paths() {
  # Codex can store a JSON notify payload inside TOML, so slash escaping may be
  # one or two layers deep: \/Users or \\/Users. Normalize both for path checks.
  sed -e 's#\\\\/#/#g' -e 's#\\/#/#g'
}

repair_codex_previous_notify_chain() {
  local cfg="$1"
  local notify_script="$2"
  local tmp
  [ -f "$cfg" ] || return 0
  if normalize_codex_escaped_paths <"$cfg" | grep -Fq "$notify_script"; then
    return 0
  fi
  if ! grep -q -- '--previous-notify' "$cfg" || ! grep -q 'memory-codex-notify\.sh' "$cfg"; then
    return 0
  fi

  tmp="$(mktemp)"
  sed -E "s#\\\\/[^\\\"]*memory-codex-notify\\.sh#${notify_script}#g" "$cfg" >"$tmp"
  mv "$tmp" "$cfg"
  log "Updated Codex previous-notify harness path: $cfg"
}

check_codex_config_wiring() {
  local cfg="$1"
  local failed=0
  local notify_value notify_script mcp_args_value mcp_cwd_value mcp_entry_path
  local mcp_url_value mcp_bearer_token_env_var
  local harness_section harness_env_section

  # notify is intentionally file-global — it lives at top-level config, not under [mcp_servers.harness]
  # Supports both array format (Codex 0.118.0+) and legacy string format:
  #   Array:  notify = ["bash", "/path/to/memory-codex-notify.sh"]
  #   Legacy: notify = "bash /path/to/memory-codex-notify.sh"
  local expected_notify_script="${HARNESS_ROOT}/scripts/hook-handlers/memory-codex-notify.sh"
  local normalized_cfg=""
  notify_script=""
  normalized_cfg="$(normalize_codex_escaped_paths <"$cfg")"
  if printf '%s\n' "$normalized_cfg" | grep -Fq "$expected_notify_script"; then
    notify_script="$expected_notify_script"
  elif grep -qE 'notify[[:space:]]*=[[:space:]]*\[' "$cfg"; then
    notify_script="$(grep -oE '"([^"]*memory-codex-notify\.sh)"' "$cfg" | head -1 | tr -d '"')"
  elif grep -qE 'notify[[:space:]]*=[[:space:]]*"' "$cfg"; then
    notify_script="$(sed -nE 's/^[[:space:]]*notify[[:space:]]*=[[:space:]]*"bash[[:space:]]+([^"]+)".*/\1/p' "$cfg" | head -n1)"
  fi
  if [ -n "$notify_script" ] && [ -f "$notify_script" ]; then
    log "Codex notify wiring path: OK"
  else
    warn "Codex notify wiring is missing or points to a non-existent script in: $cfg"
    failed=1
  fi

  if grep -qE '^\[mcp_servers\.harness\]' "$cfg"; then
    log "Codex MCP wiring section: OK"
  else
    warn "Codex MCP wiring is missing in: $cfg"
    failed=1
  fi

  # Section-scoped extraction: awk Pass 1 — [mcp_servers.harness] parent keys only
  # Stops at [mcp_servers.harness.env] or any other [section] — intentional
  # Pattern allows trailing whitespace/comments per TOML spec
  harness_section="$(awk '/^\[mcp_servers\.harness\][[:space:]]*(#.*)?$/{found=1;next} /^\[/{found=0} found' "$cfg")"
  local mcp_command_value
  mcp_command_value="$(echo "$harness_section" | sed -nE 's/^[[:space:]]*command[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' | head -n1)"
  mcp_args_value="$(echo "$harness_section" | sed -nE 's/^[[:space:]]*args[[:space:]]*=[[:space:]]*\[[[:space:]]*"([^"]+)".*/\1/p' | head -n1)"
  mcp_cwd_value="$(echo "$harness_section" | sed -nE 's/^[[:space:]]*cwd[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' | head -n1)"
  mcp_url_value="$(echo "$harness_section" | sed -nE 's/^[[:space:]]*url[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' | head -n1)"
  mcp_bearer_token_env_var="$(echo "$harness_section" | sed -nE 's/^[[:space:]]*bearer_token_env_var[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' | head -n1)"

  if [ -n "$mcp_url_value" ]; then
    local expected_mcp_url expected_token_env_var
    MCP_HTTP_CONFIG_DETECTED=1
    expected_mcp_url="$(_mcp_gateway_endpoint_url)"
    expected_token_env_var="${HARNESS_MEM_MCP_TOKEN_ENV_VAR:-HARNESS_MEM_MCP_TOKEN}"
    if [ "$mcp_url_value" != "$expected_mcp_url" ]; then
      warn "Codex MCP HTTP URL is stale in: $cfg (expected ${expected_mcp_url}, got ${mcp_url_value})"
      failed=1
    elif [ "$mcp_bearer_token_env_var" != "$expected_token_env_var" ]; then
      warn "Codex MCP HTTP bearer token env var is missing or unexpected in: $cfg"
      failed=1
    else
      log "Codex MCP HTTP wiring: OK"
    fi
    return "$failed"
  fi

  # Resolve MCP entry point: prefer command (wrapper script, v0.11.0+),
  # fall back to args[0] (legacy node + dist/index.js path).
  mcp_entry_path=""
  if [ -n "$mcp_command_value" ] && [[ "$mcp_command_value" = /* ]]; then
    mcp_entry_path="$mcp_command_value"
  elif [ -n "$mcp_command_value" ] && is_windows_shell && is_windows_style_path "$mcp_command_value"; then
    mcp_entry_path="$mcp_command_value"
  elif [ -n "$mcp_args_value" ]; then
    if [[ "$mcp_args_value" = /* ]]; then
      mcp_entry_path="$mcp_args_value"
    elif is_windows_shell && is_windows_style_path "$mcp_args_value"; then
      mcp_entry_path="$mcp_args_value"
    elif [ -n "$mcp_cwd_value" ]; then
      mcp_entry_path="${mcp_cwd_value}/${mcp_args_value}"
    fi
  fi

  if [ -n "$mcp_entry_path" ] && file_exists_for_current_shell "$mcp_entry_path"; then
    log "Codex MCP entrypoint path: OK"
  else
    warn "Codex MCP wiring points to a non-existent entrypoint in: $cfg"
    failed=1
  fi

  if [ -n "$mcp_cwd_value" ]; then
    if [ -d "$mcp_cwd_value" ]; then
      log "Codex MCP cwd path: OK"
    else
      warn "Codex MCP cwd points to a non-existent directory in: $cfg"
      failed=1
    fi
  fi

  # Section-scoped extraction: awk Pass 2 — [mcp_servers.harness.env] keys
  harness_env_section="$(awk '/^\[mcp_servers\.harness\.env\][[:space:]]*(#.*)?$/{found=1;next} /^\[/{found=0} found' "$cfg")"

  # Validate env values — trust boundary: read-only check, values are never written to
  local cfg_host cfg_port cfg_db_path
  cfg_host="$(echo "$harness_env_section" | sed -nE 's/^[[:space:]]*HARNESS_MEM_HOST[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' | head -n1)"
  cfg_port="$(echo "$harness_env_section" | sed -nE 's/^[[:space:]]*HARNESS_MEM_PORT[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' | head -n1)"
  cfg_db_path="$(echo "$harness_env_section" | sed -nE 's/^[[:space:]]*HARNESS_MEM_DB_PATH[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' | head -n1)"

  if [ -z "$cfg_host" ] || [ -z "$cfg_port" ] || [ -z "$cfg_db_path" ]; then
    warn "Codex MCP env wiring is incomplete (missing keys) in: $cfg"
    failed=1
  elif [ "$cfg_host" != "$MEM_HOST" ] || [ "$cfg_port" != "$MEM_PORT" ]; then
    warn "Codex MCP env wiring has stale HOST/PORT values in: $cfg (expected ${MEM_HOST}:${MEM_PORT}, got ${cfg_host}:${cfg_port})"
    failed=1
  elif [ -n "$cfg_db_path" ] && ! parent_dir_exists_for_current_shell "$cfg_db_path"; then
    warn "Codex MCP DB_PATH parent directory does not exist in: $cfg (${cfg_db_path})"
    failed=1
  else
    log "Codex MCP env wiring: OK"
  fi

  return "$failed"
}

codex_harness_mcp_has_http_url() {
  local cfg="$1"
  [ -f "$cfg" ] || return 1
  awk '/^\[mcp_servers\.harness\][[:space:]]*(#.*)?$/{found=1;next} /^\[/{found=0} found' "$cfg" \
    | grep -qE '^[[:space:]]*url[[:space:]]*='
}

print_codex_mcp_block() {
  case "$(_normalize_mcp_transport_option "$MCP_CONFIG_TRANSPORT" 2>/dev/null || printf 'stdio')" in
    streamable_http)
      cat <<EOF
$BEGIN_CODEX_MCP
[mcp_servers.harness]
url = "$(_mcp_gateway_endpoint_url)"
bearer_token_env_var = "${HARNESS_MEM_MCP_TOKEN_ENV_VAR:-HARNESS_MEM_MCP_TOKEN}"
$END_CODEX_MCP
EOF
      ;;
    *)
      cat <<EOF
$BEGIN_CODEX_MCP
[mcp_servers.harness]
command = "${HARNESS_ROOT}/bin/harness-mcp-server"
args = []
cwd = "${HARNESS_ROOT}"
enabled = true

[mcp_servers.harness.env]
HARNESS_MEM_HOST = "${MEM_HOST}"
HARNESS_MEM_PORT = "${MEM_PORT}"
HARNESS_MEM_DB_PATH = "${DB_PATH}"
$END_CODEX_MCP
EOF
      ;;
  esac
}

rewrite_codex_harness_wiring() {
  local cfg="$1"
  local notify_script="$2"
  local tmp backup
  local normalized_cfg has_expected_notify has_any_notify has_memory_notify has_previous_notify_chain emit_notify
  local notify_count has_managed_notify remove_memory_notify

  backup="${cfg}.bak-harness-mem-$(date +%Y%m%d%H%M%S)"
  tmp="$(mktemp)"
  cp "$cfg" "$backup" >/dev/null 2>&1 || true

  normalized_cfg="$(normalize_codex_escaped_paths <"$cfg")"
  has_expected_notify=0
  has_any_notify=0
  has_memory_notify=0
  has_previous_notify_chain=0
  notify_count=0
  has_managed_notify=0
  emit_notify=1
  remove_memory_notify=1

  if printf '%s\n' "$normalized_cfg" | grep -Fq "$notify_script"; then
    has_expected_notify=1
  fi
  if grep -qE '^[[:space:]]*notify[[:space:]]*=' "$cfg"; then
    has_any_notify=1
  fi
  if grep -q 'memory-codex-notify\.sh' "$cfg"; then
    has_memory_notify=1
  fi
  if grep -q -- '--previous-notify' "$cfg"; then
    has_previous_notify_chain=1
  fi
  notify_count="$(grep -cE '^[[:space:]]*notify[[:space:]]*=' "$cfg" 2>/dev/null || true)"
  if grep -Fq "$BEGIN_CODEX_NOTIFY" "$cfg"; then
    has_managed_notify=1
  fi

  if [ "$has_previous_notify_chain" -eq 1 ] && [ "$has_memory_notify" -eq 1 ]; then
    emit_notify=0
    remove_memory_notify=0
  elif [ "$has_expected_notify" -eq 1 ]; then
    if [ "$has_managed_notify" -eq 1 ] && [ "${notify_count:-0}" -le 1 ]; then
      emit_notify=1
    else
      emit_notify=0
    fi
  elif [ "$has_any_notify" -eq 1 ] && [ "$has_memory_notify" -eq 0 ]; then
    emit_notify=0
    remove_memory_notify=0
    warn "Codex config already has notify=. Preserving existing notify and skipping harness-mem notify injection: $cfg"
  fi

  {
    if [ "$emit_notify" -eq 1 ]; then
      cat <<EOF
$BEGIN_CODEX_NOTIFY
notify = ["bash", "${notify_script}"]
$END_CODEX_NOTIFY

EOF
    fi
    awk -v begin_notify="$BEGIN_CODEX_NOTIFY" \
      -v end_notify="$END_CODEX_NOTIFY" \
      -v begin_mcp="$BEGIN_CODEX_MCP" \
      -v end_mcp="$END_CODEX_MCP" \
      -v remove_memory_notify="$remove_memory_notify" '
        $0 == begin_notify { skip = 1; next }
        $0 == end_notify { skip = 0; next }
        $0 == begin_mcp { skip = 1; next }
        $0 == end_mcp { skip = 0; next }
        /^\[mcp_servers\.harness(\.env)?\][[:space:]]*($|#)/ { skip = 1; next }
        skip {
          if ($0 ~ /^\[/) {
            skip = 0
            print
          }
          next
        }
        remove_memory_notify && /^[[:space:]]*notify[[:space:]]*=/ && /memory-codex-notify\.sh/ { next }
        { print }
      ' "$cfg"
    echo ""
    print_codex_mcp_block
  } >"$tmp"

  mv "$tmp" "$cfg"
  log "Rewrote Codex harness wiring: $cfg (backup: $backup)"
}

ensure_codex_hooks_feature_enabled() {
  local cfg="$1"
  local tmp

  [ -f "$cfg" ] || return 1

  if rg -q '^[[:space:]]*features\.(codex_hooks|hooks)[[:space:]]*=[[:space:]]*true([[:space:]]|$)' "$cfg"; then
    return 0
  fi

  if rg -q '^[[:space:]]*features\.hooks[[:space:]]*=' "$cfg"; then
    tmp="$(mktemp)"
    sed -E 's/^[[:space:]]*features\.hooks[[:space:]]*=.*/features.hooks = true/' "$cfg" >"$tmp"
    mv "$tmp" "$cfg"
    return 0
  fi

  if rg -q '^[[:space:]]*features\.codex_hooks[[:space:]]*=' "$cfg"; then
    tmp="$(mktemp)"
    sed -E 's/^[[:space:]]*features\.codex_hooks[[:space:]]*=.*/features.hooks = true/' "$cfg" >"$tmp"
    mv "$tmp" "$cfg"
    return 0
  fi

  if rg -q '^[[:space:]]*\[features\][[:space:]]*$' "$cfg"; then
    tmp="$(mktemp)"
    awk '
      BEGIN { in_features = 0; done = 0 }
      /^[[:space:]]*\[features\][[:space:]]*$/ {
        print
        in_features = 1
        next
      }
      in_features && /^[[:space:]]*\[/ {
        if (!done) {
          print "codex_hooks = true"
          done = 1
        }
        in_features = 0
      }
      in_features && /^[[:space:]]*(codex_hooks|hooks)[[:space:]]*=/ {
        if (!done) {
          print "hooks = true"
          done = 1
        }
        next
      }
      { print }
      END {
        if (in_features && !done) {
          print "hooks = true"
        }
      }
    ' "$cfg" >"$tmp"
    mv "$tmp" "$cfg"
    return 0
  fi

  cat >>"$cfg" <<EOF

$BEGIN_CODEX_FEATURES
[features]
hooks = true
$END_CODEX_FEATURES
EOF
}

check_codex_hooks_feature_enabled() {
  local cfg="$1"

  if grep -qE '^[[:space:]]*features\.(codex_hooks|hooks)[[:space:]]*=[[:space:]]*true([[:space:]]|$)' "$cfg"; then
    log "Codex hooks feature flag: OK"
    return 0
  fi

  if awk '
    BEGIN { in_features = 0; enabled = 0 }
    /^[[:space:]]*\[features\][[:space:]]*$/ { in_features = 1; next }
    in_features && /^[[:space:]]*\[/ { in_features = 0 }
    in_features && /^[[:space:]]*(codex_hooks|hooks)[[:space:]]*=[[:space:]]*true([[:space:]]|$)/ { enabled = 1 }
    END { exit enabled ? 0 : 1 }
  ' "$cfg"; then
    log "Codex hooks feature flag: OK"
    return 0
  fi

  warn "Codex hooks feature flag is missing or disabled in: $cfg"
  return 1
}

setup_codex_wiring() {
  local codex_dir="${HOME}/.codex"
  local cfg="${codex_dir}/config.toml"
  local rules_src="${HARNESS_ROOT}/codex/.codex/rules/harness.rules"
  local rules_dst="${codex_dir}/rules/harness.rules"
  # Codex 0.118.0+ requires notify as an array, not a string.
  local notify_script="${HARNESS_ROOT}/scripts/hook-handlers/memory-codex-notify.sh"

  mkdir -p "$codex_dir"
  if [ -f "$cfg" ]; then
    repair_codex_previous_notify_chain "$cfg" "$notify_script"
  fi

  if [ ! -f "$cfg" ]; then
    {
      cat <<EOF
# Codex Team Config (generated by harness-mem)

$BEGIN_CODEX_NOTIFY
notify = ["bash", "${notify_script}"]
$END_CODEX_NOTIFY

EOF
      print_codex_mcp_block
    } >"$cfg"
    log "Created Codex config: $cfg"
  else
    local had_managed_wiring=0
    local current_has_http=0
    if rg -qi 'memory-codex-notify\.sh|^\[mcp_servers\.harness\]|bin/harness-mcp-server|mcp-server/dist/index\.js' "$cfg"; then
      had_managed_wiring=1
    fi
    if codex_harness_mcp_has_http_url "$cfg"; then
      current_has_http=1
    fi

    if ! rg -q 'memory-codex-notify\.sh' "$cfg"; then
      if rg -q '^\s*notify\s*=' "$cfg"; then
        warn "Codex config already has notify=. Add memory notify manually: $cfg"
      else
        cat >>"$cfg" <<EOF

$BEGIN_CODEX_NOTIFY
notify = ["bash", "${notify_script}"]
$END_CODEX_NOTIFY
EOF
        log "Added Codex notify wiring: $cfg"
      fi
    fi

    # Migrate legacy string format (notify = "bash /path/...") to array format
    # required by Codex 0.118.0+. The old setup wrote string format; new setup
    # writes array format. This block upgrades existing installs in place.
    if grep -qE 'notify[[:space:]]*=[[:space:]]*"bash ' "$cfg"; then
      local tmp
      tmp="$(mktemp)"
      sed -E 's|notify[[:space:]]*=[[:space:]]*"bash ([^"]+)"|notify = ["bash", "\1"]|' "$cfg" >"$tmp"
      mv "$tmp" "$cfg"
      log "Migrated Codex notify from legacy string to array format: $cfg"
    fi

    if ! rg -q '^\[mcp_servers\.harness\]' "$cfg"; then
      {
        echo ""
        print_codex_mcp_block
      } >>"$cfg"
      log "Added Codex MCP wiring: $cfg"
    fi

    if [ -n "$DOCTOR_MCP_TRANSPORT" ] && grep -Fq "$BEGIN_CODEX_MCP" "$cfg"; then
      local desired_transport
      desired_transport="$(_normalize_mcp_transport_option "$MCP_CONFIG_TRANSPORT" 2>/dev/null || printf 'stdio')"
      if { [ "$desired_transport" = "streamable_http" ] && [ "$current_has_http" -eq 0 ]; } \
        || { [ "$desired_transport" = "stdio" ] && [ "$current_has_http" -eq 1 ]; }; then
        rewrite_codex_harness_wiring "$cfg" "$notify_script"
        if codex_harness_mcp_has_http_url "$cfg"; then
          current_has_http=1
        else
          current_has_http=0
        fi
      fi
    fi

    if [ "$had_managed_wiring" -eq 1 ] && ! check_codex_config_wiring "$cfg" >/dev/null 2>&1; then
      rewrite_codex_harness_wiring "$cfg" "$notify_script"
    fi
  fi

  ensure_codex_hooks_feature_enabled "$cfg"
  log "Ensured Codex hooks feature flag: $cfg"

  if [ -f "$rules_src" ] && [ ! -f "$rules_dst" ]; then
    mkdir -p "$(dirname "$rules_dst")"
    cp "$rules_src" "$rules_dst"
    log "Copied memory-aware Codex rules: $rules_dst"
  fi

  # Codex experimental hooks engine (v0.116.0+)
  local hooks_template="${HARNESS_ROOT}/codex/.codex/hooks.json"
  local hooks_dst="${codex_dir}/hooks.json"
  local rendered_template=""
  if [ -f "$hooks_template" ]; then
    rendered_template="$(mktemp)"
    render_codex_hooks_template "$hooks_template" "$rendered_template"
    if [ ! -f "$hooks_dst" ]; then
      cp "$rendered_template" "$hooks_dst"
      log "Installed Codex hooks: $hooks_dst"
    else
      if merge_codex_hooks_json "$hooks_dst" "$rendered_template"; then
        log "Merged Codex hooks into existing hooks.json: $hooks_dst"
      else
        rm -f "$rendered_template"
        fail "Failed to merge Codex hooks: $hooks_dst"
      fi
    fi
    rm -f "$rendered_template"
  fi

  if [ "$SETUP_INSTALL_CODEX_SKILL" -eq 1 ]; then
    install_codex_skill
  fi
}

check_codex_wiring() {
  local cfg="${HOME}/.codex/config.toml"
  local failed=0

  if [ ! -f "$cfg" ]; then
    warn "Missing Codex config: $cfg"
    return 1
  fi

  if check_codex_config_wiring "$cfg"; then
    :
  else
    failed=1
  fi

  if check_codex_hooks_feature_enabled "$cfg"; then
    :
  else
    failed=1
  fi

  check_codex_skill_bundle >/dev/null 2>&1 || true

  if check_codex_hooks_wiring; then
    :
  else
    failed=1
  fi

  local prepare_hook
  prepare_hook="$(git -C "$TARGET_DIR" rev-parse --git-path hooks/prepare-commit-msg 2>/dev/null || true)"
  if [ -n "$prepare_hook" ] && [ -f "$prepare_hook" ]; then
    if grep -qiE 'codex|command_attribution|prepare-commit-msg' "$prepare_hook"; then
      log "Codex git hook coexistence: prepare-commit-msg detected ($prepare_hook)"
    else
      log "Git prepare-commit-msg hook detected (left untouched by harness-mem): $prepare_hook"
    fi
  else
    log "Git prepare-commit-msg hook: not found in current repository (coexistence check skipped)"
  fi

  return "$failed"
}

CODEX_SKILL_BUNDLE_STATUS="unknown"
check_codex_skill_bundle() {
  local missing=0
  local drift=0
  local name skill_src skill_dst
  for name in harness-mem harness-recall; do
    skill_src="${HARNESS_ROOT}/codex/skills/${name}/SKILL.md"
    skill_dst="${HOME}/.codex/skills/${name}/SKILL.md"
    if [ ! -f "$skill_src" ]; then
      warn "Codex Agent Skill source missing: $skill_src"
      missing=1
      continue
    fi
    if [ ! -f "$skill_dst" ]; then
      log "Codex Agent Skill: missing ($skill_dst)"
      missing=1
      continue
    fi
    if cmp -s "$skill_src" "$skill_dst"; then
      log "Codex Agent Skill: OK ($skill_dst)"
    else
      log "Codex Agent Skill: drift ($skill_dst)"
      drift=1
    fi
  done
  if [ "$missing" -ne 0 ]; then
    CODEX_SKILL_BUNDLE_STATUS="missing"
    return 1
  fi
  if [ "$drift" -ne 0 ]; then
    CODEX_SKILL_BUNDLE_STATUS="drift"
    return 1
  fi
  CODEX_SKILL_BUNDLE_STATUS="ok"
  return 0
}

check_codex_requirements_alignment() {
  local req="${HOME}/.codex/requirements.toml"
  local stale=0
  local harness_mentions=0
  local extracted_any=0
  local req_host req_port path

  if [ ! -f "$req" ]; then
    log "Codex requirements precedence: no requirements.toml detected"
    return 0
  fi

  if rg -qi 'harness|HARNESS_MEM_|memory-codex-notify|hook-handlers|bin/harness-mcp-server|mcp-server/dist/index\.js' "$req" 2>/dev/null; then
    harness_mentions=1
  fi

  if [ "$harness_mentions" -eq 0 ]; then
    log "Codex requirements precedence: requirements.toml has no harness-managed entries"
    return 0
  fi

  while IFS= read -r path; do
    [ -n "$path" ] || continue
    case "$path" in
        *memory-codex-notify.sh|*codex-session-start.sh|*codex-user-prompt.sh|*codex-session-stop.sh|*bin/harness-mcp-server|*mcp-server/dist/index.js|*"/.codex/skills/harness-mem/"*|*"/.codex/skills/harness-recall/"*)
        extracted_any=1
        case "$path" in
          "$HARNESS_ROOT"/*|"$HOME/.codex/"*)
            :
            ;;
          *)
            warn "Codex requirements.toml points to a stale harness path: $path"
            stale=1
            ;;
        esac
        ;;
    esac
  done < <(grep -oE '"/[^"]+"' "$req" 2>/dev/null | tr -d '"')

  req_host="$(sed -nE 's/^[[:space:]]*HARNESS_MEM_HOST[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' "$req" | head -n1)"
  req_port="$(sed -nE 's/^[[:space:]]*HARNESS_MEM_PORT[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' "$req" | head -n1)"

  if [ -n "$req_host" ] && [ "$req_host" != "$MEM_HOST" ]; then
    warn "Codex requirements.toml has stale HARNESS_MEM_HOST: expected ${MEM_HOST}, got ${req_host}"
    stale=1
  fi
  if [ -n "$req_port" ] && [ "$req_port" != "$MEM_PORT" ]; then
    warn "Codex requirements.toml has stale HARNESS_MEM_PORT: expected ${MEM_PORT}, got ${req_port}"
    stale=1
  fi

  if [ "$stale" -eq 0 ]; then
    if [ "$extracted_any" -eq 1 ] || [ -n "$req_host" ] || [ -n "$req_port" ]; then
      log "Codex requirements precedence alignment: OK"
    else
      log "Codex requirements precedence: harness hints detected but no stale managed paths found"
    fi
    return 0
  fi

  return 1
}

get_claude_config_targets() {
  local primary="${HOME}/.claude.json"
  local secondary="${HOME}/.claude/settings.json"
  local emitted=0

  if [ -f "$primary" ]; then
    printf '%s\n' "$primary"
    emitted=1
  fi

  if [ -f "$secondary" ]; then
    if jq -e 'has("mcpServers")' "$secondary" >/dev/null 2>&1; then
      printf '%s\n' "$secondary"
      emitted=1
    fi
  fi

  if [ "$emitted" -eq 0 ]; then
    printf '%s\n' "$primary"
  fi
}

claude_json_has_harness_server() {
  local file="$1"
  [ -f "$file" ] || return 1
  jq -e '.mcpServers.harness != null' "$file" >/dev/null 2>&1
}

claude_json_harness_is_http() {
  local file="$1"
  [ -f "$file" ] || return 1
  jq -e '(.mcpServers.harness.type // "") == "http" or ((.mcpServers.harness.url // "") != "")' "$file" >/dev/null 2>&1
}

effective_claude_json_transport() {
  local file="$1"
  local requested
  requested="$(_normalize_mcp_transport_option "$MCP_CONFIG_TRANSPORT" 2>/dev/null || printf 'stdio')"
  if [ "$requested" = "streamable_http" ] \
    && [ "$MCP_TRANSPORT_EXPLICIT" -eq 0 ] \
    && claude_json_has_harness_server "$file" \
    && ! claude_json_harness_is_http "$file"; then
    printf 'stdio'
    return 0
  fi
  printf '%s' "$requested"
}

effective_claude_plugin_transport() {
  local file="$1"
  local requested
  requested="$(_normalize_mcp_transport_option "$MCP_CONFIG_TRANSPORT" 2>/dev/null || printf 'stdio')"
  if [ "$requested" = "streamable_http" ] \
    && [ "$MCP_TRANSPORT_EXPLICIT" -eq 0 ] \
    && [ -f "$file" ] \
    && jq -e '.plugins.harness.mcpServers.harness != null' "$file" >/dev/null 2>&1 \
    && ! jq -e '(.plugins.harness.mcpServers.harness.type // "") == "http" or ((.plugins.harness.mcpServers.harness.url // "") != "")' "$file" >/dev/null 2>&1; then
    printf 'stdio'
    return 0
  fi
  printf '%s' "$requested"
}

upsert_claude_json() {
  local file="$1"
  # Use absolute path — Claude Code CLI ignores cwd in MCP server config (v2.1.92+)
  local mcp_entry="${HARNESS_ROOT}/bin/harness-mcp-server"
  local node_path="${HARNESS_ROOT}/mcp-server/node_modules"
  local tmp transport http_url token_env_var auth_header

  mkdir -p "$(dirname "$file")"

  if [ ! -f "$file" ]; then
    cat >"$file" <<'EOF'
{
  "mcpServers": {}
}
EOF
    log "Created Claude config: $file"
  fi

  transport="$(effective_claude_json_transport "$file")"
  tmp="$(mktemp)"
  if [ "$transport" = "streamable_http" ]; then
    http_url="$(_mcp_gateway_endpoint_url)"
    token_env_var="${HARNESS_MEM_MCP_TOKEN_ENV_VAR:-HARNESS_MEM_MCP_TOKEN}"
    auth_header="Bearer \${${token_env_var}}"
    if jq \
      --arg http_url "$http_url" \
      --arg auth_header "$auth_header" \
      '
        .mcpServers = (.mcpServers // {})
        | .mcpServers.harness = (
            (.mcpServers.harness // {})
            | .type = "http"
            | .url = $http_url
            | .enabled = true
            | .headers = ((.headers // {}) + {"Authorization": $auth_header})
            | del(.command)
            | del(.args)
            | del(.cwd)
            | del(.env)
          )
      ' "$file" >"$tmp" 2>/dev/null; then
      mv "$tmp" "$file"
      log "Updated Claude HTTP MCP wiring: $file"
      return 0
    fi

    rm -f "$tmp"
    warn "Could not parse Claude config JSON: $file"
    return 1
  fi

  if jq \
    --arg mcp_entry "$mcp_entry" \
    --arg host "$MEM_HOST" \
    --arg port "$MEM_PORT" \
    --arg db_path "$DB_PATH" \
    --arg node_path "$node_path" \
    '
      .mcpServers = (.mcpServers // {})
      | .mcpServers.harness = (.mcpServers.harness // {})
      | .mcpServers.harness.command = $mcp_entry
      | del(.mcpServers.harness.args)
      | del(.mcpServers.harness.cwd)
      | del(.mcpServers.harness.type)
      | del(.mcpServers.harness.url)
      | del(.mcpServers.harness.headers)
      | .mcpServers.harness.enabled = true
      | .mcpServers.harness.env = ((.mcpServers.harness.env // {}) + {
          "HARNESS_MEM_HOST": $host,
          "HARNESS_MEM_PORT": $port,
          "HARNESS_MEM_DB_PATH": $db_path
        })
    ' "$file" >"$tmp" 2>/dev/null; then
    mv "$tmp" "$file"
    log "Updated Claude MCP wiring: $file"
    return 0
  fi

  rm -f "$tmp"
  warn "Could not parse Claude config JSON: $file"
  return 1
}

upsert_claude_settings_plugin() {
  local file="${HOME}/.claude/settings.json"
  # Use absolute path — Claude Code CLI ignores cwd in MCP server config (v2.1.92+)
  local mcp_entry="${HARNESS_ROOT}/bin/harness-mcp-server"
  local node_path="${HARNESS_ROOT}/mcp-server/node_modules"
  local tmp transport http_url token_env_var auth_header

  mkdir -p "$(dirname "$file")"

  if [ ! -f "$file" ]; then
    cat >"$file" <<'EOF'
{}
EOF
    log "Created Claude settings: $file"
  fi

  transport="$(effective_claude_plugin_transport "$file")"
  tmp="$(mktemp)"
  if [ "$transport" = "streamable_http" ]; then
    http_url="$(_mcp_gateway_endpoint_url)"
    token_env_var="${HARNESS_MEM_MCP_TOKEN_ENV_VAR:-HARNESS_MEM_MCP_TOKEN}"
    auth_header="Bearer \${${token_env_var}}"
    if jq \
      --arg http_url "$http_url" \
      --arg auth_header "$auth_header" \
      --arg plugin_root "$HARNESS_ROOT" \
      '
        .plugins = (.plugins // {})
        | .plugins.harness = {
            "source": "settings",
            "name": "harness-mem",
            "root": $plugin_root,
            "mcpServers": {
              "harness": {
                "type": "http",
                "url": $http_url,
                "enabled": true,
                "headers": {
                  "Authorization": $auth_header
                }
              }
            }
          }
      ' "$file" >"$tmp" 2>/dev/null; then
      mv "$tmp" "$file"
      log "Updated Claude settings with HTTP inline plugin: $file"
      return 0
    fi

    rm -f "$tmp"
    warn "Could not write inline plugin to Claude settings: $file"
    return 1
  fi

  if jq \
    --arg mcp_entry "$mcp_entry" \
    --arg host "$MEM_HOST" \
    --arg port "$MEM_PORT" \
    --arg db_path "$DB_PATH" \
    --arg node_path "$node_path" \
    --arg plugin_root "$HARNESS_ROOT" \
    '
      .plugins = (.plugins // {})
      | .plugins.harness = {
          "source": "settings",
          "name": "harness-mem",
          "root": $plugin_root,
          "mcpServers": {
            "harness": {
              "command": "node",
              "args": [$mcp_entry],
              "enabled": true,
              "env": {
                "HARNESS_MEM_HOST": $host,
                "HARNESS_MEM_PORT": $port,
                "HARNESS_MEM_DB_PATH": $db_path,
                "NODE_PATH": $node_path
              }
            }
          }
        }
    ' "$file" >"$tmp" 2>/dev/null; then
    mv "$tmp" "$file"
    log "Updated Claude settings with inline plugin: $file"
    return 0
  fi

  rm -f "$tmp"
  warn "Could not write inline plugin to Claude settings: $file"
  return 1
}

setup_claude_wiring() {
  if [ "$INLINE_PLUGIN" -eq 1 ]; then
    upsert_claude_settings_plugin
    return $?
  fi
  local failed=0
  local any_target=0
  while IFS= read -r cfg; do
    [ -n "$cfg" ] || continue
    any_target=1
    upsert_claude_json "$cfg" || failed=1
  done < <(get_claude_config_targets)

  if [ "$any_target" -eq 0 ] || [ "$failed" -ne 0 ]; then
    fail "Failed to set up Claude MCP wiring"
  fi
}

upsert_cursor_hooks_json() {
  local file="$1"
  local hook_command="$2"
  local tmp
  local template="${HARNESS_ROOT}/.cursor/hooks.json.example"

  mkdir -p "$(dirname "$file")"

  if [ ! -f "$file" ]; then
    if [ -f "$template" ]; then
      cp "$template" "$file"
    else
      cat >"$file" <<EOF
{
  "version": 1,
  "hooks": {
    "sessionStart": [
      { "command": "${hook_command}" }
    ],
    "beforeSubmitPrompt": [
      { "command": "${hook_command}" }
    ],
    "afterAgentResponse": [
      { "command": "${hook_command}" }
    ],
    "afterMCPExecution": [
      { "command": "${hook_command}" }
    ],
    "afterShellExecution": [
      { "command": "${hook_command}" }
    ],
    "afterFileEdit": [
      { "command": "${hook_command}" }
    ],
    "sessionEnd": [
      { "command": "${hook_command}" }
    ],
    "stop": [
      { "command": "${hook_command}" }
    ]
  }
}
EOF
    fi
    log "Created Cursor hooks config: $file"
  fi

  tmp="$(mktemp)"
  if jq --arg cmd "$hook_command" '
    def strip_memory_hook:
      map(
        if type == "object" then
          select(((.command // "") | contains("memory-cursor-event.sh")) | not)
        else
          .
        end
      );
    def has_exact_cmd($cmd):
      any(type == "object" and ((.command // "") == $cmd));
    def ensure_cmd($cmd):
      if has_exact_cmd($cmd) then . else . + [{command: $cmd}] end;

    .version = (.version // 1)
    | .hooks = (.hooks // {})
    | .hooks.sessionStart = ((.hooks.sessionStart // []) | strip_memory_hook | ensure_cmd($cmd))
    | .hooks.beforeSubmitPrompt = ((.hooks.beforeSubmitPrompt // []) | strip_memory_hook | ensure_cmd($cmd))
    | .hooks.afterAgentResponse = ((.hooks.afterAgentResponse // []) | strip_memory_hook | ensure_cmd($cmd))
    | .hooks.afterMCPExecution = ((.hooks.afterMCPExecution // []) | strip_memory_hook | ensure_cmd($cmd))
    | .hooks.afterShellExecution = ((.hooks.afterShellExecution // []) | strip_memory_hook | ensure_cmd($cmd))
    | .hooks.afterFileEdit = ((.hooks.afterFileEdit // []) | strip_memory_hook | ensure_cmd($cmd))
    | .hooks.sessionEnd = ((.hooks.sessionEnd // []) | strip_memory_hook | ensure_cmd($cmd))
    | .hooks.stop = ((.hooks.stop // []) | strip_memory_hook | ensure_cmd($cmd))
  ' "$file" >"$tmp" 2>/dev/null; then
    mv "$tmp" "$file"
    log "Updated Cursor hooks config: $file"
  else
    rm -f "$tmp"
    warn "Could not parse Cursor hooks JSON: $file"
  fi
}

setup_cursor_hook_script() {
  local script_path="${HOME}/.cursor/hooks/memory-cursor-event.sh"
  local template="${HARNESS_ROOT}/.cursor/hooks/memory-cursor-event.sh"

  mkdir -p "$(dirname "$script_path")"

  if [ -f "$template" ]; then
    cp "$template" "$script_path"
  else
    cat >"$script_path" <<'EOF'
#!/bin/bash
set +e
exit 0
EOF
  fi

  local escaped_root
  escaped_root="$(printf '%s' "$HARNESS_ROOT" | sed 's/[\/&]/\\&/g')"
  local tmp
  tmp="$(mktemp)"
  sed "s/__HARNESS_ROOT__/${escaped_root}/g" "$script_path" >"$tmp"
  mv "$tmp" "$script_path"
  chmod +x "$script_path"
}

setup_cursor_wiring() {
  local hooks_json="${HOME}/.cursor/hooks.json"
  local mcp_json="${HOME}/.cursor/mcp.json"
  local hook_command
  hook_command="$(get_cursor_hooks_command)"
  setup_cursor_hook_script
  upsert_cursor_hooks_json "$hooks_json" "$hook_command"
  upsert_cursor_mcp_json "$mcp_json"
}

upsert_cursor_mcp_json() {
  local file="$1"
  local mcp_entry="${HARNESS_ROOT}/bin/harness-mcp-server"
  local tmp

  mkdir -p "$(dirname "$file")"

  if [ ! -f "$file" ]; then
    cat >"$file" <<'EOF'
{
  "mcpServers": {}
}
EOF
    log "Created Cursor MCP config: $file"
  fi

  tmp="$(mktemp)"
  if jq \
    --arg server_id "$CURSOR_MCP_SERVER_ID" \
    --arg legacy_server_id "$CURSOR_LEGACY_MCP_SERVER_ID" \
    --arg mcp_entry "$mcp_entry" \
    --arg host "$MEM_HOST" \
    --arg port "$MEM_PORT" \
    --arg db_path "$DB_PATH" \
    '
      .mcpServers = (.mcpServers // {})
      | .mcpServers[$server_id] = (.mcpServers[$server_id] // {})
      | .mcpServers[$server_id].type = (.mcpServers[$server_id].type // "stdio")
      | .mcpServers[$server_id].command = $mcp_entry
      | del(.mcpServers[$server_id].args)
      | .mcpServers[$server_id].env = ((.mcpServers[$server_id].env // {}) + {
          "HARNESS_MEM_HOST": $host,
          "HARNESS_MEM_PORT": $port,
          "HARNESS_MEM_DB_PATH": $db_path
        })
      | del(.mcpServers[$legacy_server_id])
    ' "$file" >"$tmp" 2>/dev/null; then
    mv "$tmp" "$file"
    log "Updated Cursor MCP wiring: $file"
    return 0
  fi

  rm -f "$tmp"
  warn "Could not parse Cursor MCP JSON: $file"
  return 1
}

check_cursor_wiring() {
  local hooks_json="${HOME}/.cursor/hooks.json"
  local mcp_json="${HOME}/.cursor/mcp.json"
  local hook_script="${HOME}/.cursor/hooks/memory-cursor-event.sh"
  local mcp_entry="${HARNESS_ROOT}/bin/harness-mcp-server"
  local hook_command
  hook_command="$(get_cursor_hooks_command)"
  local failed=0

  if [ ! -f "$hook_script" ]; then
    warn "Cursor hook script missing: $hook_script"
    failed=1
  else
    log "Cursor hook script: OK"
  fi

  if [ ! -f "$hooks_json" ]; then
    warn "Cursor hooks config missing: $hooks_json"
    failed=1
  else
    if jq -e --arg cmd "$hook_command" '
      def wired: any((.command // "") == $cmd or ((.command // "") | contains("memory-cursor-event.sh")));
      (.hooks.sessionStart // [] | wired)
      and (.hooks.beforeSubmitPrompt // [] | wired)
      and (.hooks.afterAgentResponse // [] | wired)
      and (.hooks.afterMCPExecution // [] | wired)
      and (.hooks.afterShellExecution // [] | wired)
      and (.hooks.afterFileEdit // [] | wired)
      and (.hooks.sessionEnd // [] | wired)
      and (.hooks.stop // [] | wired)
    ' "$hooks_json" >/dev/null 2>&1; then
      log "Cursor hooks JSON wiring: OK"
    else
      warn "Cursor hooks wiring is incomplete: $hooks_json"
      failed=1
    fi
  fi

  if [ ! -f "$mcp_json" ]; then
    warn "Cursor MCP config missing: $mcp_json"
    failed=1
  else
    if jq -e \
      --arg server_id "$CURSOR_MCP_SERVER_ID" \
      --arg legacy_server_id "$CURSOR_LEGACY_MCP_SERVER_ID" \
      --arg mcp_entry "$mcp_entry" '
      .mcpServers[$server_id] != null
      and (
        ((.mcpServers[$server_id].command // "") == $mcp_entry)
        or (
          (.mcpServers[$server_id].command // "") == "npx"
          and ((.mcpServers[$server_id].args // []) | any((. | tostring) | contains("harness-mcp-server") or contains("harness-mem")))
        )
        or (
          (.mcpServers[$server_id].command // "") == "node"
          and ((.mcpServers[$server_id].args // []) | any((. | tostring) | contains("mcp-server/dist/index.js")))
        )
        or ((.mcpServers[$server_id].url // "") | test("^http://127\\.0\\.0\\.1:[0-9]+/mcp$"))
      )
    ' "$mcp_json" >/dev/null 2>&1; then
      log "Cursor MCP wiring: OK"
      if jq -e \
        --arg legacy_server_id "$CURSOR_LEGACY_MCP_SERVER_ID" \
        '.mcpServers[$legacy_server_id] != null' "$mcp_json" >/dev/null 2>&1; then
        warn "Cursor MCP legacy server id '${CURSOR_LEGACY_MCP_SERVER_ID}' remains; rerun setup to migrate to '${CURSOR_MCP_SERVER_ID}' only"
      fi
    else
      warn "Cursor MCP wiring is incomplete: $mcp_json"
      failed=1
    fi
  fi

  return "$failed"
}

setup_antigravity_wiring() {
  log "Antigravity ingest uses workspace file scanning (docs/checkpoints + logs/codex-responses)"
}

check_antigravity_wiring() {
  local roots="${HARNESS_MEM_ANTIGRAVITY_ROOTS:-}"
  if [ -z "$roots" ]; then
    log "Antigravity roots are not explicitly configured (auto-discovery mode)"
    return 0
  fi

  local failed=0
  local IFS=','
  for raw in $roots; do
    local root
    root="$(printf '%s' "$raw" | xargs)"
    [ -z "$root" ] && continue
    if [ -d "$root" ]; then
      log "Antigravity root: OK ($root)"
    else
      warn "Antigravity root not found: $root"
      failed=1
    fi
  done
  return "$failed"
}

print_post_setup_next_steps() {
  log "Next step: verify wiring with doctor"
  log "  ./scripts/harness-mem doctor --project ${TARGET_DIR} --platform ${PLATFORM}"

  if is_platform_enabled cursor; then
    local hooks_json="${HOME}/.cursor/hooks.json"
    local mcp_json="${HOME}/.cursor/mcp.json"
    if [ -f "$hooks_json" ]; then
      log "Cursor wiring file: ${hooks_json}"
    else
      warn "Cursor wiring file is missing: ${hooks_json}"
    fi
    if [ -f "$mcp_json" ]; then
      log "Cursor MCP file: ${mcp_json}"
    else
      warn "Cursor MCP file is missing: ${mcp_json}"
    fi
    log "Cursor verification: send one prompt in Cursor, then check feed"
    log "  curl -sS 'http://${MEM_HOST}:${MEM_PORT}/v1/feed?project=$(basename "$TARGET_DIR")&limit=5&include_private=false' | jq '.ok, .meta.count'"
  fi

  if is_platform_enabled antigravity; then
    if [ -z "${HARNESS_MEM_ANTIGRAVITY_ROOTS:-}" ]; then
      log "Antigravity roots: auto-discovery from Antigravity workspaceStorage"
      log "  (optional) export HARNESS_MEM_ANTIGRAVITY_ROOTS=/absolute/path/to/antigravity-workspace"
    else
      log "Antigravity roots: ${HARNESS_MEM_ANTIGRAVITY_ROOTS}"
    fi
  fi
}

upsert_opencode_json() {
  local file="$1"
  local mcp_entry="${HARNESS_ROOT}/bin/harness-mcp-server"
  local tmp

  mkdir -p "$(dirname "$file")"

  if [ ! -f "$file" ]; then
    cat >"$file" <<EOF
{
  "\$schema": "https://opencode.ai/config.json",
  "mcp": {
    "harness": {
      "type": "local",
      "enabled": true,
      "command": ["${mcp_entry}"],
      "environment": {
        "HARNESS_MEM_HOST": "${MEM_HOST}",
        "HARNESS_MEM_PORT": "${MEM_PORT}",
        "HARNESS_MEM_DB_PATH": "${DB_PATH}",
        "HARNESS_MEM_MCP_PLATFORM": "opencode"
      }
    }
  }
}
EOF
    log "Created OpenCode config: $file"
    return
  fi

  tmp="$(mktemp)"
  jq \
    --arg mcp_entry "$mcp_entry" \
    --arg host "$MEM_HOST" \
    --arg port "$MEM_PORT" \
    --arg db_path "$DB_PATH" \
    '
      .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
      | .mcp = (.mcp // {})
      | .mcp.harness = (.mcp.harness // {})
      | .mcp.harness.type = "local"
      | .mcp.harness.enabled = true
      | .mcp.harness.command = [$mcp_entry]
      | .mcp.harness.environment = ((.mcp.harness.environment // {}) + {
          "HARNESS_MEM_HOST": $host,
          "HARNESS_MEM_PORT": $port,
          "HARNESS_MEM_DB_PATH": $db_path,
          "HARNESS_MEM_MCP_PLATFORM": "opencode"
        })
      | .mcp.harness |= del(.env)
      | del(.plugins)
    ' "$file" >"$tmp"
  mv "$tmp" "$file"
  log "Updated OpenCode config: $file"
}

setup_opencode_wiring() {
  local opencode_dir="${HOME}/.config/opencode"
  local plugin_src="${HARNESS_ROOT}/opencode/plugins/harness-memory/index.ts"
  local plugin_dst="${opencode_dir}/plugins/harness-memory/index.ts"
  local cfg="${opencode_dir}/opencode.json"

  [ -f "$plugin_src" ] || fail "OpenCode plugin template missing: $plugin_src"

  mkdir -p "$(dirname "$plugin_dst")"
  cp "$plugin_src" "$plugin_dst"
  log "Installed OpenCode memory plugin: $plugin_dst"

  upsert_opencode_json "$cfg"
}

check_opencode_wiring() {
  local failed=0
  local cfg="${HOME}/.config/opencode/opencode.json"
  local plugin="${HOME}/.config/opencode/plugins/harness-memory/index.ts"

  if [ ! -f "$plugin" ]; then
    warn "OpenCode memory plugin file missing: $plugin"
    failed=1
  else
    log "OpenCode global plugin: OK"
    local oc_hooks_ok=1
    for hook_name in '"chat.message"' '"session.idle"' '"session.compacted"' '"tool.execute.before"' '"tool.execute.after"'; do
      if ! rg -q "$hook_name" "$plugin"; then
        warn "OpenCode plugin missing hook: $hook_name"
        oc_hooks_ok=0
        failed=1
      fi
    done
    if [ "$oc_hooks_ok" = "1" ]; then
      log "OpenCode hook compatibility baseline: OK (chat.message/session.idle/session.compacted/tool.execute.before/tool.execute.after)"
    fi
    if rg -qi 'tool\.definition|shell\.env' "$plugin"; then
      log "OpenCode extended hook surface: detected (tool.definition/shell.env)"
    else
      log "OpenCode extended hook surface: not configured (baseline mode)"
    fi
  fi

  if [ ! -f "$cfg" ]; then
    warn "OpenCode global config missing: $cfg"
    failed=1
  else
    if jq -e '.mcp.harness.enabled == true' "$cfg" >/dev/null 2>&1; then
      log "OpenCode MCP wiring: OK"
    else
      warn "OpenCode config exists but harness MCP wiring is incomplete"
      failed=1
    fi

    log "OpenCode plugin loading: global plugins directory mode"
  fi

  return "$failed"
}

check_claude_wiring() {
  local failed=0
  local hooks="${HARNESS_ROOT}/hooks/hooks.json"
  local mcp_entry="${HARNESS_ROOT}/bin/harness-mcp-server"
  local mcp_relative_entry="bin/harness-mcp-server"
  local mcp_cwd="${HARNESS_ROOT}"
  local mcp_http_url="$(_mcp_gateway_endpoint_url)"
  local token_env_var="${HARNESS_MEM_MCP_TOKEN_ENV_VAR:-HARNESS_MEM_MCP_TOKEN}"
  local mcp_auth_header="Bearer \${${token_env_var}}"
  local found_mcp=0

  if [ ! -f "$hooks" ]; then
    warn "Claude hooks config missing in harness: $hooks"
    failed=1
  else
    local cl_hooks_ok=1
    for handler in memory-session-start memory-user-prompt memory-post-tool-use memory-stop memory-worktree-event; do
      if ! rg -q "$handler" "$hooks"; then
        warn "Claude hooks missing handler: $handler in $hooks"
        cl_hooks_ok=0
        failed=1
      fi
    done
    if [ "$cl_hooks_ok" = "1" ]; then
      log "Claude memory hooks in harness plugin: OK"
    fi
  fi

  while IFS= read -r cfg; do
    [ -n "$cfg" ] || continue
    if [ ! -f "$cfg" ]; then
      warn "Claude config missing: $cfg"
      continue
    fi

    if jq -e --arg mcp_http_url "$mcp_http_url" --arg mcp_auth_header "$mcp_auth_header" '
      .mcpServers.harness != null
      and ((.mcpServers.harness.type // "") == "http")
      and ((.mcpServers.harness.url // "") == $mcp_http_url)
      and ((.mcpServers.harness.headers.Authorization // "") == $mcp_auth_header)
    ' "$cfg" >/dev/null 2>&1; then
      MCP_HTTP_CONFIG_DETECTED=1
    fi

    if jq -e --arg mcp_entry "$mcp_entry" --arg mcp_relative_entry "$mcp_relative_entry" --arg mcp_cwd "$mcp_cwd" --arg mcp_http_url "$mcp_http_url" --arg mcp_auth_header "$mcp_auth_header" '
      .mcpServers.harness != null
      and ((.mcpServers.harness.enabled // true) != false)
      and (
        (
          ((.mcpServers.harness.type // "") == "http")
          and ((.mcpServers.harness.url // "") == $mcp_http_url)
          and ((.mcpServers.harness.headers.Authorization // "") == $mcp_auth_header)
        )
        or
        (
          ((.mcpServers.harness.command // "") == $mcp_entry)
          or (
            ((.mcpServers.harness.command // "") | endswith($mcp_relative_entry))
            and ((.mcpServers.harness.cwd // "") == $mcp_cwd)
          )
        )
        or (
          (.mcpServers.harness.command // "") == "npx"
          and ((.mcpServers.harness.args // []) | any((. | tostring) | contains("harness-mcp-server") or contains("harness-mem")))
        )
      )
    ' "$cfg" >/dev/null 2>&1; then
      log "Claude MCP wiring: OK ($cfg)"
      found_mcp=1
    fi
  done < <(get_claude_config_targets)

  # Also check inline plugin format (source: 'settings', CC v2.1.80+)
  if [ "$found_mcp" -eq 0 ]; then
    local settings_file="${HOME}/.claude/settings.json"
    if [ -f "$settings_file" ] && jq -e '.plugins.harness != null and .plugins.harness.source == "settings"' "$settings_file" >/dev/null 2>&1; then
      if jq -e --arg mcp_http_url "$mcp_http_url" --arg mcp_auth_header "$mcp_auth_header" '
        .plugins.harness.mcpServers.harness != null
        and ((.plugins.harness.mcpServers.harness.type // "") == "http")
        and ((.plugins.harness.mcpServers.harness.url // "") == $mcp_http_url)
        and ((.plugins.harness.mcpServers.harness.headers.Authorization // "") == $mcp_auth_header)
      ' "$settings_file" >/dev/null 2>&1; then
        MCP_HTTP_CONFIG_DETECTED=1
      fi
      log "Claude inline plugin wiring: OK ($settings_file)"
      found_mcp=1
    fi
  fi

  if [ "$found_mcp" -eq 0 ]; then
    warn "Claude MCP wiring is missing or incomplete (expected mcpServers.harness or plugins.harness)"
    failed=1
  fi

  return "$failed"
}

get_claude_harness_signature() {
  local file="$1"
  [ -f "$file" ] || return 0
  command -v jq >/dev/null 2>&1 || return 0

  jq -c '
    def harness_server:
      if (.plugins.harness.source? == "settings") and (.plugins.harness.mcpServers.harness? != null) then
        (.plugins.harness.mcpServers.harness + {plugin_root:(.plugins.harness.root // "")})
      elif .mcpServers.harness? != null then
        .mcpServers.harness
      else
        empty
      end;
    def entry_path($server):
      if (($server.command // "") | startswith("/")) then
        $server.command
      elif ((($server.args // [])[0] // "") | startswith("/")) then
        ($server.args[0])
      elif (($server.cwd // "") != "" and (($server.args // [])[0] // "") != "") then
        ($server.cwd + "/" + $server.args[0])
      else
        ""
      end;

    harness_server as $server
    | if $server == null then
        empty
      else
        {
          entry_path: entry_path($server),
          cwd: ($server.cwd // ""),
          enabled: ($server.enabled // true),
          type: ($server.type // (if (($server.url // "") != "") then "http" else "stdio" end)),
          url: ($server.url // ""),
          auth_header: ($server.headers.Authorization // ""),
          plugin_root: ($server.plugin_root // ""),
          host: ($server.env.HARNESS_MEM_HOST // ""),
          port: ($server.env.HARNESS_MEM_PORT // ""),
          db_path: ($server.env.HARNESS_MEM_DB_PATH // "")
        }
      end
  ' "$file" 2>/dev/null
}

check_claude_precedence_alignment() {
  local primary="${HOME}/.claude.json"
  local secondary="${HOME}/.claude/settings.json"
  local primary_sig secondary_sig

  if [ ! -f "$primary" ] || [ ! -f "$secondary" ]; then
    log "Claude precedence alignment: single config source detected"
    return 0
  fi

  primary_sig="$(get_claude_harness_signature "$primary")"
  secondary_sig="$(get_claude_harness_signature "$secondary")"

  if [ -z "$primary_sig" ] || [ -z "$secondary_sig" ]; then
    log "Claude precedence alignment: single active harness source detected"
    return 0
  fi

  if [ "$primary_sig" = "$secondary_sig" ]; then
    log "Claude precedence alignment: OK"
    return 0
  fi

  warn "Claude harness wiring differs between ~/.claude.json and ~/.claude/settings.json"
  return 1
}

start_daemon() {
  HARNESS_MEM_ENABLE_UI="${HARNESS_MEM_ENABLE_UI:-false}" \
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
    HARNESS_MEM_HOST="$MEM_HOST" \
    HARNESS_MEM_PORT="$MEM_PORT" \
    HARNESS_MEM_DB_PATH="$DB_PATH" \
    "$HARNESS_ROOT/scripts/harness-memd" start --quiet
}

run_smoke() {
  log "Running isolated smoke test"

  (
    set -euo pipefail
    tmp_home=""
    smoke_port=""
    project_name=""
    marker=""
    health=""
    default_search=""
    private_search=""
    cleanup_done=0

    tmp_home="$(mktemp -d)"
    smoke_port="$((40000 + RANDOM % 2000))"
    project_name="$(basename "$TARGET_DIR")"
    marker="smoke-$(date +%s)-$RANDOM"

    cleanup() {
      if [ "$cleanup_done" -eq 0 ]; then
        HARNESS_MEM_HOME="$tmp_home" \
        HARNESS_MEM_DB_PATH="$tmp_home/harness-mem.db" \
          HARNESS_MEM_ENABLE_UI="false" \
          HARNESS_MEM_HOST="127.0.0.1" \
          HARNESS_MEM_PORT="$smoke_port" \
          HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
          "$HARNESS_ROOT/scripts/harness-memd" stop --quiet >/dev/null 2>&1 || true
        rm -rf "$tmp_home"
        cleanup_done=1
      fi
    }

    trap cleanup EXIT

    HARNESS_MEM_HOME="$tmp_home" \
      HARNESS_MEM_DB_PATH="$tmp_home/harness-mem.db" \
      HARNESS_MEM_ENABLE_UI="false" \
      HARNESS_MEM_HOST="127.0.0.1" \
      HARNESS_MEM_PORT="$smoke_port" \
      HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
      "$HARNESS_ROOT/scripts/harness-memd" start --quiet

    health="$(
      HARNESS_MEM_HOME="$tmp_home" \
      HARNESS_MEM_DB_PATH="$tmp_home/harness-mem.db" \
      HARNESS_MEM_HOST="127.0.0.1" \
      HARNESS_MEM_PORT="$smoke_port" \
      HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
      "$HARNESS_ROOT/scripts/harness-mem-client.sh" health
    )"
    printf '%s' "$health" | jq -e '.ok == true' >/dev/null

    jq -nc \
      --arg project "$project_name" \
      --arg marker "$marker" \
      '{event:{platform:"codex",project:$project,session_id:"smoke-session",event_type:"user_prompt",payload:{content:("public " + $marker)},tags:["smoke"],privacy_tags:[]}}' \
      | HARNESS_MEM_HOME="$tmp_home" \
        HARNESS_MEM_DB_PATH="$tmp_home/harness-mem.db" \
        HARNESS_MEM_HOST="127.0.0.1" \
        HARNESS_MEM_PORT="$smoke_port" \
        HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
        "$HARNESS_ROOT/scripts/harness-mem-client.sh" record-event >/dev/null

    jq -nc \
      --arg project "$project_name" \
      --arg marker "$marker" \
      '{event:{platform:"codex",project:$project,session_id:"smoke-session",event_type:"user_prompt",payload:{content:("private " + $marker)},tags:["smoke"],privacy_tags:["private"]}}' \
      | HARNESS_MEM_HOME="$tmp_home" \
        HARNESS_MEM_DB_PATH="$tmp_home/harness-mem.db" \
        HARNESS_MEM_HOST="127.0.0.1" \
        HARNESS_MEM_PORT="$smoke_port" \
        HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
        "$HARNESS_ROOT/scripts/harness-mem-client.sh" record-event >/dev/null

    smoke_search() {
      local include_private="$1"
      local payload=""
      local response=""
      local body=""
      local status=""
      local attempt=0

      payload="$(jq -nc --arg q "$marker" --arg project "$project_name" --argjson include_private "$include_private" '{query:$q,project:$project,limit:10,include_private:$include_private}')"

      for attempt in $(seq 1 30); do
        response="$(
          curl -sS \
            -X POST "http://127.0.0.1:${smoke_port}/v1/search" \
            -H 'content-type: application/json' \
            --data-binary "$payload" \
            -w $'\n%{http_code}'
        )"
        status="${response##*$'\n'}"
        body="${response%$'\n'*}"
        if [ "$status" = "200" ]; then
          printf '%s' "$body"
          return 0
        fi
        if [ "$status" = "503" ] && printf '%s' "$body" | grep -Eq 'search_offload_unavailable|warming'; then
          sleep 0.5
          continue
        fi
        printf '%s\n' "$body" >&2
        return 1
      done

      printf '%s\n' "$body" >&2
      return 1
    }

    default_search="$(smoke_search false)"
    printf '%s' "$default_search" | jq -e '.ok == true and (.items | length) >= 1' >/dev/null
    printf '%s' "$default_search" | jq -e '[.items[] | (.privacy_tags // []) | index("private")] | map(select(. != null)) | length == 0' >/dev/null

    private_search="$(smoke_search true)"
    printf '%s' "$private_search" | jq -e '[.items[] | (.privacy_tags // []) | index("private")] | map(select(. != null)) | length >= 1' >/dev/null
  )

  log "Smoke test: OK"
}

run_quality_tests() {
  local quality_script="${HARNESS_ROOT}/tests/test-memory-search-quality.sh"
  [ -x "$quality_script" ] || fail "Quality test script missing or not executable: $quality_script"
  log "Running search quality test suite"
  "$quality_script"
}

run_import_request() {
  local payload="$1"
  HARNESS_MEM_HOME="$STATE_DIR" \
    HARNESS_MEM_DB_PATH="$DB_PATH" \
    HARNESS_MEM_HOST="$MEM_HOST" \
    HARNESS_MEM_PORT="$MEM_PORT" \
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
    "$HARNESS_ROOT/scripts/harness-mem-client.sh" import-claude-mem "$payload"
}

run_hermes_state_ingest_request() {
  local payload="$1"
  HARNESS_MEM_HOME="$STATE_DIR" \
    HARNESS_MEM_DB_PATH="$DB_PATH" \
    HARNESS_MEM_HOST="$MEM_HOST" \
    HARNESS_MEM_PORT="$MEM_PORT" \
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
    "$HARNESS_ROOT/scripts/harness-mem-client.sh" ingest-hermes-state "$payload"
}

run_repair_sqlite_vec_map_request() {
  local payload="$1"
  HARNESS_MEM_HOME="$STATE_DIR" \
    HARNESS_MEM_DB_PATH="$DB_PATH" \
    HARNESS_MEM_HOST="$MEM_HOST" \
    HARNESS_MEM_PORT="$MEM_PORT" \
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
    "$HARNESS_ROOT/scripts/harness-mem-client.sh" admin-repair-sqlite-vec-map "$payload"
}

run_vector_backfill_request() {
  local action="$1"
  local payload="${2-}"
  if [ -z "$payload" ]; then
    payload="{}"
  fi
  HARNESS_MEM_HOME="$STATE_DIR" \
    HARNESS_MEM_DB_PATH="$DB_PATH" \
    HARNESS_MEM_HOST="$MEM_HOST" \
    HARNESS_MEM_PORT="$MEM_PORT" \
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
    "$HARNESS_ROOT/scripts/harness-mem-client.sh" "admin-vector-backfill-${action}" "$payload"
}

build_hermes_state_payload() {
  local source="$1"
  local project="$2"
  local dry_run="$3"
  local limit="$4"
  local since="$5"
  local after_message_id="$6"
  local max_content_chars="$7"

  jq -nc \
    --arg source "$source" \
    --arg project "$project" \
    --arg limit "$limit" \
    --arg since "$since" \
    --arg after_message_id "$after_message_id" \
    --arg max_content_chars "$max_content_chars" \
    --argjson dry_run "$dry_run" \
    --argjson include_tool_content "$([ "$HERMES_STATE_INCLUDE_TOOL_CONTENT" -eq 1 ] && echo true || echo false)" \
    '{
      source_db_path: $source,
      project: $project,
      dry_run: $dry_run,
      include_tool_content: $include_tool_content
    }
    + (if $limit == "" then {} else {limit: ($limit | tonumber)} end)
    + (if $since == "" then {} else {since: $since} end)
    + (if $after_message_id == "" then {} else {after_message_id: ($after_message_id | tonumber)} end)
    + (if $max_content_chars == "" then {} else {max_content_chars: ($max_content_chars | tonumber)} end)'
}

build_repair_sqlite_vec_map_payload() {
  local model="$1"
  local dimension="$2"
  local limit="$3"
  local execute="$4"
  local rebuild_existing="$5"

  jq -nc \
    --arg model "$model" \
    --arg dimension "$dimension" \
    --arg limit "$limit" \
    --argjson execute "$execute" \
    --argjson rebuild_existing "$rebuild_existing" \
    '{
      model: $model,
      execute: $execute,
      rebuild_existing: $rebuild_existing
    }
    + (if $dimension == "" then {} else {dimension: ($dimension | tonumber)} end)
    + (if $limit == "" then {} else {limit: ($limit | tonumber)} end)'
}

build_vector_backfill_start_payload() {
  local compact_batch_size="$1"
  local reindex_batch_size="$2"
  local interval_ms="$3"
  local target_coverage="$4"
  local model="$5"
  local dimension="$6"
  local reset="$7"

  jq -nc \
    --arg compact_batch_size "$compact_batch_size" \
    --arg reindex_batch_size "$reindex_batch_size" \
    --arg interval_ms "$interval_ms" \
    --arg target_coverage "$target_coverage" \
    --arg model "$model" \
    --arg dimension "$dimension" \
    --argjson reset "$reset" \
    '{
    }
    + (if $compact_batch_size == "" then {} else {compact_batch_size: ($compact_batch_size | tonumber)} end)
    + (if $reindex_batch_size == "" then {} else {reindex_batch_size: ($reindex_batch_size | tonumber)} end)
    + (if $interval_ms == "" then {} else {interval_ms: ($interval_ms | tonumber)} end)
    + (if $target_coverage == "" then {} else {target_coverage: ($target_coverage | tonumber)} end)
    + (if $model == "" then {} else {model: $model} end)
    + (if $dimension == "" then {} else {dimension: ($dimension | tonumber)} end)
    + (if $reset then {reset: true} else {} end)'
}

run_verify_request() {
  local payload="$1"
  HARNESS_MEM_HOME="$STATE_DIR" \
    HARNESS_MEM_DB_PATH="$DB_PATH" \
    HARNESS_MEM_HOST="$MEM_HOST" \
    HARNESS_MEM_PORT="$MEM_PORT" \
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
    "$HARNESS_ROOT/scripts/harness-mem-client.sh" verify-import "$payload"
}

backup_impl() {
  ensure_dependencies
  start_daemon

  local payload="{}"
  if [ -n "${BACKUP_DEST_DIR:-}" ]; then
    payload="$(jq -nc --arg dest_dir "$BACKUP_DEST_DIR" '{"dest_dir": $dest_dir}')"
  fi

  local response
  response="$(
    HARNESS_MEM_HOST="$MEM_HOST" \
    HARNESS_MEM_PORT="$MEM_PORT" \
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
    "$HARNESS_ROOT/scripts/harness-mem-client.sh" admin-backup "$payload"
  )"
  echo "$response"
  echo "$response" | jq -e '.ok == true' >/dev/null || fail "Backup failed"
  local backup_path
  backup_path="$(echo "$response" | jq -r '.items[0].path // .meta.backup_path // ""' 2>/dev/null || true)"
  if [ -n "$backup_path" ]; then
    log "Backup created: $backup_path"
	  fi
}

backup_evidence_impl() {
  ensure_dependencies
  [ -n "$BACKUP_EVIDENCE_PATH" ] || fail "backup-evidence requires --backup-path <path>"
  [ -f "$BACKUP_EVIDENCE_PATH" ] || fail "backup file not found: $BACKUP_EVIDENCE_PATH"
  [[ "$BACKUP_EVIDENCE_SHA256" =~ ^[A-Fa-f0-9]{64}$ ]] || fail "backup-evidence requires --backup-sha256 <64 hex chars>"
  if [ -n "$BACKUP_EVIDENCE_TTL_SECONDS" ]; then
    is_uint "$BACKUP_EVIDENCE_TTL_SECONDS" || fail "--ttl-seconds must be a non-negative integer"
  fi
  [ "${#BACKUP_EVIDENCE_CANDIDATE_IDS[@]}" -gt 0 ] || fail "backup-evidence requires at least one --candidate-id <id>"

  local backup_dir backup_base backup_path payload candidate_ids_json
  backup_dir="$(cd "$(dirname "$BACKUP_EVIDENCE_PATH")" && pwd)"
  backup_base="$(basename "$BACKUP_EVIDENCE_PATH")"
  backup_path="${backup_dir}/${backup_base}"
  candidate_ids_json="$(printf '%s\n' "${BACKUP_EVIDENCE_CANDIDATE_IDS[@]}" | jq -Rsc 'split("\n") | map(select(length > 0))')"
  payload="$(
    jq -nc \
      --arg backup_path "$backup_path" \
      --arg backup_sha256 "$BACKUP_EVIDENCE_SHA256" \
      --argjson candidate_ids "$candidate_ids_json" \
      --arg ttl_seconds "$BACKUP_EVIDENCE_TTL_SECONDS" \
      '{
        backup_path: $backup_path,
        backup_sha256: $backup_sha256,
        candidate_ids: $candidate_ids
      } + (if $ttl_seconds == "" then {} else {ttl_seconds: ($ttl_seconds | tonumber)} end)'
  )"

  start_daemon

  local response
  response="$(
    HARNESS_MEM_HOST="$MEM_HOST" \
    HARNESS_MEM_PORT="$MEM_PORT" \
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
    "$HARNESS_ROOT/scripts/harness-mem-client.sh" admin-backup-evidence "$payload"
  )"
  echo "$response"
  echo "$response" | jq -e '.ok == true' >/dev/null || fail "Backup evidence preverification failed"
  local token
  token="$(echo "$response" | jq -r '.items[0].preverified_backup_evidence_token // ""' 2>/dev/null || true)"
  if [ -n "$token" ]; then
    log "Backup evidence token created: $token"
  fi
}

forget_maintenance_impl() {
  ensure_dependencies
  local payload
  payload="$(
    jq -nc \
      --arg reason "$FORGET_MAINTENANCE_REASON" \
      --argjson force "$FORGET_MAINTENANCE_FORCE" \
      '{
        force: ($force == 1)
      } + (if $reason == "" then {} else {reason: $reason} end)'
  )"

  start_daemon

  local response
  response="$(
    HARNESS_MEM_HOST="$MEM_HOST" \
    HARNESS_MEM_PORT="$MEM_PORT" \
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
    "$HARNESS_ROOT/scripts/harness-mem-client.sh" admin-forget-maintenance "$payload"
  )"
  echo "$response"
  echo "$response" | jq -e '.ok == true' >/dev/null || fail "Forget maintenance failed"
}

forget_status_impl() {
  ensure_dependencies
  start_daemon

  local response
  response="$(
    HARNESS_MEM_HOST="$MEM_HOST" \
    HARNESS_MEM_PORT="$MEM_PORT" \
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
    "$HARNESS_ROOT/scripts/harness-mem-client.sh" admin-forget-status "{}"
  )"
  echo "$response"
  echo "$response" | jq -e '.ok == true' >/dev/null || fail "Forget status failed"
}

forget_impl() {
  local subcommand="${1:-status}"
  case "$subcommand" in
    status)
      forget_status_impl
      ;;
    *)
      fail "Unknown forget command: $subcommand"
      ;;
  esac
}

admin_repair_sqlite_vec_map_impl() {
  ensure_dependencies
  [ -n "$REPAIR_SQLITE_VEC_MODEL" ] || fail "admin-repair-sqlite-vec-map requires --model <model>; refusing to infer an admin repair target"
  if [ -n "$REPAIR_SQLITE_VEC_DIMENSION" ]; then
    is_uint "$REPAIR_SQLITE_VEC_DIMENSION" && [ "$REPAIR_SQLITE_VEC_DIMENSION" -gt 0 ] || fail "--dimension must be a positive integer"
  fi
  if [ -n "$REPAIR_SQLITE_VEC_LIMIT" ]; then
    is_uint "$REPAIR_SQLITE_VEC_LIMIT" && [ "$REPAIR_SQLITE_VEC_LIMIT" -gt 0 ] || fail "--limit must be a positive integer"
  fi

  start_daemon

  local payload
  payload="$(build_repair_sqlite_vec_map_payload \
    "$REPAIR_SQLITE_VEC_MODEL" \
    "$REPAIR_SQLITE_VEC_DIMENSION" \
    "$REPAIR_SQLITE_VEC_LIMIT" \
    "$([ "$REPAIR_SQLITE_VEC_EXECUTE" -eq 1 ] && echo true || echo false)" \
    "$([ "$REPAIR_SQLITE_VEC_REBUILD_EXISTING" -eq 1 ] && echo true || echo false)")"

  local response
  response="$(HARNESS_MEM_CLIENT_TIMEOUT_SEC="${HARNESS_MEM_CLIENT_TIMEOUT_SEC:-60}" run_repair_sqlite_vec_map_request "$payload")"
  printf '%s\n' "$response"

  echo "$response" | jq -e '.ok == true' >/dev/null || fail "sqlite-vec map repair request failed"
  if [ "$REPAIR_SQLITE_VEC_EXECUTE" -eq 1 ]; then
    log "sqlite-vec map repair executed for model: ${REPAIR_SQLITE_VEC_MODEL}"
  else
    log "sqlite-vec map repair dry-run only. Re-run with --execute to write map/index rows."
  fi
}

admin_vector_backfill_impl() {
  local action="${1:-status}"
  case "$action" in
    start|status|stop) ;;
    *) fail "admin-vector-backfill requires action: start, status, or stop" ;;
  esac

  ensure_dependencies
  if [ -n "$VECTOR_BACKFILL_COMPACT_BATCH_SIZE" ]; then
    is_uint "$VECTOR_BACKFILL_COMPACT_BATCH_SIZE" && [ "$VECTOR_BACKFILL_COMPACT_BATCH_SIZE" -gt 0 ] || fail "--compact-batch-size must be a positive integer"
  fi
  if [ -n "$VECTOR_BACKFILL_REINDEX_BATCH_SIZE" ]; then
    is_uint "$VECTOR_BACKFILL_REINDEX_BATCH_SIZE" && [ "$VECTOR_BACKFILL_REINDEX_BATCH_SIZE" -gt 0 ] || fail "--reindex-batch-size must be a positive integer"
  fi
  if [ -n "$VECTOR_BACKFILL_INTERVAL_MS" ]; then
    is_uint "$VECTOR_BACKFILL_INTERVAL_MS" && [ "$VECTOR_BACKFILL_INTERVAL_MS" -gt 0 ] || fail "--interval-ms must be a positive integer"
  fi
  if [ -n "$VECTOR_BACKFILL_DIMENSION" ]; then
    is_uint "$VECTOR_BACKFILL_DIMENSION" && [ "$VECTOR_BACKFILL_DIMENSION" -gt 0 ] || fail "--dimension must be a positive integer"
  fi
  if [ -n "$VECTOR_BACKFILL_TARGET_COVERAGE" ]; then
    jq -en --arg value "$VECTOR_BACKFILL_TARGET_COVERAGE" '($value | tonumber) as $n | $n > 0 and $n <= 1' >/dev/null || fail "--target-coverage must be a number > 0 and <= 1"
  fi

  if ! start_daemon; then
    warn "harness-memd preflight failed before admin-vector-backfill ${action}; continuing with thin client daemon check"
  fi

  local payload="{}"
  if [ "$action" = "start" ]; then
    payload="$(build_vector_backfill_start_payload \
      "$VECTOR_BACKFILL_COMPACT_BATCH_SIZE" \
      "$VECTOR_BACKFILL_REINDEX_BATCH_SIZE" \
      "$VECTOR_BACKFILL_INTERVAL_MS" \
      "$VECTOR_BACKFILL_TARGET_COVERAGE" \
      "$VECTOR_BACKFILL_MODEL" \
      "$VECTOR_BACKFILL_DIMENSION" \
      "$([ "$VECTOR_BACKFILL_RESET" -eq 1 ] && echo true || echo false)")"
  fi

  local response
  if [ "$action" = "start" ]; then
    response="$(HARNESS_MEM_CLIENT_TIMEOUT_SEC="${HARNESS_MEM_CLIENT_TIMEOUT_SEC:-30}" run_vector_backfill_request "$action" "$payload")"
  else
    response="$(run_vector_backfill_request "$action" "$payload")"
  fi
  printf '%s\n' "$response"

  echo "$response" | jq -e '.ok == true' >/dev/null || fail "vector backfill ${action} request failed"
}

import_claude_mem_impl() {
  ensure_dependencies
  [ -n "$IMPORT_SOURCE" ] || fail "import-claude-mem requires --source <sqlite path>"
  [ -f "$IMPORT_SOURCE" ] || fail "source db not found: $IMPORT_SOURCE"

  start_daemon

  local payload
  payload="$(jq -nc \
    --arg source "$IMPORT_SOURCE" \
    --arg project "$IMPORT_PROJECT" \
    --argjson dry_run "$([ "$IMPORT_DRY_RUN" -eq 1 ] && echo true || echo false)" \
    '{
      source_db_path: $source,
      dry_run: $dry_run
    } + (if $project == "" then {} else {project: $project} end)')"

  local response
  response="$(run_import_request "$payload")"
  printf '%s\n' "$response"

  echo "$response" | jq -e '.ok == true' >/dev/null || fail "import request failed"
  local job_id
  job_id="$(echo "$response" | jq -r '.items[0].job_id // empty')"
  [ -n "$job_id" ] || fail "import response did not include job_id"
  log "Import job: $job_id"
}

ingest_hermes_state_impl() {
  ensure_dependencies
  [ -n "$HERMES_STATE_SOURCE" ] || fail "ingest-hermes-state requires --source <sqlite path>"
  if [[ "$HERMES_STATE_SOURCE" != "~/"* ]]; then
    [ -f "$HERMES_STATE_SOURCE" ] || fail "Hermes state db not found: $HERMES_STATE_SOURCE"
  fi

  start_daemon

  local project_key
  project_key="${HERMES_STATE_PROJECT:-${IMPORT_PROJECT:-${HARNESS_MEM_PROJECT_KEY:-default}}}"

  if [ "$HERMES_STATE_EXECUTE" -eq 1 ] && [ -z "$HERMES_STATE_LIMIT" ] && [ -z "$HERMES_STATE_AFTER_MESSAGE_ID" ]; then
    if ! is_uint "$HERMES_STATE_BATCH_SIZE" || [ "$HERMES_STATE_BATCH_SIZE" -le 0 ]; then
      fail "--batch-size must be a positive integer"
    fi

    local after_id="0"
    local total_messages="0"
    local total_planned=0
    local total_recorded=0
    local total_deduped=0
    local total_failed=0
    local total_seen=0
    local batches=0
    local response item_seen item_planned item_recorded item_deduped item_failed item_last

    while true; do
      local payload
      payload="$(build_hermes_state_payload \
        "$HERMES_STATE_SOURCE" \
        "$project_key" \
        false \
        "$HERMES_STATE_BATCH_SIZE" \
        "$HERMES_STATE_SINCE" \
        "$after_id" \
        "$HERMES_STATE_MAX_CONTENT_CHARS")"

      response="$(HARNESS_MEM_CLIENT_TIMEOUT_SEC="${HARNESS_MEM_CLIENT_TIMEOUT_SEC:-60}" run_hermes_state_ingest_request "$payload")"
      echo "$response" | jq -e '.ok == true' >/dev/null || {
        printf '%s\n' "$response"
        fail "Hermes state ingest batch failed"
      }

      item_seen="$(echo "$response" | jq -r '.items[0].messages_seen // 0')"
      item_planned="$(echo "$response" | jq -r '.items[0].events_planned // 0')"
      item_recorded="$(echo "$response" | jq -r '.items[0].events_recorded // 0')"
      item_deduped="$(echo "$response" | jq -r '.items[0].events_deduped // 0')"
      item_failed="$(echo "$response" | jq -r '.items[0].events_failed // 0')"
      item_last="$(echo "$response" | jq -r '.items[0].last_message_id // empty')"
      total_messages="$(echo "$response" | jq -r '.items[0].messages_total // 0')"

      total_seen=$((total_seen + item_seen))
      total_planned=$((total_planned + item_planned))
      total_recorded=$((total_recorded + item_recorded))
      total_deduped=$((total_deduped + item_deduped))
      total_failed=$((total_failed + item_failed))
      batches=$((batches + 1))

      if [ "$item_failed" -gt 0 ]; then
        printf '%s\n' "$response"
        fail "Hermes state ingest reported failed events"
      fi
      if [ "$item_seen" -eq 0 ]; then
        break
      fi
      if [ -z "$item_last" ] || [ "$item_last" -le "$after_id" ] 2>/dev/null; then
        break
      fi
      after_id="$item_last"
      if [ "$item_seen" -lt "$HERMES_STATE_BATCH_SIZE" ] 2>/dev/null; then
        break
      fi
    done

    jq -nc \
      --arg source "$HERMES_STATE_SOURCE" \
      --arg project "$project_key" \
      --argjson batches "$batches" \
      --argjson messages_seen "$total_seen" \
      --argjson messages_total "$total_messages" \
      --argjson events_planned "$total_planned" \
      --argjson events_recorded "$total_recorded" \
      --argjson events_deduped "$total_deduped" \
      --argjson events_failed "$total_failed" \
      --argjson last_message_id "$after_id" \
      '{
        ok: true,
        source: "core",
        items: [{
          source_db_path: $source,
          project: $project,
          dry_run: false,
          batched: true,
          batches: $batches,
          messages_seen: $messages_seen,
          messages_total: $messages_total,
          events_planned: $events_planned,
          events_recorded: $events_recorded,
          events_deduped: $events_deduped,
          events_failed: $events_failed,
          last_message_id: $last_message_id
        }],
        meta: {
          count: 1,
          ranking: "default",
          ingest_mode: "hermes_state_db_v1_batched"
        }
      }'
    return 0
  fi

  local payload
  payload="$(build_hermes_state_payload \
    "$HERMES_STATE_SOURCE" \
    "$project_key" \
    "$([ "$HERMES_STATE_EXECUTE" -eq 1 ] && echo false || echo true)" \
    "$HERMES_STATE_LIMIT" \
    "$HERMES_STATE_SINCE" \
    "$HERMES_STATE_AFTER_MESSAGE_ID" \
    "$HERMES_STATE_MAX_CONTENT_CHARS")"

  local response
  response="$(run_hermes_state_ingest_request "$payload")"
  printf '%s\n' "$response"

  echo "$response" | jq -e '.ok == true' >/dev/null || fail "Hermes state ingest request failed"
  if [ "$HERMES_STATE_EXECUTE" -eq 1 ]; then
    echo "$response" | jq -e '.items[0].events_failed == 0' >/dev/null || fail "Hermes state ingest reported failed events"
    log "Hermes state Backfill executed"
  else
    log "Hermes state Backfill dry-run only. Re-run with --execute to write events."
  fi
}

verify_import_impl() {
  ensure_dependencies
  [ -n "$IMPORT_JOB_ID" ] || fail "verify-import requires --job <job_id>"
  start_daemon

  local payload
  payload="$(jq -nc --arg job_id "$IMPORT_JOB_ID" '{job_id: $job_id}')"
  local response
  response="$(run_verify_request "$payload")"
  printf '%s\n' "$response"

  echo "$response" | jq -e '.ok == true' >/dev/null || fail "verify API call failed"
  echo "$response" | jq -e '.items[0].ok == true' >/dev/null || fail "import verification failed"
  log "Import verification passed: $IMPORT_JOB_ID"
}

disable_claude_mem_json_refs() {
  local file="$1"
  [ -f "$file" ] || return 0
  local tmp backup
  tmp="$(mktemp)"
  backup="${file}.pre-harness-cutover.$(date +%s)"
  cp "$file" "$backup" >/dev/null 2>&1 || true

  if jq '
      def scrub:
        if type == "object" then
          with_entries(.value |= scrub)
          | with_entries(
              select(
                (
                  (.value | tostring | ascii_downcase | contains("claude-mem"))
                  or
                  (.key | ascii_downcase | contains("claude-mem"))
                ) | not
              )
            )
        elif type == "array" then
          map(scrub)
          | map(
              select(
                (. | tostring | ascii_downcase | contains("claude-mem")) | not
              )
            )
        else .
        end;
      scrub
    ' "$file" >"$tmp" 2>/dev/null; then
    mv "$tmp" "$file"
    log "Removed claude-mem references from $file (backup: $backup)"
  else
    rm -f "$tmp"
    warn "Could not sanitize $file (invalid JSON?)"
  fi
}

stop_claude_mem_runtime() {
  local stopped=0
  if command -v claude-mem >/dev/null 2>&1; then
    claude-mem stop >/dev/null 2>&1 || true
    stopped=1
  fi

  local pids
  pids="$(pgrep -f '(^|[ /])claude-mem([[:space:]]|$)' || true)"
  if [ -n "$pids" ]; then
    echo "$pids" | while read -r pid; do
      [ -n "$pid" ] || continue
      kill -TERM "$pid" >/dev/null 2>&1 || true
    done
    sleep 1
    local remaining
    remaining="$(pgrep -f '(^|[ /])claude-mem([[:space:]]|$)' || true)"
    if [ -n "$remaining" ]; then
      echo "$remaining" | while read -r pid; do
        [ -n "$pid" ] || continue
        kill -KILL "$pid" >/dev/null 2>&1 || true
      done
    fi
    stopped=1
  fi

  local plist
  for plist in "$HOME"/Library/LaunchAgents/*claude*mem*.plist; do
    [ -e "$plist" ] || continue
    launchctl unload "$plist" >/dev/null 2>&1 || true
    mv "$plist" "${plist}.disabled" >/dev/null 2>&1 || true
    stopped=1
  done

  disable_claude_mem_json_refs "$HOME/.claude/hooks.json"
  disable_claude_mem_json_refs "$HOME/.claude/settings.json"

  if [ "$stopped" -eq 1 ]; then
    log "Claude-mem runtime/autostart disabled"
  else
    warn "No running Claude-mem process or launch agent was found"
  fi
}

cutover_claude_mem_impl() {
  ensure_dependencies
  [ -n "$IMPORT_JOB_ID" ] || fail "cutover-claude-mem requires --job <job_id>"
  [ "$STOP_NOW" -eq 1 ] || fail "cutover requires explicit --stop-now"
  start_daemon

  local payload verify_response
  payload="$(jq -nc --arg job_id "$IMPORT_JOB_ID" '{job_id: $job_id}')"
  verify_response="$(run_verify_request "$payload")"
  echo "$verify_response" | jq -e '.ok == true and .items[0].ok == true' >/dev/null \
    || fail "verify failed; cutover aborted"

  stop_claude_mem_runtime
  log "Cutover completed for job: $IMPORT_JOB_ID"
}

migrate_from_claude_mem_impl() {
  local source_db="${HOME}/.claude-mem/claude-mem.db"
  [ -f "$source_db" ] || fail "Claude-mem source DB not found: $source_db"

  ensure_dependencies
  start_daemon

  # Step 1: import
  log "Step 1/3: Importing from Claude-mem..."
  local import_payload import_response job_id
  import_payload="$(jq -nc --arg source "$source_db" '{ source_db_path: $source, dry_run: false }')"
  import_response="$(run_import_request "$import_payload")"
  echo "$import_response" | jq -e '.ok == true' >/dev/null || fail "Import failed"
  job_id="$(echo "$import_response" | jq -r '.items[0].job_id // empty')"
  [ -n "$job_id" ] || fail "Import did not return job_id"
  log "Import started: job=$job_id"

  # Step 2: verify
  log "Step 2/3: Verifying import..."
  local verify_payload verify_response
  verify_payload="$(jq -nc --arg job_id "$job_id" '{job_id: $job_id}')"
  verify_response="$(run_verify_request "$verify_payload")"
  echo "$verify_response" | jq -e '.ok == true and .items[0].ok == true' >/dev/null || fail "Verification failed. Run: harness-mem verify-import --job $job_id"
  log "Verification passed"

  # Step 3: cutover
  log "Step 3/3: Cutting over from Claude-mem..."
  stop_claude_mem_runtime
  log "Migration complete! Claude-mem has been stopped."
  log "Rollback: harness-mem rollback-claude-mem"
}

rollback_claude_mem_impl() {
  log "Rolling back to Claude-mem..."
  # Restart Claude-mem if available
  if command -v claude-mem >/dev/null 2>&1; then
    claude-mem start 2>/dev/null || true
    log "Claude-mem restarted"
  else
    warn "claude-mem command not found. Manual restart required."
  fi
  # Re-enable LaunchAgent if disabled
  local plist
  for plist in "$HOME"/Library/LaunchAgents/*claude*mem*.plist.disabled; do
    [ -e "$plist" ] || continue
    local original="${plist%.disabled}"
    mv "$plist" "$original" 2>/dev/null || true
    launchctl load "$original" 2>/dev/null || true
    log "Re-enabled: $original"
  done
  log "Rollback complete. Verify with: claude-mem status"
}

run_setup_claude_mem_import_if_requested() {
  if [ "$SETUP_IMPORT_CLAUDE_MEM" -ne 1 ]; then
    return
  fi

  local source_db="${HOME}/.claude-mem/claude-mem.db"
  if [ ! -f "$source_db" ]; then
    log "Claude-mem source DB not found; skipping optional setup import: $source_db"
    return
  fi

  start_daemon

  local payload response job_id verify_payload verify_response imported_count
  payload="$(jq -nc \
    --arg source "$source_db" \
    '{
      source_db_path: $source,
      dry_run: false
    }')"

  response="$(run_import_request "$payload")"
  echo "$response" | jq -e '.ok == true' >/dev/null || fail "Claude-mem import request failed during setup"

  job_id="$(echo "$response" | jq -r '.items[0].job_id // empty')"
  [ -n "$job_id" ] || fail "Claude-mem import did not return job_id during setup"
  log "Claude-mem import requested (job=${job_id})"

  verify_payload="$(jq -nc --arg job_id "$job_id" '{job_id: $job_id}')"
  verify_response="$(run_verify_request "$verify_payload")"
  echo "$verify_response" | jq -e '.ok == true and .items[0].ok == true' >/dev/null \
    || fail "Claude-mem import verification failed during setup"
  imported_count="$(echo "$verify_response" | jq -r '.items[0].imported_observations // 0')"
  log "Claude-mem import verified (job=${job_id}, imported_observations=${imported_count})"

  if [ "$SETUP_STOP_CLAUDE_MEM_AFTER_IMPORT" -eq 1 ]; then
    stop_claude_mem_runtime
    log "Claude-mem stop completed after setup import"
  fi
}

# §93 — Detect additional harness-mem.db files the running daemon is NOT using.
#
# Background: HARNESS_MEM_DB_PATH can be overridden via env var, and older
# releases used different defaults (plugin-scoped paths, XDG_STATE_HOME).
# Operators have been bitten by silently running against a stale DB because
# an old daemon held a different DB path. This helper inspects known
# candidate locations and prints a WARN if any candidate other than the
# current daemon DB exists with size > 0.
#
# Outputs only a warning — never changes exit code / doctor result.
# Candidate paths (in priority order):
#   1. $HARNESS_MEM_DB_PATH (if set)
#   2. $HOME/.harness-mem/harness-mem.db                    (default)
#   3. $HOME/.claude/plugins/data/*/harness-mem.db          (legacy glob)
#   4. ${XDG_STATE_HOME:-$HOME/.local/state}/harness-mem/harness-mem.db (legacy)
check_multiple_db_candidates() {
  local current_db_path="${1:-}"

  # Build de-duplicated candidate list.
  local -a candidates=()
  local _c

  _add_candidate() {
    local p="$1"
    [ -z "$p" ] && return
    [ ! -f "$p" ] && return
    # Dedup.
    local existing
    for existing in "${candidates[@]:-}"; do
      [ "$existing" = "$p" ] && return
    done
    candidates+=("$p")
  }

  # 1. env var path.
  _add_candidate "${HARNESS_MEM_DB_PATH:-}"
  # 2. default.
  _add_candidate "${HOME}/.harness-mem/harness-mem.db"
  # 3. legacy plugin-scoped (glob).
  if [ -d "${HOME}/.claude/plugins/data" ]; then
    local plugin_db
    # shellcheck disable=SC2044
    for plugin_db in "${HOME}"/.claude/plugins/data/*/harness-mem.db; do
      [ -f "$plugin_db" ] || continue
      _add_candidate "$plugin_db"
    done
  fi
  # 4. legacy XDG_STATE_HOME.
  _add_candidate "${XDG_STATE_HOME:-$HOME/.local/state}/harness-mem/harness-mem.db"

  # Helper: size in bytes (GNU/BSD compatible).
  _db_size_bytes() {
    local f="$1"
    if [ ! -f "$f" ]; then
      echo "0"
      return
    fi
    if stat -f%z "$f" >/dev/null 2>&1; then
      stat -f%z "$f" 2>/dev/null || echo "0"
    else
      stat -c%s "$f" 2>/dev/null || echo "0"
    fi
  }

  # Helper: pretty size (4.2G / 40M / 128K / 512B).
  _db_size_pretty() {
    local bytes="$1"
    if [ "$bytes" -ge 1073741824 ] 2>/dev/null; then
      awk -v b="$bytes" 'BEGIN { printf "%.1fG", b/1073741824 }'
    elif [ "$bytes" -ge 1048576 ] 2>/dev/null; then
      awk -v b="$bytes" 'BEGIN { printf "%.1fM", b/1048576 }'
    elif [ "$bytes" -ge 1024 ] 2>/dev/null; then
      awk -v b="$bytes" 'BEGIN { printf "%.1fK", b/1024 }'
    else
      printf "%sB" "$bytes"
    fi
  }

  # Collect "other" candidates (not current, > 0 byte).
  local -a others=()
  for _c in "${candidates[@]:-}"; do
    [ -z "$_c" ] && continue
    [ "$_c" = "$current_db_path" ] && continue
    local bytes
    bytes="$(_db_size_bytes "$_c")"
    [ "$bytes" -gt 0 ] 2>/dev/null || continue
    others+=("${_c}|${bytes}")
  done

  # Nothing extra — silent OK.
  if [ "${#others[@]}" -eq 0 ]; then
    return 0
  fi

  # WARN output.
  local current_bytes current_pretty
  current_bytes="$(_db_size_bytes "${current_db_path}")"
  current_pretty="$(_db_size_pretty "$current_bytes")"

  warn "additional harness-mem.db detected"
  if [ -n "$current_db_path" ]; then
    echo "  current: ${current_db_path} (${current_pretty})" >&2
  fi
  local entry other_path other_bytes other_pretty
  for entry in "${others[@]}"; do
    other_path="${entry%|*}"
    other_bytes="${entry##*|}"
    other_pretty="$(_db_size_pretty "$other_bytes")"
    echo "  other:   ${other_path} (${other_pretty})" >&2
  done
  if ui_is_en 2>/dev/null; then
    echo "  This may be leftover from an older install. If unused:" >&2
    echo "    mv <other> <other>.bak   # or: rm <other>" >&2
  else
    echo "  これは古いバージョンの運用で作成された可能性があります。" >&2
    echo "  不要なら: mv <other> <other>.bak または rm <other>" >&2
  fi
  return 0
}

_doctor_trim_ws() {
  local value="${1:-}"
  value="${value#"${value%%[![:space:]]*}"}"
  value="${value%"${value##*[![:space:]]}"}"
  printf '%s' "$value"
}

_doctor_process_ps_lines() {
  if [ -n "${HARNESS_MEM_PS_FIXTURE:-}" ]; then
    if [ -f "$HARNESS_MEM_PS_FIXTURE" ]; then
      cat "$HARNESS_MEM_PS_FIXTURE"
    fi
    return 0
  fi
  ps -axo pid=,ppid=,etime=,rss=,command= 2>/dev/null || true
}

_doctor_argv0_basename() {
  local command="${1:-}"
  local argv0="${command%%[[:space:]]*}"
  argv0="${argv0#\"}"
  argv0="${argv0%\"}"
  argv0="${argv0#\'}"
  argv0="${argv0%\'}"
  printf '%s' "${argv0##*/}"
}

_doctor_transport_estimate() {
  local executable_lc="$1"
  local command_lc="$2"
  case "$executable_lc" in
    *http*|*gateway*)
      printf 'streamable_http'
      return
      ;;
  esac
  case "$command_lc" in
    *"--transport http"*|*"--http"*|*"streamable"*|*"/mcp"*)
      printf 'streamable_http'
      ;;
    *"harness-mcp-"*)
      printf 'stdio'
      ;;
    *)
      printf 'unknown'
      ;;
  esac
}

_doctor_parent_kind() {
  local parent_lc="$1"
  case "$parent_lc" in
    *codex*) printf 'codex' ;;
    *hermes*) printf 'hermes' ;;
    *claude*) printf 'claude' ;;
    *cursor*) printf 'cursor' ;;
    *opencode*) printf 'opencode' ;;
    *gemini*) printf 'gemini' ;;
    *antigravity*) printf 'antigravity' ;;
    "") printf 'missing' ;;
    *) printf 'other' ;;
  esac
}

_doctor_gateway_listener_pid() {
  if [ -n "${HARNESS_MEM_MCP_GATEWAY_LISTENER_PID_FIXTURE:-}" ]; then
    printf '%s\n' "$HARNESS_MEM_MCP_GATEWAY_LISTENER_PID_FIXTURE"
    return 0
  fi
  _mcp_gateway_discover_listener_pid || true
}

_doctor_process_advisory_json() {
  local source="ps"
  if [ -n "${HARNESS_MEM_PS_FIXTURE:-}" ]; then
    source="fixture"
  fi

  if [ -n "${HARNESS_MEM_PS_FIXTURE:-}" ] && [ ! -f "$HARNESS_MEM_PS_FIXTURE" ]; then
    jq -n \
      --arg source "$source" \
      --arg error "HARNESS_MEM_PS_FIXTURE not found" \
      '{
        enabled: true,
        source: $source,
        inventory_error: $error,
        process_name_pattern: "harness-mcp-*",
        count: 0,
        active_count: 0,
        stale_candidate_count: 0,
        stdio_count: 0,
        streamable_http_count: 0,
        stale_candidates: [],
        processes: [],
        note: "Process inventory is advisory only and never changes doctor all_green by itself."
      }'
    return 0
  fi

  local ps_tmp items_tmp
  ps_tmp="$(mktemp "${TMPDIR:-/tmp}/harness-mem-ps.XXXXXX")"
  items_tmp="$(mktemp "${TMPDIR:-/tmp}/harness-mem-process-items.XXXXXX")"
  _doctor_process_ps_lines > "$ps_tmp"
  : > "$items_tmp"

  local -a ps_pids=()
  local -a ps_ppids=()
  local -a ps_ages=()
  local -a ps_rsses=()
  local -a ps_commands=()

  local line trimmed pid ppid age rss command old_ifs
  while IFS= read -r line || [ -n "$line" ]; do
    trimmed="$(_doctor_trim_ws "$line")"
    [ -z "$trimmed" ] && continue
    case "$trimmed" in
      \#*|PID\ *) continue ;;
    esac

    old_ifs="$IFS"
    IFS=' '
    read -r pid ppid age rss command <<<"$trimmed"
    IFS="$old_ifs"

    [[ "$pid" =~ ^[0-9]+$ ]] || continue
    [[ "$ppid" =~ ^[0-9]+$ ]] || ppid=0
    [ -n "${command:-}" ] || command=""

    ps_pids+=("$pid")
    ps_ppids+=("$ppid")
    ps_ages+=("${age:-}")
    ps_rsses+=("${rss:-}")
    ps_commands+=("$command")
  done < "$ps_tmp"

  local command_lc executable_name executable_lc parent_command parent_lc parent_kind transport parent_live stale_candidate classification
  local rss_json reasons_json item_json
  local idx parent_idx
  local gateway_listener_pid
  local -a stale_reasons=()
  gateway_listener_pid="$(_doctor_gateway_listener_pid)"
  if ! [[ "$gateway_listener_pid" =~ ^[0-9]+$ ]]; then
    gateway_listener_pid=""
  fi

  for idx in "${!ps_pids[@]}"; do
    pid="${ps_pids[$idx]}"
    command="${ps_commands[$idx]}"
    command_lc="$(printf '%s' "$command" | tr '[:upper:]' '[:lower:]')"
    executable_name="$(_doctor_argv0_basename "$command")"
    executable_lc="$(printf '%s' "$executable_name" | tr '[:upper:]' '[:lower:]')"
    case "$executable_lc" in
      harness-mcp-*) ;;
      *) continue ;;
    esac

    ppid="${ps_ppids[$idx]:-0}"
    age="${ps_ages[$idx]:-}"
    rss="${ps_rsses[$idx]:-}"
    parent_command=""
    for parent_idx in "${!ps_pids[@]}"; do
      if [ "${ps_pids[$parent_idx]}" = "$ppid" ]; then
        parent_command="${ps_commands[$parent_idx]}"
        break
      fi
    done
    parent_lc="$(printf '%s' "$parent_command" | tr '[:upper:]' '[:lower:]')"
    parent_kind="$(_doctor_parent_kind "$parent_lc")"
    transport="$(_doctor_transport_estimate "$executable_lc" "$command_lc")"
    if [ -n "$gateway_listener_pid" ] && [ "$pid" = "$gateway_listener_pid" ]; then
      transport="streamable_http"
    fi

    parent_live=false
    if [ "$ppid" -gt 1 ] 2>/dev/null && [ -n "$parent_command" ]; then
      parent_live=true
    fi

    stale_candidate=false
    stale_reasons=()
    if [ "$transport" != "streamable_http" ]; then
      if [ "$ppid" -le 1 ] 2>/dev/null; then
        stale_candidate=true
        stale_reasons+=("ppid_1_or_lower")
      elif [ "$parent_live" != true ]; then
        stale_candidate=true
        stale_reasons+=("missing_parent_process")
      fi
    fi

    if [ "$stale_candidate" = true ]; then
      classification="stale_candidate"
    elif [ "$transport" = "streamable_http" ]; then
      if [ "$parent_live" = true ]; then
        classification="active_http_gateway"
      else
        classification="unknown_parent_http_gateway"
      fi
    elif [ "$transport" = "stdio" ]; then
      classification="active_stdio_child"
    else
      classification="active_live_parent"
    fi

    rss_json="null"
    if [[ "$rss" =~ ^[0-9]+$ ]]; then
      rss_json="$rss"
    fi

    reasons_json="[]"
    if [ "${#stale_reasons[@]}" -gt 0 ]; then
      reasons_json="$(printf '%s\n' "${stale_reasons[@]}" | jq -R . | jq -s .)"
    fi

    item_json="$(jq -nc \
      --argjson pid "$pid" \
      --argjson ppid "$ppid" \
      --arg executable_name "$executable_name" \
      --arg command "$command" \
      --arg parent_command "$parent_command" \
      --arg parent_kind "$parent_kind" \
      --arg age "$age" \
      --argjson rss_kb "$rss_json" \
      --arg transport_estimate "$transport" \
      --arg classification "$classification" \
      --argjson parent_live "$parent_live" \
      --argjson stale_candidate "$stale_candidate" \
      --argjson stale_reasons "$reasons_json" \
      '{
        pid: $pid,
        ppid: $ppid,
        executable_name: $executable_name,
        command: $command,
        parent_command: (if $parent_command == "" then null else $parent_command end),
        parent_kind: $parent_kind,
        age: (if $age == "" then null else $age end),
        rss_kb: $rss_kb,
        transport_estimate: $transport_estimate,
        classification: $classification,
        parent_live: $parent_live,
        stale_candidate: $stale_candidate,
        stale_reasons: $stale_reasons
      }')"
    printf '%s\n' "$item_json" >> "$items_tmp"
  done

  local ts advisory_json
  ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
  advisory_json="$(jq -s \
    --arg source "$source" \
    --arg timestamp "$ts" \
    '{
      enabled: true,
      source: $source,
      inspected_at: $timestamp,
      process_name_pattern: "harness-mcp-*",
      count: length,
      active_count: (map(select(.stale_candidate == false)) | length),
      stale_candidate_count: (map(select(.stale_candidate == true)) | length),
      stdio_count: (map(select(.transport_estimate == "stdio")) | length),
      streamable_http_count: (map(select(.transport_estimate == "streamable_http")) | length),
      stale_candidates: map(select(.stale_candidate == true)),
      processes: .,
      note: "Process inventory is advisory only and never changes doctor all_green by itself."
    }' "$items_tmp")"

  rm -f "$ps_tmp" "$items_tmp"
  printf '%s\n' "$advisory_json"
}

_mcp_cleanup_parse_duration_seconds() {
  local value="${1:-}"
  local amount unit
  if [[ "$value" =~ ^([0-9]+)([sSmMhHdD]?)$ ]]; then
    amount="${BASH_REMATCH[1]}"
    unit="${BASH_REMATCH[2]}"
  else
    return 1
  fi

  case "$unit" in
    ""|s|S) printf '%s\n' "$amount" ;;
    m|M) printf '%s\n' "$((amount * 60))" ;;
    h|H) printf '%s\n' "$((amount * 60 * 60))" ;;
    d|D) printf '%s\n' "$((amount * 24 * 60 * 60))" ;;
    *) return 1 ;;
  esac
}

_mcp_cleanup_selection_json() {
  local advisory_json="$1"
  local mode="$2"
  local older_than="$3"
  local older_than_seconds="$4"

  jq -n \
    --argjson advisory "$advisory_json" \
    --arg mode "$mode" \
    --arg older_than "$older_than" \
    --argjson older_than_seconds "$older_than_seconds" \
    '
    def age_seconds:
      if . == null then null
      elif test("^[0-9]+-[0-9]+:[0-9]+:[0-9]+$") then
        capture("^(?<d>[0-9]+)-(?<h>[0-9]+):(?<m>[0-9]+):(?<s>[0-9]+)$")
        | ((.d | tonumber) * 86400) + ((.h | tonumber) * 3600) + ((.m | tonumber) * 60) + (.s | tonumber)
      elif test("^[0-9]+:[0-9]+:[0-9]+$") then
        capture("^(?<h>[0-9]+):(?<m>[0-9]+):(?<s>[0-9]+)$")
        | ((.h | tonumber) * 3600) + ((.m | tonumber) * 60) + (.s | tonumber)
      elif test("^[0-9]+:[0-9]+$") then
        capture("^(?<m>[0-9]+):(?<s>[0-9]+)$")
        | ((.m | tonumber) * 60) + (.s | tonumber)
      else null
      end;
    def with_age_seconds:
      . + {age_seconds: (.age | age_seconds)};
    def base_candidate:
      .stale_candidate == true
      and .classification == "stale_candidate"
      and .transport_estimate == "stdio";
    def passes_age:
      ($older_than_seconds < 0)
      or ((.age_seconds // -1) >= $older_than_seconds);
    def skip_reason:
      if (base_candidate and (passes_age | not)) then "younger_than_threshold"
      elif (.transport_estimate == "streamable_http") then "streamable_http_gateway_not_cleanup_target"
      elif (.parent_live == true) then "active_parent"
      elif (.stale_candidate == true and .transport_estimate != "stdio") then "not_stdio_candidate"
      elif (.stale_candidate == false) then "not_stale"
      else "not_eligible"
      end;
    ($advisory.processes // [] | map(with_age_seconds)) as $processes
    | ($processes | map(select(base_candidate and passes_age))) as $candidates
    | {
        schema: "cleanup-stale-mcp.v1",
        mode: $mode,
        older_than: (if $older_than == "" then null else $older_than end),
        older_than_seconds: (if $older_than_seconds < 0 then null else $older_than_seconds end),
        source: ($advisory.source // "unknown"),
        inspected_at: ($advisory.inspected_at // null),
        candidates: $candidates,
        skipped: ($processes | map(select((base_candidate and passes_age) | not) | . + {skip_reason: skip_reason})),
        attempted: [],
        killed: [],
        safety_note: "Dry-run is the default. Execute only targets stale stdio harness-mcp-* children older than the requested threshold; live Codex/Hermes/etc. parents and streamable HTTP gateways are skipped."
      }
    '
}

_mcp_cleanup_revalidation_advisory_json() {
  if [ -n "${HARNESS_MEM_MCP_CLEANUP_REVALIDATE_PS_FIXTURE:-}" ]; then
    (
      export HARNESS_MEM_PS_FIXTURE="$HARNESS_MEM_MCP_CLEANUP_REVALIDATE_PS_FIXTURE"
      _doctor_process_advisory_json
    )
    return
  fi
  _doctor_process_advisory_json
}

_mcp_cleanup_revalidate_pid_json() {
  local pid="$1"
  local older_than_seconds="$2"
  local advisory_json
  advisory_json="$(_mcp_cleanup_revalidation_advisory_json)"

  jq -n \
    --argjson advisory "$advisory_json" \
    --argjson pid "$pid" \
    --argjson older_than_seconds "$older_than_seconds" \
    '
    def age_seconds:
      if . == null then null
      elif test("^[0-9]+-[0-9]+:[0-9]+:[0-9]+$") then
        capture("^(?<d>[0-9]+)-(?<h>[0-9]+):(?<m>[0-9]+):(?<s>[0-9]+)$")
        | ((.d | tonumber) * 86400) + ((.h | tonumber) * 3600) + ((.m | tonumber) * 60) + (.s | tonumber)
      elif test("^[0-9]+:[0-9]+:[0-9]+$") then
        capture("^(?<h>[0-9]+):(?<m>[0-9]+):(?<s>[0-9]+)$")
        | ((.h | tonumber) * 3600) + ((.m | tonumber) * 60) + (.s | tonumber)
      elif test("^[0-9]+:[0-9]+$") then
        capture("^(?<m>[0-9]+):(?<s>[0-9]+)$")
        | ((.m | tonumber) * 60) + (.s | tonumber)
      else null
      end;
    def with_age_seconds:
      . + {age_seconds: (.age | age_seconds)};
    def base_candidate:
      .stale_candidate == true
      and .classification == "stale_candidate"
      and .transport_estimate == "stdio";
    def passes_age:
      ($older_than_seconds < 0)
      or ((.age_seconds // -1) >= $older_than_seconds);
    def failure_reason:
      if (.transport_estimate == "streamable_http") then "streamable_http_gateway_not_cleanup_target"
      elif (.parent_live == true) then "active_parent"
      elif (.classification != "stale_candidate") then "classification_not_stale_candidate"
      elif (.transport_estimate != "stdio") then "transport_not_stdio"
      elif ((passes_age | not)) then "younger_than_threshold"
      else "not_eligible"
      end;
    if ($advisory.inventory_error? // "") != "" then
      {
        valid: false,
        error: ("inventory_error: " + ($advisory.inventory_error | tostring)),
        process: null
      }
    else
      ($advisory.processes // [] | map(select(.pid == $pid)) | .[0] // null) as $process
      | if $process == null then
          {
            valid: false,
            error: "pid_not_found_or_argv0_not_harness_mcp",
            process: null
          }
        else
          ($process | with_age_seconds) as $p
          | if ($p | base_candidate and passes_age) then
              {
                valid: true,
                error: null,
                process: $p
              }
            else
              {
                valid: false,
                error: ($p | failure_reason),
                process: $p
              }
            end
        end
    end
    '
}

_mcp_cleanup_execute_candidates_json() {
  local cleanup_json="$1"
  local older_than_seconds="$2"
  local attempts_tmp
  attempts_tmp="$(mktemp "${TMPDIR:-/tmp}/harness-mem-mcp-cleanup-attempts.XXXXXX")"
  : > "$attempts_tmp"

  local pid command reason result error_msg revalidation_json revalidation_valid revalidation_error revalidated_command
  while IFS=$'\t' read -r pid command reason; do
    [ -n "${pid:-}" ] || continue
    result="failed"
    error_msg=""

    revalidation_json="$(_mcp_cleanup_revalidate_pid_json "$pid" "$older_than_seconds")"
    revalidation_valid="$(printf '%s' "$revalidation_json" | jq -r '.valid')"
    revalidation_error="$(printf '%s' "$revalidation_json" | jq -r '.error // ""')"
    revalidated_command="$(printf '%s' "$revalidation_json" | jq -r '.process.command // ""')"

    if [ "$revalidation_valid" != "true" ]; then
      result="skipped_revalidation_failed"
      error_msg="$revalidation_error"
    else
      if [ -n "$revalidated_command" ]; then
        command="$revalidated_command"
      fi
      if [ -n "${HARNESS_MEM_MCP_CLEANUP_KILL_LOG:-}" ]; then
        # Test-only hook: records intended terminations after the same revalidation
        # gate used by the real signal path.
        printf '%s\tSIGTERM\t%s\n' "$pid" "$reason" >> "$HARNESS_MEM_MCP_CLEANUP_KILL_LOG"
        result="test_logged"
      else
        local kill_error_tmp
        kill_error_tmp="$(mktemp "${TMPDIR:-/tmp}/harness-mem-mcp-kill-error.XXXXXX")"
        if kill -TERM "$pid" 2>"$kill_error_tmp"; then
          result="killed"
        else
          error_msg="$(cat "$kill_error_tmp" 2>/dev/null || true)"
        fi
        rm -f "$kill_error_tmp"
      fi
    fi
    jq -nc \
      --argjson pid "$pid" \
      --arg signal "TERM" \
      --arg result "$result" \
      --arg command "$command" \
      --arg reason "$reason" \
      --arg error "$error_msg" \
      --argjson revalidation "$revalidation_json" \
      '{
        pid: $pid,
        signal: $signal,
        result: $result,
        command: $command,
        reason: $reason,
        error: (if $error == "" then null else $error end),
        revalidation: $revalidation
      }' >> "$attempts_tmp"
  done < <(printf '%s' "$cleanup_json" | jq -r '.candidates[]? | [.pid, .command, (.stale_reasons // [] | join(","))] | @tsv')

  jq -s . "$attempts_tmp"
  rm -f "$attempts_tmp"
}

_mcp_cleanup_print_human() {
  local cleanup_json="$1"
  local mode candidate_count skipped_active_count skipped_gateway_count
  mode="$(printf '%s' "$cleanup_json" | jq -r '.mode')"
  candidate_count="$(printf '%s' "$cleanup_json" | jq -r '.candidates | length')"
  skipped_active_count="$(printf '%s' "$cleanup_json" | jq -r '[.skipped[]? | select(.skip_reason == "active_parent")] | length')"
  skipped_gateway_count="$(printf '%s' "$cleanup_json" | jq -r '[.skipped[]? | select(.skip_reason == "streamable_http_gateway_not_cleanup_target")] | length')"

  echo "[harness-mem] stale MCP cleanup: ${mode}"
  echo "  candidates: ${candidate_count}"
  echo "  skipped active/live-parent processes: ${skipped_active_count}"
  echo "  skipped streamable HTTP gateways: ${skipped_gateway_count}"
  echo "  note: dry-run is the default; --execute also requires --older-than <duration>."

  if [ "$candidate_count" -gt 0 ] 2>/dev/null; then
    echo ""
    echo "Kill candidates:"
    printf '%s' "$cleanup_json" | jq -r '
      .candidates[]?
      | "  - pid=\(.pid) reason=\((.stale_reasons // []) | join(",")) age=\(.age // "n/a") ppid=\(.ppid)\n    cmd=\(.command)"
    '
  fi

  if [ "$skipped_active_count" -gt 0 ] 2>/dev/null; then
    echo ""
    echo "Skipped active stdio MCP children:"
    printf '%s' "$cleanup_json" | jq -r '
      .skipped[]?
      | select(.skip_reason == "active_parent")
      | "  - pid=\(.pid) parent=\(.parent_kind) ppid=\(.ppid) age=\(.age // "n/a")\n    cmd=\(.command)"
    '
  fi
}

cleanup_stale_mcp_impl() {
  if [ "$MCP_CLEANUP_EXECUTE" -eq 1 ] && [ "$MCP_CLEANUP_DRY_RUN" -eq 1 ]; then
    fail "cleanup-stale-mcp accepts either --dry-run or --execute, not both"
  fi

  local mode="dry_run"
  if [ "$MCP_CLEANUP_EXECUTE" -eq 1 ]; then
    mode="execute"
    if [ -z "$MCP_CLEANUP_OLDER_THAN" ]; then
      fail "cleanup-stale-mcp --execute requires --older-than <duration> (example: --older-than 10m)"
    fi
  fi

  local older_than_seconds=-1
  if [ -n "$MCP_CLEANUP_OLDER_THAN" ]; then
    if ! older_than_seconds="$(_mcp_cleanup_parse_duration_seconds "$MCP_CLEANUP_OLDER_THAN")"; then
      fail "--older-than must be a duration like 30s, 10m, 2h, or 1d"
    fi
  fi

  local advisory_json cleanup_json attempts_json
  advisory_json="$(_doctor_process_advisory_json)"
  if printf '%s' "$advisory_json" | jq -e '.inventory_error? // empty' >/dev/null 2>&1; then
    fail "Could not inspect harness-mcp-* processes: $(printf '%s' "$advisory_json" | jq -r '.inventory_error')"
  fi

  cleanup_json="$(_mcp_cleanup_selection_json "$advisory_json" "$mode" "$MCP_CLEANUP_OLDER_THAN" "$older_than_seconds")"

  if [ "$mode" = "execute" ]; then
    attempts_json="$(_mcp_cleanup_execute_candidates_json "$cleanup_json" "$older_than_seconds")"
    cleanup_json="$(jq \
      --argjson attempts "$attempts_json" \
      '.attempted = $attempts
       | .killed = ($attempts | map(select(.result == "killed" or .result == "test_logged")))' \
      <<<"$cleanup_json")"
  fi

  if [ "$JSON_OUTPUT" -eq 1 ]; then
    printf '%s\n' "$cleanup_json"
  else
    _mcp_cleanup_print_human "$cleanup_json"
  fi
}

_pid_is_running() {
  local pid="${1:-}"
  [ -n "$pid" ] || return 1
  kill -0 "$pid" >/dev/null 2>&1
}

_read_mcp_gateway_pid_file() {
  if [ -f "$MCP_GATEWAY_PID_FILE" ]; then
    tr -dc '0-9' < "$MCP_GATEWAY_PID_FILE" 2>/dev/null || true
  fi
}

_read_process_args() {
  local pid="${1:-}"
  [ -n "$pid" ] || return 0
  ps -p "$pid" -o args= 2>/dev/null || true
}

_mcp_gateway_env_token() {
  if [ -n "${HARNESS_MEM_MCP_TOKEN:-}" ]; then
    printf '%s' "$HARNESS_MEM_MCP_TOKEN"
  fi
}

_mcp_gateway_file_token() {
  if [ -f "$MCP_GATEWAY_TOKEN_FILE" ]; then
    LC_ALL=C tr -d '\r\n' < "$MCP_GATEWAY_TOKEN_FILE" 2>/dev/null | head -c 4096 || true
  fi
}

_mcp_gateway_token() {
  local token
  token="$(_mcp_gateway_env_token)"
  if [ -n "$token" ]; then
    printf '%s' "$token"
    return
  fi
  token="$(_mcp_gateway_file_token)"
  if [ -n "$token" ]; then
    printf '%s' "$token"
    return
  fi
  printf '%s' "${HARNESS_MEM_REMOTE_TOKEN:-}"
}

_shell_single_quote() {
  local value="${1:-}"
  printf "'%s'" "$(printf '%s' "$value" | sed "s/'/'\\\\''/g")"
}

_generate_mcp_gateway_token() {
  if command -v openssl >/dev/null 2>&1; then
    openssl rand -hex 32 2>/dev/null && return 0
  fi
  if command -v node >/dev/null 2>&1; then
    node -e 'process.stdout.write(require("crypto").randomBytes(32).toString("hex"))' 2>/dev/null && return 0
  fi
  return 1
}

_write_mcp_gateway_token_file() {
  local token="$1"
  mkdir -p "$(dirname "$MCP_GATEWAY_TOKEN_FILE")"
  (
    umask 077
    printf '%s\n' "$token" > "$MCP_GATEWAY_TOKEN_FILE"
  )
  chmod 600 "$MCP_GATEWAY_TOKEN_FILE" >/dev/null 2>&1 || true
}

_sync_mcp_gateway_token_env() {
  local token="$1" quoted
  mkdir -p "$(dirname "$MCP_GATEWAY_ENV_FILE")"
  quoted="$(_shell_single_quote "$token")"
  (
    umask 077
    {
      printf '# Generated by harness-mem. Do not commit.\n'
      printf 'export HARNESS_MEM_MCP_TOKEN=%s\n' "$quoted"
    } > "$MCP_GATEWAY_ENV_FILE"
  )
  chmod 600 "$MCP_GATEWAY_ENV_FILE" >/dev/null 2>&1 || true

  if [ "$(uname -s 2>/dev/null || printf '')" = "Darwin" ] && command -v launchctl >/dev/null 2>&1; then
    launchctl setenv HARNESS_MEM_MCP_TOKEN "$token" >/dev/null 2>&1 || true
  fi
}

_ensure_mcp_gateway_token() {
  local token
  token="$(_mcp_gateway_token)"
  if [ -z "$token" ]; then
    token="$(_generate_mcp_gateway_token)" || fail "Could not generate local MCP gateway token. Install openssl or node."
  fi
  if [ -z "$(_mcp_gateway_file_token)" ]; then
    _write_mcp_gateway_token_file "$token"
  else
    chmod 600 "$MCP_GATEWAY_TOKEN_FILE" >/dev/null 2>&1 || true
  fi
  _sync_mcp_gateway_token_env "$token"
  printf '%s' "$token"
}

_curl_config_escape() {
  local value="${1:-}"
  value="${value//\\/\\\\}"
  value="${value//\"/\\\"}"
  printf '%s' "$value"
}

_mcp_gateway_auth_mode() {
  if [ -n "$(_mcp_gateway_token)" ]; then
    printf 'token'
  else
    printf 'missing_token'
  fi
}

_mcp_gateway_port() {
  local port
  port="$(printf '%s' "$MCP_GATEWAY_ADDR" | sed -nE 's/^.*:([0-9]+)$/\1/p' | head -n1)"
  printf '%s' "$port"
}

_mcp_gateway_endpoint_url() {
  local addr="$MCP_GATEWAY_ADDR"
  if [[ "$addr" == :* ]]; then
    printf 'http://127.0.0.1%s%s' "$addr" "$MCP_GATEWAY_ENDPOINT"
  else
    printf 'http://%s%s' "$addr" "$MCP_GATEWAY_ENDPOINT"
  fi
}

_mcp_gateway_go_bin_path() {
  printf '%s/%s\n' "${HARNESS_ROOT}/bin" "$(_resolve_go_bin_name)"
}

ensure_mcp_gateway_runtime() {
  download_go_binary 2>/dev/null || true
  local go_bin_path
  go_bin_path="$(_mcp_gateway_go_bin_path)"
  if [ ! -x "$go_bin_path" ]; then
    fail "Streamable HTTP MCP gateway requires the Go MCP binary: ${go_bin_path}. Run: harness-mem setup"
  fi
}

_mcp_gateway_discover_listener_pid() {
  local port
  port="$(_mcp_gateway_port)"
  [ -n "$port" ] || return 1
  if ! command -v lsof >/dev/null 2>&1; then
    return 1
  fi
  lsof -nP -tiTCP:"$port" -sTCP:LISTEN 2>/dev/null | head -n1 || true
}

_mcp_gateway_pid_listens() {
  local pid="${1:-}"
  local port
  [ -n "$pid" ] || return 1
  port="$(_mcp_gateway_port)"
  [ -n "$port" ] || return 1
  command -v lsof >/dev/null 2>&1 || return 1
  lsof -nP -a -p "$pid" -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1
}

_mcp_gateway_is_expected_pid() {
  local pid="${1:-}"
  local args
  [ -n "$pid" ] || return 1
  _pid_is_running "$pid" || return 1
  args="$(_read_process_args "$pid")"
  case "$args" in
    *harness-mcp-*|*bin/harness-mcp-server*)
      _mcp_gateway_pid_listens "$pid"
      return $?
      ;;
  esac
  return 1
}

_mcp_gateway_memory_health_json() {
  local url="http://${MEM_HOST}:${MEM_PORT}/health/ready"
  local tmp status ok status_name
  tmp="$(mktemp "${TMPDIR:-/tmp}/harness-mem-mcp-gateway-memory-health.XXXXXX")"
  status="$(curl --silent --show-error --max-time 2 -o "$tmp" -w '%{http_code}' "$url" 2>/dev/null || true)"
  ok=false
  status_name="unreachable"
  if [ "$status" = "200" ] && jq -e '.ok == true' "$tmp" >/dev/null 2>&1; then
    ok=true
    status_name="healthy"
  fi
  jq -n \
    --argjson ok "$ok" \
    --arg status "$status_name" \
    --arg url "$url" \
    --arg http_status "$status" \
    '{
      ok: $ok,
      status: $status,
      url: $url,
      http_status: (if $http_status == "" or $http_status == "000" then null else ($http_status | tonumber) end)
    }'
  rm -f "$tmp"
}

_mcp_gateway_probe_json() {
  local endpoint token tmp status ok status_name error_excerpt payload endpoint_config token_config
  endpoint="$(_mcp_gateway_endpoint_url)"
  token="$(_mcp_gateway_token)"
  if [ -z "$token" ]; then
    jq -n \
      --arg endpoint "$endpoint" \
      '{
        ok: false,
        status: "missing_token",
        endpoint: $endpoint,
        http_status: null,
        error: "local MCP gateway token is missing; run harness-mem mcp-gateway start"
      }'
    return 0
  fi

  payload='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"harness-mem-gateway-doctor","version":"0.1.0"}}}'
  tmp="$(mktemp "${TMPDIR:-/tmp}/harness-mem-mcp-gateway-probe.XXXXXX")"
  endpoint_config="$(_curl_config_escape "$endpoint")"
  token_config="$(_curl_config_escape "$token")"
  status="$(
    {
      printf 'url = "%s"\n' "$endpoint_config"
      printf 'request = "POST"\n'
      printf 'header = "Content-Type: application/json"\n'
      printf 'header = "Authorization: Bearer %s"\n' "$token_config"
    } | curl --silent --show-error --max-time 3 \
      -o "$tmp" \
      -w '%{http_code}' \
      --config - \
      -d "$payload" 2>/dev/null || true
  )"

  ok=false
  status_name="unreachable"
  error_excerpt=""
  if [ "$status" = "200" ] && jq -e '.result.protocolVersion? // empty' "$tmp" >/dev/null 2>&1; then
    ok=true
    status_name="healthy"
  else
    error_excerpt="$(tr '\n' ' ' < "$tmp" 2>/dev/null | cut -c1-240 || true)"
    case "$status" in
      401) status_name="unauthorized" ;;
      403) status_name="forbidden" ;;
      000|"") status_name="unreachable" ;;
      *) status_name="bad_response" ;;
    esac
  fi

  jq -n \
    --argjson ok "$ok" \
    --arg status "$status_name" \
    --arg endpoint "$endpoint" \
    --arg http_status "$status" \
    --arg error "$error_excerpt" \
    '{
      ok: $ok,
      status: $status,
      endpoint: $endpoint,
      http_status: (if $http_status == "" or $http_status == "000" then null else ($http_status | tonumber) end),
      error: (if $error == "" then null else $error end)
    }'
  rm -f "$tmp"
}

_mcp_gateway_launchd_loaded_json() {
  local loaded="null"
  if command -v launchctl >/dev/null 2>&1; then
    local uid target
    uid="$(id -u 2>/dev/null || true)"
    if [ -n "$uid" ]; then
      target="gui/${uid}/${MCP_GATEWAY_LAUNCHD_LABEL}"
      if launchctl print "$target" >/dev/null 2>&1; then
        loaded="true"
      else
        loaded="false"
      fi
    fi
  fi
  jq -n \
    --arg label "$MCP_GATEWAY_LAUNCHD_LABEL" \
    --argjson loaded "$loaded" \
    '{label: $label, loaded: $loaded}'
}

_mcp_gateway_status_json() {
  local pid listener_pid pid_running=false listener_running=false pid_json="null" listener_pid_json="null"
  local probe_json memory_json launchd_json status endpoint auth_mode
  endpoint="$(_mcp_gateway_endpoint_url)"
  auth_mode="$(_mcp_gateway_auth_mode)"

  pid="$(_read_mcp_gateway_pid_file)"
  if [ -n "$pid" ] && _pid_is_running "$pid"; then
    pid_running=true
    pid_json="$pid"
  fi

  listener_pid="$(_mcp_gateway_discover_listener_pid || true)"
  if [ -n "$listener_pid" ]; then
    listener_running=true
    listener_pid_json="$listener_pid"
    if [ "$pid_json" = "null" ]; then
      pid_json="$listener_pid"
    fi
  fi

  probe_json="$(_mcp_gateway_probe_json)"
  memory_json="$(_mcp_gateway_memory_health_json)"
  launchd_json="$(_mcp_gateway_launchd_loaded_json)"

  if printf '%s' "$probe_json" | jq -e '.ok == true' >/dev/null 2>&1; then
    status="running"
  elif [ "$pid_running" = true ] || [ "$listener_running" = true ]; then
    status="degraded"
  else
    status="stopped"
  fi

  jq -n \
    --arg schema "mcp-gateway.status.v1" \
    --arg status "$status" \
    --arg endpoint "$endpoint" \
    --arg addr "$MCP_GATEWAY_ADDR" \
    --arg auth_mode "$auth_mode" \
    --arg pid_file "$MCP_GATEWAY_PID_FILE" \
    --arg log_file "$MCP_GATEWAY_LOG_FILE" \
    --argjson pid "$pid_json" \
    --argjson pid_running "$pid_running" \
    --argjson listener_pid "$listener_pid_json" \
    --argjson listener_running "$listener_running" \
    --argjson gateway "$probe_json" \
    --argjson memory_daemon "$memory_json" \
    --argjson launchd "$launchd_json" \
    '{
      schema: $schema,
      status: $status,
      endpoint: $endpoint,
      addr: $addr,
      pid: $pid,
      pid_running: $pid_running,
      listener_pid: $listener_pid,
      listener_running: $listener_running,
      pid_file: $pid_file,
      log_file: $log_file,
      auth_mode: $auth_mode,
      gateway: $gateway,
      memory_daemon: $memory_daemon,
      launchd: $launchd
    }'
}

_mcp_gateway_print_status_human() {
  local status_json="$1"
  printf '%s\n' "$status_json" | jq -r '
    "[harness-mem] MCP gateway status: \(.status)",
    "  endpoint: \(.endpoint)",
    "  pid: \(.pid // "n/a")",
    "  auth: \(.auth_mode)",
    "  gateway: \(.gateway.status) http=\(.gateway.http_status // "n/a")",
    "  memory daemon: \(.memory_daemon.status) \(.memory_daemon.url)",
    "  pidfile: \(.pid_file)",
    "  log: \(.log_file)",
    "  launchd: \(.launchd.label) loaded=\(.launchd.loaded // "unknown")"
  '
}

_mcp_gateway_wait_healthy() {
  local retries attempt status_json
  retries=$((MCP_GATEWAY_START_TIMEOUT_SEC * 2))
  attempt=0
  while [ "$attempt" -lt "$retries" ]; do
    status_json="$(_mcp_gateway_status_json)"
    if printf '%s' "$status_json" | jq -e '.gateway.ok == true' >/dev/null 2>&1; then
      return 0
    fi
    sleep 0.5
    attempt=$((attempt + 1))
  done
  return 1
}

mcp_gateway_start_impl() {
  require_cmd curl
  require_cmd jq
  ensure_mcp_gateway_runtime
  mkdir -p "$STATE_DIR"

  local token go_bin_path status_json pid listener_pid listener_args
  token="$(_ensure_mcp_gateway_token)"
  [ -n "$token" ] || fail "mcp-gateway start could not resolve a local token"

  if [ "$MCP_GATEWAY_FOREGROUND" -ne 1 ]; then
    status_json="$(_mcp_gateway_status_json)"
    if printf '%s' "$status_json" | jq -e '.gateway.ok == true' >/dev/null 2>&1; then
      if [ "$JSON_OUTPUT" -eq 1 ]; then
        printf '%s\n' "$status_json"
      else
        _mcp_gateway_print_status_human "$status_json"
      fi
      return 0
    fi

    pid="$(_read_mcp_gateway_pid_file)"
    if [ -n "$pid" ] && _pid_is_running "$pid"; then
      fail "mcp-gateway pid file points to a running but unhealthy process (pid=${pid}). Run: harness-mem mcp-gateway stop"
    fi

    listener_pid="$(_mcp_gateway_discover_listener_pid || true)"
    if [ -n "$listener_pid" ]; then
      listener_args="$(_read_process_args "$listener_pid")"
      fail "MCP gateway port $(_mcp_gateway_port) is already in use by pid=${listener_pid}: ${listener_args}"
    fi
  fi

  HARNESS_MEM_ENABLE_UI="${HARNESS_MEM_ENABLE_UI:-false}" start_daemon >/dev/null 2>&1 || fail "memory daemon could not be started before mcp-gateway"
  go_bin_path="$(_mcp_gateway_go_bin_path)"

  if [ "$MCP_GATEWAY_FOREGROUND" -eq 1 ]; then
    log "Starting MCP gateway in foreground: $(_mcp_gateway_endpoint_url)"
    exec env \
      HARNESS_MEM_HOST="$MEM_HOST" \
      HARNESS_MEM_PORT="$MEM_PORT" \
      HARNESS_MEM_DB_PATH="$DB_PATH" \
      HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
      HARNESS_MEM_MCP_TRANSPORT="http" \
      HARNESS_MEM_MCP_ADDR="$MCP_GATEWAY_ADDR" \
      HARNESS_MEM_MCP_TOKEN="$token" \
      "$go_bin_path"
  fi

  log "Starting MCP gateway: $(_mcp_gateway_endpoint_url)"
  (
    local gateway_pid
    cd "$HARNESS_ROOT"
    nohup env \
      HARNESS_MEM_HOST="$MEM_HOST" \
      HARNESS_MEM_PORT="$MEM_PORT" \
      HARNESS_MEM_DB_PATH="$DB_PATH" \
      HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
      HARNESS_MEM_MCP_TRANSPORT="http" \
      HARNESS_MEM_MCP_ADDR="$MCP_GATEWAY_ADDR" \
      HARNESS_MEM_MCP_TOKEN="$token" \
      "$go_bin_path" >> "$MCP_GATEWAY_LOG_FILE" 2>&1 < /dev/null &
    gateway_pid=$!
    printf '%s\n' "$gateway_pid" > "$MCP_GATEWAY_PID_FILE"
    disown "$gateway_pid" >/dev/null 2>&1 || true
  )

  if _mcp_gateway_wait_healthy; then
    status_json="$(_mcp_gateway_status_json)"
    if [ "$JSON_OUTPUT" -eq 1 ]; then
      printf '%s\n' "$status_json"
    else
      _mcp_gateway_print_status_human "$status_json"
    fi
    return 0
  fi

  pid="$(_read_mcp_gateway_pid_file)"
  if [ -n "$pid" ] && _pid_is_running "$pid"; then
    kill -TERM "$pid" >/dev/null 2>&1 || true
  fi
  rm -f "$MCP_GATEWAY_PID_FILE"
  fail "MCP gateway did not become healthy. See log: ${MCP_GATEWAY_LOG_FILE}"
}

mcp_gateway_stop_impl() {
  local pid listener_pid waited
  mkdir -p "$STATE_DIR"
  pid="$(_read_mcp_gateway_pid_file)"
  if [ -z "$pid" ]; then
    listener_pid="$(_mcp_gateway_discover_listener_pid || true)"
    if [ -n "$listener_pid" ] && _mcp_gateway_is_expected_pid "$listener_pid"; then
      pid="$listener_pid"
    fi
  fi

  if [ -z "$pid" ]; then
    rm -f "$MCP_GATEWAY_PID_FILE"
    log "mcp-gateway is not running"
    return 0
  fi

  if ! _pid_is_running "$pid"; then
    rm -f "$MCP_GATEWAY_PID_FILE"
    log "mcp-gateway pid file was stale and has been cleaned"
    return 0
  fi

  if ! _mcp_gateway_is_expected_pid "$pid"; then
    fail "Refusing to stop pid=${pid}; it is not the expected MCP gateway listener"
  fi

  log "Stopping MCP gateway (pid=${pid})..."
  kill -TERM "$pid" >/dev/null 2>&1 || true
  waited=0
  while _pid_is_running "$pid" && [ "$waited" -lt "$MCP_GATEWAY_STOP_TIMEOUT_SEC" ]; do
    sleep 1
    waited=$((waited + 1))
  done
  if _pid_is_running "$pid"; then
    warn "MCP gateway did not exit in ${MCP_GATEWAY_STOP_TIMEOUT_SEC}s, sending SIGKILL"
    kill -KILL "$pid" >/dev/null 2>&1 || true
  fi
  rm -f "$MCP_GATEWAY_PID_FILE"
  log "MCP gateway stopped"
}

mcp_gateway_status_impl() {
  require_cmd curl
  require_cmd jq
  local status_json status
  status_json="$(_mcp_gateway_status_json)"
  if [ "$JSON_OUTPUT" -eq 1 ]; then
    printf '%s\n' "$status_json"
  else
    _mcp_gateway_print_status_human "$status_json"
  fi
  status="$(printf '%s' "$status_json" | jq -r '.status')"
  [ "$status" = "running" ]
}

mcp_gateway_impl() {
  local subcmd="${1:-status}"
  case "$subcmd" in
    start)
      mcp_gateway_start_impl
      ;;
    stop)
      mcp_gateway_stop_impl
      ;;
    status)
      mcp_gateway_status_impl
      ;;
    *)
      fail "Unknown mcp-gateway subcommand: ${subcmd}. Use: start | stop | status"
      ;;
  esac
}

_normalize_mcp_transport_option() {
  local raw="${1:-}"
  raw="$(printf '%s' "$raw" | tr '[:upper:]' '[:lower:]' | xargs)"
  case "$raw" in
    ""|stdio) printf 'stdio' ;;
    http|streamable_http) printf 'streamable_http' ;;
    *) return 1 ;;
  esac
}

apply_mcp_transport_default_for_command() {
  local command="${1:-}"
  local default_transport normalized_default
  [ "$command" = "setup" ] || return 0
  [ "$MCP_TRANSPORT_EXPLICIT" -eq 0 ] || return 0
  if ! is_platform_enabled codex && ! is_platform_enabled claude; then
    return 0
  fi

  default_transport="${HARNESS_MEM_SETUP_MCP_TRANSPORT_DEFAULT:-http}"
  if ! normalized_default="$(_normalize_mcp_transport_option "$default_transport")"; then
    fail "HARNESS_MEM_SETUP_MCP_TRANSPORT_DEFAULT must be stdio or http"
  fi
  MCP_CONFIG_TRANSPORT="$normalized_default"
}

_mcp_config_transport_is_http() {
  [ "$(_normalize_mcp_transport_option "$MCP_CONFIG_TRANSPORT" 2>/dev/null || printf 'stdio')" = "streamable_http" ]
}

_doctor_check_mcp_gateway() {
  local transport="$1"
  local status_json gateway_status gateway_ok fix
  if [ "$transport" != "streamable_http" ]; then
    _doctor_record "mcp_gateway" "ok:stdio_default" ""
    return 0
  fi

  status_json="$(_mcp_gateway_status_json)"
  gateway_status="$(printf '%s' "$status_json" | jq -r '.gateway.status // "unknown"')"
  gateway_ok="$(printf '%s' "$status_json" | jq -r '.gateway.ok // false')"
  fix="harness-mem mcp-gateway start"
  if [ "$gateway_ok" = "true" ]; then
    _doctor_record "mcp_gateway" "ok:http" ""
    log "MCP gateway: OK ($(_mcp_gateway_endpoint_url))"
    return 0
  fi

  _doctor_record "mcp_gateway" "$gateway_status" "$fix"
  warn "MCP gateway check failed: ${gateway_status} ($(_mcp_gateway_endpoint_url))"
  return 1
}

_doctor_print_process_advisory() {
  local advisory_json="$1"
  local count stale active
  count="$(printf '%s' "$advisory_json" | jq -r '.count // 0' 2>/dev/null || echo 0)"
  active="$(printf '%s' "$advisory_json" | jq -r '.active_count // 0' 2>/dev/null || echo 0)"
  stale="$(printf '%s' "$advisory_json" | jq -r '.stale_candidate_count // 0' 2>/dev/null || echo 0)"

  echo ""
  echo "[harness-mem] MCP process advisory:"
  echo "  total harness-mcp-* processes: ${count} (active/live-parent: ${active}, stale candidates: ${stale})"
  echo "  note: process count alone is not a doctor failure."
  if [ "$stale" -gt 0 ] 2>/dev/null; then
    echo "  stale candidates are usually safe to inspect first, then clean up only when the parent is gone."
  fi
  printf '%s' "$advisory_json" | jq -r '
    .processes[]?
    | "  - pid=\(.pid) ppid=\(.ppid) class=\(.classification) transport=\(.transport_estimate) age=\(.age // "n/a") rss_kb=\(.rss_kb // "n/a") parent=\(.parent_command // "missing")\n    cmd=\(.command)"
  ' 2>/dev/null || true
}

doctor_impl() {
  local failed=0
  # JSON出力用チェック結果配列
  local -a doctor_checks=()

  # ヘルパー: チェック結果を記録する
  # 引数: name status fix
  _doctor_record() {
    local name="$1" status="$2" fix="$3"
    doctor_checks+=("$(printf '%s\t%s\t%s' "$name" "$status" "$fix")")
  }

  # Backend mode check
  local current_backend
  current_backend="$(read_backend_mode)"
  _doctor_record "backend_mode" "ok:${current_backend}" ""
  log "Backend mode: ${current_backend}"

    # config.json existence
    if [ -f "$CONFIG_PATH" ]; then
      _doctor_record "config" "ok" ""
    elif [ "$READ_ONLY_MODE" -eq 1 ]; then
      _doctor_record "config" "missing" "harness-mem setup"
      failed=1
    else
      ensure_config
      _doctor_record "config" "ok" ""
      log "Created default config at ${CONFIG_PATH}"
  fi

  # managed backend connectivity (only for managed/hybrid)
  if [ "$current_backend" = "managed" ] || [ "$current_backend" = "hybrid" ]; then
    local managed_ep
    managed_ep="$(read_managed_endpoint)"
    if [ -n "$managed_ep" ]; then
      if curl --silent --show-error --fail --max-time 3 "${managed_ep}/health" >/dev/null 2>&1; then
        _doctor_record "managed_backend" "ok" ""
      else
        _doctor_record "managed_backend" "unreachable" "check managed endpoint: ${managed_ep}"
        failed=1
      fi
    else
      _doctor_record "managed_backend" "not_configured" "set managed.endpoint in ${CONFIG_PATH}"
      failed=1
    fi
  fi

  # 依存コマンドチェック
  if [ "$FIX_MODE" -eq 1 ]; then
    ensure_ripgrep
  fi

  if check_cmd bun; then
    _doctor_record "bun" "ok" ""
  else
    warn "bun is missing"
    _doctor_record "bun" "missing" "brew install bun"
    failed=1
  fi
  if check_cmd curl; then
    _doctor_record "curl" "ok" ""
  else
    warn "curl is missing"
    _doctor_record "curl" "missing" "brew install curl"
    failed=1
  fi
  if check_cmd jq; then
    _doctor_record "jq" "ok" ""
  else
    warn "jq is missing"
    _doctor_record "jq" "missing" "brew install jq"
    failed=1
  fi
  if check_cmd node; then
    _doctor_record "node" "ok" ""
  else
    warn "node is missing"
    _doctor_record "node" "missing" "brew install node"
    failed=1
  fi
  if check_cmd npm; then
    _doctor_record "npm" "ok" ""
  else
    warn "npm is missing"
    _doctor_record "npm" "missing" "brew install node"
    failed=1
  fi
  if check_cmd rg; then
    _doctor_record "rg" "ok" ""
  else
    warn "rg (ripgrep) is missing"
    _doctor_record "rg" "missing" "brew install ripgrep"
    failed=1
  fi

  # Go MCP binary check
  #
  # Status semantics must match _doctor_emit_json's "ok" / "ok:*" contract
  # so that failed_count and all_green stay consistent. Go binary absence
  # is an acceptable state (the wrapper script falls back to Node.js), so
  # it is reported with an "ok:*" prefix. A broken-but-present binary is
  # a real failure and flips the failed flag.
  local go_bin_name go_bin_path
  go_bin_name="$(_resolve_go_bin_name)"
  go_bin_path="${HARNESS_ROOT}/bin/${go_bin_name}"
  if [ -x "$go_bin_path" ]; then
    # Smoke test: send initialize and check for valid response
    if printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"doctor","version":"0.1"}}}\n' \
      | "$go_bin_path" 2>/dev/null | grep -q '"serverInfo"'; then
      _doctor_record "go_mcp_binary" "ok" ""
      log "Go MCP binary: ${go_bin_path} (healthy)"
    else
      _doctor_record "go_mcp_binary" "broken" "rm ${go_bin_path} && harness-mem setup"
      warn "Go MCP binary exists but smoke test failed: ${go_bin_path}"
      failed=1
    fi
  else
    _doctor_record "go_mcp_binary" "ok:nodejs_fallback" "harness-mem setup (optional: auto-downloads Go binary from GitHub Releases)"
    log "Go MCP binary: not installed (Node.js fallback active)"
  fi

  if [ -n "$DOCTOR_MCP_TRANSPORT" ]; then
    local normalized_mcp_transport
    if ! normalized_mcp_transport="$(_normalize_mcp_transport_option "$DOCTOR_MCP_TRANSPORT")"; then
      _doctor_record "mcp_gateway" "invalid_transport" "use --mcp-transport stdio or --mcp-transport http"
      failed=1
    elif ! check_cmd jq || ! check_cmd curl; then
      _doctor_record "mcp_gateway" "skipped" "install jq and curl, then rerun doctor --mcp-transport http"
    elif ! _doctor_check_mcp_gateway "$normalized_mcp_transport"; then
      failed=1
    fi
  fi

    if check_cmd bun && check_cmd curl && check_cmd npm; then
      if [ "$READ_ONLY_MODE" -ne 1 ]; then
        ensure_mcp_runtime || failed=1
      fi
      local health_url="http://${MEM_HOST}:${MEM_PORT}/health"
      if curl --silent --show-error --fail --max-time 1 "$health_url" >/dev/null 2>&1; then
        log "Daemon endpoint already reachable: ${health_url}"
      elif [ "$READ_ONLY_MODE" -eq 1 ]; then
        log "Daemon endpoint not reachable in read-only doctor mode: ${health_url}"
      else
        HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
          HARNESS_MEM_ENABLE_UI="${HARNESS_MEM_ENABLE_UI:-false}" \
          HARNESS_MEM_HOST="$MEM_HOST" \
        HARNESS_MEM_PORT="$MEM_PORT" \
        HARNESS_MEM_DB_PATH="$DB_PATH" \
        "$HARNESS_ROOT/scripts/harness-memd" start --quiet >/dev/null 2>&1 || true
    fi

      if [ "$READ_ONLY_MODE" -ne 1 ]; then
        HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
          HARNESS_MEM_HOST="$MEM_HOST" \
          HARNESS_MEM_PORT="$MEM_PORT" \
          HARNESS_MEM_DB_PATH="$DB_PATH" \
          "$HARNESS_ROOT/scripts/harness-memd" cleanup-stale --quiet >/dev/null 2>&1 || true
      fi

      if [ "$READ_ONLY_MODE" -eq 1 ] && ! curl --silent --show-error --fail --max-time 1 "$health_url" >/dev/null 2>&1; then
        _doctor_record "daemon" "skipped" "read-only mode did not start harness-memd"
      elif HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
        HARNESS_MEM_HOST="$MEM_HOST" \
        HARNESS_MEM_PORT="$MEM_PORT" \
      HARNESS_MEM_DB_PATH="$DB_PATH" \
      "$HARNESS_ROOT/scripts/harness-memd" doctor >/dev/null; then
      log "Daemon doctor: OK"
      _doctor_record "daemon" "ok" ""
    else
      warn "Daemon doctor reported warnings"
      if curl --silent --show-error --fail --max-time 1 "$health_url" >/dev/null 2>&1; then
        warn "Daemon health endpoint is reachable; treating daemon doctor result as warning only"
        _doctor_record "daemon" "ok:reachable_with_warnings" "harness-memd cleanup-stale"
      else
        _doctor_record "daemon" "unhealthy" "harness-memd start"
        failed=1
      fi
    fi

    # §93 — Detect additional harness-mem.db candidates the running daemon
    # is NOT using. Pulls db_path from the daemon's /health response and
    # compares against a fixed set of known candidate locations. Emits WARN
    # only; never affects failed / exit code.
    local _doctor_current_db=""
    if _doctor_health_json="$(curl --silent --show-error --fail --max-time 2 "$health_url" 2>/dev/null)"; then
      _doctor_current_db="$(echo "$_doctor_health_json" | jq -r '.items[0].db_path // ""' 2>/dev/null || echo "")"
    fi
    # Fall back to configured DB_PATH if /health lookup failed.
    if [ -z "$_doctor_current_db" ]; then
      _doctor_current_db="${HARNESS_MEM_DB_PATH:-${DB_PATH:-}}"
    fi
    check_multiple_db_candidates "$_doctor_current_db" || true

    # S81-A04: Lease + Signal availability probe.
    # Drives the daemon's /v1/lease/acquire and /v1/signal/send endpoints with
    # a throwaway agent_id so we verify the mem_leases / mem_signals schema is
    # present end-to-end. Reachability alone is sufficient — detailed contract
    # coverage lives in tests/unit/{lease,signal}-store.test.ts.
    #
    # S81-A04 (Codex round 6 P2.3): forward the caller's bearer token when
    # set so the probe does not get a 401 on auth-enabled daemons.
      if [ "$READ_ONLY_MODE" -eq 1 ] && ! curl --silent --show-error --fail --max-time 1 "$health_url" >/dev/null 2>&1; then
        _doctor_record "lease_primitive" "skipped" "read-only mode did not start harness-memd"
        _doctor_record "signal_primitive" "skipped" "read-only mode did not start harness-memd"
      else
      local _auth_hdr=()
    local _probe_token="${HARNESS_MEM_REMOTE_TOKEN:-${HARNESS_MEM_ADMIN_TOKEN:-}}"
    if [ -n "$_probe_token" ]; then
      _auth_hdr=(-H "Authorization: Bearer $_probe_token")
    fi

    local lease_probe_payload lease_probe_status lease_probe_id
    lease_probe_payload='{"target":"__doctor_probe__","agent_id":"__doctor__","ttl_ms":1000}'
    if lease_probe_status="$(curl --silent --show-error --max-time 2 -o /tmp/harness-lease-probe.json -w '%{http_code}' \
        -H 'Content-Type: application/json' \
        ${_auth_hdr[@]+"${_auth_hdr[@]}"} \
        -X POST "${health_url%/health}/v1/lease/acquire" \
        -d "$lease_probe_payload" 2>/dev/null)" && [ "$lease_probe_status" = "200" ]; then
      _doctor_record "lease_primitive" "ok" ""
      # Best-effort release so the probe lease does not linger.
      lease_probe_id="$(jq -r '.lease.leaseId // empty' < /tmp/harness-lease-probe.json 2>/dev/null || true)"
      if [ -n "$lease_probe_id" ]; then
        curl --silent --show-error --max-time 2 -o /dev/null \
          -H 'Content-Type: application/json' \
          ${_auth_hdr[@]+"${_auth_hdr[@]}"} \
          -X POST "${health_url%/health}/v1/lease/release" \
          -d "{\"lease_id\":\"$lease_probe_id\",\"agent_id\":\"__doctor__\"}" 2>/dev/null || true
      fi
    else
      _doctor_record "lease_primitive" "unreachable" "S81-A02: restart daemon, set HARNESS_MEM_REMOTE_TOKEN, or run 'bun test tests/unit/lease-store.test.ts'"
      failed=1
    fi
    rm -f /tmp/harness-lease-probe.json

    local signal_probe_status
    if signal_probe_status="$(curl --silent --show-error --max-time 2 -o /dev/null -w '%{http_code}' \
        -H 'Content-Type: application/json' \
        ${_auth_hdr[@]+"${_auth_hdr[@]}"} \
        -X POST "${health_url%/health}/v1/signal/read" \
        -d '{"agent_id":"__doctor__","limit":1}' 2>/dev/null)" && [ "$signal_probe_status" = "200" ]; then
      _doctor_record "signal_primitive" "ok" ""
      else
        _doctor_record "signal_primitive" "unreachable" "S81-A03: restart daemon, set HARNESS_MEM_REMOTE_TOKEN, or run 'bun test tests/unit/signal-store.test.ts'"
        failed=1
      fi
      fi
    fi

  # Platform wiring checks — when --platform is not explicitly set,
  # only check platforms that are actually installed on this machine.
  _should_check_platform() {
    local plat="$1"
    # If user explicitly specified --platform, respect that
    if [ "$PLATFORM_EXPLICIT" -eq 1 ]; then
      is_platform_enabled "$plat"
      return $?
    fi
    # Auto-detect: only check platforms where harness-mem wiring
    # has been previously set up (partial or full). Skip platforms
    # the user has never configured — avoids noisy FAIL for unused tools.
    case "$plat" in
      codex)       [ -f "$HOME/.codex/config.toml" ] && grep -q "harness" "$HOME/.codex/config.toml" 2>/dev/null ;;
      opencode)    [ -f "$HOME/.config/opencode/plugins/harness-memory/index.ts" ] ;;
      claude)      check_claude_wiring 2>/dev/null ;;
      cursor)      [ -f "$HOME/.cursor/mcp.json" ] && grep -q "harness" "$HOME/.cursor/mcp.json" 2>/dev/null ;;
      antigravity) [ -f "$HOME/.antigravity/config.json" ] && grep -q "harness" "$HOME/.antigravity/config.json" 2>/dev/null ;;
      *) return 1 ;;
    esac
  }

    if _should_check_platform codex; then
      if check_codex_wiring; then
        _doctor_record "codex_wiring" "ok" ""
    else
      _doctor_record "codex_wiring" "missing" "harness-mem setup --platform codex"
        failed=1
      fi
      if check_codex_skill_bundle; then
        _doctor_record "codex_skill_drift" "ok" ""
      else
        _doctor_record "codex_skill_drift" "${CODEX_SKILL_BUNDLE_STATUS:-missing}" "harness-mem setup --platform codex"
        failed=1
      fi
      if check_codex_requirements_alignment; then
        _doctor_record "codex_requirements_precedence" "ok" ""
      else
        _doctor_record "codex_requirements_precedence" "drift" "align ~/.codex/requirements.toml with ~/.codex/config.toml and ~/.codex/hooks.json, or rerun harness-mem setup --platform codex"
        failed=1
      fi
      local codex_post_health_url="http://${MEM_HOST}:${MEM_PORT}/health"
      if curl --silent --show-error --fail --max-time 1 "$codex_post_health_url" >/dev/null 2>&1; then
        _doctor_record "codex_post_doctor_liveness" "ok" ""
      elif [ "$READ_ONLY_MODE" -eq 1 ]; then
        _doctor_record "codex_post_doctor_liveness" "skipped" "read-only mode did not start harness-memd"
      else
        _doctor_record "codex_post_doctor_liveness" "unhealthy" "harness-memd start && harness-mem doctor --platform codex"
        failed=1
      fi
    fi
  if _should_check_platform opencode; then
    if check_opencode_wiring; then
      _doctor_record "opencode_wiring" "ok" ""
    else
      _doctor_record "opencode_wiring" "missing" "harness-mem setup --platform opencode"
      failed=1
    fi
  fi
  if _should_check_platform claude; then
    if check_claude_wiring; then
      _doctor_record "claude_wiring" "ok" ""
    else
      _doctor_record "claude_wiring" "missing" "harness-mem setup --platform claude"
      failed=1
    fi
    if check_claude_precedence_alignment; then
      _doctor_record "claude_precedence" "ok" ""
    else
      _doctor_record "claude_precedence" "drift" "align ~/.claude.json and ~/.claude/settings.json, then rerun harness-mem setup --platform claude"
      failed=1
    fi
  fi
  if _should_check_platform cursor; then
    if check_cursor_wiring; then
      _doctor_record "cursor_wiring" "ok" ""
    else
      _doctor_record "cursor_wiring" "missing" "harness-mem setup --platform cursor"
      failed=1
    fi
  fi
  if _should_check_platform antigravity; then
    if check_antigravity_wiring; then
      _doctor_record "antigravity_wiring" "ok" ""
    else
      _doctor_record "antigravity_wiring" "missing" "harness-mem setup --platform antigravity"
      failed=1
    fi
  fi

  if [ -z "$DOCTOR_MCP_TRANSPORT" ] && [ "$MCP_HTTP_CONFIG_DETECTED" -eq 1 ]; then
    if ! check_cmd jq || ! check_cmd curl; then
      _doctor_record "mcp_gateway" "skipped" "install jq and curl, then rerun doctor --mcp-transport http"
    elif ! _doctor_check_mcp_gateway "streamable_http"; then
      failed=1
    fi
  fi

  # Managed backend connectivity check
  local _doctor_backend_mode
  _doctor_backend_mode="$(read_backend_mode)"
  if [ "$_doctor_backend_mode" = "hybrid" ] || [ "$_doctor_backend_mode" = "managed" ]; then
    local _doctor_managed_ep
    _doctor_managed_ep="$(jq -r '.managed.endpoint // ""' "$CONFIG_PATH" 2>/dev/null || echo "")"
    if [ -z "$_doctor_managed_ep" ]; then
      _doctor_record "managed_endpoint" "missing" "Set managed.endpoint in ${CONFIG_PATH}"
      failed=1
    else
      # Check if daemon reports managed backend status
      local _doctor_shadow_json
      _doctor_shadow_json="$(curl -sf "http://127.0.0.1:${HARNESS_MEM_PORT:-37888}/v1/admin/shadow-metrics" 2>/dev/null)"
      if [ -n "$_doctor_shadow_json" ]; then
        local _doctor_conn_state
        _doctor_conn_state="$(echo "$_doctor_shadow_json" | jq -r '.items[0].connection_state // "unknown"' 2>/dev/null)"
        case "$_doctor_conn_state" in
          connected)
            _doctor_record "managed_endpoint" "ok:${_doctor_backend_mode}" ""
            ;;
          degraded)
            _doctor_record "managed_endpoint" "degraded" "Managed backend connection degraded"
            ;;
          *)
            _doctor_record "managed_endpoint" "ok:configured" "endpoint configured but daemon connection state: ${_doctor_conn_state}"
            ;;
        esac
      else
        _doctor_record "managed_endpoint" "ok:configured" "endpoint configured (daemon not reachable for status check)"
      fi
    fi
  fi

  if [ "$VERSION_CHECK_IN_SETUP" -eq 1 ] && [ "$JSON_OUTPUT" -ne 1 ]; then
    versions_impl || warn "Version snapshot failed during doctor (non-fatal)"
  fi

  local process_advisory_json="null"
  if [ "$DOCTOR_PROCESSES" -eq 1 ]; then
    process_advisory_json="$(_doctor_process_advisory_json)"
    if [ "$JSON_OUTPUT" -ne 1 ] && [ "$QUIET" -ne 1 ]; then
      _doctor_print_process_advisory "$process_advisory_json"
    fi
  fi

  # JSON出力モード
  if [ "$JSON_OUTPUT" -eq 1 ]; then
    _doctor_emit_json "$failed" "$process_advisory_json" "${doctor_checks[@]}"
    if [ "$failed" -ne 0 ] && [ "$STRICT_EXIT" -eq 1 ]; then
      return 1
    fi
    return 0
  fi

  if [ "$failed" -eq 0 ]; then
    log "Doctor result: healthy"
    return 0
  fi

  if [ "$FIX_MODE" -eq 1 ] && [ "$FIX_PLAN" -ne 1 ] && [ "$READ_ONLY_MODE" -ne 1 ]; then
    warn "Doctor found issues. Running auto-fix."
    if _should_check_platform codex && ! check_codex_skill_bundle >/dev/null 2>&1; then
      SETUP_INSTALL_CODEX_SKILL=1
    fi
    setup_impl
    return
  fi

  # 失敗時のガイド出力
  _doctor_print_next_steps "${doctor_checks[@]}"

  if [ "$FIX_MODE" -eq 1 ] && [ "$FIX_PLAN" -eq 1 ]; then
    fail "Doctor found issues. Repair plan printed; no changes applied because --fix --plan was used."
  fi

  fail "Doctor found issues. Run with --fix to auto-repair."
}

_doctor_status_result() {
  local status="${1:-}"
  case "$status" in
    ok:reachable_with_warnings|degraded|warn|warning) printf 'warn' ;;
    ok|ok:*) printf 'pass' ;;
    skipped|skip|not_installed) printf 'skip' ;;
    *) printf 'fail' ;;
  esac
}

# doctor JSON出力
_doctor_emit_json() {
  local failed="$1"
  local process_advisory_json="${2:-null}"
  if [ -z "$process_advisory_json" ]; then
    process_advisory_json="null"
  fi
  if ! printf '%s' "$process_advisory_json" | jq -e . >/dev/null 2>&1; then
    process_advisory_json="null"
  fi
  shift
  shift
  local -a checks=("$@")
  local status_str overall_status_str all_green_str
  local checked_count="${#checks[@]}"
  local failed_count=0 warn_count=0 skip_count=0
  if [ "$failed" -eq 0 ]; then
    status_str="healthy"
    overall_status_str="healthy"
    all_green_str="true"
  else
    status_str="unhealthy"
    overall_status_str="broken"
    all_green_str="false"
  fi

  local checks_json="["
  local first=1
  local entry name status fix result severity reason_code
  for entry in "${checks[@]}"; do
    IFS=$'\t' read -r name status fix <<<"$entry"
    result="$(_doctor_status_result "$status")"
    if [ "$result" = "fail" ]; then
      failed_count=$((failed_count + 1))
    fi
    case "$result" in
      warn) warn_count=$((warn_count + 1)) ;;
      skip) skip_count=$((skip_count + 1)) ;;
    esac
    if [ "$result" = "warn" ] && [ "$overall_status_str" = "healthy" ]; then
      overall_status_str="degraded"
      all_green_str="false"
    fi
    case "$result" in
      pass) severity="info" ;;
      warn) severity="warning" ;;
      skip) severity="info" ;;
      *) severity="error" ;;
    esac
    reason_code="${name}_${status%%:*}"
    if [ "$first" -eq 0 ]; then
      checks_json+=","
    fi
    checks_json+="$(jq -nc \
      --arg name "$name" \
      --arg status "$status" \
      --arg result "$result" \
      --arg severity "$severity" \
      --arg reason_code "$reason_code" \
      --arg fix "$fix" \
      '{
        name: $name,
        status: $status,
        result: $result,
        severity: $severity,
        reason_code: $reason_code,
        fix: (if $fix == "" then null else $fix end),
        repair: (if $fix == "" then null else {command: $fix, risk: "low"} end)
      }')"
    first=0
  done
  checks_json+="]"

  local ts
  ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

  local current_backend harness_mem_version
  current_backend="$(read_backend_mode)"
  harness_mem_version="$(read_current_harness_mem_version 2>/dev/null || true)"

    jq -n \
      --arg schema_version "doctor.v2" \
      --arg contract_version "$COMPANION_CONTRACT_VERSION" \
      --arg harness_mem_version "$harness_mem_version" \
      --arg status "$status_str" \
      --arg overall_status "$overall_status_str" \
      --argjson all_green "$all_green_str" \
      --argjson failed_count "$failed_count" \
      --argjson warn_count "$warn_count" \
      --argjson skip_count "$skip_count" \
      --argjson checked_count "$checked_count" \
      --arg timestamp "$ts" \
      --arg backend_mode "$current_backend" \
      --argjson checks "$checks_json" \
      --argjson process_advisory "$process_advisory_json" \
      '({
        schema_version: $schema_version,
        contract_version: $contract_version,
        harness_mem_version: ($harness_mem_version | select(. != "") // "unknown"),
        status: $status,
        overall_status: $overall_status,
        all_green: $all_green,
        backend_mode: $backend_mode,
        failed_count: $failed_count,
        warn_count: $warn_count,
        skip_count: $skip_count,
        checked_count: $checked_count,
        timestamp: $timestamp,
        checks: $checks,
        fix_command: "harness-mem doctor --fix",
        repair_plan: {
          command: "harness-mem doctor --fix",
          plan_command: "harness-mem doctor --fix --plan",
          read_only_command: "harness-mem doctor --read-only --json",
          strict_ci_command: "harness-mem doctor --json --strict-exit"
        }
      } + (if $process_advisory == null then {} else {process_advisory: $process_advisory} end))'
  }

# doctor失敗時のガイド表示
_doctor_print_next_steps() {
  local -a checks=("$@")
  local entry name status fix
  local has_missing=0
  for entry in "${checks[@]}"; do
    IFS=$'\t' read -r name status fix <<<"$entry"
    if [ "$status" != "ok" ]; then
      has_missing=1
      break
    fi
  done
  [ "$has_missing" -eq 0 ] && return

  if ui_is_en; then
    echo ""
    echo "[harness-mem] Next steps to reach doctor all-green:"
  else
    echo ""
    echo "[harness-mem] doctor 全green到達のための次のアクション:"
  fi

  for entry in "${checks[@]}"; do
    IFS=$'\t' read -r name status fix <<<"$entry"
    [ "$status" = "ok" ] && continue
    if ui_is_en; then
      printf "  [FAIL] %s => %s\n" "$name" "$fix"
    else
      printf "  [FAIL] %s => %s\n" "$name" "$fix"
    fi
  done

  echo ""
  if ui_is_en; then
    echo "  Quick fix: harness-mem doctor --fix"
  else
    echo "  まとめて修復: harness-mem doctor --fix"
  fi
  echo ""
}

setup_has_http_tier1_config() {
  if is_platform_enabled codex && [ -f "${HOME}/.codex/config.toml" ]; then
    if awk '/^\[mcp_servers\.harness\][[:space:]]*(#.*)?$/{found=1;next} /^\[/{found=0} found' "${HOME}/.codex/config.toml" \
      | grep -qE '^[[:space:]]*url[[:space:]]*='; then
      return 0
    fi
  fi

  if is_platform_enabled claude; then
    local cfg
    while IFS= read -r cfg; do
      [ -n "$cfg" ] || continue
      [ -f "$cfg" ] || continue
      if jq -e '((.mcpServers.harness.url // "") != "") or ((.mcpServers.harness.type // "") == "http")' "$cfg" >/dev/null 2>&1; then
        return 0
      fi
    done < <(get_claude_config_targets)
    if [ -f "${HOME}/.claude/settings.json" ] \
      && jq -e '((.plugins.harness.mcpServers.harness.url // "") != "") or ((.plugins.harness.mcpServers.harness.type // "") == "http")' "${HOME}/.claude/settings.json" >/dev/null 2>&1; then
      return 0
    fi
  fi

  return 1
}

setup_impl() {
  # 各ステップの成否を追跡する配列
  # 形式: "step_name\tstatus\tfix_command"
  local -a SETUP_RESULTS=()

  _setup_record() {
    local step="$1" status="$2" fix="$3"
    SETUP_RESULTS+=("$(printf '%s\t%s\t%s' "$step" "$status" "$fix")")
  }

  # Backend mode 設定
  ensure_config
  if [ -n "$BACKEND_MODE" ]; then
    write_backend_mode "$BACKEND_MODE"
    log "Backend mode set to: ${BACKEND_MODE}"
    _setup_record "backend_mode" "ok:${BACKEND_MODE}" ""
  else
    local current_backend
    current_backend="$(read_backend_mode)"
    log "Backend mode: ${current_backend}"
    _setup_record "backend_mode" "ok:${current_backend}" ""
  fi

  local auto_update_enabled
  if [ "$SETUP_AUTO_UPDATE_OPT_IN" -eq 1 ]; then
    write_auto_update_enabled 1
    auto_update_enabled=1
  elif [ "$SETUP_AUTO_UPDATE_OPT_IN" -eq 0 ]; then
    write_auto_update_enabled 0
    auto_update_enabled=0
  else
    auto_update_enabled="$(read_auto_update_enabled)"
  fi

  if [ "$auto_update_enabled" -eq 1 ]; then
    log "Auto-update opt-in: enabled"
    _setup_record "auto_update_opt_in" "ok:enabled" ""
  else
    log "Auto-update opt-in: disabled"
    _setup_record "auto_update_opt_in" "ok:disabled" ""
  fi

  remember_auto_update_repair_platforms "$PLATFORM"

  # 依存チェック
  if ensure_dependencies 2>/dev/null; then
    _setup_record "dependencies" "ok" ""
  else
    ensure_dependencies  # エラーメッセージを出す
    _setup_record "dependencies" "failed" "brew install bun curl jq node ripgrep"
    _setup_print_repair_suggestions "${SETUP_RESULTS[@]}"
    return 1
  fi

  if ensure_mcp_runtime 2>/dev/null; then
    _setup_record "mcp_runtime" "ok" ""
  else
    ensure_mcp_runtime
    _setup_record "mcp_runtime" "failed" "cd ${HARNESS_ROOT}/mcp-server && npm install --include=dev && npm run build"
    _setup_print_repair_suggestions "${SETUP_RESULTS[@]}"
    return 1
  fi

  if _mcp_config_transport_is_http && { is_platform_enabled codex || is_platform_enabled claude; }; then
    if _ensure_mcp_gateway_token >/dev/null 2>&1; then
      _setup_record "mcp_gateway_token" "ok" ""
    else
      _setup_record "mcp_gateway_token" "failed" "harness-mem mcp-gateway start"
      _setup_print_repair_suggestions "${SETUP_RESULTS[@]}"
      return 1
    fi
  fi

  if is_platform_enabled codex; then
    if setup_codex_wiring 2>/dev/null; then
      _setup_record "codex_wiring" "ok" ""
    else
      setup_codex_wiring
      _setup_record "codex_wiring" "failed" "harness-mem setup --platform codex"
    fi
  fi
  if is_platform_enabled opencode; then
    if setup_opencode_wiring 2>/dev/null; then
      _setup_record "opencode_wiring" "ok" ""
    else
      setup_opencode_wiring
      _setup_record "opencode_wiring" "failed" "harness-mem setup --platform opencode"
    fi
  fi
  if is_platform_enabled claude; then
    if setup_claude_wiring 2>/dev/null; then
      _setup_record "claude_wiring" "ok" ""
      check_claude_wiring || true
    else
      setup_claude_wiring
      _setup_record "claude_wiring" "failed" "harness-mem setup --platform claude"
    fi
  fi
  if is_platform_enabled cursor; then
    if setup_cursor_wiring 2>/dev/null; then
      _setup_record "cursor_wiring" "ok" ""
    else
      setup_cursor_wiring
      _setup_record "cursor_wiring" "failed" "harness-mem setup --platform cursor"
    fi
  fi
  if is_platform_enabled antigravity; then
    if setup_antigravity_wiring 2>/dev/null; then
      _setup_record "antigravity_wiring" "ok" ""
    else
      setup_antigravity_wiring
      _setup_record "antigravity_wiring" "failed" "harness-mem setup --platform antigravity"
    fi
  fi
  if [ "$SKIP_START" -eq 0 ]; then
    if HARNESS_MEM_ENABLE_UI="${HARNESS_MEM_ENABLE_UI:-true}" start_daemon 2>/dev/null; then
      _setup_record "daemon_start" "ok" ""
      log "Daemon started: http://${MEM_HOST}:${MEM_PORT}"
      local setup_ui_port setup_ui_health
      setup_ui_port="${HARNESS_MEM_UI_PORT:-37901}"
      setup_ui_health="http://${MEM_HOST}:${setup_ui_port}/api/health"
      if curl --silent --show-error --fail --max-time 1 "$setup_ui_health" >/dev/null 2>&1; then
        log "Mem UI started: http://${MEM_HOST}:${setup_ui_port}"
      else
        warn "Mem UI endpoint is not reachable: http://${MEM_HOST}:${setup_ui_port}"
      fi
    else
      HARNESS_MEM_ENABLE_UI="${HARNESS_MEM_ENABLE_UI:-true}" start_daemon || true
      _setup_record "daemon_start" "failed" "harness-memd start"
    fi
  fi

  if [ "$SKIP_START" -eq 0 ] && setup_has_http_tier1_config; then
    if mcp_gateway_start_impl >/dev/null 2>&1; then
      _setup_record "mcp_gateway_start" "ok" ""
      log "MCP gateway started: $(_mcp_gateway_endpoint_url)"
    else
      mcp_gateway_start_impl || true
      _setup_record "mcp_gateway_start" "failed" "harness-mem mcp-gateway start"
    fi
  fi

  if [ "$SKIP_SMOKE" -eq 0 ]; then
    if run_smoke 2>/dev/null; then
      _setup_record "smoke_test" "ok" ""
    else
      run_smoke || true
      _setup_record "smoke_test" "failed" "harness-mem smoke"
    fi
  fi

  if [ "$SKIP_QUALITY" -eq 0 ]; then
    if run_quality_tests 2>/dev/null; then
      _setup_record "quality_test" "ok" ""
    else
      run_quality_tests || true
      if is_windows_shell; then
        warn "Search quality test did not pass within the local Windows shell budget; setup will continue. Re-run later with: harness-mem smoke"
        _setup_record "quality_test" "ok:windows_warning" ""
      else
        _setup_record "quality_test" "failed" "harness-mem smoke"
      fi
    fi
  fi

  run_setup_claude_mem_import_if_requested

  if [ "$VERSION_CHECK_IN_SETUP" -eq 1 ]; then
    versions_impl || warn "Version snapshot failed during setup (non-fatal)"
  fi

  # Doctor JSON 診断を保存（セットアップ時チェック標準化）
  local doctor_artifact="${STATE_DIR}/runtime/doctor-last.json"
  mkdir -p "$(dirname "$doctor_artifact")"
  local -a doctor_post_args=(doctor --json --skip-version-check --platform "$PLATFORM")
  if setup_has_http_tier1_config; then
    doctor_post_args+=(--mcp-transport http)
  fi
  if [ "$SKIP_START" -ne 0 ]; then
    doctor_post_args+=(--read-only)
  fi
  if HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
     HARNESS_MEM_HOST="$MEM_HOST" \
     HARNESS_MEM_PORT="$MEM_PORT" \
     HARNESS_MEM_DB_PATH="$DB_PATH" \
     "$HARNESS_ROOT/scripts/harness-mem" "${doctor_post_args[@]}" > "$doctor_artifact" 2>/dev/null; then
    if jq -e '.all_green == true' "$doctor_artifact" >/dev/null 2>&1; then
      _setup_record "doctor_post_check" "ok" ""
      log "Doctor artifact saved: $doctor_artifact"
    else
      _setup_record "doctor_post_check" "failed" "harness-mem doctor --fix"
      local failed_checks
      failed_checks="$(jq -r '.checks[]? | select(.status != "ok" and (.status | startswith("ok:") | not)) | .name' "$doctor_artifact" 2>/dev/null | paste -sd ',' -)"
      if [ -n "$failed_checks" ]; then
        log "Doctor post-check: not all green (checks: ${failed_checks}, artifact: $doctor_artifact)"
      else
        log "Doctor post-check: not all green (artifact: $doctor_artifact)"
      fi
    fi
  else
    _setup_record "doctor_post_check" "failed" "harness-mem doctor --fix"
    warn "Doctor post-check failed (artifact: $doctor_artifact)"
  fi

  # 失敗ステップがあれば修復提案を出す
  local has_failure=0
  local entry step status fix
  for entry in "${SETUP_RESULTS[@]}"; do
    IFS=$'\t' read -r step status fix <<<"$entry"
    if [ "$status" = "failed" ]; then
      has_failure=1
      break
    fi
  done

  if [ "$has_failure" -eq 1 ]; then
    _setup_print_repair_suggestions "${SETUP_RESULTS[@]}"
  else
    log "Setup complete (project=${TARGET_DIR}, platform=${PLATFORM})"
    print_post_setup_next_steps
  fi
}

# setup失敗時の修復提案を表示する
_setup_print_repair_suggestions() {
  local -a results=("$@")
  local entry step status fix
  local has_failure=0
  for entry in "${results[@]}"; do
    IFS=$'\t' read -r step status fix <<<"$entry"
    if [ "$status" = "failed" ]; then
      has_failure=1
      break
    fi
  done
  [ "$has_failure" -eq 0 ] && return

  echo ""
  if ui_is_en; then
    echo "[harness-mem] Setup had failures. Suggested repair steps:"
  else
    echo "[harness-mem] セットアップで失敗がありました。修復手順:"
  fi

  for entry in "${results[@]}"; do
    IFS=$'\t' read -r step status fix <<<"$entry"
    if [ "$status" = "failed" ]; then
      printf "  [FAIL] %-20s => %s\n" "$step" "$fix"
    fi
  done

  echo ""
  if ui_is_en; then
    echo "  Auto-repair: harness-mem doctor --fix --platform ${PLATFORM}"
    echo "  Re-run:      harness-mem setup --platform ${PLATFORM} --skip-smoke --skip-quality"
  else
    echo "  自動修復:    harness-mem doctor --fix --platform ${PLATFORM}"
    echo "  再実行:      harness-mem setup --platform ${PLATFORM} --skip-smoke --skip-quality"
  fi
  echo ""
}

uninstall_codex_wiring() {
  local cfg="${HOME}/.codex/config.toml"
  local tmp
  if [ ! -f "$cfg" ]; then
    return
  fi

  remove_marked_block "$cfg" "$BEGIN_CODEX_NOTIFY" "$END_CODEX_NOTIFY"
  remove_marked_block "$cfg" "$BEGIN_CODEX_MCP" "$END_CODEX_MCP"
  tmp="$(mktemp)"
  awk '
    /^\[mcp_servers\.harness(\.env)?\]/ { skip = 1; next }
    skip == 1 {
      if ($0 ~ /^\[/) {
        skip = 0
        print $0
      }
      next
    }
    /memory-codex-notify\.sh/ { next }
    { print $0 }
  ' "$cfg" >"$tmp"
  mv "$tmp" "$cfg"
  log "Removed Codex marker-managed wiring: $cfg"
}

remove_opencode_wiring_json() {
  local file="$1"
  local tmp
  if [ ! -f "$file" ]; then
    return
  fi
  tmp="$(mktemp)"
  if jq 'del(.plugins["harness-memory"]) | del(.mcp.harness)' "$file" >"$tmp" 2>/dev/null; then
    mv "$tmp" "$file"
    log "Removed OpenCode memory wiring from: $file"
  else
    rm -f "$tmp"
    warn "Could not parse JSON for cleanup: $file"
  fi
}

remove_claude_wiring_json() {
  local file="$1"
  local tmp
  if [ ! -f "$file" ]; then
    return
  fi
  tmp="$(mktemp)"
  if jq 'if has("mcpServers") then .mcpServers |= del(.harness) else . end' "$file" >"$tmp" 2>/dev/null; then
    mv "$tmp" "$file"
    log "Removed Claude harness MCP wiring from: $file"
  else
    rm -f "$tmp"
    warn "Could not parse JSON for cleanup: $file"
  fi
}

uninstall_claude_wiring() {
  remove_claude_wiring_json "${HOME}/.claude.json"
  remove_claude_wiring_json "${HOME}/.claude/settings.json"
  # Also remove inline plugin format (source: 'settings', CC v2.1.80+)
  local settings_file="${HOME}/.claude/settings.json"
  if [ -f "$settings_file" ] && jq -e '.plugins.harness != null' "$settings_file" >/dev/null 2>&1; then
    local tmp
    tmp="$(mktemp)"
    if jq 'if has("plugins") then .plugins |= del(.harness) else . end' "$settings_file" >"$tmp" 2>/dev/null; then
      mv "$tmp" "$settings_file"
      log "Removed Claude inline plugin wiring from: $settings_file"
    else
      rm -f "$tmp"
    fi
  fi
}

uninstall_opencode_wiring() {
  local cfg="${HOME}/.config/opencode/opencode.json"
  local plugin="${HOME}/.config/opencode/plugins/harness-memory/index.ts"

  remove_opencode_wiring_json "$cfg"
  rm -f "$plugin"
  log "Removed OpenCode global memory plugin file: $plugin"
}

uninstall_cursor_wiring() {
  local hooks_json="${HOME}/.cursor/hooks.json"
  local mcp_json="${HOME}/.cursor/mcp.json"
  local tmp
  if [ -f "$hooks_json" ]; then
    tmp="$(mktemp)"
    if jq '
      def strip: map(select(((.command // "") | contains("memory-cursor-event.sh")) | not));
      .hooks = (.hooks // {})
      | .hooks.sessionStart = ((.hooks.sessionStart // []) | strip)
      | .hooks.beforeSubmitPrompt = ((.hooks.beforeSubmitPrompt // []) | strip)
      | .hooks.afterAgentResponse = ((.hooks.afterAgentResponse // []) | strip)
      | .hooks.afterMCPExecution = ((.hooks.afterMCPExecution // []) | strip)
      | .hooks.afterShellExecution = ((.hooks.afterShellExecution // []) | strip)
      | .hooks.afterFileEdit = ((.hooks.afterFileEdit // []) | strip)
      | .hooks.sessionEnd = ((.hooks.sessionEnd // []) | strip)
      | .hooks.stop = ((.hooks.stop // []) | strip)
    ' "$hooks_json" >"$tmp" 2>/dev/null; then
      mv "$tmp" "$hooks_json"
      log "Removed Cursor memory hooks from: $hooks_json"
    else
      rm -f "$tmp"
      warn "Could not parse Cursor hooks JSON for cleanup: $hooks_json"
    fi
  fi

  rm -f "${HOME}/.cursor/hooks/memory-cursor-event.sh"
  log "Removed Cursor memory hook script"

  if [ -f "$mcp_json" ]; then
    tmp="$(mktemp)"
    if jq \
      --arg server_id "$CURSOR_MCP_SERVER_ID" \
      --arg legacy_server_id "$CURSOR_LEGACY_MCP_SERVER_ID" \
      'if has("mcpServers") then .mcpServers |= (del(.[$server_id]) | del(.[$legacy_server_id])) else . end' \
      "$mcp_json" >"$tmp" 2>/dev/null; then
      mv "$tmp" "$mcp_json"
      log "Removed Cursor harness MCP wiring from: $mcp_json"
    else
      rm -f "$tmp"
      warn "Could not parse Cursor MCP JSON for cleanup: $mcp_json"
    fi
  fi
}

uninstall_antigravity_wiring() {
  log "Antigravity wiring cleanup: no project files to remove"
}

uninstall_impl() {
  if check_cmd bun; then
    HARNESS_MEM_CODEX_PROJECT_ROOT="$TARGET_DIR" \
      HARNESS_MEM_HOST="$MEM_HOST" \
      HARNESS_MEM_PORT="$MEM_PORT" \
      HARNESS_MEM_DB_PATH="$DB_PATH" \
      "$HARNESS_ROOT/scripts/harness-memd" stop --quiet >/dev/null 2>&1 || true
  fi

  if is_platform_enabled codex; then
    uninstall_codex_wiring
  fi
  if is_platform_enabled opencode; then
    uninstall_opencode_wiring
  fi
  if is_platform_enabled claude; then
    uninstall_claude_wiring
  fi
  if is_platform_enabled cursor; then
    uninstall_cursor_wiring
  fi
  if is_platform_enabled antigravity; then
    uninstall_antigravity_wiring
  fi
  forget_auto_update_repair_platforms "$PLATFORM"

  rm -f "${STATE_DIR}/daemon.pid" "${STATE_DIR}/daemon.lock" "${STATE_DIR}/daemon.heartbeat"
  rm -rf "${STATE_DIR}/runtime"
  log "Removed runtime cache: ${STATE_DIR}/runtime"
  if [ "$PURGE_DB" -eq 1 ]; then
    rm -f "$DB_PATH"
    log "Purged DB: $DB_PATH"
  fi

  log "Uninstall complete (project=${TARGET_DIR}, platform=${PLATFORM})"
}

_check_shadow_metrics_gate() {
  # Query daemon shadow metrics and check promotion SLA.
  # Returns 0 if SLA met, 1 if not. Outputs reasons to stderr.
  local port="${HARNESS_MEM_PORT:-37888}"
  local token="${HARNESS_MEM_ADMIN_TOKEN:-}"
  local curl_opts=(-sf --max-time 5)
  if [ -n "$token" ]; then
    curl_opts+=(-H "x-harness-mem-token: ${token}")
  fi
  local shadow_json
  shadow_json="$(curl "${curl_opts[@]}" "http://127.0.0.1:${port}/v1/admin/shadow-metrics" 2>/dev/null)" || {
    warn "Cannot reach daemon at port ${port}. Is it running?"
    return 1
  }

  local managed_backend
  managed_backend="$(echo "$shadow_json" | jq -r '.items[0].managed_backend // empty' 2>/dev/null)"
  if [ -z "$managed_backend" ] || [ "$managed_backend" = "null" ]; then
    warn "Managed backend not initialized. Start daemon in hybrid mode first."
    return 1
  fi

  local shadow_reads shadow_match_rate managed_replications replication_failures
  shadow_reads="$(echo "$shadow_json" | jq -r '.items[0].shadow_metrics.shadow_reads // 0')"
  shadow_match_rate="$(echo "$shadow_json" | jq -r '.items[0].shadow_metrics.shadow_match_rate // 0')"
  managed_replications="$(echo "$shadow_json" | jq -r '.items[0].shadow_metrics.managed_replications // 0')"
  replication_failures="$(echo "$shadow_json" | jq -r '.items[0].shadow_metrics.replication_failures // 0')"

  local gate_pass=true
  local reasons=""

  # Gate 1: minimum shadow reads >= 100
  if [ "$shadow_reads" -lt 100 ] 2>/dev/null; then
    gate_pass=false
    reasons="${reasons}  - Insufficient shadow reads: ${shadow_reads}/100\n"
  fi

  # Gate 2: shadow match rate >= 95%
  local match_pct
  match_pct="$(echo "$shadow_match_rate * 100" | bc -l 2>/dev/null || echo "0")"
  local match_check
  match_check="$(echo "$shadow_match_rate < 0.95" | bc -l 2>/dev/null || echo "1")"
  if [ "$match_check" = "1" ]; then
    gate_pass=false
    reasons="${reasons}  - Shadow match rate too low: ${match_pct}% (need >=95%)\n"
  fi

  # Gate 3: replication failure rate < 1%
  local total_rep failure_rate failure_check
  total_rep="$(( managed_replications + replication_failures ))"
  if [ "$total_rep" -gt 0 ] 2>/dev/null; then
    failure_rate="$(echo "scale=4; $replication_failures / $total_rep" | bc -l 2>/dev/null || echo "0")"
    failure_check="$(echo "$failure_rate > 0.01" | bc -l 2>/dev/null || echo "0")"
    if [ "$failure_check" = "1" ]; then
      gate_pass=false
      local failure_pct
      failure_pct="$(echo "$failure_rate * 100" | bc -l 2>/dev/null || echo "0")"
      reasons="${reasons}  - Replication failure rate too high: ${failure_pct}% (need <1%)\n"
    fi
  fi

  if [ "$gate_pass" = "false" ]; then
    warn "Shadow metrics gate FAILED. Promotion denied."
    warn "Current metrics:"
    warn "  shadow_reads:        ${shadow_reads}"
    warn "  shadow_match_rate:   ${match_pct}%"
    warn "  replications:        ${managed_replications}"
    warn "  replication_failures: ${replication_failures}"
    warn ""
    warn "Reasons:"
    printf "$reasons" >&2
    return 1
  fi

  log "Shadow metrics gate PASSED."
  log "  shadow_reads:        ${shadow_reads}"
  log "  shadow_match_rate:   ${match_pct}%"
  log "  replications:        ${managed_replications}"
  log "  replication_failures: ${replication_failures}"
  return 0
}

promote_impl() {
  ensure_config
  local current_backend
  current_backend="$(read_backend_mode)"

  case "$current_backend" in
    local)
      log "Current backend mode: local"
      log "Promoting to hybrid (shadow phase)..."
      write_backend_mode "hybrid"
      log "Backend mode set to: hybrid"
      log ""
      log "Next steps:"
      log "  1. Configure managed endpoint: set managed.endpoint in ${CONFIG_PATH}"
      log "  2. Restart daemon: scripts/harness-memd restart"
      log "  3. Run proof-pack to verify: scripts/harness-mem-proof-pack.sh"
      log "  4. Once shadow metrics pass SLA, run: harness-mem promote (again)"
      ;;
    hybrid)
      log "Current backend mode: hybrid"
      local managed_ep
      managed_ep="$(read_managed_endpoint)"
      if [ -z "$managed_ep" ]; then
        warn "Cannot promote to managed: managed endpoint not configured"
        warn "Set managed.endpoint in ${CONFIG_PATH}"
        return 1
      fi

      # Shadow metrics gate: must pass SLA before promotion
      log "Checking shadow metrics gate..."
      if ! _check_shadow_metrics_gate; then
        warn ""
        warn "Promotion blocked: shadow metrics SLA not met."
        warn "Continue running in hybrid mode to accumulate metrics."
        warn "Required: shadow_reads >= 100, match_rate >= 95%, repl_failure < 1%"
        return 1
      fi

      log "Promoting to managed..."
      write_backend_mode "managed"
      log "Backend mode set to: managed"
      log ""
      log "Next steps:"
      log "  1. Restart daemon: scripts/harness-memd restart"
      log "  2. Run proof-pack to verify: scripts/harness-mem-proof-pack.sh"
      log "  3. If issues arise, run: harness-mem rollback"
      ;;
    managed)
      log "Already at managed backend mode. No further promotion available."
      ;;
  esac
}

rollback_impl() {
  ensure_config
  local current_backend
  current_backend="$(read_backend_mode)"

  case "$current_backend" in
    local)
      log "Already at local backend mode. Nothing to rollback."
      ;;
    hybrid|managed)
      log "Current backend mode: ${current_backend}"
      log "Rolling back to local..."
      write_backend_mode "local"
      log "Backend mode set to: local"
      log ""
      log "Next steps:"
      log "  1. Restart daemon: scripts/harness-memd restart"
      log "  2. Run doctor to verify: harness-mem doctor"
      ;;
  esac
}

update_impl() {
  ensure_config
  require_cmd npm

  local auto_update_enabled
  if [ "$UPDATE_AUTO_UPDATE_OPT_IN" -eq 1 ]; then
    write_auto_update_enabled 1
    auto_update_enabled=1
  elif [ "$UPDATE_AUTO_UPDATE_OPT_IN" -eq 0 ]; then
    write_auto_update_enabled 0
    auto_update_enabled=0
  else
    auto_update_enabled="$(read_auto_update_enabled)"
  fi

  if [ "$auto_update_enabled" -eq 1 ]; then
    log "Auto-update opt-in: enabled"
  else
    log "Auto-update opt-in: disabled"
  fi

  log "Updating harness-mem: npm install -g ${AUTO_UPDATE_PACKAGE}@${AUTO_UPDATE_CHANNEL}"
  npm install -g "${AUTO_UPDATE_PACKAGE}@${AUTO_UPDATE_CHANNEL}"
  if [ "$SETUP_INSTALL_CODEX_SKILL" -eq 1 ]; then
    install_codex_skill
  fi
  run_post_update_repair "manual-update"
  log "Update complete. Restart daemon/UI if running: harness-memd restart"
}

# ---------------------------------------------------------------------------
# model subcommand: list / pull / use / status
# ---------------------------------------------------------------------------
MODEL_CATALOG_IDS=(
  "ruri-v3-30m"
  "gte-small"
  "e5-small-v2"
  "bge-small"
  "multilingual-e5"
  "nomic-embed"
)
MODEL_CATALOG_DISPLAY_NAMES=(
  "Ruri V3 30M (Japanese)"
  "GTE Small (English)"
  "E5 Small v2 (English)"
  "BGE Small v1.5 (English/Chinese)"
  "Multilingual E5 Small (100+ languages)"
  "Nomic Embed Text v1 (English)"
)
MODEL_CATALOG_SIZES=(
  "~153MB"
  "~67MB"
  "~67MB"
  "~67MB"
  "~117MB"
  "~274MB"
)
MODEL_CATALOG_LANGS=(
  "ja"
  "en"
  "en"
  "en"
  "multilingual"
  "en"
)
MODEL_CATALOG_DIMS=(
  "256"
  "384"
  "384"
  "384"
  "384"
  "768"
)

model_dir_for_id() {
  local model_id="$1"
  echo "${STATE_DIR}/models/${model_id}"
}

model_is_installed() {
  local model_id="$1"
  local dir
  dir="$(model_dir_for_id "$model_id")"
  [ -f "${dir}/tokenizer.json" ] && [ -f "${dir}/onnx/model.onnx" ]
}

sync_launchagent_embedding_env() {
  local provider="$1"
  local model="$2"
  local plist="${HOME}/Library/LaunchAgents/com.harness-mem.daemon.plist"

  [ -f "$plist" ] || return 0
  if ! command -v plutil >/dev/null 2>&1; then
    warn "LaunchAgent plist exists but plutil is unavailable; skipped env sync: ${plist}"
    return 0
  fi

  local ok=1
  for key in HARNESS_MEM_EMBEDDING_PROVIDER HARNESS_MEM_EMBEDDING_MODEL; do
    local value="$provider"
    if [ "$key" = "HARNESS_MEM_EMBEDDING_MODEL" ]; then
      value="$model"
    fi

    if plutil -replace "EnvironmentVariables.${key}" -string "$value" "$plist" >/dev/null 2>&1; then
      continue
    fi

    plutil -insert EnvironmentVariables -xml '<dict/>' "$plist" >/dev/null 2>&1 || true
    if plutil -insert "EnvironmentVariables.${key}" -string "$value" "$plist" >/dev/null 2>&1; then
      continue
    fi

    ok=0
  done

  if [ "$ok" -eq 1 ]; then
    log "LaunchAgent embedding env synced: ${plist}"
  else
    warn "Could not sync LaunchAgent embedding env; config was updated: ${plist}"
  fi
}

model_impl() {
  local subcmd="${1:-list}"
  shift || true

  case "$subcmd" in
    list)
      echo "Available models:"
      echo ""
      for i in "${!MODEL_CATALOG_IDS[@]}"; do
        local id="${MODEL_CATALOG_IDS[$i]}"
        local display="${MODEL_CATALOG_DISPLAY_NAMES[$i]}"
        local size="${MODEL_CATALOG_SIZES[$i]}"
        local lang="${MODEL_CATALOG_LANGS[$i]}"
        local dim="${MODEL_CATALOG_DIMS[$i]}"
        local status="not installed"
        if model_is_installed "$id"; then
          status="installed"
        fi
        printf "  %-20s  %-30s  %-6s  %-6s  dim=%-4s  [%s]\n" \
          "$id" "$display" "$size" "$lang" "$dim" "$status"
      done
      echo ""
      echo "To download a model: harness-mem model pull <id>"
      echo "To activate:         harness-mem model use <id>"
      echo "To enable adaptive:  harness-mem model use-adaptive"
      ;;

    pull)
      local model_id="${1:-}"
      [ -n "$model_id" ] || fail "Usage: harness-mem model pull <id>"
      shift || true

      local auto_confirm=0
      while [ "$#" -gt 0 ]; do
        case "$1" in
          --yes|-y)
            auto_confirm=1
            ;;
          *)
            fail "Unknown option for model pull: $1"
            ;;
        esac
        shift || true
      done

      # Validate id is in catalog
      local found=0
      local display="" size="" dim=""
      for i in "${!MODEL_CATALOG_IDS[@]}"; do
        if [ "${MODEL_CATALOG_IDS[$i]}" = "$model_id" ]; then
          found=1
          display="${MODEL_CATALOG_DISPLAY_NAMES[$i]}"
          size="${MODEL_CATALOG_SIZES[$i]}"
          dim="${MODEL_CATALOG_DIMS[$i]}"
          break
        fi
      done
      [ "$found" -eq 1 ] || fail "Unknown model id: \"${model_id}\". Run 'harness-mem model list' for available models."

      local model_dir
      model_dir="$(model_dir_for_id "$model_id")"

      if model_is_installed "$model_id"; then
        log "Model \"${model_id}\" is already installed at ${model_dir}"
        return 0
      fi

      echo ""
      echo "  Model : ${display}"
      echo "  Size  : ${size}"
      echo "  Dim   : ${dim}"
      echo "  Dest  : ${model_dir}"
      echo ""
      if [ "$auto_confirm" -eq 1 ]; then
        log "Auto-confirm enabled for model download (${model_id})"
      else
        printf "Download this model? [y/N] "
        local answer=""
        read -r answer </dev/tty || true
        case "$answer" in
          y|Y|yes|Yes|YES) ;;
          *) echo "Aborted."; return 1 ;;
        esac
      fi

      log "Pulling model ${model_id}..."
      bun --eval "
        import { ModelManager } from '${HARNESS_ROOT}/memory-server/src/embedding/model-manager.ts';
        const mgr = new ModelManager('${STATE_DIR}/models');
        mgr.pullModel('${model_id}').then(p => {
          console.error('[harness-mem] Model downloaded to: ' + p);
        }).catch(e => {
          console.error('[harness-mem][error] ' + e.message);
          process.exit(1);
        });
      " 2>&1
      log "Done. Activate with: harness-mem model use ${model_id}"
      ;;

    use)
      local model_id="${1:-}"
      [ -n "$model_id" ] || fail "Usage: harness-mem model use <id>"

      if ! model_is_installed "$model_id"; then
        warn "Model \"${model_id}\" is not installed. Run: harness-mem model pull ${model_id}"
        return 1
      fi

      write_embedding_provider "local"
      write_embedding_model "$model_id"
      log "Active embedding config set to local:${model_id}"
      echo ""
      echo "  Config updated: ${CONFIG_PATH}"
      echo "  embedding_provider=local"
      echo "  embedding_model=${model_id}"
      echo "  Restart daemon to apply: scripts/harness-memd restart"
      ;;

    use-adaptive)
      local japanese_model="ruri-v3-30m"
      local general_model="multilingual-e5"
      local missing_models=()

      if ! model_is_installed "$japanese_model"; then
        missing_models+=("$japanese_model")
      fi
      if ! model_is_installed "$general_model"; then
        missing_models+=("$general_model")
      fi
      if [ "${#missing_models[@]}" -gt 0 ]; then
        warn "Adaptive embedding requires installed models: ${japanese_model}, ${general_model}"
        warn "Missing: ${missing_models[*]}"
        warn "Install missing models with: harness-mem model pull <id> --yes"
        return 1
      fi

      write_embedding_provider "adaptive"
      write_embedding_model "adaptive"
      sync_launchagent_embedding_env "adaptive" "adaptive"
      log "Active embedding config set to adaptive (${japanese_model} + ${general_model})"
      echo ""
      echo "  Config updated: ${CONFIG_PATH}"
      echo "  embedding_provider=adaptive"
      echo "  embedding_model=adaptive"
      echo "  Japanese route=${japanese_model}"
      echo "  General route=${general_model}"
      echo "  Restart daemon to apply: scripts/harness-memd restart"
      ;;

    status)
      local model_id="${1:-}"
      if [ -n "$model_id" ]; then
        if model_is_installed "$model_id"; then
          local dir
          dir="$(model_dir_for_id "$model_id")"
          local size_kb
          size_kb="$(du -sk "$dir" 2>/dev/null | awk '{print $1}' || echo "?")"
          echo "Model: ${model_id}"
          echo "  Status : installed"
          echo "  Path   : ${dir}"
          echo "  Size   : ${size_kb} KB on disk"
        else
          echo "Model: ${model_id}"
          echo "  Status : not installed"
        fi
      else
        echo "Installed models:"
        local any_found=0
        for id in "${MODEL_CATALOG_IDS[@]}"; do
          if model_is_installed "$id"; then
            any_found=1
            local dir
            dir="$(model_dir_for_id "$id")"
            local size_kb
            size_kb="$(du -sk "$dir" 2>/dev/null | awk '{print $1}' || echo "?")"
            echo "  ${id}  (${size_kb} KB)  ${dir}"
          fi
        done
        if [ "$any_found" -eq 0 ]; then
          echo "  (none)"
          echo ""
          echo "Run 'harness-mem model pull <id>' to download a model."
        fi
      fi
      ;;

    *)
      fail "Unknown model subcommand: ${subcmd}. Use: list | pull | use | status"
      ;;
  esac
}

recall_explain_impl() {
  local query=""
  local project=""
  local session_id=""
  local limit="5"
  local url="http://${MEM_HOST}:${MEM_PORT}/v1/recall"

  while [ "$#" -gt 0 ]; do
    case "$1" in
      --query)
        query="${2:-}"
        shift 2
        ;;
      --project)
        project="${2:-}"
        shift 2
        ;;
      --session|--session-id)
        session_id="${2:-}"
        shift 2
        ;;
      --limit)
        limit="${2:-}"
        shift 2
        ;;
      --url)
        url="${2:-}"
        shift 2
        ;;
      --help|-h)
        cat <<'EOF'
Usage: harness-mem recall explain --query <text> (--project <name>|--session <id>) [--limit <n>] [--url <url>]

Prints compact recall explanations from /v1/recall without memory body text.
EOF
        return 0
        ;;
      *)
        fail "Unknown recall explain option: $1"
        ;;
    esac
  done

  [ -n "$query" ] || fail "recall explain requires --query"
  [ -n "$project$session_id" ] || fail "recall explain requires --project or --session"
  case "$limit" in
    ''|*[!0-9]*)
      fail "recall explain --limit must be a positive integer"
      ;;
  esac

  require_cmd curl
  require_cmd jq

  local payload
  payload="$(
    jq -n \
      --arg query "$query" \
      --arg project "$project" \
      --arg session_id "$session_id" \
      --argjson limit "$limit" \
      '{
        query: $query,
        limit: $limit
      }
      + (if $project != "" then {project: $project} else {} end)
      + (if $session_id != "" then {session_id: $session_id} else {} end)'
  )"

  curl --silent --show-error --fail --max-time 8 \
    -H "content-type: application/json" \
    -d "$payload" \
    "$url" |
    jq '{
      ok,
      items: [(.items // [])[] | {
        id,
        title,
        recall_type,
        source_type,
        source_ref,
        explanation
      }],
      meta: {
        count: .meta.count,
        ranking: .meta.ranking,
        recall_degraded: .meta.recall_degraded,
        recall_degraded_reason: .meta.recall_degraded_reason
      }
    }'
}

recall_impl() {
  local subcmd="${1:-status}"
  case "$subcmd" in
    on|quiet|off)
      write_recall_mode "$subcmd"
      if ui_is_en; then
        echo "contextual recall mode: $subcmd"
      else
        echo "contextual recall mode: $subcmd"
      fi
      ;;
    status)
      echo "contextual recall mode: $(read_recall_mode)"
      ;;
    explain)
      shift
      recall_explain_impl "$@"
      ;;
    *)
      fail "Unknown recall subcommand: ${subcmd}. Use: on | quiet | off | status | explain"
      ;;
  esac
}

_telemetry_admin_get() {
  local path="${1:-}"
  local endpoint="http://${MEM_HOST}:${MEM_PORT}${path}"
  local endpoint_config token token_config
  endpoint_config="$(_curl_config_escape "$endpoint")"
  token="${HARNESS_MEM_ADMIN_TOKEN:-}"
  {
    printf 'url = "%s"\n' "$endpoint_config"
    printf 'request = "GET"\n'
    printf 'header = "Accept: application/json"\n'
    if [ -n "$token" ]; then
      token_config="$(_curl_config_escape "$token")"
      printf 'header = "x-harness-mem-token: %s"\n' "$token_config"
    fi
  } | curl --silent --show-error --fail --max-time 5 --config -
}

_telemetry_print_status_human() {
  local payload="${1:-}"
  if command -v jq >/dev/null 2>&1; then
    printf '%s\n' "$payload" | jq -r '
      "[harness-mem] telemetry: \(.status.exporter.mode)",
      "  service: \(.status.service_name)@\(.status.service_version)",
      "  spans: pending=\(.status.exporter.pending_spans) total=\(.summary.span_count_total) flushed=\(.status.exporter.flushed_spans)",
      "  last flush: ok=\(.status.exporter.last_flush_ok // "n/a") error=\(.status.exporter.last_flush_error // "n/a")",
      "  span counts: \(.summary.span_counts | to_entries | map("\(.key)=\(.value)") | join(", ") // "none")",
      "  metrics: \(.summary.metrics | map("\(.name) count=\(.count) latest=\(.latest)") | join(", ") // "none")"
    '
    return
  fi
  printf '%s\n' "$payload"
}

telemetry_impl() {
  local subcmd="${1:-status}"
  if [ "$#" -gt 0 ]; then
    shift || true
  fi
  local json_output=0
  local limit=""
  while [ "$#" -gt 0 ]; do
    case "$1" in
      --json)
        json_output=1
        ;;
      --limit)
        shift || true
        [ "$#" -gt 0 ] || fail "telemetry --limit requires a value"
        limit="$1"
        ;;
      *)
        fail "Unknown telemetry option: $1. Use: telemetry status|export [--limit <n>] [--json]"
        ;;
    esac
    shift || true
  done

  local path payload
  case "$subcmd" in
    status)
      path="/v1/admin/telemetry/status"
      ;;
    export)
      path="/v1/admin/telemetry/export"
      if [ -n "$limit" ]; then
        path="${path}?limit=${limit}"
      fi
      json_output=1
      ;;
    *)
      fail "Unknown telemetry subcommand: ${subcmd}. Use: status | export"
      ;;
  esac

  payload="$(_telemetry_admin_get "$path")" || {
    warn "Cannot reach daemon telemetry endpoint at http://${MEM_HOST}:${MEM_PORT}${path}"
    return 1
  }

  if [ "$json_output" -eq 1 ]; then
    printf '%s\n' "$payload"
  else
    _telemetry_print_status_human "$payload"
  fi
}

main() {
  local command="${1:-help}"
  shift || true

  # model subcommand uses positional args, bypass parse_options
  if [ "$command" = "model" ]; then
    model_impl "$@"
    return
  fi
  if [ "$command" = "recall" ]; then
    recall_impl "$@"
    return
  fi
  if [ "$command" = "telemetry" ]; then
    telemetry_impl "$@"
    return
  fi
  if [ "$command" = "work" ]; then
    work_impl "$@"
    return
  fi
  if [ "$command" = "adr" ]; then
    adr_impl "$@"
    return
  fi
  if [ "$command" = "forget" ]; then
    local forget_subcmd="${1:-status}"
    if [ "$#" -gt 0 ]; then
      shift || true
    fi
    parse_options "$@"
    normalize_ui_lang
    TARGET_DIR="$(abs_dir "$TARGET_DIR")"
    if should_use_stable_runtime_root; then
      sync_to_stable_runtime_root
    fi
    maybe_auto_update "$command"
    forget_impl "$forget_subcmd"
    return
  fi
  if [ "$command" = "mcp-gateway" ]; then
    local gateway_subcmd="${1:-status}"
    if [ "$#" -gt 0 ]; then
      shift || true
    fi
    parse_options "$@"
    normalize_ui_lang
    TARGET_DIR="$(abs_dir "$TARGET_DIR")"
    if should_use_stable_runtime_root; then
      sync_to_stable_runtime_root
    fi
    maybe_auto_update "$command"
    mcp_gateway_impl "$gateway_subcmd"
    return
  fi
  if [ "$command" = "admin-vector-backfill" ]; then
    local vector_backfill_action="${1:-status}"
    if [ "$#" -gt 0 ]; then
      shift || true
    fi
    parse_options "$@"
    normalize_ui_lang
    TARGET_DIR="$(abs_dir "$TARGET_DIR")"
    if should_use_stable_runtime_root; then
      sync_to_stable_runtime_root
    fi
    maybe_auto_update "$command"
    admin_vector_backfill_impl "$vector_backfill_action"
    return
  fi
  if [ "$command" = "mcp-config" ]; then
    node "${HARNESS_ROOT}/scripts/lib/mcp-config.js" "$@"
    return
  fi

  parse_options "$@"
  normalize_ui_lang
  if should_prompt_platform_selection "$command"; then
    prompt_language_selection
    prompt_platform_selection
    SETUP_INTERACTIVE_PROMPT_USED=1
  fi
  if should_prompt_setup_migration_selection "$command"; then
    prompt_setup_migration_selection
  fi
  if should_prompt_setup_auto_update_selection "$command"; then
    prompt_setup_auto_update_selection
  fi
  if should_prompt_update_auto_update_selection "$command"; then
    prompt_update_auto_update_selection
  fi
  if should_prompt_codex_skill_install "$command"; then
    prompt_codex_skill_install
  elif { [ "$command" = "setup" ] || [ "$command" = "update" ]; } \
    && is_platform_enabled "codex"; then
    if { [ ! -t 0 ] || [ "${HARNESS_MEM_NON_INTERACTIVE:-0}" = "1" ]; } \
      && ! check_codex_skill_bundle >/dev/null 2>&1; then
      SETUP_INSTALL_CODEX_SKILL=1
    fi
  fi

  TARGET_DIR="$(abs_dir "$TARGET_DIR")"

  if should_use_stable_runtime_root; then
    sync_to_stable_runtime_root
  fi

  validate_platform_selection
  apply_mcp_transport_default_for_command "$command"
  maybe_auto_update "$command"

  case "$command" in
    setup)
      setup_impl
      ;;
    doctor)
      doctor_impl
      ;;
    cleanup-stale-mcp)
      cleanup_stale_mcp_impl
      ;;
    recall)
      recall_impl "$@"
      ;;
    smoke)
      ensure_dependencies
      ensure_mcp_runtime
      run_smoke
      ;;
    versions)
      versions_impl
      ;;
    update)
      update_impl
      ;;
    uninstall)
      uninstall_impl
      ;;
    backup)
      backup_impl
      ;;
    backup-evidence)
      backup_evidence_impl
      ;;
    forget-maintenance)
      forget_maintenance_impl
      ;;
    forget)
      forget_impl "$@"
      ;;
    import-claude-mem)
      import_claude_mem_impl
      ;;
    admin-repair-sqlite-vec-map)
      admin_repair_sqlite_vec_map_impl
      ;;
    admin-vector-backfill)
      admin_vector_backfill_impl status
      ;;
    ingest-hermes-state)
      ingest_hermes_state_impl
      ;;
    verify-import)
      verify_import_impl
      ;;
    cutover-claude-mem)
      cutover_claude_mem_impl
      ;;
    migrate-from-claude-mem)
      migrate_from_claude_mem_impl
      ;;
    rollback-claude-mem)
      rollback_claude_mem_impl
      ;;
    promote)
      promote_impl
      ;;
    rollback)
      rollback_impl
      ;;
    help|-h|--help)
      usage
      ;;
    *)
      fail "Unknown command: $command"
      ;;
  esac
}

main "$@"
