Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions .claude/plans/pi-agent-default-heartbeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Plan: Make Pi Agent Default for HEARTBEAT

**Issue:** [ryaneggz/open-harness#1](https://github.com/ryaneggz/open-harness/issues/1)
**Branch:** `feat/pi-agent-default-heartbeat`

## Summary

Change the default `HEARTBEAT_AGENT` from `claude` to `pi` across all configuration surfaces, add a dedicated `pi` case in the heartbeat dispatcher, and promote Pi Agent from optional to default installation.

## Changes Required

### 1. `install/heartbeat.sh`
- **Line 16:** Change default `HEARTBEAT_AGENT="${HEARTBEAT_AGENT:-claude}"` to `HEARTBEAT_AGENT="${HEARTBEAT_AGENT:-pi}"`
- **Lines 136-146:** Add a dedicated `pi)` case in the agent dispatch switch, matching Pi Agent's CLI invocation pattern (`pi -p "$prompt" --dangerously-skip-permissions`)

### 2. `Makefile`
- **Line 8:** Change `HEARTBEAT_AGENT ?= claude` to `HEARTBEAT_AGENT ?= pi`

### 3. `docker-compose.yml`
- **Line 14:** Change `HEARTBEAT_AGENT=${HEARTBEAT_AGENT:-claude}` to `HEARTBEAT_AGENT=${HEARTBEAT_AGENT:-pi}`

### 4. `install/setup.sh`
- **Line 27:** Change `INSTALL_PI_AGENT=false` to `INSTALL_PI_AGENT=true` (Pi Agent should be installed by default since it's now the default heartbeat agent)
- **Line 57:** Change interactive prompt from `[y/N]` to `[Y/n]` and invert the check to default-yes

## TDD Approach

### Test Framework
- Use `bats-core` (Bash Automated Testing System) for shell script testing
- Tests live in `tests/` directory

### Tests (written first, expected to fail)
1. **Default config tests:** Verify `HEARTBEAT_AGENT` defaults to `pi` in heartbeat.sh, Makefile, docker-compose.yml
2. **Dispatch case test:** Verify `pi` has a dedicated case in heartbeat.sh switch statement
3. **Install default test:** Verify `INSTALL_PI_AGENT` defaults to `true` in setup.sh

## Rollback

Users can override back to Claude via:
- `HEARTBEAT_AGENT=claude` in environment
- `make HEARTBEAT_AGENT=claude heartbeat`
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ REGISTRY = ghcr.io/ruska-ai
HEARTBEAT_INTERVAL ?= 900
HEARTBEAT_ACTIVE_START ?=
HEARTBEAT_ACTIVE_END ?=
HEARTBEAT_AGENT ?= claude
HEARTBEAT_AGENT ?= pi

# NAME is required — fail fast with a helpful message
ifndef NAME
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ make NAME=my-sandbox heartbeat-stop # stop the loop
| `HEARTBEAT_INTERVAL` | `1800` | Seconds between cycles |
| `HEARTBEAT_ACTIVE_START` | _(unset)_ | Hour to start (0-23) |
| `HEARTBEAT_ACTIVE_END` | _(unset)_ | Hour to stop (0-23) |
| `HEARTBEAT_AGENT` | `claude` | Agent CLI to invoke |
| `HEARTBEAT_AGENT` | `pi` | Primary agent CLI to invoke |
| `HEARTBEAT_FALLBACK` | `claude codex` | Space-separated fallback agents (tried in order if primary fails) |

If `HEARTBEAT.md` contains only headers or comments, the cycle is skipped (saves API costs). If the agent has nothing to report, it replies `HEARTBEAT_OK` and the response is suppressed.

Expand Down
3 changes: 2 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ services:
- HEARTBEAT_INTERVAL=${HEARTBEAT_INTERVAL:-900}
- HEARTBEAT_ACTIVE_START=${HEARTBEAT_ACTIVE_START:-}
- HEARTBEAT_ACTIVE_END=${HEARTBEAT_ACTIVE_END:-}
- HEARTBEAT_AGENT=${HEARTBEAT_AGENT:-claude}
- HEARTBEAT_AGENT=${HEARTBEAT_AGENT:-pi}
- HEARTBEAT_FALLBACK=${HEARTBEAT_FALLBACK:-claude codex}
68 changes: 50 additions & 18 deletions install/heartbeat.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ LOG_FILE="${HEARTBEAT_DIR}/heartbeat.log"
HEARTBEAT_INTERVAL="${HEARTBEAT_INTERVAL:-1800}"
HEARTBEAT_ACTIVE_START="${HEARTBEAT_ACTIVE_START:-}"
HEARTBEAT_ACTIVE_END="${HEARTBEAT_ACTIVE_END:-}"
HEARTBEAT_AGENT="${HEARTBEAT_AGENT:-claude}"
HEARTBEAT_AGENT="${HEARTBEAT_AGENT:-pi}"
HEARTBEAT_FALLBACK="${HEARTBEAT_FALLBACK:-claude codex}"
HEARTBEAT_FILE="${HEARTBEAT_FILE:-${HOME}/workspace/HEARTBEAT.md}"
SOUL_FILE="${SOUL_FILE:-${HOME}/workspace/SOUL.md}"
MEMORY_DIR="${MEMORY_DIR:-${HOME}/workspace/memory}"
Expand Down Expand Up @@ -83,6 +84,27 @@ is_heartbeat_ok() {
(( ${#response} < 300 )) && [[ "$response" == *"HEARTBEAT_OK"* ]]
}

# Invoke a single agent. Sets caller's `response` and returns the exit code.
invoke_agent() {
local agent="$1" prompt="$2"
local ec=0
case "$agent" in
claude)
response=$(timeout 300 claude -p "$prompt" --dangerously-skip-permissions 2>&1) || ec=$?
;;
codex)
response=$(timeout 300 codex "$prompt" 2>&1) || ec=$?
;;
pi)
response=$(timeout 300 pi -p "$prompt" --dangerously-skip-permissions 2>&1) || ec=$?
;;
*)
response=$(timeout 300 "$agent" -p "$prompt" 2>&1) || ec=$?
;;
esac
return $ec
}

# ---------------------------------------------------------------------------
# Core
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -128,28 +150,38 @@ HEARTBEAT.md:
${heartbeat_content}
---"

log "Running heartbeat (agent: ${HEARTBEAT_AGENT})"
# Build ordered agent list: primary + fallbacks
local agents=("$HEARTBEAT_AGENT")
for fb in $HEARTBEAT_FALLBACK; do
[[ "$fb" != "$HEARTBEAT_AGENT" ]] && agents+=("$fb")
done

local response=""
local exit_code=0
local succeeded=false

for agent in "${agents[@]}"; do
log "Running heartbeat (agent: ${agent})"
response=""
exit_code=0
invoke_agent "$agent" "$prompt" || exit_code=$?

if (( exit_code == 124 )); then
log "Agent ${agent} timed out (300s limit)"
elif (( exit_code != 0 )); then
log "Agent ${agent} failed (exit code ${exit_code}): ${response:0:500}"
else
succeeded=true
break
fi

case "$HEARTBEAT_AGENT" in
claude)
response=$(timeout 300 claude -p "$prompt" --dangerously-skip-permissions 2>&1) || exit_code=$?
;;
codex)
response=$(timeout 300 codex "$prompt" 2>&1) || exit_code=$?
;;
*)
response=$(timeout 300 "$HEARTBEAT_AGENT" -p "$prompt" 2>&1) || exit_code=$?
;;
esac
if (( ${#agents[@]} > 1 )); then
log "Falling back to next agent..."
fi
done

if (( exit_code == 124 )); then
log "Heartbeat timed out (300s limit)"
return 0
elif (( exit_code != 0 )); then
log "Heartbeat failed (exit code ${exit_code}): ${response:0:500}"
if [[ "$succeeded" != true ]]; then
log "All agents failed"
return 0
fi

Expand Down
6 changes: 3 additions & 3 deletions install/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ SANDBOX_HOME="/home/$SANDBOX_USER"
INSTALL_BROWSER=true
INSTALL_CLAUDE_CODE=true
INSTALL_CODEX=false
INSTALL_PI_AGENT=false
INSTALL_PI_AGENT=true
INSTALL_AGENTMAIL=false
SSH_PUBKEY=""
GH_TOKEN=""
Expand Down Expand Up @@ -54,8 +54,8 @@ if [[ "$NON_INTERACTIVE" == false ]]; then
[[ "$answer" =~ ^[Yy]$ ]] && INSTALL_CODEX=true

printf "\n Install Pi Coding Agent? (https://shittycodingagent.ai)\n"
read -rp " Install Pi Agent? [y/N]: " answer
[[ "$answer" =~ ^[Yy]$ ]] && INSTALL_PI_AGENT=true
read -rp " Install Pi Agent? [Y/n]: " answer
[[ "$answer" =~ ^[Nn]$ ]] && INSTALL_PI_AGENT=false

printf "\n Install AgentMail CLI? (https://docs.agentmail.to/integrations/cli)\n"
read -rp " Install AgentMail? [y/N]: " answer
Expand Down