#!/usr/bin/env bash
# verify.sh — mobile slot verification (readiness/proof layer)
#
# Purpose:
#   Recipe-tree verification for ONE mobile instance: static checks plus
#   runtime/bridge verification, with the auto-start gate deciding whether
#   verify may start the app or only attach.
#
# Inputs (flags / env):
#   --target <metamask-mobile> (default $PWD), --platform ios|android,
#   --preflight-mode fast|auto|default|rebuild-native|clean,
#   --auto-start/--no-auto-start (env RECIPE_HARNESS_MOBILE_AUTO_START,
#   default false), --static-only, --artifacts-dir <dir>
#   env RECIPE_HARNESS_ROOT, RECIPE_RUNTIME_DIR, RECIPE_HARNESS_PLATFORM
#
# Outputs:
#   <artifacts>/summary.json + logs/.
#   Exit 0 — verify pass; 1 — checks failed; 2 — bad args.
#
# Never touches: product source files; app startup unless auto-start is
# enabled (launch owns startup in live runs).
set -euo pipefail

TARGET="$PWD"
ARTIFACTS=""
STATIC_ONLY=false
AUTO_START="${RECIPE_HARNESS_MOBILE_AUTO_START:-false}"
PLATFORM="${RECIPE_HARNESS_PLATFORM:-${PLATFORM:-ios}}"
PREFLIGHT_MODE="${RECIPE_HARNESS_MOBILE_PREFLIGHT_MODE:-fast}"
require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
while [ "$#" -gt 0 ]; do
  case "$1" in
    --target) require_value "$@"; TARGET="$2"; shift 2 ;;
    --artifacts-dir) require_value "$@"; ARTIFACTS="$2"; shift 2 ;;
    --static-only) STATIC_ONLY=true; shift ;;
    --platform) require_value "$@"; PLATFORM="$2"; shift 2 ;;
    --preflight-mode) require_value "$@"; PREFLIGHT_MODE="$2"; shift 2 ;;
    --auto-start) AUTO_START=true; shift ;;
    --no-auto-start) AUTO_START=false; shift ;;
    -h|--help) echo "Usage: verify.sh [--target <metamask-mobile>] [--artifacts-dir <dir>] [--static-only] [--platform ios|android] [--preflight-mode fast|auto|default|rebuild-native|clean] [--no-auto-start]"; exit 0 ;;
    *) echo "Unknown arg: $1" >&2; exit 2 ;;
  esac
done
case "$PREFLIGHT_MODE" in
  fast|auto|default|rebuild-native|clean) ;;
  *) echo "Unknown --preflight-mode: $PREFLIGHT_MODE" >&2; exit 2 ;;
esac
case "$PLATFORM" in
  ios|android) ;;
  *) echo "Unknown --platform: $PLATFORM (expected ios or android)" >&2; exit 2 ;;
esac

case "$AUTO_START" in
  1|true|TRUE|True|yes|YES|Yes|on|ON|On) AUTO_START=true ;;
  0|false|FALSE|False|no|NO|No|off|OFF|Off|"") AUTO_START=false ;;
  *) echo "Unknown RECIPE_HARNESS_MOBILE_AUTO_START value: $AUTO_START" >&2; exit 2 ;;
esac

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../../adapters/shared/harness-path.sh" "$SCRIPT_DIR/../shared/harness-path.sh"; do
  [ -f "$_hp" ] && { . "$_hp"; break; }
done
unset _hp
if ! command -v harness_root >/dev/null 2>&1; then
  echo "mobile verify: shared lib adapters/shared/harness-path.sh not found; reinstall the runner." >&2
  exit 1
fi
TARGET="$(cd "$TARGET" && pwd)"
RUNTIME_DIR="$(recipe_runtime_dir)"
HARNESS_ROOT="$(harness_root)"
HARNESS_REL="$HARNESS_ROOT/mobile"
HARNESS_DIR="$(harness_dir "$TARGET" mobile)"
RUNNER_BIN="$HARNESS_DIR/runner/bin/mm-harness"
ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/verify/$(date -u +%Y%m%dT%H%M%SZ)}"
mkdir -p "$ARTIFACTS/logs"

status="pass"
checks=()

add_note() {
  printf '%s\n' "$1" >> "$ARTIFACTS/logs/runtime-notes.txt"
}

fixture_status_json() {
  TARGET_FOR_FIXTURE="$TARGET" RECIPE_RUNTIME_DIR="$RUNTIME_DIR" node <<'NODE'
const fs = require('fs');
const crypto = require('crypto');
const path = require('path');
const target = process.env.TARGET_FOR_FIXTURE;
const runtimeDir = process.env.RECIPE_RUNTIME_DIR;
if (!runtimeDir) throw new Error('RECIPE_RUNTIME_DIR is required');
const candidates = [
  runtimeDir + '/wallet-fixture.json',
].map((rel) => path.join(target, rel));
const fixture = candidates.find((file) => fs.existsSync(file));
if (!fixture) {
  console.log(JSON.stringify({
    status: 'MISSING_FIXTURES',
    message: `No wallet fixture found. This run may spend time repairing wallet/perps state manually. For a clean isolated sandbox, create ${runtimeDir}/wallet-fixture.json.`,
    setupCommand: `mkdir -p ${runtimeDir} && edit ${runtimeDir}/wallet-fixture.json with password + accounts`,
  }));
  process.exit(0);
}
const fixtureRaw = fs.readFileSync(fixture);
let parsed = null;
let valid = false;
let accountCount = null;
let hasPassword = false;
try {
  parsed = JSON.parse(fixtureRaw.toString('utf8'));
  valid = true;
  accountCount = Array.isArray(parsed.accounts) ? parsed.accounts.length : 0;
  hasPassword = typeof parsed.password === 'string' && parsed.password.length > 0;
} catch {
  valid = false;
}
const stat = fs.statSync(fixture);
console.log(JSON.stringify({
  status: valid && hasPassword && accountCount > 0 ? 'READY' : 'STALE_OR_INVALID',
  path: path.relative(target, fixture),
  sha256: crypto.createHash('sha256').update(fixtureRaw).digest('hex'),
  modifiedAt: stat.mtime.toISOString(),
  accountCount,
  hasPassword,
  message: valid && hasPassword && accountCount > 0
    ? `Fixture status: READY (${path.relative(target, fixture)}, accounts=${accountCount}).`
    : `Fixture status: STALE_OR_INVALID (${path.relative(target, fixture)}). Validate password/accounts before relying on a clean sandbox.`,
}));
NODE
}

port_holder_json() {
  local port="$1"
  PORT_FOR_STATUS="$port" node <<'NODE'
const cp = require('child_process');
const port = process.env.PORT_FOR_STATUS;
function run(cmd) {
  try { return cp.execSync(cmd, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); }
  catch { return ''; }
}
const pid = run(`lsof -iTCP:${port} -sTCP:LISTEN -t | head -1`);
let command = '';
// Validate pid is numeric before interpolating it into the `ps -p` shell string.
if (/^[0-9]+$/.test(pid)) command = run(`ps -p ${pid} -o command=`);
console.log(JSON.stringify({
  port,
  listening: Boolean(pid),
  pid: pid || null,
  command: command || null,
  metroStatusReachable: null,
  metroHttpProbeSkipped: true,
  note: 'HTTP /status probing is skipped during live verify because the React Native bridge is the authoritative readiness path for Mobile.',
}));
NODE
}

fixture_check_json() {
  local fixture_status_path="$1"
  node - "$fixture_status_path" <<'NODE'
const fs = require('fs');
const v = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
console.log(JSON.stringify({
  name: 'fixture status',
  status: v.status === 'READY' ? 'pass' : 'warn',
  detail: v.path || v.status || '',
  message: v.message || v.status,
}));
NODE
}

watcher_port() {
  TARGET_FOR_WATCHER_PORT="$TARGET" node <<'NODE'
const fs = require('fs');
const path = require('path');
const target = process.env.TARGET_FOR_WATCHER_PORT;
// Precedence: an explicit WATCHER_PORT in the process env (which carries the
// --watcher-port flag applied by launch) wins over the slot's .js.env, which wins
// over the 8081 default. This lets a run be pointed at an alternate Metro without
// editing a shared slot .js.env.
let port = process.env.WATCHER_PORT || '';
if (!port) {
  for (const file of ['.js.env', '.env', '.env.local']) {
    const full = path.join(target, file);
    if (!fs.existsSync(full)) continue;
    const text = fs.readFileSync(full, 'utf8');
    const match = text.match(/^\s*(?:export\s+)?WATCHER_PORT=(["']?)([0-9]+)\1/m);
    if (match) { port = match[2]; break; }
  }
}
if (!port) port = '8081';
console.log(port);
NODE
}

# Resolve a runtime env var (e.g. IOS_SIMULATOR, ADB_SERIAL) from the process
# env or the target repo's .js.env so the runner can bind device-scoped proof
# (screenshots) to the same device as the bridge commands.
jsenv_value() {
  TARGET_FOR_JSENV="$TARGET" JSENV_NAME="$1" node <<'NODE'
const fs = require('fs');
const path = require('path');
const target = process.env.TARGET_FOR_JSENV;
const name = process.env.JSENV_NAME;
let value = process.env[name] || '';
if (!value) {
  const re = new RegExp("^\\s*(?:export\\s+)?" + name + "=([\"']?)([^\"'\\n]+)\\1", "m");
  for (const file of ['.js.env', '.env', '.env.local']) {
    const full = path.join(target, file);
    if (!fs.existsSync(full)) continue;
    const match = fs.readFileSync(full, 'utf8').match(re);
    if (match) { value = match[2]; break; }
  }
}
process.stdout.write(value);
NODE
}

runtime_context_value() {
  TARGET_FOR_RUNTIME_CONTEXT="$TARGET" RUNTIME_DIR_FOR_RUNTIME_CONTEXT="$RUNTIME_DIR" RUNTIME_CONTEXT_NAME="$1" node <<'NODE'
const fs = require('fs');
const path = require('path');
const target = process.env.TARGET_FOR_RUNTIME_CONTEXT;
const runtimeDir = process.env.RUNTIME_DIR_FOR_RUNTIME_CONTEXT;
const names = process.env.RUNTIME_CONTEXT_NAME.split(',');
const file = path.join(target, runtimeDir, 'agentic-runtime.json');
let data = {};
try { data = JSON.parse(fs.readFileSync(file, 'utf8')); } catch {}
for (const name of names) {
  const value = data && data[name];
  if (value !== undefined && value !== null && value !== '') {
    process.stdout.write(String(value));
    process.exit(0);
  }
}
NODE
}

mobile_device_target() {
  if [ "$PLATFORM" = "android" ]; then
    printf '%s' "${ADB_SERIAL:-${ANDROID_SERIAL:-${ANDROID_DEVICE:-$(runtime_context_value adbSerial,androidDevice,device)}}}"
    return
  fi
  printf '%s' "${SIM_UDID:-${IOS_SIMULATOR:-$(runtime_context_value simulator,iosSimulator,device)}}"
}

check_file() {
  local rel="$1"
  if [ -e "$TARGET/$rel" ]; then
    checks+=("{\"name\":\"$rel\",\"status\":\"pass\"}")
  else
    checks+=("{\"name\":\"$rel\",\"status\":\"fail\"}")
    status="fail"
  fi
}

check_file "$HARNESS_REL/manifest.json"
check_file "$HARNESS_REL/action-manifest.json"
check_file "$HARNESS_REL/runner/bin/mm-harness"
check_file "package.json"
check_file "app/dev-tools/AgenticService/AgenticService.ts"

if ! grep -q "AgenticService.install" "$TARGET/app/core/NavigationService/NavigationService.ts" 2>/dev/null; then
  checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"fail\"}")
  status="fail"
else
  checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"pass\"}")
fi

if ! grep -q "AgentStepHud" "$TARGET/app/components/Nav/App/App.tsx" 2>/dev/null; then
  checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"fail\"}")
  status="fail"
else
  checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"pass\"}")
fi

run_with_timeout() {
  local log_path="$1"
  local timeout_s="$2"
  shift 2
  [[ "$timeout_s" =~ ^[0-9]+$ ]] || {
    echo "Invalid timeout seconds: $timeout_s" >&2
    return 2
  }
  "$@" > "$log_path" 2>&1 &
  local pid=$!
  local waited=0
  while kill -0 "$pid" 2>/dev/null; do
    if [ "$waited" -ge "$timeout_s" ]; then
      echo "Timed out after ${timeout_s}s: $*" >> "$log_path"
      kill "$pid" 2>/dev/null || true
      sleep 1
      kill -9 "$pid" 2>/dev/null || true
      wait "$pid" 2>/dev/null || true
      return 124
    fi
    sleep 1
    waited=$((waited + 1))
  done
  wait "$pid"
}

live_status_ok() {
  local log_path="$1"
  local device_target="$2"
  cat > "$ARTIFACTS/mobile-status-smoke.recipe.json" <<'JSON'
{
  "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
  "title": "Mobile runner status smoke",
  "description": "Checks that the installed runner can read Mobile app status before live verification.",
  "workflow": {
    "entry": "status",
    "nodes": {
      "status": { "action": "app.status", "intent": "Read Mobile app status through the runner-owned bridge", "next": "done" },
      "done": { "action": "end", "status": "pass" }
    }
  }
}
JSON
  (
    cd "$TARGET"
    device_args=()
    [ -n "$device_target" ] && device_args=(--device "$device_target")
    "$RUNNER_BIN" run "$ARTIFACTS/mobile-status-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --watcher-port "$port" --artifacts-dir "$ARTIFACTS/runner-status-smoke" --json
  ) > "$log_path" 2>&1
}

evm_rpc_ready() {
  local log_path="$1"
  local bridge="$HARNESS_DIR/bridge-runtime/cdp-bridge.cjs"
  local expression
  expression="(async function(){try{var engine=globalThis.Engine;var controller=engine&&engine.context&&engine.context.NetworkController;if(!controller||typeof controller.getSelectedNetworkClient!=='function')throw new Error('NetworkController selected client is unavailable');var client=controller.getSelectedNetworkClient();if(!client||!client.provider||typeof client.provider.request!=='function')throw new Error('selected EVM provider is unavailable');var result=await client.provider.request({method:'eth_blockNumber'});if(typeof result!=='string'||!/^0x[0-9a-f]+$/i.test(result))throw new Error('eth_blockNumber returned an invalid result');return {ok:true,result:result};}catch(error){return {ok:false,error:error&&(error.message||String(error))};}})()"
  [ -f "$bridge" ] || {
    printf 'Mobile bridge is missing at %s\n' "$bridge" > "$log_path"
    return 1
  }
  if ! run_with_timeout "$log_path" 25 env \
    APP_ROOT="$TARGET" \
    WATCHER_PORT="$port" \
    IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
    ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
    ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
    node "$bridge" eval-async "$expression"; then
    return 1
  fi
  node - "$log_path" <<'NODE'
const fs = require('fs');
try {
  const result = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
  process.exit(result.ok === true ? 0 : 1);
} catch {
  process.exit(1);
}
NODE
}

ensure_live_runtime() {
  local device_target="$1"
  local attempt
  for attempt in 1 2 3 4 5; do
    if live_status_ok "$ARTIFACTS/logs/app-status-precheck.log" "$device_target"; then
      return 0
    fi
    # A freshly booted simulator can expose its Metro bridge just before
    # `simctl list devices booted` publishes the device. Bound that launch→verify
    # race instead of reporting a false DEVICE_NOT_FOUND immediately.
    if [ "$attempt" -lt 5 ] && grep -q '"code"[[:space:]]*:[[:space:]]*"DEVICE_NOT_FOUND"' "$ARTIFACTS/logs/app-status-precheck.log"; then
      echo "Waiting for Mobile device discovery (${attempt}/5): ${device_target:-configured target}" >&2
      sleep 1
      continue
    fi
    break
  done
  if [ "$AUTO_START" = true ]; then
    cat >&2 <<'EOF'
Mobile auto-start is not allowed from product-local scripts. Start or prepare the app through the runner/slot runtime, then rerun verify with --no-auto-start.
EOF
  fi
  return 1
}

if [ "$STATIC_ONLY" = false ]; then
  port="$(watcher_port)"
  # Reject a non-numeric port before it reaches runner arguments or the
  # port-holder shell probe.
  case "$port" in
    ""|*[!0-9]*) echo "Refusing mobile verify: resolved watcher port is not numeric: '$port' (check WATCHER_PORT)" >&2; exit 2 ;;
  esac

  device_target="$(mobile_device_target)"
  if [ -z "$device_target" ]; then
    if [ "$PLATFORM" = "android" ]; then
      device_target="$(jsenv_value ADB_SERIAL)"
      [ -n "$device_target" ] || device_target="$(jsenv_value ANDROID_DEVICE)"
    else
      device_target="$(jsenv_value IOS_SIMULATOR)"
    fi
  fi

  if ensure_live_runtime "$device_target"; then
    checks+=('{"name":"mobile runtime controllable precheck","status":"pass"}')
  else
    checks+=('{"name":"mobile runtime controllable precheck","status":"fail","detail":"see logs/app-status-precheck.log"}')
    status="fail"
  fi

  fixture_json="$(fixture_status_json)"
  printf '%s\n' "$fixture_json" > "$ARTIFACTS/logs/fixture-status.json"
  fixture_check_json="$(fixture_check_json "$ARTIFACTS/logs/fixture-status.json")"
  fixture_message="$(node -e 'const v=JSON.parse(process.argv[1]); console.log(v.message || v.detail);' "$fixture_check_json")"
  echo "$fixture_message" >&2
  add_note "$fixture_message"
  checks+=("$fixture_check_json")

  port_holder_json "$port" > "$ARTIFACTS/logs/port-holder.json"

  cat > "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" <<'JSON'
{
  "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
  "title": "Mobile v1 runner live bridge smoke",
  "description": "Verifies the installed MetaMask runner can read the React Native debug bridge without mutating wallet state.",
  "workflow": {
    "entry": "status",
    "nodes": {
      "status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
      "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "require_reachable": true, "timeout_ms": 15000, "next": "done" },
      "done": { "action": "end", "status": "pass" }
    }
  }
}
JSON

  ios_simulator_resolved="$(jsenv_value IOS_SIMULATOR)"
  adb_serial_resolved="$(jsenv_value ADB_SERIAL)"
  device_args=()
  [ -n "$device_target" ] && device_args=(--device "$device_target")
  if (
    cd "$TARGET"
    METAMASK_RECIPE_AUTO_HUD=0 \
    IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
    ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
    ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
    "$RUNNER_BIN" run "$ARTIFACTS/mobile-v1-live-smoke.recipe.json" --adapter mobile --target "$TARGET" "${device_args[@]}" --watcher-port "$port" --artifacts-dir "$ARTIFACTS/runner-live-smoke" --json
  ) > "$ARTIFACTS/logs/runner-live-smoke.log" 2>&1; then
    checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"pass\"}")
  else
    checks+=("{\"name\":\"runner v1 live bridge smoke\",\"status\":\"fail\",\"detail\":\"see logs/runner-live-smoke.log\"}")
    add_note "Runner v1 live bridge smoke failed; inspect logs/runner-live-smoke.log and runner-live-smoke/trace.json."
    status="fail"
  fi

  if evm_rpc_ready "$ARTIFACTS/logs/evm-rpc-readiness.log"; then
    checks+=("{\"name\":\"live EVM RPC readiness\",\"status\":\"pass\"}")
  else
    checks+=("{\"name\":\"live EVM RPC readiness\",\"status\":\"fail\",\"detail\":\"see logs/evm-rpc-readiness.log\"}")
    add_note "EVM RPC readiness probe failed. Infura configured ≠ RPC reachable. Next: confirm $TARGET/.js.env has a real MM_INFURA_PROJECT_ID (not 00000000000), then rerun mm-harness launch $PLATFORM --verify --target '$TARGET'."
    status="fail"
  fi
fi

RECIPE_HARNESS_PREFLIGHT_MODE="$PREFLIGHT_MODE" RECIPE_HARNESS_ROOT_EXCLUDE="$HARNESS_ROOT" node - "$ARTIFACTS" "$TARGET" "$status" "${checks[@]}" <<'NODE'
const fs = require('fs');
const path = require('path');
const cp = require('child_process');
const [artifacts, target, status, ...checks] = process.argv.slice(2);
const parsedChecks = checks.map((entry) => JSON.parse(entry));
let fixtureStatus = null;
let portHolder = null;
let runtimeNotes = [];
const startedRuntime = fs.existsSync(path.join(artifacts, 'logs/harness-started-runtime'));
try { fixtureStatus = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/fixture-status.json'), 'utf8')); } catch {}
try { portHolder = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/port-holder.json'), 'utf8')); } catch {}
try { runtimeNotes = fs.readFileSync(path.join(artifacts, 'logs/runtime-notes.txt'), 'utf8').trim().split('\n').filter(Boolean); } catch {}
function runGit(args) {
  try {
    return cp.execFileSync('git', ['-C', target, ...args], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
  } catch (error) {
    // Git metadata is diagnostic-only; non-git targets still produce a usable verify summary.
    return null;
  }
}
const harnessRootExclude = process.env.RECIPE_HARNESS_ROOT_EXCLUDE;
if (!harnessRootExclude) throw new Error('RECIPE_HARNESS_ROOT_EXCLUDE is required');
const statusShort = runGit(['status', '--short', '--', '.', `:(exclude)${harnessRootExclude}`]);
const gitStatus = {
  branch: runGit(['branch', '--show-current']),
  head: runGit(['rev-parse', '--short', 'HEAD']),
  dirtyCount: statusShort ? statusShort.split('\n').filter(Boolean).length : 0,
  dirtyPreview: statusShort ? statusShort.split('\n').filter(Boolean).slice(0, 25) : [],
};
const liveRuntimeCheck = parsedChecks.find((check) => check.name === 'runner v1 live bridge smoke');
const runtimeOwner = !portHolder
  ? 'static-only'
  : startedRuntime
    ? 'harness-owned'
    : portHolder.listening
      ? (liveRuntimeCheck?.status === 'pass' ? 'compatible-external-or-harness' : 'incompatible-external-or-stale')
      : 'none';
const recipeControllable = liveRuntimeCheck?.status === 'pass';
fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
  adapter: 'mobile',
  status,
  runtimeClassification: {
    runtimeOwner,
    recipeControllable,
    startedByVerify: startedRuntime,
  },
  cleanupOwnership: {
    mayStop: startedRuntime,
    reason: startedRuntime
      ? 'verify launched the runtime through harness preflight'
      : 'verify did not launch this runtime; do not stop human-owned or pre-existing processes automatically',
  },
  gitStatus,
  runtimePolicy: {
    preflightMode: process.env.RECIPE_HARNESS_PREFLIGHT_MODE || 'fast',
    nativeBuildPolicy: (process.env.RECIPE_HARNESS_PREFLIGHT_MODE || 'fast') === 'fast'
      ? 'verify does not start product-local Mobile scripts; start/reuse a runner-owned or slot-owned runtime before live proof'
      : 'verify did not start product-local Mobile scripts',
  },
  fixtureStatus,
  portHolder,
  runtimeNotes,
  checks: parsedChecks,
  generatedAt: new Date().toISOString(),
}, null, 2)}\n`);
fs.writeFileSync(path.join(artifacts, 'artifact-manifest.json'), `${JSON.stringify({
  artifacts: fs.readdirSync(artifacts).map((name) => ({ path: name })),
}, null, 2)}\n`);
NODE

echo "Mobile harness verify $status: $ARTIFACTS/summary.json"
[ "$status" = "pass" ]
