#!/usr/bin/env bash # Append a run heartbeat for long agent dispatches. # Binding: wiki/process/agent-scale.md slice G. # # Usage: # tools/heartbeat.sh start [--worktree path] [--phase text] # tools/heartbeat.sh phase # tools/heartbeat.sh end [--status ok|fail] # # Writes machine-local (gitignored) files under .agents/runs//. set -euo pipefail script_root=$(cd "$(dirname "$0")/.." && pwd) root=$script_root if [ -f "$script_root/.git" ]; then common=$(git -C "$script_root" rev-parse --path-format=absolute --git-common-dir) case "$common" in */.git) root=${common%/.git} ;; esac fi RUNS_DIR=${MISALIGNED_RUNS_DIR:-"$root/.agents/runs"} usage() { sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//' exit 2 } run_dir() { local id="$1" case "$id" in ""|*/*|*..*) echo "FAIL: bad run id" >&2; exit 2 ;; esac printf '%s/%s\n' "$RUNS_DIR" "$id" } now() { date -u +"%Y-%m-%dT%H:%M:%SZ"; } cmd_start() { local id="${1:-}"; shift || true [ -n "$id" ] || usage local worktree="" phase="start" while [ $# -gt 0 ]; do case "$1" in --worktree) shift; worktree="${1:-}"; shift || true ;; --phase) shift; phase="${1:-start}"; shift || true ;; *) usage ;; esac done local dir dir=$(run_dir "$id") mkdir -p "$dir" cat > "$dir/status.json" <> "$dir/events.ndjson" echo "heartbeat: started $id -> $dir" } cmd_phase() { local id="${1:-}" phase="${2:-}" [ -n "$id" ] && [ -n "$phase" ] || usage local dir dir=$(run_dir "$id") mkdir -p "$dir" local started if [ -f "$dir/status.json" ]; then started=$(grep -o '"started_at":"[^"]*"' "$dir/status.json" | head -1 | sed 's/.*"started_at":"//;s/"$//') fi started=${started:-$(now)} cat > "$dir/status.json" <> "$dir/events.ndjson" echo "heartbeat: $id -> $phase" } cmd_end() { local id="${1:-}"; shift || true [ -n "$id" ] || usage local status=ok while [ $# -gt 0 ]; do case "$1" in --status) shift; status="${1:-ok}"; shift || true ;; *) usage ;; esac done local dir dir=$(run_dir "$id") mkdir -p "$dir" local started phase started=$(now); phase=end if [ -f "$dir/status.json" ]; then started=$(grep -o '"started_at":"[^"]*"' "$dir/status.json" | head -1 | sed 's/.*"started_at":"//;s/"$//' || echo "$started") phase=$(grep -o '"phase":"[^"]*"' "$dir/status.json" | head -1 | sed 's/.*"phase":"//;s/"$//' || echo end) fi cat > "$dir/status.json" <> "$dir/events.ndjson" echo "heartbeat: ended $id ($status)" } cmd="${1:-}"; shift || true case "$cmd" in start) cmd_start "$@" ;; phase) cmd_phase "$@" ;; end) cmd_end "$@" ;; *) usage ;; esac