#!/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"