Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
236 changes: 178 additions & 58 deletions .agents/scripts/pulse-session-helper.sh
Original file line number Diff line number Diff line change
@@ -1,31 +1,32 @@
#!/usr/bin/env bash
# pulse-session-helper.sh - Session-based pulse control
# pulse-session-helper.sh - Pulse consent and session control
#
# Enables/disables the supervisor pulse for bounded work sessions.
# Users start the pulse when they begin working and stop it when done,
# avoiding unattended overnight API spend and unreviewed PR accumulation.
# Controls the supervisor pulse via a layered consent model:
# 1. Stop flag (~/.aidevops/logs/pulse-session.stop) — highest priority, pauses pulse
# 2. Session flag (~/.aidevops/logs/pulse-session.flag) — explicit start, doesn't survive reboots
# 3. Config consent (orchestration.supervisor_pulse=true) — persistent, survives reboots
#
# Usage:
# pulse-session-helper.sh start # Enable pulse (create session flag)
# pulse-session-helper.sh stop # Graceful stop (let workers finish, then disable)
# pulse-session-helper.sh status # Show pulse session state
# pulse-session-helper.sh start # Clear stop flag, create session flag
# pulse-session-helper.sh stop # Create stop flag, remove session flag, wait for workers
# pulse-session-helper.sh status # Show consent layers, workers, repos
# pulse-session-helper.sh help # Show usage
#
# How it works:
# - `start` creates a session flag file that pulse-wrapper.sh checks
# - `stop` removes the flag and optionally waits for in-flight workers
# - pulse-wrapper.sh skips the pulse cycle when the flag is absent
# - The launchd plist stays loaded — it just becomes a no-op when disabled
#
# Flag file: ~/.aidevops/logs/pulse-session.flag
# Contains: started_at ISO timestamp, started_by username
# The launchd plist stays loaded — pulse-wrapper.sh checks these consent
# layers on each cycle and skips if none grant permission.

set -euo pipefail

export PATH="/bin:/usr/bin:/usr/local/bin:/opt/homebrew/bin:${PATH}"

# Source config-helper for _jsonc_get (shared JSONC config reader)
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=config-helper.sh
source "${SCRIPT_DIR}/config-helper.sh" 2>/dev/null || true

# Configuration
readonly SESSION_FLAG="${HOME}/.aidevops/logs/pulse-session.flag"
readonly STOP_FLAG="${HOME}/.aidevops/logs/pulse-session.stop"
readonly LOGFILE="${HOME}/.aidevops/logs/pulse.log"
readonly PIDFILE="${HOME}/.aidevops/logs/pulse.pid"
readonly MAX_WORKERS_FILE="${HOME}/.aidevops/logs/pulse-max-workers"
Expand Down Expand Up @@ -92,20 +93,37 @@ count_workers() {
is_pulse_running() {
if [[ -f "$PIDFILE" ]]; then
local pid
pid=$(cat "$PIDFILE" 2>/dev/null || echo "")
pid=$(cat "$PIDFILE" || echo "")
if [[ -n "$pid" ]] && ps -p "$pid" >/dev/null 2>&1; then
return 0
fi
fi
return 1
}

#######################################
# Check if config consent is enabled
# Delegates to config_enabled from config-helper.sh (sourced above),
# which handles: env var override (AIDEVOPS_SUPERVISOR_PULSE) >
# user JSONC config > defaults JSONC config. Single canonical
# implementation shared with pulse-wrapper.sh via shared-constants.sh.
# Returns: 0 if enabled, 1 if not
#######################################
is_config_consent_enabled() {
if type config_enabled &>/dev/null; then
config_enabled "orchestration.supervisor_pulse"
return $?
fi
# Fallback if config-helper.sh failed to load entirely
return 1
}

#######################################
# Get pulse-enabled repo count
#######################################
get_pulse_repo_count() {
if [[ -f "$REPOS_JSON" ]] && command -v jq &>/dev/null; then
jq '[.initialized_repos[] | select(.pulse == true)] | length' "$REPOS_JSON" 2>/dev/null || echo "0"
jq '[.initialized_repos[] | select(.pulse == true)] | length' "$REPOS_JSON" || echo "0"
else
echo "?"
fi
Expand All @@ -118,7 +136,7 @@ get_pulse_repo_count() {
get_last_pulse_time() {
if [[ -f "$LOGFILE" ]]; then
local last_line
last_line=$(grep 'Starting pulse at' "$LOGFILE" 2>/dev/null | tail -1)
last_line=$(grep 'Starting pulse at' "$LOGFILE" | tail -1)
if [[ -n "$last_line" ]]; then
echo "$last_line" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z' | tail -1
return 0
Expand All @@ -132,9 +150,12 @@ get_last_pulse_time() {
# Start pulse session
#######################################
cmd_start() {
# Remove stop flag if present (user is explicitly resuming)
rm -f "$STOP_FLAG"

if is_session_active; then
local started_at
started_at=$(grep '^started_at=' "$SESSION_FLAG" 2>/dev/null | cut -d= -f2)
started_at=$(grep '^started_at=' "$SESSION_FLAG" | cut -d= -f2)
print_warning "Pulse session already active (started: ${started_at:-unknown})"
echo ""
echo " To restart: aidevops pulse stop && aidevops pulse start"
Expand All @@ -158,7 +179,7 @@ EOF
repo_count=$(get_pulse_repo_count)
local max_workers="?"
if [[ -f "$MAX_WORKERS_FILE" ]]; then
max_workers=$(cat "$MAX_WORKERS_FILE" 2>/dev/null || echo "?")
max_workers=$(cat "$MAX_WORKERS_FILE" || echo "?")
fi

print_success "Pulse session started"
Expand All @@ -175,9 +196,10 @@ EOF
#######################################
# Stop pulse session (graceful)
#
# 1. Remove the session flag (prevents new pulse cycles)
# 2. Wait for in-flight workers to finish (up to grace period)
# 3. Optionally kill remaining workers if --force is passed
# 1. Create stop flag (overrides all consent layers)
# 2. Remove the session flag
# 3. Wait for in-flight workers to finish (up to grace period)
# 4. Optionally kill remaining workers if --force is passed
#######################################
cmd_stop() {
local force=false
Expand All @@ -193,22 +215,43 @@ cmd_stop() {
esac
done

if ! is_session_active; then
print_info "Pulse session is not active"
return 0
# Check if already stopped — but allow --force through so it can kill workers
if [[ -f "$STOP_FLAG" ]] && ! is_session_active; then
local worker_count_check
worker_count_check=$(count_workers)
if [[ "$force" != "true" ]] && [[ "$worker_count_check" -eq 0 ]] && ! is_pulse_running; then
print_info "Pulse is already stopped"
return 0
fi
fi

local started_at
started_at=$(grep '^started_at=' "$SESSION_FLAG" 2>/dev/null | cut -d= -f2)
# If no session flag and no config consent, nothing to stop
if ! is_session_active && ! is_config_consent_enabled; then
print_info "Pulse is not enabled (no session flag, no config consent)"
return 0
fi

# Remove session flag — this prevents new pulse cycles immediately
rm -f "$SESSION_FLAG"
local started_at=""
if is_session_active; then
started_at=$(grep '^started_at=' "$SESSION_FLAG" | cut -d= -f2)
fi

# Create stop flag — this overrides all consent layers immediately
local now_iso
now_iso=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local user
user=$(whoami)
cat >"$STOP_FLAG" <<EOF
stopped_at=${now_iso}
stopped_by=${user}
EOF

# Also remove session flag for clean state
rm -f "$SESSION_FLAG"

echo "[pulse-session] Session stopped at ${now_iso} (was started: ${started_at:-unknown})" >>"$LOGFILE"

print_success "Pulse session stopped (no new pulse cycles will start)"
print_success "Pulse stopped (no new pulse cycles will start)"

# Check for in-flight workers
local worker_count
Expand Down Expand Up @@ -292,30 +335,89 @@ cmd_stop() {
# Show pulse session status
#######################################
cmd_status() {
echo -e "${BOLD}Pulse Session Status${NC}"
echo "─────────────────────"
echo -e "${BOLD}Pulse Status${NC}"
echo "────────────"
echo ""

# Session state
# Consent layers (mirrors check_session_gate() in pulse-wrapper.sh)
local effective_state="disabled"
local effective_reason=""

# Layer 1: Stop flag (highest priority — overrides everything)
local has_stop_flag=false
if [[ -f "$STOP_FLAG" ]]; then
has_stop_flag=true
fi

# Layer 2: Session flag (explicit user action)
local has_session_flag=false
if is_session_active; then
has_session_flag=true
fi

# Layer 3: Config consent (persistent, survives reboots)
local has_config_consent=false
if is_config_consent_enabled; then
has_config_consent=true
fi

# Determine effective state
if [[ "$has_stop_flag" == "true" ]]; then
effective_state="stopped"
effective_reason="stop flag (aidevops pulse stop)"
elif [[ "$has_session_flag" == "true" ]]; then
effective_state="enabled"
effective_reason="session flag (aidevops pulse start)"
elif [[ "$has_config_consent" == "true" ]]; then
effective_state="enabled"
effective_reason="config consent (orchestration.supervisor_pulse=true)"
else
effective_state="disabled"
effective_reason="no consent layer active"
fi

# Display effective state
if [[ "$effective_state" == "enabled" ]]; then
echo -e " Pulse: ${GREEN}enabled${NC} via ${effective_reason}"
elif [[ "$effective_state" == "stopped" ]]; then
echo -e " Pulse: ${RED}stopped${NC} via ${effective_reason}"
else
echo -e " Pulse: ${YELLOW}disabled${NC} (${effective_reason})"
fi

# Show consent layer details
# Sanitize values from flag files to prevent terminal escape injection
echo ""
echo -e " ${BOLD}Consent layers:${NC}"
if [[ "$has_stop_flag" == "true" ]]; then
local stopped_at
stopped_at=$(grep '^stopped_at=' "$STOP_FLAG" | cut -d= -f2 | tr -cd '[:alnum:]T:Z.+-')
echo -e " Stop flag: ${RED}set${NC} (${stopped_at:-unknown})"
else
echo -e " Stop flag: ${GREEN}clear${NC}"
fi
if [[ "$has_session_flag" == "true" ]]; then
local started_at started_by
started_at=$(grep '^started_at=' "$SESSION_FLAG" 2>/dev/null | cut -d= -f2)
started_by=$(grep '^started_by=' "$SESSION_FLAG" 2>/dev/null | cut -d= -f2)
echo -e " Session: ${GREEN}active${NC}"
echo " Started: ${started_at:-unknown}"
echo " Started by: ${started_by:-unknown}"
started_at=$(grep '^started_at=' "$SESSION_FLAG" | cut -d= -f2 | tr -cd '[:alnum:]T:Z.+-')
started_by=$(grep '^started_by=' "$SESSION_FLAG" | cut -d= -f2 | tr -cd '[:alnum:]._-')
echo -e " Session flag: ${GREEN}active${NC} (${started_at:-unknown} by ${started_by:-unknown})"
else
echo -e " Session flag: ${YELLOW}inactive${NC}"
fi
if [[ "$has_config_consent" == "true" ]]; then
echo -e " Config consent: ${GREEN}enabled${NC}"
else
echo -e " Session: ${YELLOW}inactive${NC} (pulse will skip cycles)"
echo -e " Config consent: ${YELLOW}disabled${NC}"
fi
echo ""

# Pulse process
if is_pulse_running; then
local pulse_pid
pulse_pid=$(cat "$PIDFILE" 2>/dev/null || echo "?")
echo -e " Pulse: ${GREEN}running${NC} (PID ${pulse_pid})"
pulse_pid=$(cat "$PIDFILE" || echo "?")
echo -e " Process: ${GREEN}running${NC} (PID ${pulse_pid})"
else
echo -e " Pulse: ${BLUE}idle${NC} (waiting for next launchd cycle)"
echo -e " Process: ${BLUE}idle${NC} (waiting for next launchd cycle)"
fi

# Workers
Expand All @@ -330,7 +432,7 @@ cmd_status() {
# Max workers
local max_workers="?"
if [[ -f "$MAX_WORKERS_FILE" ]]; then
max_workers=$(cat "$MAX_WORKERS_FILE" 2>/dev/null || echo "?")
max_workers=$(cat "$MAX_WORKERS_FILE" || echo "?")
fi
echo " Max workers: ${max_workers}"

Expand Down Expand Up @@ -367,11 +469,14 @@ cmd_status() {
fi

# Hint
if is_session_active; then
if [[ "$effective_state" == "enabled" ]]; then
echo " Stop: aidevops pulse stop"
echo " Force: aidevops pulse stop --force"
elif [[ "$effective_state" == "stopped" ]]; then
echo " Resume: aidevops pulse start"
else
echo " Start: aidevops pulse start"
echo " Start: aidevops pulse start"
echo " Or set: orchestration.supervisor_pulse=true in config.jsonc"
fi
return 0
}
Expand All @@ -381,34 +486,49 @@ cmd_status() {
#######################################
cmd_help() {
cat <<'EOF'
pulse-session-helper.sh - Session-based pulse control
pulse-session-helper.sh - Pulse consent and session control

USAGE:
aidevops pulse <command> [options]

COMMANDS:
start Enable the pulse for this work session
stop [--force] Gracefully stop the pulse session
status Show pulse session state, workers, repos
start Enable the pulse (clears stop flag, creates session flag)
stop [--force] Stop the pulse (creates stop flag, removes session flag)
status Show consent layers, workers, repos

STOP OPTIONS:
--force, -f Send SIGTERM to workers immediately instead of waiting

ENVIRONMENT:
PULSE_STOP_GRACE_SECONDS Grace period for workers on stop (default: 300)
AIDEVOPS_SUPERVISOR_PULSE Override config consent (true/false)

HOW IT WORKS:
The supervisor pulse runs every 2 minutes via launchd. When no session
is active, pulse-wrapper.sh skips the cycle (no-op). Starting a session
creates a flag file that enables the pulse. Stopping removes the flag
and optionally waits for in-flight workers to finish.
The supervisor pulse runs every 2 minutes via launchd/cron. Whether it
actually does work depends on a layered consent model (checked in order):

1. Stop flag (~/.aidevops/logs/pulse-session.stop)
Highest priority. If present, pulse is paused regardless of other
layers. Created by 'aidevops pulse stop', cleared by 'start'.

2. Session flag (~/.aidevops/logs/pulse-session.flag)
Explicit user action. Created by 'aidevops pulse start'.
Does NOT survive reboots — use for bounded work sessions.

3. Config consent (orchestration.supervisor_pulse=true)
Persistent setting in ~/.config/aidevops/config.jsonc.
Survives reboots — the pulse runs unattended after reboot.
Set during 'aidevops init' or manually in config.

If none of the above are set, the pulse skips (no-op).

This gives you bounded automation: the pulse runs while you're available
to monitor outcomes, and stops when you're not.
For unattended operation: set config consent and don't use start/stop.
For bounded sessions: use 'aidevops pulse start' and 'stop'.
To pause temporarily: 'aidevops pulse stop' (overrides config consent).

EXAMPLES:
aidevops pulse start # Begin work session
aidevops pulse status # Check what's running
aidevops pulse start # Enable pulse for this session
aidevops pulse status # Show consent layers and workers
aidevops pulse stop # Graceful stop (wait for workers)
aidevops pulse stop --force # Stop immediately

Expand Down
Loading