From 51371b18cebfbb14e4db81c80569d9133a3bd007 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Sun, 12 Apr 2026 06:48:12 +0200 Subject: [PATCH] feat(templates): add isolated agentTurn and systemEvent cron templates --- scripts/templates/cron/README.md | 51 +++++++++ scripts/templates/cron/agent-turn.sh | 143 +++++++++++++++++++++++++ scripts/templates/cron/system-event.sh | 42 ++++++++ 3 files changed, 236 insertions(+) create mode 100644 scripts/templates/cron/README.md create mode 100644 scripts/templates/cron/agent-turn.sh create mode 100644 scripts/templates/cron/system-event.sh diff --git a/scripts/templates/cron/README.md b/scripts/templates/cron/README.md new file mode 100644 index 0000000..b057346 --- /dev/null +++ b/scripts/templates/cron/README.md @@ -0,0 +1,51 @@ +# Cron Execution Templates + +Two execution types for scheduled agent work, inspired by OpenClaw's cron service. + +## agentTurn (agent-turn.sh) + +Full background autonomy. Creates/resumes its own named session. + +**Use for:** +- Complex multi-step work +- Tasks that need full context and tool access +- Background autonomous operation (proactive agent cycles) + +**Features:** +- Named sessions via `--name` for resume capability +- Orphan detection (checks PID before starting new run) +- Session rollover: fresh session after 200 turns or 72 hours +- Context loading from SESSION-STATE.md and HEARTBEAT.md +- Dated log files per agent per day + +**Session naming (VERIFIED):** +Uses `--name "agent::turn"` with `--resume` for continuity. +Note: `--session-id` requires valid UUIDs. Named sessions are the +correct approach for human-readable, resumable agent sessions. + +## systemEvent (system-event.sh) + +Injects text into an existing session. No new session created. + +**Use for:** +- Notifications ("new data available") +- Simple status checks +- Triggering a specific action in a running session + +**Limitations:** +- Requires an active named session to resume +- Limited to 3 turns (quick action, not full autonomy) +- If the session doesn't exist, the command fails gracefully + +## Scheduling examples + +```bash +# agentTurn: run full agent turn every hour +0 * * * * /path/to/agent-turn.sh >> /tmp/agent-turn.log 2>&1 + +# systemEvent: notify agent of new data every 30 min +*/30 * * * * /path/to/system-event.sh "agent:processor:turn" "Check /data/inbox for new files" + +# agentTurn with catchup on reboot +@reboot /path/to/agent-turn.sh >> /tmp/agent-turn.log 2>&1 +``` diff --git a/scripts/templates/cron/agent-turn.sh b/scripts/templates/cron/agent-turn.sh new file mode 100644 index 0000000..aa31790 --- /dev/null +++ b/scripts/templates/cron/agent-turn.sh @@ -0,0 +1,143 @@ +#!/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" diff --git a/scripts/templates/cron/system-event.sh b/scripts/templates/cron/system-event.sh new file mode 100644 index 0000000..578d462 --- /dev/null +++ b/scripts/templates/cron/system-event.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# System Event: Inject a text event into an existing Claude Code session. +# Lighter than agentTurn — does not create a new session. +# +# Bash 3.2 compatible. +# +# Usage: ./system-event.sh "session-name" "Event text to inject" +# +# Placeholders: +# {{WORKING_DIR}} - absolute path to project directory + +WORKING_DIR="{{WORKING_DIR}}" +SESSION_NAME="${1:-}" +EVENT_TEXT="${2:-}" + +if [ -z "$SESSION_NAME" ] || [ -z "$EVENT_TEXT" ]; then + echo "Usage: $0 " + echo "Example: $0 'agent:researcher:turn' 'New data available in /data/inbox'" + exit 1 +fi + +LOG_DIR="$WORKING_DIR/logs" +mkdir -p "$LOG_DIR" + +TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) +echo "$TIMESTAMP | system-event | INJECT: $SESSION_NAME -- $EVENT_TEXT" >> "$LOG_DIR/system-event.log" + +cd "$WORKING_DIR" + +# Resume the named session and inject the event +OUTPUT=$(claude --resume "$SESSION_NAME" -p "$EVENT_TEXT" \ + --output-format text \ + --max-turns 3 2>&1) + +EXIT_CODE=$? +TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) + +if [ "$EXIT_CODE" -eq 0 ]; then + echo "$TIMESTAMP | system-event | DELIVERED: $SESSION_NAME (exit $EXIT_CODE)" >> "$LOG_DIR/system-event.log" +else + echo "$TIMESTAMP | system-event | FAILED: $SESSION_NAME (exit $EXIT_CODE)" >> "$LOG_DIR/system-event.log" +fi