Files
cleveragents-core/tools/launch_fork.sh
T
drew 2658deee94 feat(auto-agents): PR State Warmer substrate + supporting infra
Adds a long-lived sidecar (pr_state_warmer.py) that polls Forgejo's
/pulls endpoint every 30s and writes the full PR snapshot to a
shared SQLite store, eliminating the dispatcher's per-cycle
cold-cache stalls (24-30s rebuilds on flaky cycles) and the silent
50-PR pagination cap on the legacy single-page fetch.

Substrate
- tools/_pr_state_cache.py  — SQLite store with (owner, repo) PK,
  WAL mode, additive v2→v3 migration (comments_refreshed_updated_at),
  bounded fcntl.flock migration lock, threading.Lock for per-process
  init, @_with_reheal decorator (catches OperationalError no-such-
  table + DatabaseError corruption with file quarantine), atomic
  TEMP-table chunking for >32k seen-set, _normalize_updated_at to
  canonicalize Forgejo tz-marker drift
- tools/pr_state_warmer.py — poll/upsert/vanish/comments-refresh
  loop with fcntl.flock singleton (rejects second warmer), bounded
  comments-refresh cap, persistent deferral via SQL pending query,
  PermissionError-tolerant lock setup, cold-start log suppression
- tools/_pr_classification_cache.py — three-layer fall-through
  (warmer cache → list cache → live fetch) with staleness gate
  (PR_STATE_WARMER_STALE_AFTER_S floored at 30s in prod)

Comments cache hardening
- Bot-filter at write time drops bot status/claim/release/sentinel
  while preserving **Implementation Attempt** markers (94.6%
  reduction on bot-heavy PRs like #30's 19k-comment thread)
- _normalize_since_cursor strips microsecond precision before
  building ?since= query (fixes the live-observed Forgejo HTTP 422
  bug on PRs #25 + #28); handles uppercase Z, lowercase z, ±HH:MM
  offsets (including non-zero like +05:30), naive ISO
- Lazy migration of legacy null-key by_author entries on _read_cache
- _newest_cursor walks tail-back skipping malformed entries

Supporting infrastructure (cumulative dmpipeline-v2 work)
- Telemetry server: SSE live tail, run-sessions enumeration,
  cost/token tracking, app.js UI rewrite with collapsible sections
- MCP servers (mcp_ci_server, mcp_forgejo_server, mcp_git_server,
  mcp_handoff_server, mcp_graphify_server) for opencode worker
  context access
- Live log writer (tools/live_log_writer.py) — SSE-streaming
  dispatcher event log
- Tier-dispatcher escalation flow with prompts trimmed for budget
- Shared bot-logins resolver (tools/_bot_logins.py) replacing two
  drift-prone copies
- token_usage_audit.py for opencode cost analysis

Tests
- 2259 passing across 65 changed/new files
- New suites: test_pr_state_cache, test_pr_state_warmer,
  test_pr_state_warmer_integration, test_pr_classification_cache,
  test_pr_list_cache_backoff, test_mcp_* (5 servers),
  test_live_log_writer_sse, test_telemetry_run_sessions,
  test_review_post_ready_label
- Test_pr_comments_cache expanded with bot-filter coverage,
  cursor-normalization regression pins, format-drift, atomicity,
  failed-comments-not-stamped (silent-data-loss class)
- Parametrized @_with_reheal coverage across 7 wrapped APIs
- Real fault-inject atomicity test for chunked mark_vanished path
  via Connection wrapper class
- Subprocess-based singleton flock test (cross-process contract)
- Event-driven SIGTERM-mid-poll test (no fixed-sleep flake)

Architecture notes
- Schema v3 migration is additive (ALTER ADD COLUMN); v0/v1 still
  need destructive rebuild because pre-v2 column shape lacks
  owner/repo. Cross-process drop-table-ping-pong prevented by the
  fcntl migration lock + per-process _initialized flag.
- Comments-refresh deferral is persistent via
  comments_refreshed_updated_at column — survives warmer restart,
  picks up next cycle even if PR didn't change again. Replaces
  in-memory changed_numbers list.
- Rollback path: PR_STATE_WARMER_PREFER=0 bypasses the warmer
  cache and reverts to live-fetch behavior. PR_STATE_CACHE_DISABLE=1
  short-circuits the warmer process at startup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 10:02:53 -04:00

468 lines
20 KiB
Bash
Executable File

#!/usr/bin/env bash
# tools/launch_fork.sh — Pin fork-mode env for the auto-agents pipeline.
#
# Why this exists
# ---------------
#
# The auto-agents pipeline has three complementary host-level entry
# points:
#
# 1. `scripts/dispatchers-launcher.sh` — runs the deterministic
# Python dispatchers (`tools/dispatch_review.py` and
# `tools/dispatch_implementer.py`) that own all reviewer /
# implementer orchestration. They read `FORGEJO_*` env vars
# directly and invoke the LLM workers (`pr-review-worker`,
# `implementation-worker`) per work item via OpenCode HTTP.
# These daemons replaced the legacy `pr-review-supervisor` /
# `implementation-supervisor` LLM agents that were
# decommissioned 2026-05-09.
#
# 2. `scripts/opencode-builder.sh` — boots the OpenCode server (or
# attaches to one already running on `127.0.0.1:4096`) and starts
# the `auto-agents` watchdog session. The watchdog reads
# dispatcher heartbeat files and logs WARNINGs; it does NOT
# restart anything. The OpenCode server it boots is also what
# the dispatchers above use for their HTTP worker invocations.
# Pass `OPENCODE_BUILDER_SERVER_ONLY=1` to skip the watchdog
# session and run server-only.
#
# 3. `tools/merge_drive.py` — the deterministic merge driver that
# enforces the train-build CI invariant. It reads `FORGEJO_OWNER`,
# `FORGEJO_REPO`, `FORGEJO_API_BASE`, and `GITEA_TOKEN` directly.
#
# Both entry points fall through to the canonical
# `cleveragents/cleveragents-core` when env vars aren't set, so a
# fork-mode test launched without a preamble will SILENTLY operate
# against the canonical repo. This script is the source-able preamble
# that pins everything to the operator's fork in one place.
#
# What gets exported (canonical names per auto-agents.md)
# -------------------------------------------------------
#
# Required by the auto-agents env contract:
# GIT_USER_NAME — git user.name for HAL9000's commits
# GIT_USER_EMAIL — git user.email for HAL9000's commits
# FORGEJO_PAT — HAL9000 PAT (LLM worker + driver identity)
# FORGEJO_USERNAME — HAL9000's login (resolved via whoami)
# FORGEJO_PASSWORD — HAL9000 web password (CI-log scraping)
# FORGEJO_REVIEWER_PAT — HAL9001 PAT (umbrella-PR auto-approver)
# FORGEJO_REVIEWER_USERNAME — HAL9001's login (resolved via whoami)
# FORGEJO_REVIEWER_PASSWORD — HAL9001 web password
#
# Driver-side compat:
# GITEA_TOKEN — synthesized from FORGEJO_PAT for the
# deterministic Python tools that still
# read this name (merge_drive.py,
# verify_invariant.py, audit_branch_protection.py,
# setup_*.py, etc.). They authenticate as
# HAL9000 — same identity as production runs.
# Pre-export GITEA_TOKEN before sourcing if
# you want to use a different identity (e.g.
# drew's admin PAT for one-time setup).
#
# Optional / driver-side:
# FORGEJO_URL — base URL (default: https://git.cleverthis.com)
# FORGEJO_OWNER — fork owner (default: drew)
# FORGEJO_REPO — fork repo (default: cleveragents-core)
# FORGEJO_API_BASE — derived from FORGEJO_URL
# FORGEJO_DEFAULT_BRANCH — default: master
# FORGEJO_ORG — org-label fallback only (default: cleveragents)
# CA_MAX_PARALLEL_WORKERS — historical knob; the deterministic
# dispatchers don't read it today, but
# the value is still re-exported so any
# future opt-in worker-pool implementation
# picks it up without operator changes.
# (Originally a supervisor-pool size for
# the now-decommissioned LLM supervisors.)
#
# Resolution priority for each name:
# 1. value already in the environment
# 2. canonical name in .devcontainer/.env or .env
# 3. HAL_* alias in .devcontainer/.env (PAT/password)
# 4. derive via API whoami (USERNAME only) or hard-coded default
#
# Usage
# -----
#
# source tools/launch_fork.sh
# tools/preflight_phase01.sh
# .venv/bin/python3 tools/merge_drive.py --status
# scripts/opencode-builder.sh
#
# # Override fork target:
# FORK_OWNER=otheruser FORK_REPO=myfork source tools/launch_fork.sh
#
# Safety guarantees
# -----------------
#
# - Validates the fork via a single read-only `GET /repos/<owner>/<repo>`
# (must exist, must be a fork, calling user must have push).
# - All resolved values stay in LOCAL variables until validation passes.
# On any failure, the parent shell's environment is left untouched —
# no FORGEJO_*, no GIT_*, no GITEA_TOKEN leakage.
# - A `printenv FORGEJO_PASSWORD | head -c 4` style probe in the parent
# shell is the operator's audit primitive; values are never echoed
# verbatim in the banner.
#
# Exit / return codes
# -------------------
#
# 0 — env exported, fork validated
# 1 — validation failed; nothing exported
# 2 — argument error / missing prerequisite
# Detect sourced vs executed once, up front. ${BASH_SOURCE[0]} == ${0}
# means "executed directly"; anything else means "sourced".
__lf_sourced=0
if [[ "${BASH_SOURCE[0]:-}" != "${0}" ]]; then
__lf_sourced=1
fi
# Wrap all logic in a function so we can `return` at any point without
# falling through into the export block. The function only exports
# anything on its successful path; failure paths print an error and
# return non-zero.
__lf_main() {
local script_path="${BASH_SOURCE[0]:-$0}"
local repo_root
repo_root="$(cd "$(dirname "$script_path")/.." && pwd)"
# ─── Defaults (overridable via FORK_* env vars) ───────────────────
local fork_owner="${FORK_OWNER:-drew}"
local fork_repo="${FORK_REPO:-cleveragents-core}"
local fork_url="${FORK_URL:-https://git.cleverthis.com}"
local fork_api_base="${FORK_API_BASE:-${fork_url%/}/api/v1}"
local fork_default_branch="${FORK_DEFAULT_BRANCH:-master}"
# ─── Step 0 — pick a python3 we can rely on ───────────────────────
local py
if [[ -x "$repo_root/.venv/bin/python3" ]]; then
py="$repo_root/.venv/bin/python3"
else
py="$(command -v python3 || true)"
fi
if [[ -z "$py" ]]; then
echo "ERROR: python3 not found (needed for fork validation and JSON parsing)" >&2
return 2
fi
# ─── Step 1 — small .env reader ───────────────────────────────────
# Reads KEY=value or KEY="value" from `.devcontainer/.env` then `.env`,
# returning the first match. Single-quoted values are also handled.
__lf_resolve_from_env() {
local key="$1"
local val=""
for f in "$repo_root/.devcontainer/.env" "$repo_root/.env"; do
[[ -f "$f" ]] || continue
val="$(awk -v k="$key" -F= '
$0 ~ "^[[:space:]]*"k"[[:space:]]*=" {
# Strip key + leading whitespace and `=`
sub("^[[:space:]]*"k"[[:space:]]*=[[:space:]]*", "")
# Strip optional surrounding quotes
if (substr($0,1,1) == "\"" || substr($0,1,1) == "'\''") {
q = substr($0,1,1)
sub("^"q, "")
sub(q".*$", "")
} else {
# Unquoted: stop at first whitespace or `#`
sub(/[[:space:]#].*$/, "")
}
print
exit
}' "$f" 2>/dev/null || true)"
if [[ -n "$val" ]]; then
printf '%s' "$val"
return 0
fi
done
return 1
}
# Resolve <CANONICAL_NAME> with fallback to one or more aliases.
# Echoes the resolved value and returns 0; returns 1 if nothing found.
__lf_resolve_with_aliases() {
local canonical="$1"; shift
local existing="${!canonical:-}"
if [[ -n "$existing" ]]; then
printf '%s' "$existing"
return 0
fi
local v
if v="$(__lf_resolve_from_env "$canonical")"; then
printf '%s' "$v"
return 0
fi
local alias
for alias in "$@"; do
if v="$(__lf_resolve_from_env "$alias")"; then
printf '%s' "$v"
return 0
fi
done
return 1
}
# Helper: resolve a Forgejo login from a PAT via /api/v1/user.
__lf_whoami() {
local token="$1"
[[ -n "$token" ]] || return 1
curl -sf -H "Authorization: token $token" "$fork_api_base/user" 2>/dev/null \
| "$py" -c 'import json,sys
try:
print(json.load(sys.stdin).get("login") or "")
except Exception:
pass' 2>/dev/null || true
}
# ─── Step 2 — resolve every value into local variables ────────────
# Primary bot (HAL9000) PAT + password.
local resolved_pat
resolved_pat="$(__lf_resolve_with_aliases FORGEJO_PAT HAL_9000_API_KEY)" || resolved_pat=""
local resolved_pwd
resolved_pwd="$(__lf_resolve_with_aliases FORGEJO_PASSWORD HAL_9000_FORGEJO_PASSWORD)" || resolved_pwd=""
# GITEA_TOKEN is synthesized from FORGEJO_PAT (HAL9000) by default — that's
# the production-fidelity identity for the deterministic Python tools.
# Operators who want to use a different identity (e.g. drew's admin PAT
# for one-time fork setup) can pre-export GITEA_TOKEN before sourcing.
local resolved_gitea="${GITEA_TOKEN:-$resolved_pat}"
if [[ -z "$resolved_gitea" ]]; then
echo "ERROR: cannot resolve a token for GITEA_TOKEN — neither GITEA_TOKEN" >&2
echo " nor FORGEJO_PAT (or HAL_9000_API_KEY) is set." >&2
return 2
fi
# Reviewer bot (HAL9001) PAT + password.
local resolved_rev_pat
resolved_rev_pat="$(__lf_resolve_with_aliases FORGEJO_REVIEWER_PAT HAL_9001_API_KEY)" || resolved_rev_pat=""
local resolved_rev_pwd
resolved_rev_pwd="$(__lf_resolve_with_aliases FORGEJO_REVIEWER_PASSWORD HAL_9001_FORGEJO_PASSWORD)" || resolved_rev_pwd=""
# Git identity for HAL9000's commits. Defaults match Jeff's confirmed
# production env (`CleverThis` / `hal9000@cleverthis.com`) — also the
# author of every existing HAL9000 commit on the canonical repo.
local resolved_git_name
resolved_git_name="$(__lf_resolve_with_aliases GIT_USER_NAME)" || resolved_git_name="CleverThis"
local resolved_git_email
resolved_git_email="$(__lf_resolve_with_aliases GIT_USER_EMAIL)" || resolved_git_email="hal9000@cleverthis.com"
# CA_MAX_PARALLEL_WORKERS is optional; we pass it through if it exists.
local resolved_workers
resolved_workers="$(__lf_resolve_with_aliases CA_MAX_PARALLEL_WORKERS)" || resolved_workers=""
# FORGEJO_USERNAME / FORGEJO_REVIEWER_USERNAME come from .env or whoami.
local resolved_user
resolved_user="$(__lf_resolve_with_aliases FORGEJO_USERNAME)" || resolved_user=""
if [[ -z "$resolved_user" && -n "$resolved_pat" ]]; then
resolved_user="$(__lf_whoami "$resolved_pat")"
fi
local resolved_rev_user
resolved_rev_user="$(__lf_resolve_with_aliases FORGEJO_REVIEWER_USERNAME)" || resolved_rev_user=""
if [[ -z "$resolved_rev_user" && -n "$resolved_rev_pat" ]]; then
resolved_rev_user="$(__lf_whoami "$resolved_rev_pat")"
fi
# ─── Step 3 — required-var validation ─────────────────────────────
# The auto-agents agent will exit on startup if any of these are
# missing, so we surface that failure here instead of letting it crash
# 30 minutes into a run.
local -a missing=()
[[ -n "$resolved_pat" ]] || missing+=("FORGEJO_PAT (or HAL_9000_API_KEY)")
[[ -n "$resolved_pwd" ]] || missing+=("FORGEJO_PASSWORD (or HAL_9000_FORGEJO_PASSWORD)")
[[ -n "$resolved_user" ]] || missing+=("FORGEJO_USERNAME (no whoami match — PAT invalid?)")
[[ -n "$resolved_rev_pat" ]] || missing+=("FORGEJO_REVIEWER_PAT (or HAL_9001_API_KEY)")
[[ -n "$resolved_rev_pwd" ]] || missing+=("FORGEJO_REVIEWER_PASSWORD (or HAL_9001_FORGEJO_PASSWORD)")
[[ -n "$resolved_rev_user" ]] || missing+=("FORGEJO_REVIEWER_USERNAME (no whoami match — reviewer PAT invalid?)")
if (( ${#missing[@]} > 0 )); then
echo "ERROR: required env vars per .opencode/agents/auto-agents.md missing or unresolvable:" >&2
local m
for m in "${missing[@]}"; do
echo " - $m" >&2
done
echo "Refusing to launch fork-mode pipeline; nothing exported." >&2
return 1
fi
# ─── Step 4 — validate fork repo + driver permission ─────────────
local validation_json
if ! validation_json="$(
GITEA_TOKEN="$resolved_gitea" \
FORK_OWNER="$fork_owner" \
FORK_REPO="$fork_repo" \
FORK_API_BASE="$fork_api_base" \
"$py" - <<'PY'
import json
import os
import sys
import urllib.error
import urllib.request
token = os.environ["GITEA_TOKEN"]
owner = os.environ["FORK_OWNER"]
repo = os.environ["FORK_REPO"]
base = os.environ["FORK_API_BASE"].rstrip("/")
url = f"{base}/repos/{owner}/{repo}"
req = urllib.request.Request(
url,
headers={"Authorization": f"token {token}", "Accept": "application/json"},
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
body = json.loads(resp.read())
except urllib.error.HTTPError as e:
msg = e.read().decode("utf-8", "replace")[:200]
print(f"ERROR: GET {url} returned HTTP {e.code}: {msg}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"ERROR: GET {url} failed: {e!r}", file=sys.stderr)
sys.exit(1)
problems: list[str] = []
if not body.get("fork"):
problems.append(
f"target {owner}/{repo} is not a fork (fork={body.get('fork')!r}); "
"refusing to launch fork-mode pipeline against a non-fork."
)
parent_full = (body.get("parent") or {}).get("full_name")
perms = body.get("permissions") or {}
if not perms.get("push"):
problems.append(
f"calling user lacks push permission on {owner}/{repo} "
f"(permissions={perms!r}); driver cannot push train branches."
)
if problems:
for p in problems:
print(f"ERROR: {p}", file=sys.stderr)
sys.exit(1)
print(json.dumps({
"owner": owner,
"repo": repo,
"full_name": body.get("full_name"),
"parent_full_name": parent_full,
"default_branch": body.get("default_branch"),
}))
PY
)"; then
echo "ERROR: fork validation failed; nothing exported." >&2
return 1
fi
# ─── Step 5 — commit resolved values to the parent shell ─────────
# Driver-side
export GITEA_TOKEN="$resolved_gitea"
export FORGEJO_OWNER="$fork_owner"
export FORGEJO_REPO="$fork_repo"
export FORGEJO_URL="$fork_url"
export FORGEJO_API_BASE="$fork_api_base"
export FORGEJO_DEFAULT_BRANCH="$fork_default_branch"
export FORGEJO_ORG="${FORGEJO_ORG:-cleveragents}"
# Auto-agents env contract (8 required + 1 optional)
export GIT_USER_NAME="$resolved_git_name"
export GIT_USER_EMAIL="$resolved_git_email"
export FORGEJO_PAT="$resolved_pat"
export FORGEJO_USERNAME="$resolved_user"
export FORGEJO_PASSWORD="$resolved_pwd"
export FORGEJO_REVIEWER_PAT="$resolved_rev_pat"
export FORGEJO_REVIEWER_USERNAME="$resolved_rev_user"
export FORGEJO_REVIEWER_PASSWORD="$resolved_rev_pwd"
if [[ -n "$resolved_workers" ]]; then
export CA_MAX_PARALLEL_WORKERS="$resolved_workers"
fi
# G11 (2026-05-15): default the implementer estimator ON for fork-mode runs
# so the dispatcher exercises the new confidence-driven tier selection
# rather than the Tier-0 short-circuit. Operators can opt out for a single
# cycle by pre-exporting IMPLEMENTER_ESTIMATOR_ENABLED=0 before sourcing.
export IMPLEMENTER_ESTIMATOR_ENABLED="${IMPLEMENTER_ESTIMATOR_ENABLED:-1}"
# Merge driver is normally silent on idle cycles (only WARN/ERROR surface
# at default INFO level). Fork-mode runs are operator-driven debugging
# sessions where the cycle-by-cycle "what did the driver check / why did
# it skip this PR" trace is the load-bearing signal. Default to DEBUG so
# the loop-merge log is useful out of the box; pre-export
# MERGE_DRIVER_LOG_LEVEL=INFO before sourcing if you want production-style
# quiet output for a one-off test.
export MERGE_DRIVER_LOG_LEVEL="${MERGE_DRIVER_LOG_LEVEL:-DEBUG}"
# Phase 2 of the .drew/planning/fix list_prs_by_filter.md cutover
# (2026-05-16): reviewer dispatcher uses the Python-side delta-cached
# PR enumeration (`_pr_classification_cache.refresh_then_filter`)
# for the 5 work-group filters instead of subprocessing the
# `list_prs_*.ts` scripts. Eliminates the recurring 120s
# subprocess.TimeoutExpired on `list_prs_missing_ci_checks.ts`.
# Default-ON in fork-mode; pre-export `=0` for emergency rollback
# to the legacy TS-script path.
export REVIEW_DISPATCHER_USE_PYTHON_FILTERS="${REVIEW_DISPATCHER_USE_PYTHON_FILTERS:-1}"
# ─── Step 6 — banner ─────────────────────────────────────────────
local parent_full
parent_full="$(printf '%s' "$validation_json" | "$py" -c '
import json,sys
d=json.load(sys.stdin)
print(d.get("parent_full_name") or "(none)")')"
# Identity check on the synthesized GITEA_TOKEN (typically same as
# FORGEJO_PAT, but operators can override with an admin PAT).
local gitea_login
gitea_login="$(__lf_whoami "$resolved_gitea")"
local gitea_note=""
if [[ "$resolved_gitea" == "$resolved_pat" ]]; then
gitea_note="(== FORGEJO_PAT)"
else
gitea_note="(operator-overridden)"
fi
echo "=================================================================="
echo " Fork-mode env locked for the auto-agents pipeline"
echo " ----------------------------------------------------------------"
echo " Repo target"
echo " FORGEJO_OWNER = $FORGEJO_OWNER"
echo " FORGEJO_REPO = $FORGEJO_REPO"
echo " FORGEJO_URL = $FORGEJO_URL"
echo " FORGEJO_API_BASE = $FORGEJO_API_BASE"
echo " FORGEJO_DEFAULT_BRANCH = $FORGEJO_DEFAULT_BRANCH"
echo " FORGEJO_ORG = $FORGEJO_ORG (org-label fallback only)"
echo " Fork parent = $parent_full"
echo
echo " Identities (production-fidelity: HAL9000 + HAL9001 only)"
echo " FORGEJO_PAT -> ${FORGEJO_USERNAME} (driver + LLM workers, ${#FORGEJO_PAT} chars)"
echo " FORGEJO_PASSWORD = set (${#FORGEJO_PASSWORD} chars)"
echo " FORGEJO_REVIEWER_PAT -> ${FORGEJO_REVIEWER_USERNAME} (umbrella approver, ${#FORGEJO_REVIEWER_PAT} chars)"
echo " FORGEJO_REVIEWER_PASSWORD = set (${#FORGEJO_REVIEWER_PASSWORD} chars)"
echo " GITEA_TOKEN -> ${gitea_login:-<unknown>} $gitea_note"
echo
echo " Git author for HAL9000 commits"
echo " GIT_USER_NAME = $GIT_USER_NAME"
echo " GIT_USER_EMAIL = $GIT_USER_EMAIL"
if [[ -n "${CA_MAX_PARALLEL_WORKERS:-}" ]]; then
echo
echo " Parallelism"
echo " CA_MAX_PARALLEL_WORKERS = $CA_MAX_PARALLEL_WORKERS"
fi
echo "=================================================================="
return 0
}
__lf_main "$@"
__lf_rc=$?
if [[ $__lf_sourced -eq 0 && $__lf_rc -eq 0 ]]; then
echo
echo "WARNING: this script was EXECUTED, not sourced. The exports above"
echo "will not persist into your shell. Re-run as:"
echo
echo " source tools/launch_fork.sh"
echo
fi
# Bash treats `return` outside a function as an error when EXECUTED, so
# we have to branch on sourced-vs-executed here. When sourced we MUST
# `return` (an `exit` would terminate the parent shell). When executed
# we MUST `exit`.
if [[ $__lf_sourced -eq 1 ]]; then
return $__lf_rc
else
exit $__lf_rc
fi