From b5854e66fe45f799575553645f5b937eb53c4a92 Mon Sep 17 00:00:00 2001 From: Baudbot Date: Tue, 24 Feb 2026 15:11:51 -0500 Subject: [PATCH 1/2] feat: replace `baudbot attach` with `baudbot debug` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old `attach` command spawned a new pi instance connected to the control-agent's control socket. This didn't actually attach to the running session — it created a duplicate agent, causing confusion (two control-agents responding, bridge routing conflicts, etc.). Replace with `baudbot debug` which launches the debug-agent skill: a dedicated observability tool with a live dashboard showing control-agent activity, health metrics, and system state. Key differences: - No longer pretends to "attach" — clearly a separate debug tool - Includes live dashboard (debug-dashboard.ts extension) - Has --session-control so it can send_to_session to running agents - Auto-detects model from API keys (uses mid-tier: sonnet/gpt-4.1) - Supports --model override - Unsets PKG_EXECPATH to avoid varlock poisoning in child processes - Requires root (sudo baudbot debug) like other admin commands Removed: - cmd_attach and all attach-related helpers (pause_before_attach, etc.) - --pi/--tmux mode flags (no longer applicable) --- bin/baudbot | 6 +- bin/lib/baudbot-runtime.sh | 183 +++++++++++++++---------------------- 2 files changed, 75 insertions(+), 114 deletions(-) diff --git a/bin/baudbot b/bin/baudbot index 525ae7a..373dcc3 100755 --- a/bin/baudbot +++ b/bin/baudbot @@ -137,7 +137,7 @@ usage() { echo " restart Restart the agent" echo " status Show agent status + deployed version + broker connection" echo " logs Tail agent logs" - echo " attach Attach to control-agent by default; supports --pi/--tmux" + echo " debug Launch debug agent with live dashboard for system observability" echo " sessions List agent tmux and pi sessions (name → id)" echo "" echo -e "${BOLD}Setup:${RESET}" @@ -324,7 +324,7 @@ else cmd_status() { echo "❌ Missing CLI runtime helper. Run: sudo baudbot deploy"; exit 1; } cmd_logs() { echo "❌ Missing CLI runtime helper. Run: sudo baudbot deploy"; exit 1; } cmd_sessions() { echo "❌ Missing CLI runtime helper. Run: sudo baudbot deploy"; exit 1; } - cmd_attach() { echo "❌ Missing CLI runtime helper. Run: sudo baudbot deploy"; exit 1; } + cmd_debug() { echo "❌ Missing CLI runtime helper. Run: sudo baudbot deploy"; exit 1; } fi require_systemd_runtime() { @@ -411,7 +411,7 @@ register_command "restart" "function" "cmd_systemctl_restart" "1" "0" "" register_command "status" "function" "cmd_status" "0" "0" "" register_command "logs" "function" "cmd_logs" "0" "0" "" register_command "sessions" "function" "cmd_sessions" "0" "0" "" -register_command "attach" "function" "cmd_attach" "0" "0" "" +register_command "debug" "function" "cmd_debug" "1" "0" "" register_command "config" "exec" "$BAUDBOT_ROOT/bin/config.sh" "0" "0" "" register_command "env" "exec" "$BAUDBOT_ROOT/bin/env.sh" "0" "0" "" register_command "deploy" "exec" "$BAUDBOT_ROOT/bin/deploy.sh" "1" "0" "" diff --git a/bin/lib/baudbot-runtime.sh b/bin/lib/baudbot-runtime.sh index 3295a7f..6b374c3 100644 --- a/bin/lib/baudbot-runtime.sh +++ b/bin/lib/baudbot-runtime.sh @@ -324,20 +324,6 @@ resolve_pi_session_id() { return 1 } -pause_before_attach() { - if [ "${BAUDBOT_ATTACH_NO_PAUSE:-0}" = "1" ]; then - return 0 - fi - - if [ -t 0 ] && [ -t 1 ]; then - echo -e "${DIM}Press Enter to attach (Ctrl+C to cancel)...${RESET}" - # shellcheck disable=SC2162 - read _ - else - sleep 2 - fi -} - cmd_status() { if has_systemd && systemctl is-enabled baudbot &>/dev/null; then local status_rc=0 @@ -433,128 +419,103 @@ cmd_sessions() { fi } -cmd_attach() { - require_root "attach" +cmd_debug() { + require_root "debug" local AGENT_USER="baudbot_agent" local AGENT_HOME="/home/$AGENT_USER" - local ATTACH_MODE="auto" - local TARGET="" - local tmux_target pi_target + local MODEL="" while [ "$#" -gt 0 ]; do case "$1" in - --pi) - ATTACH_MODE="pi" - shift - ;; - --tmux) - ATTACH_MODE="tmux" - shift + --model) + MODEL="$2" + shift 2 ;; -h|--help) - echo "Usage: sudo baudbot attach [--pi|--tmux] [session-name|session-id]" + echo "Usage: sudo baudbot debug [--model ]" echo "" - echo "Examples:" - echo " sudo baudbot attach # defaults to control-agent" - echo " sudo baudbot attach --pi control-agent" - echo " sudo baudbot attach --pi " - echo " sudo baudbot attach --tmux sentry-agent" + echo "Launch a debug agent with a live dashboard showing control-agent" + echo "activity, health metrics, and system state. Use send_to_session" + echo "to communicate with running agents." + echo "" + echo "Options:" + echo " --model LLM model to use (default: auto-detect from API keys)" + echo "" + echo "Exit: Ctrl+C (does NOT affect the running control-agent)" exit 0 ;; *) - if [ -n "$TARGET" ]; then - echo "❌ Too many arguments for attach" - exit 1 - fi - TARGET="$1" - shift + echo "❌ Unknown option: $1" + echo "Usage: sudo baudbot debug [--model ]" + exit 1 ;; esac done - if [ -z "$TARGET" ]; then - TARGET="control-agent" - fi - - attach_tmux_session() { - local tmux_target="$1" - echo -e "${BOLD}${CYAN}Attaching to tmux session:${RESET} $tmux_target" - echo -e "${GREEN}Safe detach:${RESET} Ctrl+b, d ${DIM}(keeps agent running)${RESET}" - echo "" - pause_before_attach - exec sudo -u "$AGENT_USER" tmux attach-session -t "$tmux_target" - } - - attach_pi_session() { - local pi_target="$1" - echo -e "${BOLD}${CYAN}Attaching to pi session:${RESET} $pi_target" - echo -e "${BOLD}${YELLOW}Safe detach (does NOT stop the agent):${RESET}" - echo -e " ${YELLOW}1)${RESET} Press Ctrl+C once to clear input/cancel local prompt" - echo -e " ${YELLOW}2)${RESET} Press Ctrl+C again to exit this client" - echo -e " ${GREEN}Agent keeps running under systemd in the background.${RESET}" - echo "" - pause_before_attach - local node_bin_dir="" - node_bin_dir="$(bb_resolve_runtime_node_bin_dir "$AGENT_HOME")" - exec sudo -u "$AGENT_USER" bash -lc "export PATH='$AGENT_HOME/.varlock/bin:$node_bin_dir':\$PATH; cd ~; varlock run --path ~/.config/ -- pi --session '$pi_target'" - } - - choose_tmux_target() { - local requested="${1:-}" - local first - - if [ -n "$requested" ]; then - if sudo -u "$AGENT_USER" tmux has-session -t "$requested" 2>/dev/null; then - echo "$requested" - return 0 + # Auto-detect model from env if not specified + if [ -z "$MODEL" ]; then + # Load env vars to check API keys + local env_file="$AGENT_HOME/.config/.env" + if [ -r "$env_file" ]; then + local env_val="" + env_val="$(grep -E '^BAUDBOT_MODEL=' "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- || true)" + if [ -n "$env_val" ]; then + MODEL="$env_val" + else + env_val="$(grep -E '^ANTHROPIC_API_KEY=' "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- || true)" + if [ -n "$env_val" ]; then + MODEL="anthropic/claude-sonnet-4-5" + else + env_val="$(grep -E '^OPENAI_API_KEY=' "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- || true)" + if [ -n "$env_val" ]; then + MODEL="openai/gpt-4.1" + else + env_val="$(grep -E '^GEMINI_API_KEY=' "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- || true)" + if [ -n "$env_val" ]; then + MODEL="google/gemini-3-flash-preview" + fi + fi + fi fi - return 1 fi - first=$(sudo -u "$AGENT_USER" tmux ls -F '#{session_name}' 2>/dev/null | head -1) - [ -n "$first" ] || return 1 - echo "$first" - return 0 - } - - choose_pi_target() { - local requested="${1:-}" - local resolved - - if ! resolved=$(resolve_pi_session_id "$AGENT_USER" "$requested"); then - return 1 - fi - - [ -n "$resolved" ] || return 1 - echo "$resolved" - return 0 - } - - if [ "$ATTACH_MODE" = "tmux" ]; then - if tmux_target=$(choose_tmux_target "$TARGET"); then - attach_tmux_session "$tmux_target" + if [ -z "$MODEL" ]; then + echo "❌ No LLM API key found. Set --model or configure API keys in $AGENT_HOME/.config/.env" + exit 1 fi - echo "❌ tmux session not found. See: sudo baudbot sessions" - exit 1 fi - if [ "$ATTACH_MODE" = "pi" ]; then - if pi_target=$(choose_pi_target "$TARGET"); then - attach_pi_session "$pi_target" - fi - echo "❌ pi session not found. See: sudo baudbot sessions" - exit 1 + local SKILL_DIR="$AGENT_HOME/.pi/agent/skills/debug-agent" + if [ ! -f "$SKILL_DIR/SKILL.md" ]; then + # Fall back to deployed location + SKILL_DIR="/opt/baudbot/current/pi/skills/debug-agent" fi - if pi_target=$(choose_pi_target "$TARGET"); then - attach_pi_session "$pi_target" + if [ ! -f "$SKILL_DIR/SKILL.md" ]; then + echo "❌ Debug agent skill not found. Run: sudo baudbot deploy" + exit 1 fi - if tmux_target=$(choose_tmux_target "$TARGET"); then - attach_tmux_session "$tmux_target" - fi + echo -e "${BOLD}${CYAN}Launching debug agent${RESET}" + echo -e "${DIM}Model: $MODEL${RESET}" + echo -e "${DIM}Skill: $SKILL_DIR${RESET}" + echo -e "${GREEN}Exit: Ctrl+C (does NOT affect the running control-agent)${RESET}" + echo "" - echo "❌ No matching tmux/pi session found. See: sudo baudbot sessions" - exit 1 + local node_bin_dir="" + node_bin_dir="$(bb_resolve_runtime_node_bin_dir "$AGENT_HOME")" + + exec sudo -u "$AGENT_USER" bash -lc " + unset PKG_EXECPATH + export PATH='$AGENT_HOME/.varlock/bin:$node_bin_dir':\$PATH + export VARLOCK_TELEMETRY_DISABLED=1 + cd ~ + varlock run --path ~/.config/ -- pi \ + --session-control \ + --model '$MODEL' \ + --skill '$SKILL_DIR' \ + -e '$SKILL_DIR/debug-dashboard.ts' \ + '/skill:debug-agent' + " } From b417447826f2b7379d8a07c12773f8b6436e9bfc Mon Sep 17 00:00:00 2001 From: Ben Vinegar Date: Tue, 24 Feb 2026 15:19:37 -0500 Subject: [PATCH 2/2] =?UTF-8?q?tests:=20fix=20CI=20failures=20for=20attach?= =?UTF-8?q?=E2=86=92debug=20rename,=20lint=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Update baudbot.test.sh: rename attach tests to debug (matches cmd rename) - Update test mock runtime stubs: cmd_attach → cmd_debug - heartbeat.ts: remove unused 'resolve' import, isNaN → Number.isNaN, remove useless continue, remove unused catch binding - heartbeat.test.mjs: remove unused imports (fs, path, os, beforeEach, afterEach) and unused helpers (setup, teardown, writeFile, and DEFAULT_INTERVAL_MS/MIN_INTERVAL_MS constants) - bin/baudbot: suppress shellcheck SC2034 for YELLOW (used by config.sh) - baudbot-runtime.sh: add MODEL input validation as defense-in-depth --- bin/baudbot | 1 + bin/baudbot.test.sh | 14 +++++++------- bin/lib/baudbot-runtime.sh | 6 ++++++ pi/extensions/heartbeat.test.mjs | 24 +----------------------- pi/extensions/heartbeat.ts | 8 ++++---- 5 files changed, 19 insertions(+), 34 deletions(-) diff --git a/bin/baudbot b/bin/baudbot index 373dcc3..c206790 100755 --- a/bin/baudbot +++ b/bin/baudbot @@ -50,6 +50,7 @@ json_get_string_stdin_or_empty() { } # Colors (disabled if not a terminal) +# shellcheck disable=SC2034 # YELLOW used by sourced scripts (config.sh) if [ -t 1 ]; then BOLD='\033[1m' DIM='\033[2m' diff --git a/bin/baudbot.test.sh b/bin/baudbot.test.sh index 8867be2..0cb940d 100644 --- a/bin/baudbot.test.sh +++ b/bin/baudbot.test.sh @@ -59,7 +59,7 @@ test_status_dispatches_via_runtime_module() { cmd_status() { echo "status-dispatch-ok"; } cmd_logs() { echo "logs-dispatch-ok"; } cmd_sessions() { echo "sessions-dispatch-ok"; } -cmd_attach() { echo "attach-dispatch-ok"; } +cmd_debug() { echo "debug-dispatch-ok"; } has_systemd() { return 1; } EOF @@ -68,7 +68,7 @@ EOF ) } -test_attach_requires_root() { +test_debug_requires_root() { ( set -euo pipefail local tmp fakebin out @@ -89,12 +89,12 @@ fi EOF chmod +x "$fakebin/id" - if PATH="$fakebin:$PATH" BAUDBOT_ROOT="$REPO_ROOT" bash "$CLI" attach >/tmp/baudbot-attach.out 2>&1; then + if PATH="$fakebin:$PATH" BAUDBOT_ROOT="$REPO_ROOT" bash "$CLI" debug >/tmp/baudbot-debug.out 2>&1; then return 1 fi - out="$(cat /tmp/baudbot-attach.out)" - rm -f /tmp/baudbot-attach.out + out="$(cat /tmp/baudbot-debug.out)" + rm -f /tmp/baudbot-debug.out printf '%s\n' "$out" | grep -q "requires root" ) } @@ -148,7 +148,7 @@ has_systemd() { return 0; } cmd_status() { :; } cmd_logs() { :; } cmd_sessions() { :; } -cmd_attach() { :; } +cmd_debug() { :; } EOF cat > "$fakebin/id" <<'EOF' @@ -189,7 +189,7 @@ echo "" run_test "version reads package.json" test_version_uses_package_json run_test "status dispatches via runtime module" test_status_dispatches_via_runtime_module -run_test "attach requires root" test_attach_requires_root +run_test "debug requires root" test_debug_requires_root run_test "broker register requires root" test_broker_register_requires_root run_test "restart restarts systemd" test_restart_restarts_systemd diff --git a/bin/lib/baudbot-runtime.sh b/bin/lib/baudbot-runtime.sh index 6b374c3..121cb0d 100644 --- a/bin/lib/baudbot-runtime.sh +++ b/bin/lib/baudbot-runtime.sh @@ -486,6 +486,12 @@ cmd_debug() { fi fi + # Validate MODEL — must be a safe provider/model string (alphanumeric, hyphens, dots, slashes) + if [[ ! "$MODEL" =~ ^[a-zA-Z0-9._/-]+$ ]]; then + echo "❌ Invalid model name: $MODEL" + exit 1 + fi + local SKILL_DIR="$AGENT_HOME/.pi/agent/skills/debug-agent" if [ ! -f "$SKILL_DIR/SKILL.md" ]; then # Fall back to deployed location diff --git a/pi/extensions/heartbeat.test.mjs b/pi/extensions/heartbeat.test.mjs index 3138b3f..11c63df 100644 --- a/pi/extensions/heartbeat.test.mjs +++ b/pi/extensions/heartbeat.test.mjs @@ -8,16 +8,11 @@ * Run: npx vitest run pi/extensions/heartbeat.test.mjs */ -import { describe, it, beforeEach, afterEach } from "vitest"; +import { describe, it } from "vitest"; import assert from "node:assert/strict"; -import fs from "node:fs"; -import path from "node:path"; -import os from "node:os"; // ── Replicate pure functions from heartbeat.ts v2 ─────────────────────────── -const DEFAULT_INTERVAL_MS = 10 * 60 * 1000; // 10 min -const MIN_INTERVAL_MS = 2 * 60 * 1000; // 2 min const BACKOFF_MULTIPLIER = 2; const MAX_BACKOFF_MS = 60 * 60 * 1000; // 1 hour const STUCK_TODO_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours @@ -70,23 +65,6 @@ function parseTodo(content) { // ── Test helpers ──────────────────────────────────────────────────────────── -let tmpDir; - -function setup() { - tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "heartbeat-test-")); -} - -function teardown() { - fs.rmSync(tmpDir, { recursive: true, force: true }); -} - -function writeFile(name, content) { - const p = path.join(tmpDir, name); - fs.mkdirSync(path.dirname(p), { recursive: true }); - fs.writeFileSync(p, content, "utf-8"); - return p; -} - // ── Tests ─────────────────────────────────────────────────────────────────── describe("heartbeat v2: isDisabledByEnv", () => { diff --git a/pi/extensions/heartbeat.ts b/pi/extensions/heartbeat.ts index 0f7f89b..c529160 100644 --- a/pi/extensions/heartbeat.ts +++ b/pi/extensions/heartbeat.ts @@ -25,7 +25,7 @@ import { Type } from "@sinclair/typebox"; import { StringEnum } from "@mariozechner/pi-ai"; import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { homedir } from "node:os"; -import { join, resolve } from "node:path"; +import { join } from "node:path"; const DEFAULT_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes const MIN_INTERVAL_MS = 2 * 60 * 1000; // 2 minutes @@ -270,7 +270,7 @@ function checkStuckTodos(): CheckResult[] { if (!createdAt) continue; const createdTime = new Date(createdAt).getTime(); - if (isNaN(createdTime)) continue; + if (Number.isNaN(createdTime)) continue; const age = now - createdTime; if (age < STUCK_TODO_THRESHOLD_MS) continue; @@ -341,7 +341,7 @@ function hasMatchingInProgressTodo(worktreeName: string): boolean { if (content.includes(pathPattern) || boundaryPattern.test(content)) return true; } } catch { - continue; + // skip unreadable todo files } } } catch { @@ -460,7 +460,7 @@ export default function heartbeatExtension(pi: ExtensionAPI): void { state.consecutiveErrors = 0; saveState(); - } catch (err) { + } catch { state.consecutiveErrors += 1; try { saveState();