Skip to content

Commit a4d7b06

Browse files
baudbot-agentBaudbotbenvinegar
authored
feat: replace baudbot attach with baudbot debug (#168)
Co-authored-by: Baudbot <hornet@agentmail.to> Co-authored-by: Ben Vinegar <ben@benv.ca>
1 parent 0a13f48 commit a4d7b06

5 files changed

Lines changed: 92 additions & 146 deletions

File tree

bin/baudbot

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ json_get_string_stdin_or_empty() {
5050
}
5151

5252
# Colors (disabled if not a terminal)
53+
# shellcheck disable=SC2034 # YELLOW used by sourced scripts (config.sh)
5354
if [ -t 1 ]; then
5455
BOLD='\033[1m'
5556
DIM='\033[2m'
@@ -137,7 +138,7 @@ usage() {
137138
echo " restart Restart the agent"
138139
echo " status Show agent status + deployed version + broker connection"
139140
echo " logs Tail agent logs"
140-
echo " attach Attach to control-agent by default; supports --pi/--tmux"
141+
echo " debug Launch debug agent with live dashboard for system observability"
141142
echo " sessions List agent tmux and pi sessions (name → id)"
142143
echo ""
143144
echo -e "${BOLD}Setup:${RESET}"
@@ -324,7 +325,7 @@ else
324325
cmd_status() { echo "❌ Missing CLI runtime helper. Run: sudo baudbot deploy"; exit 1; }
325326
cmd_logs() { echo "❌ Missing CLI runtime helper. Run: sudo baudbot deploy"; exit 1; }
326327
cmd_sessions() { echo "❌ Missing CLI runtime helper. Run: sudo baudbot deploy"; exit 1; }
327-
cmd_attach() { echo "❌ Missing CLI runtime helper. Run: sudo baudbot deploy"; exit 1; }
328+
cmd_debug() { echo "❌ Missing CLI runtime helper. Run: sudo baudbot deploy"; exit 1; }
328329
fi
329330

330331
require_systemd_runtime() {
@@ -411,7 +412,7 @@ register_command "restart" "function" "cmd_systemctl_restart" "1" "0" ""
411412
register_command "status" "function" "cmd_status" "0" "0" ""
412413
register_command "logs" "function" "cmd_logs" "0" "0" ""
413414
register_command "sessions" "function" "cmd_sessions" "0" "0" ""
414-
register_command "attach" "function" "cmd_attach" "0" "0" ""
415+
register_command "debug" "function" "cmd_debug" "1" "0" ""
415416
register_command "config" "exec" "$BAUDBOT_ROOT/bin/config.sh" "0" "0" ""
416417
register_command "env" "exec" "$BAUDBOT_ROOT/bin/env.sh" "0" "0" ""
417418
register_command "deploy" "exec" "$BAUDBOT_ROOT/bin/deploy.sh" "1" "0" ""

bin/baudbot.test.sh

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ test_status_dispatches_via_runtime_module() {
5959
cmd_status() { echo "status-dispatch-ok"; }
6060
cmd_logs() { echo "logs-dispatch-ok"; }
6161
cmd_sessions() { echo "sessions-dispatch-ok"; }
62-
cmd_attach() { echo "attach-dispatch-ok"; }
62+
cmd_debug() { echo "debug-dispatch-ok"; }
6363
has_systemd() { return 1; }
6464
EOF
6565

@@ -68,7 +68,7 @@ EOF
6868
)
6969
}
7070

71-
test_attach_requires_root() {
71+
test_debug_requires_root() {
7272
(
7373
set -euo pipefail
7474
local tmp fakebin out
@@ -89,12 +89,12 @@ fi
8989
EOF
9090
chmod +x "$fakebin/id"
9191

92-
if PATH="$fakebin:$PATH" BAUDBOT_ROOT="$REPO_ROOT" bash "$CLI" attach >/tmp/baudbot-attach.out 2>&1; then
92+
if PATH="$fakebin:$PATH" BAUDBOT_ROOT="$REPO_ROOT" bash "$CLI" debug >/tmp/baudbot-debug.out 2>&1; then
9393
return 1
9494
fi
9595

96-
out="$(cat /tmp/baudbot-attach.out)"
97-
rm -f /tmp/baudbot-attach.out
96+
out="$(cat /tmp/baudbot-debug.out)"
97+
rm -f /tmp/baudbot-debug.out
9898
printf '%s\n' "$out" | grep -q "requires root"
9999
)
100100
}
@@ -148,7 +148,7 @@ has_systemd() { return 0; }
148148
cmd_status() { :; }
149149
cmd_logs() { :; }
150150
cmd_sessions() { :; }
151-
cmd_attach() { :; }
151+
cmd_debug() { :; }
152152
EOF
153153

154154
cat > "$fakebin/id" <<'EOF'
@@ -189,7 +189,7 @@ echo ""
189189

190190
run_test "version reads package.json" test_version_uses_package_json
191191
run_test "status dispatches via runtime module" test_status_dispatches_via_runtime_module
192-
run_test "attach requires root" test_attach_requires_root
192+
run_test "debug requires root" test_debug_requires_root
193193
run_test "broker register requires root" test_broker_register_requires_root
194194
run_test "restart restarts systemd" test_restart_restarts_systemd
195195

bin/lib/baudbot-runtime.sh

Lines changed: 76 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -324,20 +324,6 @@ resolve_pi_session_id() {
324324
return 1
325325
}
326326

327-
pause_before_attach() {
328-
if [ "${BAUDBOT_ATTACH_NO_PAUSE:-0}" = "1" ]; then
329-
return 0
330-
fi
331-
332-
if [ -t 0 ] && [ -t 1 ]; then
333-
echo -e "${DIM}Press Enter to attach (Ctrl+C to cancel)...${RESET}"
334-
# shellcheck disable=SC2162
335-
read _
336-
else
337-
sleep 2
338-
fi
339-
}
340-
341327
cmd_status() {
342328
if has_systemd && systemctl is-enabled baudbot &>/dev/null; then
343329
local status_rc=0
@@ -433,128 +419,109 @@ cmd_sessions() {
433419
fi
434420
}
435421

436-
cmd_attach() {
437-
require_root "attach"
422+
cmd_debug() {
423+
require_root "debug"
438424

439425
local AGENT_USER="baudbot_agent"
440426
local AGENT_HOME="/home/$AGENT_USER"
441-
local ATTACH_MODE="auto"
442-
local TARGET=""
443-
local tmux_target pi_target
427+
local MODEL=""
444428

445429
while [ "$#" -gt 0 ]; do
446430
case "$1" in
447-
--pi)
448-
ATTACH_MODE="pi"
449-
shift
450-
;;
451-
--tmux)
452-
ATTACH_MODE="tmux"
453-
shift
431+
--model)
432+
MODEL="$2"
433+
shift 2
454434
;;
455435
-h|--help)
456-
echo "Usage: sudo baudbot attach [--pi|--tmux] [session-name|session-id]"
436+
echo "Usage: sudo baudbot debug [--model <model>]"
437+
echo ""
438+
echo "Launch a debug agent with a live dashboard showing control-agent"
439+
echo "activity, health metrics, and system state. Use send_to_session"
440+
echo "to communicate with running agents."
457441
echo ""
458-
echo "Examples:"
459-
echo " sudo baudbot attach # defaults to control-agent"
460-
echo " sudo baudbot attach --pi control-agent"
461-
echo " sudo baudbot attach --pi <uuid>"
462-
echo " sudo baudbot attach --tmux sentry-agent"
442+
echo "Options:"
443+
echo " --model <model> LLM model to use (default: auto-detect from API keys)"
444+
echo ""
445+
echo "Exit: Ctrl+C (does NOT affect the running control-agent)"
463446
exit 0
464447
;;
465448
*)
466-
if [ -n "$TARGET" ]; then
467-
echo "❌ Too many arguments for attach"
468-
exit 1
469-
fi
470-
TARGET="$1"
471-
shift
449+
echo "❌ Unknown option: $1"
450+
echo "Usage: sudo baudbot debug [--model <model>]"
451+
exit 1
472452
;;
473453
esac
474454
done
475455

476-
if [ -z "$TARGET" ]; then
477-
TARGET="control-agent"
478-
fi
479-
480-
attach_tmux_session() {
481-
local tmux_target="$1"
482-
echo -e "${BOLD}${CYAN}Attaching to tmux session:${RESET} $tmux_target"
483-
echo -e "${GREEN}Safe detach:${RESET} Ctrl+b, d ${DIM}(keeps agent running)${RESET}"
484-
echo ""
485-
pause_before_attach
486-
exec sudo -u "$AGENT_USER" tmux attach-session -t "$tmux_target"
487-
}
488-
489-
attach_pi_session() {
490-
local pi_target="$1"
491-
echo -e "${BOLD}${CYAN}Attaching to pi session:${RESET} $pi_target"
492-
echo -e "${BOLD}${YELLOW}Safe detach (does NOT stop the agent):${RESET}"
493-
echo -e " ${YELLOW}1)${RESET} Press Ctrl+C once to clear input/cancel local prompt"
494-
echo -e " ${YELLOW}2)${RESET} Press Ctrl+C again to exit this client"
495-
echo -e " ${GREEN}Agent keeps running under systemd in the background.${RESET}"
496-
echo ""
497-
pause_before_attach
498-
local node_bin_dir=""
499-
node_bin_dir="$(bb_resolve_runtime_node_bin_dir "$AGENT_HOME")"
500-
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'"
501-
}
502-
503-
choose_tmux_target() {
504-
local requested="${1:-}"
505-
local first
506-
507-
if [ -n "$requested" ]; then
508-
if sudo -u "$AGENT_USER" tmux has-session -t "$requested" 2>/dev/null; then
509-
echo "$requested"
510-
return 0
456+
# Auto-detect model from env if not specified
457+
if [ -z "$MODEL" ]; then
458+
# Load env vars to check API keys
459+
local env_file="$AGENT_HOME/.config/.env"
460+
if [ -r "$env_file" ]; then
461+
local env_val=""
462+
env_val="$(grep -E '^BAUDBOT_MODEL=' "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- || true)"
463+
if [ -n "$env_val" ]; then
464+
MODEL="$env_val"
465+
else
466+
env_val="$(grep -E '^ANTHROPIC_API_KEY=' "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- || true)"
467+
if [ -n "$env_val" ]; then
468+
MODEL="anthropic/claude-sonnet-4-5"
469+
else
470+
env_val="$(grep -E '^OPENAI_API_KEY=' "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- || true)"
471+
if [ -n "$env_val" ]; then
472+
MODEL="openai/gpt-4.1"
473+
else
474+
env_val="$(grep -E '^GEMINI_API_KEY=' "$env_file" 2>/dev/null | tail -1 | cut -d= -f2- || true)"
475+
if [ -n "$env_val" ]; then
476+
MODEL="google/gemini-3-flash-preview"
477+
fi
478+
fi
479+
fi
511480
fi
512-
return 1
513-
fi
514-
515-
first=$(sudo -u "$AGENT_USER" tmux ls -F '#{session_name}' 2>/dev/null | head -1)
516-
[ -n "$first" ] || return 1
517-
echo "$first"
518-
return 0
519-
}
520-
521-
choose_pi_target() {
522-
local requested="${1:-}"
523-
local resolved
524-
525-
if ! resolved=$(resolve_pi_session_id "$AGENT_USER" "$requested"); then
526-
return 1
527481
fi
528482

529-
[ -n "$resolved" ] || return 1
530-
echo "$resolved"
531-
return 0
532-
}
533-
534-
if [ "$ATTACH_MODE" = "tmux" ]; then
535-
if tmux_target=$(choose_tmux_target "$TARGET"); then
536-
attach_tmux_session "$tmux_target"
483+
if [ -z "$MODEL" ]; then
484+
echo "❌ No LLM API key found. Set --model or configure API keys in $AGENT_HOME/.config/.env"
485+
exit 1
537486
fi
538-
echo "❌ tmux session not found. See: sudo baudbot sessions"
539-
exit 1
540487
fi
541488

542-
if [ "$ATTACH_MODE" = "pi" ]; then
543-
if pi_target=$(choose_pi_target "$TARGET"); then
544-
attach_pi_session "$pi_target"
545-
fi
546-
echo "❌ pi session not found. See: sudo baudbot sessions"
489+
# Validate MODEL — must be a safe provider/model string (alphanumeric, hyphens, dots, slashes)
490+
if [[ ! "$MODEL" =~ ^[a-zA-Z0-9._/-]+$ ]]; then
491+
echo "❌ Invalid model name: $MODEL"
547492
exit 1
548493
fi
549494

550-
if pi_target=$(choose_pi_target "$TARGET"); then
551-
attach_pi_session "$pi_target"
495+
local SKILL_DIR="$AGENT_HOME/.pi/agent/skills/debug-agent"
496+
if [ ! -f "$SKILL_DIR/SKILL.md" ]; then
497+
# Fall back to deployed location
498+
SKILL_DIR="/opt/baudbot/current/pi/skills/debug-agent"
552499
fi
553500

554-
if tmux_target=$(choose_tmux_target "$TARGET"); then
555-
attach_tmux_session "$tmux_target"
501+
if [ ! -f "$SKILL_DIR/SKILL.md" ]; then
502+
echo "❌ Debug agent skill not found. Run: sudo baudbot deploy"
503+
exit 1
556504
fi
557505

558-
echo "❌ No matching tmux/pi session found. See: sudo baudbot sessions"
559-
exit 1
506+
echo -e "${BOLD}${CYAN}Launching debug agent${RESET}"
507+
echo -e "${DIM}Model: $MODEL${RESET}"
508+
echo -e "${DIM}Skill: $SKILL_DIR${RESET}"
509+
echo -e "${GREEN}Exit: Ctrl+C (does NOT affect the running control-agent)${RESET}"
510+
echo ""
511+
512+
local node_bin_dir=""
513+
node_bin_dir="$(bb_resolve_runtime_node_bin_dir "$AGENT_HOME")"
514+
515+
exec sudo -u "$AGENT_USER" bash -lc "
516+
unset PKG_EXECPATH
517+
export PATH='$AGENT_HOME/.varlock/bin:$node_bin_dir':\$PATH
518+
export VARLOCK_TELEMETRY_DISABLED=1
519+
cd ~
520+
varlock run --path ~/.config/ -- pi \
521+
--session-control \
522+
--model '$MODEL' \
523+
--skill '$SKILL_DIR' \
524+
-e '$SKILL_DIR/debug-dashboard.ts' \
525+
'/skill:debug-agent'
526+
"
560527
}

pi/extensions/heartbeat.test.mjs

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,11 @@
88
* Run: npx vitest run pi/extensions/heartbeat.test.mjs
99
*/
1010

11-
import { describe, it, beforeEach, afterEach } from "vitest";
11+
import { describe, it } from "vitest";
1212
import assert from "node:assert/strict";
13-
import fs from "node:fs";
14-
import path from "node:path";
15-
import os from "node:os";
1613

1714
// ── Replicate pure functions from heartbeat.ts v2 ───────────────────────────
1815

19-
const DEFAULT_INTERVAL_MS = 10 * 60 * 1000; // 10 min
20-
const MIN_INTERVAL_MS = 2 * 60 * 1000; // 2 min
2116
const BACKOFF_MULTIPLIER = 2;
2217
const MAX_BACKOFF_MS = 60 * 60 * 1000; // 1 hour
2318
const STUCK_TODO_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
@@ -70,23 +65,6 @@ function parseTodo(content) {
7065

7166
// ── Test helpers ────────────────────────────────────────────────────────────
7267

73-
let tmpDir;
74-
75-
function setup() {
76-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "heartbeat-test-"));
77-
}
78-
79-
function teardown() {
80-
fs.rmSync(tmpDir, { recursive: true, force: true });
81-
}
82-
83-
function writeFile(name, content) {
84-
const p = path.join(tmpDir, name);
85-
fs.mkdirSync(path.dirname(p), { recursive: true });
86-
fs.writeFileSync(p, content, "utf-8");
87-
return p;
88-
}
89-
9068
// ── Tests ───────────────────────────────────────────────────────────────────
9169

9270
describe("heartbeat v2: isDisabledByEnv", () => {

pi/extensions/heartbeat.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { Type } from "@sinclair/typebox";
2525
import { StringEnum } from "@mariozechner/pi-ai";
2626
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2727
import { homedir } from "node:os";
28-
import { join, resolve } from "node:path";
28+
import { join } from "node:path";
2929

3030
const DEFAULT_INTERVAL_MS = 10 * 60 * 1000; // 10 minutes
3131
const MIN_INTERVAL_MS = 2 * 60 * 1000; // 2 minutes
@@ -270,7 +270,7 @@ function checkStuckTodos(): CheckResult[] {
270270
if (!createdAt) continue;
271271

272272
const createdTime = new Date(createdAt).getTime();
273-
if (isNaN(createdTime)) continue;
273+
if (Number.isNaN(createdTime)) continue;
274274

275275
const age = now - createdTime;
276276
if (age < STUCK_TODO_THRESHOLD_MS) continue;
@@ -341,7 +341,7 @@ function hasMatchingInProgressTodo(worktreeName: string): boolean {
341341
if (content.includes(pathPattern) || boundaryPattern.test(content)) return true;
342342
}
343343
} catch {
344-
continue;
344+
// skip unreadable todo files
345345
}
346346
}
347347
} catch {
@@ -460,7 +460,7 @@ export default function heartbeatExtension(pi: ExtensionAPI): void {
460460

461461
state.consecutiveErrors = 0;
462462
saveState();
463-
} catch (err) {
463+
} catch {
464464
state.consecutiveErrors += 1;
465465
try {
466466
saveState();

0 commit comments

Comments
 (0)