#!/usr/bin/env bash # # debug-agent-prompts.sh — Dump the exact system prompt the context engine # would produce for every built-in agent (plus the main / orchestrator # agent), so prompt-engineering changes can be reviewed in one place. # # Each prompt is written to a numbered file under the output directory # along with a side-car `.meta.txt` containing the metadata banner # (agent id, model, tool count, cache boundary, …) that the CLI prints # to stderr. Useful workflow: # # bash scripts/debug-agent-prompts.sh # diff -u prompts.before/skills_agent.md prompts.after/skills_agent.md # # When run with `--stub-composio` (the default) the dumper injects the # five Composio meta-tools into the registry even on machines that are # not signed in, so `skills_agent` always renders the full Composio # surface. Pass `--no-stub-composio` to see the raw on-disk state instead # (useful for sanity-checking the unauthed onboarding path). # # The dumper runs against the currently-logged-in user's workspace # (`$OPENHUMAN_WORKSPACE`, falling back to `~/.openhuman/workspace`) so # onboarding-generated files like `PROFILE.md` appear in the dump. Export # `OPENHUMAN_WORKSPACE=` before running if you want to target a # different workspace. # # Usage: # bash scripts/debug-agent-prompts.sh [--out ] [--no-stub-composio] [--with-tools] [-v] # # The output directory is wiped and recreated at the start of each run # so the snapshot only reflects the current agent set — stale files from # an earlier run cannot hide a regression. # # Defaults: # --out ./prompt-dumps (deleted + recreated each run) # --stub-composio ON (override with --no-stub-composio) # --with-tools OFF (pass to also list each agent's tool names) # set -euo pipefail # ── Locate repo root + binary ───────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" BIN="${REPO_ROOT}/target/debug/openhuman-core" # Always run `cargo build` — it no-ops when the binary is already # up-to-date, and re-links quickly when it isn't. The old `-x` existence # check let a stale debug binary survive across agent-registry changes # (e.g. new entries in `agents::BUILTINS`), which made this script # silently skip newly added agents like `welcome`. echo "[debug-agent-prompts] building openhuman-core (no-op if up-to-date) …" >&2 ( cd "${REPO_ROOT}" && cargo build --manifest-path Cargo.toml --bin openhuman-core >&2 ) # ── Parse flags ─────────────────────────────────────────────────────────── OUT_DIR="" STUB_COMPOSIO=1 WITH_TOOLS=0 VERBOSE_FLAG=() while [[ $# -gt 0 ]]; do case "$1" in --out) if [[ -z "${2-}" ]] || [[ "${2-}" == -* ]]; then echo "[debug-agent-prompts] missing value for --out" >&2 exit 64 fi OUT_DIR="$2" shift 2 ;; --stub-composio) STUB_COMPOSIO=1 shift ;; --no-stub-composio) STUB_COMPOSIO=0 shift ;; --with-tools) WITH_TOOLS=1 shift ;; -v|--verbose) VERBOSE_FLAG=(-v) shift ;; -h|--help) sed -n '2,30p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "[debug-agent-prompts] unknown flag: $1" >&2 exit 64 ;; esac done if [[ -z "${OUT_DIR}" ]]; then OUT_DIR="${REPO_ROOT}/prompt-dumps" fi # ── Validate & canonicalize OUT_DIR before `rm -rf` ───────────────────── # The output directory is wiped at the start of each run. Literal string # matching against "/" / $HOME / $REPO_ROOT is not enough on its own: # trailing slashes, ".", "..", or symlinked paths can slip past and # trigger `rm -rf` on a sensitive target. So: # # 1. Reject obviously bad inputs up-front ("", ".", "..", relative). # 2. Canonicalize OUT_DIR and REPO_ROOT via `realpath` (falling back # to python when realpath is unavailable on barebones macOS). # 3. Match the canonicalized form against the disallow list. # 4. Only then `rm -rf` the canonicalized path. case "${OUT_DIR}" in "" | "." | "..") echo "[debug-agent-prompts] refusing to wipe --out='${OUT_DIR}' (relative/empty)" >&2 exit 64 ;; esac if [[ "${OUT_DIR}" != /* ]]; then echo "[debug-agent-prompts] --out must be an absolute path (starts with '/'), got '${OUT_DIR}'" >&2 exit 64 fi canonicalize() { local p="$1" # `realpath` is GNU + modern macOS (coreutils), and `readlink -f` on # Linux. Try both; if neither resolves the path (target missing) we # fall back to python3, which handles symlinks even for non-existent # leaves via `os.path.realpath`. if command -v realpath >/dev/null 2>&1; then realpath -m -- "${p}" 2>/dev/null && return 0 fi if command -v readlink >/dev/null 2>&1 && readlink -f / >/dev/null 2>&1; then readlink -f -- "${p}" 2>/dev/null && return 0 fi python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "${p}" } resolved_out="$(canonicalize "${OUT_DIR}")" resolved_repo="$(canonicalize "${REPO_ROOT}")" resolved_home="$(canonicalize "${HOME}")" if [[ -z "${resolved_out}" ]]; then echo "[debug-agent-prompts] failed to canonicalize --out='${OUT_DIR}'" >&2 exit 64 fi case "${resolved_out}" in "/" | "${resolved_home}" | "${resolved_repo}") echo "[debug-agent-prompts] refusing to wipe --out (resolves to ${resolved_out})" >&2 exit 64 ;; esac # Use the canonicalized path from here on so every subsequent command # (rm, mkdir, per-agent dump writes) operates on the same resolved # target — no symlink window between validation and deletion. OUT_DIR="${resolved_out}" rm -rf "${OUT_DIR}" mkdir -p "${OUT_DIR}" # Point the binary at the real, currently-logged-in user's workspace so # onboarding-generated files like `PROFILE.md` appear in the dump. A # throwaway mktemp workspace would silently hide them. # # Resolution order (matches what the desktop app does): # 1. $OPENHUMAN_WORKSPACE if explicitly exported. # 2. ~/.openhuman/users//workspace, where active_user_id # is read from ~/.openhuman/active_user.toml. This is the path the # Tauri app writes PROFILE.md into after onboarding. # 3. ~/.openhuman/workspace as a last-resort fallback for pre-multi-user # installs. # # The binary auto-seeds SOUL.md/IDENTITY.md/HEARTBEAT.md when they are # missing, so pointing at the real workspace is read-mostly — the only # writes are the same self-healing writes that happen on every normal # agent run. if [[ -n "${OPENHUMAN_WORKSPACE:-}" ]]; then WORKSPACE="${OPENHUMAN_WORKSPACE}" else ACTIVE_USER_TOML="${HOME}/.openhuman/active_user.toml" ACTIVE_USER_ID="" if [[ -f "${ACTIVE_USER_TOML}" ]]; then # Extract `user_id = "…"` without pulling in a TOML parser — the file # is one or two lines and the shape is stable (config/ops.rs writes # it with a plain quoted string). ACTIVE_USER_ID="$(sed -n 's/^[[:space:]]*user_id[[:space:]]*=[[:space:]]*"\([^"]*\)"[[:space:]]*$/\1/p' "${ACTIVE_USER_TOML}" | head -n 1)" fi if [[ -n "${ACTIVE_USER_ID}" && -d "${HOME}/.openhuman/users/${ACTIVE_USER_ID}/workspace" ]]; then WORKSPACE="${HOME}/.openhuman/users/${ACTIVE_USER_ID}/workspace" else WORKSPACE="${HOME}/.openhuman/workspace" fi fi if [[ ! -d "${WORKSPACE}" ]]; then echo "[debug-agent-prompts] workspace not found: ${WORKSPACE}" >&2 echo "[debug-agent-prompts] complete onboarding in the app first, or export OPENHUMAN_WORKSPACE=." >&2 exit 66 fi if [[ -f "${WORKSPACE}/PROFILE.md" ]]; then PROFILE_STATE="present" else PROFILE_STATE="MISSING (onboarding enrichment has not run)" fi echo "[debug-agent-prompts] output dir : ${OUT_DIR}" >&2 echo "[debug-agent-prompts] workspace : ${WORKSPACE}" >&2 echo "[debug-agent-prompts] PROFILE.md : ${PROFILE_STATE}" >&2 echo "[debug-agent-prompts] stub composio: $([[ ${STUB_COMPOSIO} -eq 1 ]] && echo on || echo off)" >&2 echo >&2 # ── Discover agent ids from `agent list --json` ─────────────────────────── # `mapfile` is bash 4+, but macOS ships bash 3 — use a portable # read-while-IFS loop instead so the script works out of the box on a # vanilla `/bin/bash`. AGENT_LIST_JSON="$("${BIN}" agent list --workspace "${WORKSPACE}" --json 2>/dev/null)" AGENT_IDS=() while IFS= read -r line; do [[ -n "${line}" ]] && AGENT_IDS+=("${line}") done < <(printf '%s' "${AGENT_LIST_JSON}" | python3 -c ' import json, sys for entry in json.load(sys.stdin): aid = entry.get("id", "") # The synthetic `fork` definition replays the parent verbatim and # has no standalone prompt — skip it. if aid and aid != "fork": print(aid) ') # Always include the main / orchestrator prompt as the first dump. TARGETS=("main" "${AGENT_IDS[@]}") # ── Build common dump-prompt flag list ──────────────────────────────────── DUMP_FLAGS=(--workspace "${WORKSPACE}") if [[ ${STUB_COMPOSIO} -eq 1 ]]; then DUMP_FLAGS+=(--stub-composio) fi if [[ ${WITH_TOOLS} -eq 1 ]]; then DUMP_FLAGS+=(--with-tools) fi if [[ ${#VERBOSE_FLAG[@]} -gt 0 ]]; then DUMP_FLAGS+=("${VERBOSE_FLAG[@]}") fi # ── Dump every target ───────────────────────────────────────────────────── INDEX=0 SUMMARY="" for AGENT in "${TARGETS[@]}"; do INDEX=$((INDEX + 1)) SAFE_NAME="$(printf '%s' "${AGENT}" | tr -c 'A-Za-z0-9._-' '_')" PROMPT_PATH="${OUT_DIR}/${INDEX}_${SAFE_NAME}.md" META_PATH="${OUT_DIR}/${INDEX}_${SAFE_NAME}.meta.txt" printf '[debug-agent-prompts] %-20s → %s\n' "${AGENT}" "${PROMPT_PATH}" >&2 if "${BIN}" agent dump-prompt --agent "${AGENT}" "${DUMP_FLAGS[@]}" \ > "${PROMPT_PATH}" 2> "${META_PATH}"; then LINES="$(wc -l < "${PROMPT_PATH}" | tr -d ' ')" TOOL_COUNT="$(grep -E '^tool_count:' "${META_PATH}" | awk '{print $2}')" SKILL_COUNT="$(grep -E '^skill_tools:' "${META_PATH}" | awk '{print $2}')" SUMMARY+="$(printf '%-20s lines=%-5s tools=%-4s skill=%-4s\n' \ "${AGENT}" "${LINES}" "${TOOL_COUNT:-?}" "${SKILL_COUNT:-?}") " else echo "[debug-agent-prompts] ✘ failed to dump ${AGENT} (see ${META_PATH})" >&2 SUMMARY+="$(printf '%-20s FAILED — see %s\n' "${AGENT}" "${META_PATH}") " fi done # ── Write a summary index file alongside the dumps ──────────────────────── SUMMARY_PATH="${OUT_DIR}/SUMMARY.txt" { echo "OpenHuman agent prompt dump summary" echo "Generated: $(date -u +%Y-%m-%dT%H:%M:%SZ)" echo "Workspace: ${WORKSPACE}" echo "Stub composio: $([[ ${STUB_COMPOSIO} -eq 1 ]] && echo on || echo off)" echo echo "${SUMMARY}" } > "${SUMMARY_PATH}" echo >&2 echo "[debug-agent-prompts] done — ${INDEX} prompts dumped" >&2 echo "[debug-agent-prompts] summary : ${SUMMARY_PATH}" >&2