agent-builder/scripts/templates/cron/agent-turn.sh

143 lines
4.4 KiB
Bash

#!/bin/bash
# Agent Turn: Full background autonomy for Claude Code agents.
# Fires a complete agent turn with its own named session.
#
# Bash 3.2 compatible. Uses python3 for JSON/date operations.
#
# Placeholders:
# {{AGENT_NAME}} - name of the agent
# {{WORKING_DIR}} - absolute path to project directory
# {{MAX_TURNS}} - max turns per agent turn (default: 15)
AGENT_NAME="{{AGENT_NAME}}"
WORKING_DIR="{{WORKING_DIR}}"
MAX_TURNS="${MAX_TURNS:-15}"
SESSION_NAME="agent:${AGENT_NAME}:turn"
PID_FILE="$WORKING_DIR/.agent-turn-${AGENT_NAME}.pid"
LOG_DIR="$WORKING_DIR/logs"
STATE_FILE="$WORKING_DIR/.agent-turn-state.json"
mkdir -p "$LOG_DIR"
# --- Orphan detection ---
# Check if a previous agent turn is still running
if [ -f "$PID_FILE" ]; then
OLD_PID=$(cat "$PID_FILE" 2>/dev/null)
if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | agent-turn | SKIP: previous run still active (PID $OLD_PID)" >> "$LOG_DIR/agent-turn.log"
exit 0
else
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) | agent-turn | Cleaned orphan PID file (PID $OLD_PID)" >> "$LOG_DIR/agent-turn.log"
rm -f "$PID_FILE"
fi
fi
# --- Session rollover check ---
# After 200 turns or 72 hours, start a fresh session
NEEDS_FRESH=$(python3 -c "
import json, time, os
state_file = '$STATE_FILE'
agent = '$AGENT_NAME'
try:
state = json.load(open(state_file))
agent_state = state.get(agent, {})
turns = agent_state.get('turn_count', 0)
started = agent_state.get('session_started', 0)
age_hours = (time.time() - started) / 3600 if started else 999
if turns >= 200 or age_hours >= 72:
print('true')
else:
print('false')
except:
print('true')
" 2>/dev/null)
if [ "$NEEDS_FRESH" = "true" ]; then
# Use a new session name with timestamp for rollover
SESSION_NAME="agent:${AGENT_NAME}:turn:$(date +%s)"
# Reset state
python3 -c "
import json, time, os
state_file = '$STATE_FILE'
agent = '$AGENT_NAME'
try:
state = json.load(open(state_file))
except:
state = {}
state[agent] = {'turn_count': 0, 'session_started': time.time(), 'session_name': '$SESSION_NAME'}
with open(state_file, 'w') as f:
json.dump(state, f, indent=2)
"
fi
# --- Load context ---
# Build prompt from HEARTBEAT.md and SESSION-STATE.md
PROMPT=$(python3 -c "
import os
working_dir = '$WORKING_DIR'
parts = []
# Read SESSION-STATE.md for current context
ss_memory = os.path.join(working_dir, 'memory', 'SESSION-STATE.md')
ss_root = os.path.join(working_dir, 'SESSION-STATE.md')
if os.path.exists(ss_memory):
parts.append('## Current session state:\n' + open(ss_memory).read()[:2000])
elif os.path.exists(ss_root):
parts.append('## Current session state:\n' + open(ss_root).read()[:2000])
# Read HEARTBEAT.md for tasks
hb = os.path.join(working_dir, 'HEARTBEAT.md')
if os.path.exists(hb):
parts.append('## Heartbeat tasks:\n' + open(hb).read()[:2000])
if parts:
print('\n\n'.join(parts))
else:
print('No context files found. Check the working directory and proceed with any pending tasks.')
" 2>/dev/null)
# --- Run agent turn ---
echo $$ > "$PID_FILE"
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
echo "$TIMESTAMP | agent-turn | START: $AGENT_NAME (session: $SESSION_NAME)" >> "$LOG_DIR/agent-turn.log"
cd "$WORKING_DIR"
OUTPUT=$(claude -p "$PROMPT" \
--name "$SESSION_NAME" \
--output-format text \
--max-turns "$MAX_TURNS" 2>&1)
EXIT_CODE=$?
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# Save output to dated log
echo "--- $TIMESTAMP ---" >> "$LOG_DIR/agent-turn-${AGENT_NAME}-$(date +%Y-%m-%d).log"
echo "$OUTPUT" >> "$LOG_DIR/agent-turn-${AGENT_NAME}-$(date +%Y-%m-%d).log"
echo "" >> "$LOG_DIR/agent-turn-${AGENT_NAME}-$(date +%Y-%m-%d).log"
# Update turn count
python3 -c "
import json, time
state_file = '$STATE_FILE'
agent = '$AGENT_NAME'
try:
state = json.load(open(state_file))
except:
state = {}
if agent not in state:
state[agent] = {'turn_count': 0, 'session_started': time.time(), 'session_name': '$SESSION_NAME'}
state[agent]['turn_count'] = state[agent].get('turn_count', 0) + 1
state[agent]['last_run'] = time.time()
with open(state_file, 'w') as f:
json.dump(state, f, indent=2)
"
if [ "$EXIT_CODE" -eq 0 ]; then
echo "$TIMESTAMP | agent-turn | COMPLETE: $AGENT_NAME (exit $EXIT_CODE)" >> "$LOG_DIR/agent-turn.log"
else
echo "$TIMESTAMP | agent-turn | ERROR: $AGENT_NAME (exit $EXIT_CODE)" >> "$LOG_DIR/agent-turn.log"
fi
# Cleanup
rm -f "$PID_FILE"