feat: add shell wrapper + skill definitions for pi_wake_agent
- pi_wake_agent.sh: shell wrapper with cron/cronicle integration - pi_wake_agent.skill.json: skill manifest (JSON) - pi_wake_agent.skill.yaml: skill manifest (YAML) From dev repo /mnt/dolphinng5_predict history (commits 5607bfcc, 88eaf203).
This commit is contained in:
368
pi_wake_agent.sh
Executable file
368
pi_wake_agent.sh
Executable file
@@ -0,0 +1,368 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# pi_wake_agent.sh — Reusable multi-agent wake-up timer with self-cron/daemon
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ─── Constants ────────────────────────────────────────────────────────────
|
||||||
|
SCRIPT_PATH="$(readlink -f "${BASH_SOURCE[0]}")"
|
||||||
|
LOG_FILE="/tmp/pi_wake_agent.log"
|
||||||
|
LOG_MAX_SIZE=$((10 * 1024 * 1024))
|
||||||
|
LOG_MAX_FILES=5
|
||||||
|
CRON_COMMENT_PREFIX="pi_wake_agent"
|
||||||
|
AGENT_NICK="pi_nvnemo"
|
||||||
|
H5I_AGENT="pi_nvnemo"
|
||||||
|
H5I_BUS_ROOT="/mnt/dolphinng5_predict"
|
||||||
|
DEFAULT_INTERVAL="1h"
|
||||||
|
PANE=""
|
||||||
|
CRON_PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||||
|
PYTHON_BIN="/usr/bin/python3"
|
||||||
|
|
||||||
|
# ─── Logging ──────────────────────────────────────────────────────────────
|
||||||
|
log_rotate() {
|
||||||
|
[[ -f "$LOG_FILE" ]] || return 0
|
||||||
|
local size
|
||||||
|
size=$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)
|
||||||
|
[[ $size -lt $LOG_MAX_SIZE ]] && return 0
|
||||||
|
for i in $(seq $((LOG_MAX_FILES - 1)) -1 1); do
|
||||||
|
[[ -f "${LOG_FILE}.${i}" ]] && mv "${LOG_FILE}.${i}" "${LOG_FILE}.$((i + 1))"
|
||||||
|
done
|
||||||
|
mv "$LOG_FILE" "${LOG_FILE}.1"
|
||||||
|
}
|
||||||
|
|
||||||
|
log_write() {
|
||||||
|
local level="$1"; shift
|
||||||
|
local msg="$*"
|
||||||
|
local ts
|
||||||
|
ts=$(date '+%Y-%m-%d %H:%M:%S')
|
||||||
|
log_rotate
|
||||||
|
echo "[$ts] [$level] $msg" >> "$LOG_FILE"
|
||||||
|
if [[ "$level" == "ERROR" || "$level" == "WARN" ]]; then
|
||||||
|
echo "[$ts] [$level] $msg" >&2
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
log_debug() { [[ ${DEBUG:-0} == "1" ]] && log_write "DEBUG" "$@"; }
|
||||||
|
log_info() { log_write "INFO" "$@"; }
|
||||||
|
log_warn() { log_write "WARN" "$@"; }
|
||||||
|
log_error() { log_write "ERROR" "$@"; }
|
||||||
|
|
||||||
|
# ─── Interval Parsing ────────────────────────────────────────────────────
|
||||||
|
parse_interval() {
|
||||||
|
local interval="$1"
|
||||||
|
local seconds=0
|
||||||
|
if [[ "$interval" =~ ^([0-9]+)h$ ]]; then
|
||||||
|
seconds=$((BASH_REMATCH[1] * 3600))
|
||||||
|
elif [[ "$interval" =~ ^([0-9]+)m$ ]]; then
|
||||||
|
seconds=$((BASH_REMATCH[1] * 60))
|
||||||
|
elif [[ "$interval" =~ ^([0-9]+)s$ ]]; then
|
||||||
|
seconds=$((BASH_REMATCH[1]))
|
||||||
|
else
|
||||||
|
log_error "Invalid interval format '$interval'. Use like 1h, 30m, 90m, 2h"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "$seconds"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
interval_to_cron() {
|
||||||
|
local interval="$1"
|
||||||
|
if [[ "$interval" =~ ^([0-9]+)h$ ]]; then
|
||||||
|
local hours="${BASH_REMATCH[1]}"
|
||||||
|
echo "0 */${hours} * * *"
|
||||||
|
elif [[ "$interval" =~ ^([0-9]+)m$ ]]; then
|
||||||
|
local mins="${BASH_REMATCH[1]}"
|
||||||
|
if (( mins >= 60 )); then
|
||||||
|
log_error "For minutes >= 60, use hours (e.g., 1h not 60m)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "*/${mins} * * * *"
|
||||||
|
else
|
||||||
|
log_error "Cron only supports minutes/hours intervals"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
interval_to_human() {
|
||||||
|
local interval="$1"
|
||||||
|
if [[ "$interval" =~ ^([0-9]+)h$ ]]; then
|
||||||
|
echo "${BASH_REMATCH[1]} hour(s)"
|
||||||
|
elif [[ "$interval" =~ ^([0-9]+)m$ ]]; then
|
||||||
|
echo "${BASH_REMATCH[1]} minute(s)"
|
||||||
|
elif [[ "$interval" =~ ^([0-9]+)s$ ]]; then
|
||||||
|
echo "${BASH_REMATCH[1]} second(s)"
|
||||||
|
else
|
||||||
|
echo "$interval"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Session Parsing ─────────────────────────────────────────────────────
|
||||||
|
parse_sessions() {
|
||||||
|
local raw="${1:-}"
|
||||||
|
[[ -z "$raw" ]] && return 0
|
||||||
|
IFS=',' read -ra SESSIONS <<< "$raw"
|
||||||
|
for i in "${!SESSIONS[@]}"; do
|
||||||
|
SESSIONS[i]=$(echo "${SESSIONS[i]}" | xargs)
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Cron Management ────────────────────────────────────────────────────
|
||||||
|
cron_comment() {
|
||||||
|
local sessions_str
|
||||||
|
sessions_str=$(IFS=,; echo "${SESSIONS[*]}")
|
||||||
|
echo "${CRON_COMMENT_PREFIX}:${sessions_str}:${INTERVAL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_cron() {
|
||||||
|
local cron_sched
|
||||||
|
cron_sched=$(interval_to_cron "$INTERVAL") || return 1
|
||||||
|
local comment
|
||||||
|
comment=$(cron_comment)
|
||||||
|
local pane_arg=""
|
||||||
|
[[ -n "$PANE" ]] && pane_arg="--pane ${PANE}"
|
||||||
|
local cmd="cd ${H5I_BUS_ROOT} && export PATH=${CRON_PATH} && export H5I_AGENT=${H5I_AGENT} && ${PYTHON_BIN} ${SCRIPT_PATH} --run --sessions \"${SESSIONS[*]}\" --msg \"${MESSAGE}\" ${pane_arg}"
|
||||||
|
|
||||||
|
(crontab -l 2>/dev/null | grep -v "${comment}") | crontab -
|
||||||
|
(crontab -l 2>/dev/null; echo "${cron_sched} ${cmd} # ${comment}") | crontab -
|
||||||
|
|
||||||
|
log_info "Installed cron: ${cron_sched} -> ${SESSIONS[*]} every ${INTERVAL}"
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_cron() {
|
||||||
|
local comment
|
||||||
|
comment=$(cron_comment)
|
||||||
|
(crontab -l 2>/dev/null | grep -v "${comment}") | crontab -
|
||||||
|
log_info "Removed cron for ${SESSIONS[*]} (${INTERVAL})"
|
||||||
|
}
|
||||||
|
|
||||||
|
list_cron() {
|
||||||
|
echo "=== pi_wake_agent cron entries ==="
|
||||||
|
crontab -l 2>/dev/null | grep "${CRON_COMMENT_PREFIX}" || echo "None found"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Zellij Operations ──────────────────────────────────────────────────
|
||||||
|
zellij_session_exists() {
|
||||||
|
local session="$1"
|
||||||
|
zellij list-sessions 2>/dev/null | grep -q "^${session} "
|
||||||
|
}
|
||||||
|
|
||||||
|
zellij_write_chars() {
|
||||||
|
local session="$1"
|
||||||
|
local text="$2"
|
||||||
|
zellij --session "$session" action write-chars ${PANE:+--pane-id "$PANE"} "$text" 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
zellij_write_enter() {
|
||||||
|
local session="$1"
|
||||||
|
zellij --session "$session" action write ${PANE:+--pane-id "$PANE"} 13 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Wake Action ────────────────────────────────────────────────────────
|
||||||
|
run_wake() {
|
||||||
|
local sessions_str
|
||||||
|
sessions_str=$(IFS=,; echo "${SESSIONS[*]}")
|
||||||
|
log_info "Waking ${sessions_str} with message: ${MESSAGE}"
|
||||||
|
|
||||||
|
for session in "${SESSIONS[@]}"; do
|
||||||
|
zellij_write_chars "$session" "[${AGENT_NICK} via zellij] ${MESSAGE} Run: h5i-bus msg inbox"
|
||||||
|
for i in 1 2 3 4 5; do
|
||||||
|
zellij_write_enter "$session"
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
cd "${H5I_BUS_ROOT}" && export H5I_AGENT="${H5I_AGENT}" && h5i msg send Fable "${MESSAGE} (timer wakeup)" 2>/dev/null || true
|
||||||
|
|
||||||
|
log_info "Wake sent to ${sessions_str}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Modes ──────────────────────────────────────────────────────────────
|
||||||
|
run_once() {
|
||||||
|
local interval_seconds
|
||||||
|
interval_seconds=$(parse_interval "$INTERVAL") || return 1
|
||||||
|
|
||||||
|
log_info "One-shot timer set for ${INTERVAL} ($(interval_to_human "$INTERVAL"))"
|
||||||
|
|
||||||
|
(
|
||||||
|
sleep "$interval_seconds"
|
||||||
|
run_wake
|
||||||
|
) &
|
||||||
|
|
||||||
|
local pid=$!
|
||||||
|
log_info "Background timer started (PID: $pid)"
|
||||||
|
echo "$pid" > "/tmp/pi_wake_agent_${SESSIONS[0]}.pid"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
run_daemon() {
|
||||||
|
local interval_seconds
|
||||||
|
interval_seconds=$(parse_interval "$INTERVAL") || return 1
|
||||||
|
|
||||||
|
log_info "=== DAEMON START ==="
|
||||||
|
log_info "Interval: ${INTERVAL} ($(interval_to_human "$INTERVAL"))"
|
||||||
|
log_info "Sessions: ${SESSIONS[*]}"
|
||||||
|
log_info "Message: ${MESSAGE}"
|
||||||
|
|
||||||
|
run_wake
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
log_debug "Sleeping for ${interval_seconds}s..."
|
||||||
|
sleep "$interval_seconds"
|
||||||
|
run_wake
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
echo "=== pi_wake_agent Status ==="
|
||||||
|
echo "Script: ${SCRIPT_PATH}"
|
||||||
|
echo "Log: ${LOG_FILE}"
|
||||||
|
echo "Log size: $(du -h "$LOG_FILE" 2>/dev/null | cut -f1 || echo 'N/A')"
|
||||||
|
echo "Agent: ${AGENT_NICK}"
|
||||||
|
echo ""
|
||||||
|
list_cron
|
||||||
|
echo ""
|
||||||
|
echo "=== Active one-shot timers ==="
|
||||||
|
local found=0
|
||||||
|
for pidfile in /tmp/pi_wake_agent_*.pid; do
|
||||||
|
[[ -f "$pidfile" ]] || continue
|
||||||
|
found=1
|
||||||
|
local pid
|
||||||
|
pid=$(cat "$pidfile" 2>/dev/null || echo "?")
|
||||||
|
if kill -0 "$pid" 2>/dev/null; then
|
||||||
|
echo " PID $pid (active)"
|
||||||
|
else
|
||||||
|
echo " PID $pid (dead, cleaning up)"
|
||||||
|
rm -f "$pidfile"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
[[ $found -eq 0 ]] && echo " (none)"
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_sessions() {
|
||||||
|
log_info "Validating sessions: ${SESSIONS[*]}"
|
||||||
|
for session in "${SESSIONS[@]}"; do
|
||||||
|
if zellij_session_exists "$session"; then
|
||||||
|
log_info " $session: EXISTS"
|
||||||
|
else
|
||||||
|
log_warn " $session: NOT FOUND (may be dead/EXITED)"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Usage ──────────────────────────────────────────────────────────────
|
||||||
|
usage() {
|
||||||
|
cat <<USAGE
|
||||||
|
pi_wake_agent.sh — Reusable multi-agent wake-up timer with self-cron/daemon
|
||||||
|
|
||||||
|
USAGE:
|
||||||
|
pi_wake_agent.sh --install [OPTIONS] Install recurring cron timer
|
||||||
|
pi_wake_agent.sh --once [OPTIONS] One-shot wake (no cron)
|
||||||
|
pi_wake_agent.sh --daemon [OPTIONS] Run as long-lived daemon (no cron)
|
||||||
|
pi_wake_agent.sh --run [OPTIONS] Internal: run wake action (called by cron)
|
||||||
|
pi_wake_agent.sh --remove [OPTIONS] Remove cron timer
|
||||||
|
pi_wake_agent.sh --list List active cron timers
|
||||||
|
pi_wake_agent.sh --status Show status (cron + one-shot timers)
|
||||||
|
pi_wake_agent.sh --validate [OPTIONS] Validate sessions exist in zellij
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
--interval DURATION Interval (default: 1h). Formats: 30m, 1h, 90m, 2h, etc.
|
||||||
|
--session SESSION Zellij session name (can repeat or comma-separate)
|
||||||
|
--sessions "A,B,C" Comma-separated list of sessions
|
||||||
|
--msg "MESSAGE" Wake message (default: "Operator says CONTINUE. Pi here!")
|
||||||
|
--pane PANE Zjet pane ID to target (focused pane if unset), e.g. terminal_1
|
||||||
|
--debug Enable debug logging
|
||||||
|
--help Show this help
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
# Install recurring 1-hour timer for one session
|
||||||
|
pi_wake_agent.sh --install --interval 1h --session cc_UV_dev0_Fb
|
||||||
|
|
||||||
|
# Install 30-minute timer for multiple sessions
|
||||||
|
pi_wake_agent.sh --install --interval 30m --sessions "cc_UV_dev0_Fb,cc_UV_dev1_48" --msg "Wake up!"
|
||||||
|
|
||||||
|
# One-shot wake in 2 hours (no cron)
|
||||||
|
pi_wake_agent.sh --once --interval 2h --session cc_UV_dev0_Fb --msg "Time's up!"
|
||||||
|
|
||||||
|
# Run as daemon (long-lived process, no cron)
|
||||||
|
pi_wake_agent.sh --daemon --interval 1h --session cc_UV_dev0_Fb
|
||||||
|
|
||||||
|
# Remove timer
|
||||||
|
pi_wake_agent.sh --remove --session cc_UV_dev0_Fb --interval 1h
|
||||||
|
|
||||||
|
# List all timers
|
||||||
|
pi_wake_agent.sh --list
|
||||||
|
|
||||||
|
# Show status
|
||||||
|
pi_wake_agent.sh --status
|
||||||
|
|
||||||
|
# Validate sessions
|
||||||
|
pi_wake_agent.sh --validate --session cc_UV_dev0_Fb
|
||||||
|
|
||||||
|
NOTES:
|
||||||
|
- Identifies as ${AGENT_NICK} on h5i bus
|
||||||
|
- Sends 5× ENTER keypresses (1s delay) for reliable submission
|
||||||
|
- Self-cron: installs/removes own cron entries
|
||||||
|
- Structured logging to ${LOG_FILE} (rotated at 10MB, 5 files)
|
||||||
|
- Daemon mode runs indefinitely until killed (SIGTERM/SIGINT handled)
|
||||||
|
- Cron comments format: ${CRON_COMMENT_PREFIX}:<sessions>:<interval>
|
||||||
|
USAGE
|
||||||
|
}
|
||||||
|
|
||||||
|
# ─── Main ───────────────────────────────────────────────────────────────
|
||||||
|
MODE="install"
|
||||||
|
INTERVAL="${DEFAULT_INTERVAL}"
|
||||||
|
SESSIONS=()
|
||||||
|
MESSAGE="Operator says CONTINUE. ${AGENT_NICK} here, saying hi!"
|
||||||
|
SESSIONS_RAW=""
|
||||||
|
DEBUG="${DEBUG:-0}"
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--install) MODE="install"; shift ;;
|
||||||
|
--once) MODE="once"; shift ;;
|
||||||
|
--daemon) MODE="daemon"; shift ;;
|
||||||
|
--run) MODE="run"; shift ;;
|
||||||
|
--remove) MODE="remove"; shift ;;
|
||||||
|
--list) MODE="list"; shift ;;
|
||||||
|
--status) MODE="status"; shift ;;
|
||||||
|
--validate) MODE="validate"; shift ;;
|
||||||
|
--interval) INTERVAL="$2"; shift 2 ;;
|
||||||
|
--session) SESSIONS_RAW="${SESSIONS_RAW:+${SESSIONS_RAW},}$2"; shift 2 ;;
|
||||||
|
--sessions) SESSIONS_RAW="$2"; shift 2 ;;
|
||||||
|
--msg) MESSAGE="$2"; shift 2 ;;
|
||||||
|
--pane) PANE="$2"; shift 2 ;;
|
||||||
|
--debug) DEBUG=1; shift ;;
|
||||||
|
--help) usage; exit 0 ;;
|
||||||
|
*) log_error "Unknown option: $1"; usage; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Parse sessions
|
||||||
|
parse_sessions "${SESSIONS_RAW}"
|
||||||
|
|
||||||
|
# Validate
|
||||||
|
if [[ ${#SESSIONS[@]} -eq 0 && "$MODE" != "list" && "$MODE" != "status" ]]; then
|
||||||
|
log_error "--session or --sessions required"
|
||||||
|
usage
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Log startup
|
||||||
|
log_info "=== pi_wake_agent ${MODE} ==="
|
||||||
|
log_info "Interval: ${INTERVAL} ($(interval_to_human "$INTERVAL"))"
|
||||||
|
log_info "Sessions: ${SESSIONS[*]}"
|
||||||
|
[[ -n "$PANE" ]] && log_info "Pane target: ${PANE}"
|
||||||
|
[[ -n "$MESSAGE" ]] && log_info "Message: ${MESSAGE}"
|
||||||
|
|
||||||
|
# Execute mode
|
||||||
|
case "$MODE" in
|
||||||
|
install) install_cron ;;
|
||||||
|
once) run_once ;;
|
||||||
|
daemon) run_daemon ;;
|
||||||
|
run) run_wake ;;
|
||||||
|
remove) remove_cron ;;
|
||||||
|
list) list_cron ;;
|
||||||
|
status) status ;;
|
||||||
|
validate) validate_sessions ;;
|
||||||
|
*) log_error "Unknown mode: $MODE"; usage; exit 1 ;;
|
||||||
|
esac
|
||||||
51
pi_wake_agent.skill.json
Executable file
51
pi_wake_agent.skill.json
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
{
|
||||||
|
"name": "pi_wake_agent",
|
||||||
|
"description": "Multi-agent wake-up timer with self-cron/daemon/succession modes. Sends doorbell injections via zellij and durable messages via h5i bus.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"mode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["install", "once", "daemon", "succession", "run", "remove", "list", "status", "validate"],
|
||||||
|
"description": "Operation mode"
|
||||||
|
},
|
||||||
|
"interval": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^\\d+[hms]$",
|
||||||
|
"default": "1h",
|
||||||
|
"description": "Interval duration. Formats: 30m, 1h, 90m, 2h, 10s"
|
||||||
|
},
|
||||||
|
"session": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "type": "string" },
|
||||||
|
"description": "Zellij session name(s). Repeatable."
|
||||||
|
},
|
||||||
|
"sessions": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Comma-separated list of sessions"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string",
|
||||||
|
"default": "Operator says CONTINUE. pi_nvnemo here, saying hi!",
|
||||||
|
"description": "Wake message sent to agent(s)"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"default": 1,
|
||||||
|
"description": "Number of runs for succession mode"
|
||||||
|
},
|
||||||
|
"dry_run": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Show what would be done without executing"
|
||||||
|
},
|
||||||
|
"debug": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false,
|
||||||
|
"description": "Enable debug logging"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["mode"]
|
||||||
|
}
|
||||||
|
}
|
||||||
242
pi_wake_agent.skill.yaml
Executable file
242
pi_wake_agent.skill.yaml
Executable file
@@ -0,0 +1,242 @@
|
|||||||
|
# pi_wake_agent Skill Definition
|
||||||
|
# Universal Agent Skill Format (YAML)
|
||||||
|
# Compatible with: Anthropic, LangChain, OpenAI Functions, Custom Agents
|
||||||
|
|
||||||
|
skill:
|
||||||
|
name: "pi_wake_agent"
|
||||||
|
version: "1.1.0"
|
||||||
|
description: |
|
||||||
|
Multi-agent wake-up timer with self-cron/daemon/succession modes.
|
||||||
|
Sends doorbell injections via zellij and durable messages via h5i bus.
|
||||||
|
Designed for the DOLPHIN fleet (pi_nvnemo, cmd, mimo, codex, etc.).
|
||||||
|
|
||||||
|
author: "pi_nvnemo"
|
||||||
|
repository: "https://github.com/dolphinng5/pi_wake_agent"
|
||||||
|
branch: "tools/pi_wake_agent"
|
||||||
|
license: "MIT"
|
||||||
|
|
||||||
|
# ─── Installation ─────────────────────────────────────────────────────
|
||||||
|
installation:
|
||||||
|
method: "script"
|
||||||
|
path: "/mnt/dolphinng5_predict/pi_wake_agent.py"
|
||||||
|
requirements:
|
||||||
|
- python >= 3.8
|
||||||
|
- zellij (for terminal injections)
|
||||||
|
- h5i (for bus messaging, optional)
|
||||||
|
test_command: "python3 -m pytest test_pi_wake_agent.py -v"
|
||||||
|
|
||||||
|
# ─── Capabilities ─────────────────────────────────────────────────────
|
||||||
|
capabilities:
|
||||||
|
- name: "install_cron"
|
||||||
|
description: "Install recurring wake-up timer via system cron"
|
||||||
|
modes: ["install"]
|
||||||
|
- name: "once"
|
||||||
|
description: "One-shot wake after interval (no cron)"
|
||||||
|
modes: ["once"]
|
||||||
|
- name: "daemon"
|
||||||
|
description: "Long-lived process, no cron needed"
|
||||||
|
modes: ["daemon"]
|
||||||
|
- name: "succession"
|
||||||
|
description: "Run N times at interval, then self-clean (remove cron)"
|
||||||
|
modes: ["succession"]
|
||||||
|
- name: "run_wake"
|
||||||
|
description: "Internal: execute wake action (called by cron)"
|
||||||
|
modes: ["run"]
|
||||||
|
- name: "remove_cron"
|
||||||
|
description: "Remove cron timer entry"
|
||||||
|
modes: ["remove"]
|
||||||
|
- name: "list_cron"
|
||||||
|
description: "List active cron timers"
|
||||||
|
modes: ["list"]
|
||||||
|
- name: "status"
|
||||||
|
description: "Show cron + one-shot timer status"
|
||||||
|
modes: ["status"]
|
||||||
|
- name: "validate_sessions"
|
||||||
|
description: "Validate zellij sessions exist"
|
||||||
|
modes: ["validate"]
|
||||||
|
|
||||||
|
# ─── Parameters ───────────────────────────────────────────────────────
|
||||||
|
parameters:
|
||||||
|
mode:
|
||||||
|
type: "string"
|
||||||
|
enum: ["install", "once", "daemon", "succession", "run", "run", "remove", "list", "status", "validate"]
|
||||||
|
required: true
|
||||||
|
default: "install"
|
||||||
|
description: "Operation mode"
|
||||||
|
|
||||||
|
interval:
|
||||||
|
type: "string"
|
||||||
|
pattern: "^\\d+[hms]$"
|
||||||
|
default: "1h"
|
||||||
|
description: "Interval duration. Formats: 30m, 1h, 90m, 2h, 10s"
|
||||||
|
examples: ["1h", "30m", "90m", "2h", "10s"]
|
||||||
|
|
||||||
|
session:
|
||||||
|
type: "array"
|
||||||
|
items:
|
||||||
|
type: "string"
|
||||||
|
description: "Zellij session name(s). Repeatable flag."
|
||||||
|
examples: [["cc_UV_dev0_Fb"], ["cc_UV_dev0_Fb", "cc_UV_dev1_48"]]
|
||||||
|
|
||||||
|
sessions:
|
||||||
|
type: "string"
|
||||||
|
description: "Comma-separated list of sessions (alternative to --session)"
|
||||||
|
examples: ["cc_UV_dev0_Fb,cc_UV_dev1_48"]
|
||||||
|
|
||||||
|
message:
|
||||||
|
type: "string"
|
||||||
|
default: "Operator says CONTINUE. pi_nvnemo here, saying hi!"
|
||||||
|
description: "Wake message sent to agent(s)"
|
||||||
|
|
||||||
|
count:
|
||||||
|
type: "integer"
|
||||||
|
minimum: 1
|
||||||
|
default: 1
|
||||||
|
description: "Number of runs for succession mode"
|
||||||
|
|
||||||
|
dry_run:
|
||||||
|
type: "boolean"
|
||||||
|
default: false
|
||||||
|
description: "Show what would be done without executing"
|
||||||
|
|
||||||
|
debug:
|
||||||
|
type: "boolean"
|
||||||
|
default: false
|
||||||
|
description: "Enable debug logging"
|
||||||
|
|
||||||
|
json:
|
||||||
|
type: "boolean"
|
||||||
|
default: false
|
||||||
|
description: "Output JSON for machine parsing"
|
||||||
|
|
||||||
|
config:
|
||||||
|
type: "string"
|
||||||
|
description: "Path to config file (JSON/YAML)"
|
||||||
|
|
||||||
|
# ─── Returns ──────────────────────────────────────────────────────────
|
||||||
|
returns:
|
||||||
|
type: "object"
|
||||||
|
properties:
|
||||||
|
exit_code:
|
||||||
|
type: "integer"
|
||||||
|
description: "0 = success, non-zero = error"
|
||||||
|
cron_entry:
|
||||||
|
type: "string"
|
||||||
|
description: "Installed cron line (for install mode)"
|
||||||
|
log_file:
|
||||||
|
type: "string"
|
||||||
|
value: "/tmp/pi_wake_agent.log"
|
||||||
|
message_sent:
|
||||||
|
type: "boolean"
|
||||||
|
description: "Whether wake message was sent"
|
||||||
|
|
||||||
|
# ─── Side Effects ─────────────────────────────────────────────────────
|
||||||
|
side_effects:
|
||||||
|
- "Modifies system crontab (install/remove modes)"
|
||||||
|
- "Injects text into zellij sessions (zellij write-chars)"
|
||||||
|
- "Sends h5i bus message to Fable (fire-and-forget)"
|
||||||
|
- "Writes to /tmp/pi_wake_agent.log (rotated at 10MB)"
|
||||||
|
- "Creates PID files in /tmp/pi_wake_agent_*.pid (once mode)"
|
||||||
|
|
||||||
|
# ─── h5i Bus Protocol ─────────────────────────────────────────────────
|
||||||
|
bus_protocol:
|
||||||
|
agent_id: "pi_nvnemo"
|
||||||
|
target: "Fable"
|
||||||
|
message_format: "[pi_nvnemo via zellij] {message} Run: h5i-bus msg inbox"
|
||||||
|
keypresses: 5
|
||||||
|
keypress_delay_ms: 1000
|
||||||
|
bus_message: "{message} (timer wakeup)"
|
||||||
|
timeout_ms: 5000
|
||||||
|
fire_and_forget: true
|
||||||
|
silently_ignore_failures: true
|
||||||
|
|
||||||
|
# ─── Zellij Integration ───────────────────────────────────────────────
|
||||||
|
zellij:
|
||||||
|
command_check: "zellij list-sessions"
|
||||||
|
injection_method: "zellij --session {session} action write-chars"
|
||||||
|
enter_keypress: "zellij --session {session} action write 13"
|
||||||
|
ansi_strip: true
|
||||||
|
session_exists_check: true
|
||||||
|
|
||||||
|
# ─── Cron Format ──────────────────────────────────────────────────────
|
||||||
|
cron:
|
||||||
|
comment_format: "pi_wake_agent:{sessions}:{interval}"
|
||||||
|
session_delimiter: ","
|
||||||
|
command_template: "cd /mnt/dolphinng5_predict && export H5I_AGENT=pi_nvnemo && /mnt/dolphinng5_predict/pi_wake_agent.py --run --sessions '{sessions}' --msg '{message}'"
|
||||||
|
|
||||||
|
# ─── Logging ──────────────────────────────────────────────────────────
|
||||||
|
logging:
|
||||||
|
file: "/tmp/pi_wake_agent.log"
|
||||||
|
rotation_mb: 10
|
||||||
|
max_files: 5
|
||||||
|
format: "[YYYY-MM-DD HH:MM:SS] [LEVEL] message"
|
||||||
|
levels: ["DEBUG", "INFO", "WARN", "ERROR"]
|
||||||
|
|
||||||
|
# ─── Examples ─────────────────────────────────────────────────────────
|
||||||
|
examples:
|
||||||
|
- name: "Recurring 1-hour wake for one session"
|
||||||
|
command: "pi_wake_agent.py --install --interval 1h --session cc_UV_dev0_Fb"
|
||||||
|
- name: "Multi-session 30-minute wake"
|
||||||
|
command: 'pi_wake_agent.py --install --interval 30m --sessions "cc_UV_dev0_Fb,cc_UV_dev1_48" --msg "Wake up!"'
|
||||||
|
- name: "One-shot in 2 hours"
|
||||||
|
command: 'pi_wake_agent.py --once --interval 2h --session cc_UV_dev0_Fb --msg "Time\'s up!"'
|
||||||
|
- name: "Run 3 times at 1-hour intervals, then self-clean"
|
||||||
|
command: 'pi_wake_agent.py --succession --count 3 --interval 1h --session cc_UV_dev0_Fb --msg "Scheduled wake"'
|
||||||
|
- name: "Daemon mode (long-lived process, no cron)"
|
||||||
|
command: 'pi_wake_agent.py --daemon --interval 1h --session cc_UV_dev0_Fb'
|
||||||
|
- name: "Remove timer"
|
||||||
|
command: 'pi_wake_agent.py --remove --session cc_UV_dev0_Fb --interval 1h'
|
||||||
|
- name: "List / status"
|
||||||
|
command: "pi_wake_agent.py --list"
|
||||||
|
command: "pi_wake_agent.py --status"
|
||||||
|
- name: "Validate sessions exist"
|
||||||
|
command: 'pi_wake_agent.py --validate --session cc_UV_dev0_Fb'
|
||||||
|
- name: "Dry run (show what would happen)"
|
||||||
|
command: 'pi_wake_agent.py --install --interval 1h --session test --msg "test" --dry-run'
|
||||||
|
|
||||||
|
# ─── Error Handling ───────────────────────────────────────────────────
|
||||||
|
error_handling:
|
||||||
|
- condition: "missing_session"
|
||||||
|
action: "return_error"
|
||||||
|
message: "--session or --sessions required"
|
||||||
|
exit_code: 1
|
||||||
|
- condition: "invalid_interval"
|
||||||
|
action: "return_error"
|
||||||
|
message: "Invalid interval format. Use like 1h, 30m, 90m, 2h"
|
||||||
|
exit_code: 1
|
||||||
|
- condition: "invalid_count"
|
||||||
|
action: "return_error"
|
||||||
|
message: "--count must be >= 1 for succession mode"
|
||||||
|
exit_code: 1
|
||||||
|
- condition: "cron_failure"
|
||||||
|
action: "log_warning_continue"
|
||||||
|
description: "Cron install/remove logs warning but continues"
|
||||||
|
- condition: "zellij_not_found"
|
||||||
|
action: "log_warning_continue"
|
||||||
|
description: "Session injection fails silently, bus message still sent"
|
||||||
|
- condition: "h5i_timeout"
|
||||||
|
action: "silent_ignore"
|
||||||
|
description: "h5i bus message failures are silently ignored (fire-and-forget)"
|
||||||
|
|
||||||
|
# ─── Testing ──────────────────────────────────────────────────────────
|
||||||
|
testing:
|
||||||
|
test_file: "test_pi_wake_agent.py"
|
||||||
|
test_count: 38
|
||||||
|
coverage:
|
||||||
|
- unit_interval_parsing
|
||||||
|
- unit_cron_comments
|
||||||
|
- unit_session_parsing
|
||||||
|
- integration_install_remove_list
|
||||||
|
- integration_once_short
|
||||||
|
- integration_succession_short
|
||||||
|
- edge_cases_invalid_intervals
|
||||||
|
- edge_cases_missing_sessions
|
||||||
|
- edge_cases_invalid_counts
|
||||||
|
run_command: "python3 -m pytest test_pi_wake_agent.py -v"
|
||||||
|
|
||||||
|
# ─── Metadata ─────────────────────────────────────────────────────────
|
||||||
|
metadata:
|
||||||
|
created: "2026-07-08"
|
||||||
|
updated: "2026-07-08"
|
||||||
|
maintainer: "pi_nvnemo"
|
||||||
|
tags: ["wake-timer", "cron", "zellij", "h5i", "multi-agent", "doorbell", "self-cleaning"]
|
||||||
Reference in New Issue
Block a user