From ab8c1be92c28c11ff0ab4411b713e8c51a89856e Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Fri, 6 Mar 2026 01:46:06 +0000 Subject: [PATCH 1/3] fix: replace mandatory session gate with layered consent model for pulse MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pulse required a session flag file (pulse-session.flag) that didn't survive reboots, making RunAtLoad=true meaningless — the pulse would start but immediately skip because no flag existed. Replace the single mandatory gate with a layered consent model: 1. Stop flag (pulse-session.stop) — highest priority, pauses pulse 2. Session flag (pulse-session.flag) — explicit start, bounded sessions 3. Config consent (orchestration.supervisor_pulse=true) — persistent, survives reboots for unattended operation pulse-wrapper.sh check_session_gate() checks layers in order. pulse-session-helper.sh start/stop/status updated to manage all layers. Closes #2942 --- .agents/scripts/pulse-session-helper.sh | 235 +++++++++++++++++++----- .agents/scripts/pulse-wrapper.sh | 62 ++++++- 2 files changed, 241 insertions(+), 56 deletions(-) diff --git a/.agents/scripts/pulse-session-helper.sh b/.agents/scripts/pulse-session-helper.sh index 6239d61b3..119427546 100755 --- a/.agents/scripts/pulse-session-helper.sh +++ b/.agents/scripts/pulse-session-helper.sh @@ -1,24 +1,19 @@ #!/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 @@ -26,6 +21,7 @@ export PATH="/bin:/usr/bin:/usr/local/bin:/opt/homebrew/bin:${PATH}" # 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" @@ -100,6 +96,45 @@ is_pulse_running() { return 1 } +####################################### +# Check if config consent is enabled +# Mirrors the config check in pulse-wrapper.sh check_session_gate() +# Returns: 0 if enabled, 1 if not +####################################### +is_config_consent_enabled() { + # NOTE: This mirrors the config check in pulse-wrapper.sh check_session_gate(). + # Kept separate because pulse-wrapper.sh is a standalone 2600+ line script + # with its own initialization — sourcing it here would be impractical. + local pulse_config="" + + # JSONC config (primary) — strip // comments before matching + local config_file="${HOME}/.config/aidevops/config.jsonc" + if [[ -f "$config_file" ]]; then + pulse_config=$(sed 's|//.*||' "$config_file" | grep -o '"supervisor_pulse"[[:space:]]*:[[:space:]]*[a-z]*' | tail -1 | grep -o '[a-z]*$' || echo "") + fi + + # Legacy .conf fallback + if [[ -z "$pulse_config" ]]; then + local legacy_conf="${HOME}/.config/aidevops/feature-toggles.conf" + if [[ -f "$legacy_conf" ]]; then + pulse_config=$(grep -E '^supervisor_pulse=' "$legacy_conf" | tail -1 | cut -d= -f2 || echo "") + fi + fi + + # Env var override + if [[ -n "${AIDEVOPS_SUPERVISOR_PULSE:-}" ]]; then + pulse_config="$AIDEVOPS_SUPERVISOR_PULSE" + fi + + local pulse_lower + pulse_lower=$(echo "$pulse_config" | tr '[:upper:]' '[:lower:]') + + if [[ "$pulse_lower" == "true" ]]; then + return 0 + fi + return 1 +} + ####################################### # Get pulse-enabled repo count ####################################### @@ -132,9 +167,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" @@ -175,9 +213,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 @@ -193,22 +232,39 @@ cmd_stop() { esac done - if ! is_session_active; then - print_info "Pulse session is not active" + # Check if already stopped (stop flag present and no session flag) + if [[ -f "$STOP_FLAG" ]] && ! is_session_active; then + print_info "Pulse is already stopped" return 0 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" <>"$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 @@ -292,20 +348,79 @@ 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 "" @@ -313,9 +428,9 @@ cmd_status() { 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})" + 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 @@ -367,11 +482,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 } @@ -381,34 +499,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 [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 diff --git a/.agents/scripts/pulse-wrapper.sh b/.agents/scripts/pulse-wrapper.sh index 6e2dd9d6e..c68f710d5 100755 --- a/.agents/scripts/pulse-wrapper.sh +++ b/.agents/scripts/pulse-wrapper.sh @@ -2403,16 +2403,68 @@ _Auto-generated by pulse-wrapper.sh daily quality sweep. The supervisor will rev } ####################################### -# Check if a pulse session is active -# Returns: 0 if session active (proceed), 1 if not (skip) +# Check if the pulse is allowed to run. +# +# Consent model (layered, highest priority first): +# 1. Session stop flag — `aidevops pulse stop` creates this to pause +# the pulse without uninstalling it. Checked first so stop always wins. +# 2. Session start flag — `aidevops pulse start` creates this. If present, +# the pulse runs regardless of config (explicit user action). +# 3. Config consent — setup.sh writes orchestration.supervisor_pulse=true +# when the user consents. This is the persistent, reboot-surviving gate. +# +# If none of the above are set, the pulse was installed without config +# consent (shouldn't happen after GH#2926) — skip as a safety fallback. +# +# Returns: 0 if pulse should run, 1 if not ####################################### check_session_gate() { local session_flag="${HOME}/.aidevops/logs/pulse-session.flag" - if [[ ! -f "$session_flag" ]]; then - echo "[pulse-wrapper] No active pulse session — skipping cycle (start with: aidevops pulse start)" >>"$LOGFILE" + local stop_flag="${HOME}/.aidevops/logs/pulse-session.stop" + + # Stop flag takes priority — user explicitly paused + if [[ -f "$stop_flag" ]]; then + echo "[pulse-wrapper] Pulse paused (stop flag present) — resume with: aidevops pulse start" >>"$LOGFILE" return 1 fi - return 0 + + # Session start flag — explicit user action, always allowed + if [[ -f "$session_flag" ]]; then + return 0 + fi + + # Config consent — the persistent gate that survives reboots + # Check JSONC config first, then legacy .conf, then env var + local pulse_config="" + + # JSONC config (primary) — strip // comments before matching + local config_file="${HOME}/.config/aidevops/config.jsonc" + if [[ -f "$config_file" ]]; then + pulse_config=$(sed 's|//.*||' "$config_file" | grep -o '"supervisor_pulse"[[:space:]]*:[[:space:]]*[a-z]*' | tail -1 | grep -o '[a-z]*$' || echo "") + fi + + # Legacy .conf fallback + if [[ -z "$pulse_config" ]]; then + local legacy_conf="${HOME}/.config/aidevops/feature-toggles.conf" + if [[ -f "$legacy_conf" ]]; then + pulse_config=$(grep -E '^supervisor_pulse=' "$legacy_conf" | tail -1 | cut -d= -f2 || echo "") + fi + fi + + # Env var override (highest priority for config layer) + if [[ -n "${AIDEVOPS_SUPERVISOR_PULSE:-}" ]]; then + pulse_config="$AIDEVOPS_SUPERVISOR_PULSE" + fi + + local pulse_lower + pulse_lower=$(echo "$pulse_config" | tr '[:upper:]' '[:lower:]') + + if [[ "$pulse_lower" == "true" ]]; then + return 0 + fi + + echo "[pulse-wrapper] Pulse not enabled — set orchestration.supervisor_pulse=true in config or run: aidevops pulse start" >>"$LOGFILE" + return 1 } ####################################### From 4a3b0e0b2d4acc4010dea50656421145334ac400 Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:20:40 +0000 Subject: [PATCH 2/3] fix: address review feedback on pulse consent model - Deduplicate config consent: use shared _jsonc_get from config-helper.sh instead of duplicated grep/sed JSONC parsing in both scripts - Fix cmd_stop early return blocking --force when stop flag already exists - Add stop flag re-check inside run_pulse() watchdog loop and before run_pulse() call so 'aidevops pulse stop' during active cycle is honored - Remove unnecessary 2>/dev/null after file existence checks (6 instances) - Retain grep fallback in pulse-session-helper.sh for when config-helper.sh fails to load (defensive coding for standalone execution) --- .agents/scripts/pulse-session-helper.sh | 59 +++++++++++++------------ .agents/scripts/pulse-wrapper.sh | 35 ++++++++------- 2 files changed, 49 insertions(+), 45 deletions(-) diff --git a/.agents/scripts/pulse-session-helper.sh b/.agents/scripts/pulse-session-helper.sh index 119427546..7109f46b0 100755 --- a/.agents/scripts/pulse-session-helper.sh +++ b/.agents/scripts/pulse-session-helper.sh @@ -19,6 +19,11 @@ 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" @@ -88,7 +93,7 @@ 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 @@ -98,32 +103,26 @@ is_pulse_running() { ####################################### # Check if config consent is enabled -# Mirrors the config check in pulse-wrapper.sh check_session_gate() +# Uses _jsonc_get from config-helper.sh (shared with pulse-wrapper.sh +# via shared-constants.sh) to avoid duplicating JSONC parsing logic. +# Env var AIDEVOPS_SUPERVISOR_PULSE overrides config. # Returns: 0 if enabled, 1 if not ####################################### is_config_consent_enabled() { - # NOTE: This mirrors the config check in pulse-wrapper.sh check_session_gate(). - # Kept separate because pulse-wrapper.sh is a standalone 2600+ line script - # with its own initialization — sourcing it here would be impractical. local pulse_config="" - # JSONC config (primary) — strip // comments before matching - local config_file="${HOME}/.config/aidevops/config.jsonc" - if [[ -f "$config_file" ]]; then - pulse_config=$(sed 's|//.*||' "$config_file" | grep -o '"supervisor_pulse"[[:space:]]*:[[:space:]]*[a-z]*' | tail -1 | grep -o '[a-z]*$' || echo "") - fi - - # Legacy .conf fallback - if [[ -z "$pulse_config" ]]; then - local legacy_conf="${HOME}/.config/aidevops/feature-toggles.conf" - if [[ -f "$legacy_conf" ]]; then - pulse_config=$(grep -E '^supervisor_pulse=' "$legacy_conf" | tail -1 | cut -d= -f2 || echo "") - fi - fi - - # Env var override + # Env var override (highest priority) if [[ -n "${AIDEVOPS_SUPERVISOR_PULSE:-}" ]]; then pulse_config="$AIDEVOPS_SUPERVISOR_PULSE" + elif type _jsonc_get &>/dev/null; then + # Use shared config reader (handles JSONC stripping, defaults merging) + pulse_config=$(_jsonc_get "orchestration.supervisor_pulse" "false") + else + # Fallback if config-helper.sh failed to load — basic grep with comment stripping + local config_file="${HOME}/.config/aidevops/config.jsonc" + if [[ -f "$config_file" ]]; then + pulse_config=$(sed 's|//.*||' "$config_file" | grep -o '"supervisor_pulse"[[:space:]]*:[[:space:]]*[a-z]*' | tail -1 | grep -o '[a-z]*$' || echo "") + fi fi local pulse_lower @@ -140,7 +139,7 @@ is_config_consent_enabled() { ####################################### 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 @@ -153,7 +152,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 @@ -196,7 +195,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" @@ -232,10 +231,14 @@ cmd_stop() { esac done - # Check if already stopped (stop flag present and no session flag) + # Check if already stopped — but allow --force through so it can kill workers if [[ -f "$STOP_FLAG" ]] && ! is_session_active; then - print_info "Pulse is already stopped" - return 0 + 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 # If no session flag and no config consent, nothing to stop @@ -427,7 +430,7 @@ cmd_status() { # Pulse process if is_pulse_running; then local pulse_pid - pulse_pid=$(cat "$PIDFILE" 2>/dev/null || echo "?") + pulse_pid=$(cat "$PIDFILE" || echo "?") echo -e " Process: ${GREEN}running${NC} (PID ${pulse_pid})" else echo -e " Process: ${BLUE}idle${NC} (waiting for next launchd cycle)" @@ -445,7 +448,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}" diff --git a/.agents/scripts/pulse-wrapper.sh b/.agents/scripts/pulse-wrapper.sh index c68f710d5..377f5970f 100755 --- a/.agents/scripts/pulse-wrapper.sh +++ b/.agents/scripts/pulse-wrapper.sh @@ -1150,8 +1150,11 @@ ${state_content} local elapsed=$((now - start_epoch)) local kill_reason="" + # Check 0: Stop flag — user ran `aidevops pulse stop` during this cycle (t2943) + if [[ -f "${HOME}/.aidevops/logs/pulse-session.stop" ]]; then + kill_reason="Stop flag detected during active pulse — user requested stop" # Check 1: Wall-clock stale threshold (hard ceiling) - if [[ "$elapsed" -gt "$PULSE_STALE_THRESHOLD" ]]; then + elif [[ "$elapsed" -gt "$PULSE_STALE_THRESHOLD" ]]; then kill_reason="Pulse exceeded stale threshold (${elapsed}s > ${PULSE_STALE_THRESHOLD}s)" # Check 2: Idle detection — CPU usage of the process tree (t1398.3) # Skip idle checks during the first 2 minutes to allow startup/init. @@ -2434,26 +2437,15 @@ check_session_gate() { fi # Config consent — the persistent gate that survives reboots - # Check JSONC config first, then legacy .conf, then env var + # Uses _jsonc_get from config-helper.sh (sourced via shared-constants.sh) + # which handles JSONC comment stripping, defaults merging, and jq parsing. + # Env var override has highest priority. local pulse_config="" - # JSONC config (primary) — strip // comments before matching - local config_file="${HOME}/.config/aidevops/config.jsonc" - if [[ -f "$config_file" ]]; then - pulse_config=$(sed 's|//.*||' "$config_file" | grep -o '"supervisor_pulse"[[:space:]]*:[[:space:]]*[a-z]*' | tail -1 | grep -o '[a-z]*$' || echo "") - fi - - # Legacy .conf fallback - if [[ -z "$pulse_config" ]]; then - local legacy_conf="${HOME}/.config/aidevops/feature-toggles.conf" - if [[ -f "$legacy_conf" ]]; then - pulse_config=$(grep -E '^supervisor_pulse=' "$legacy_conf" | tail -1 | cut -d= -f2 || echo "") - fi - fi - - # Env var override (highest priority for config layer) if [[ -n "${AIDEVOPS_SUPERVISOR_PULSE:-}" ]]; then pulse_config="$AIDEVOPS_SUPERVISOR_PULSE" + elif type _jsonc_get &>/dev/null; then + pulse_config=$(_jsonc_get "orchestration.supervisor_pulse" "false") fi local pulse_lower @@ -2484,6 +2476,15 @@ main() { calculate_max_workers check_session_count >/dev/null prefetch_state + + # Re-check stop flag immediately before run_pulse() — a stop may have + # been issued during the prefetch/cleanup phase above (t2943) + local stop_flag_recheck="${HOME}/.aidevops/logs/pulse-session.stop" + if [[ -f "$stop_flag_recheck" ]]; then + echo "[pulse-wrapper] Stop flag appeared during setup — aborting before run_pulse()" >>"$LOGFILE" + return 0 + fi + run_pulse run_daily_quality_sweep update_health_issues From f2311e2059cba62eb9eebd85d63e39cfbf6d9965 Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Fri, 6 Mar 2026 02:33:13 +0000 Subject: [PATCH 3/3] refactor: deduplicate consent resolution into shared config_enabled helper Address CodeRabbit review feedback: both pulse-session-helper.sh and pulse-wrapper.sh had independent implementations of config consent checking (env var > JSONC config > fallback). Replace with calls to config_enabled('orchestration.supervisor_pulse') from config-helper.sh, which already handles the full precedence chain correctly. This eliminates the fragile grep-based JSONC fallback in pulse-session-helper.sh that could match commented-out lines (Gemini security finding), and ensures both scripts use the same canonical implementation that won't drift apart. --- .agents/scripts/pulse-session-helper.sh | 32 +++++++------------------ .agents/scripts/pulse-wrapper.sh | 22 +++++------------ 2 files changed, 14 insertions(+), 40 deletions(-) diff --git a/.agents/scripts/pulse-session-helper.sh b/.agents/scripts/pulse-session-helper.sh index 7109f46b0..77fcc0db2 100755 --- a/.agents/scripts/pulse-session-helper.sh +++ b/.agents/scripts/pulse-session-helper.sh @@ -103,34 +103,18 @@ is_pulse_running() { ####################################### # Check if config consent is enabled -# Uses _jsonc_get from config-helper.sh (shared with pulse-wrapper.sh -# via shared-constants.sh) to avoid duplicating JSONC parsing logic. -# Env var AIDEVOPS_SUPERVISOR_PULSE overrides config. +# 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() { - local pulse_config="" - - # Env var override (highest priority) - if [[ -n "${AIDEVOPS_SUPERVISOR_PULSE:-}" ]]; then - pulse_config="$AIDEVOPS_SUPERVISOR_PULSE" - elif type _jsonc_get &>/dev/null; then - # Use shared config reader (handles JSONC stripping, defaults merging) - pulse_config=$(_jsonc_get "orchestration.supervisor_pulse" "false") - else - # Fallback if config-helper.sh failed to load — basic grep with comment stripping - local config_file="${HOME}/.config/aidevops/config.jsonc" - if [[ -f "$config_file" ]]; then - pulse_config=$(sed 's|//.*||' "$config_file" | grep -o '"supervisor_pulse"[[:space:]]*:[[:space:]]*[a-z]*' | tail -1 | grep -o '[a-z]*$' || echo "") - fi - fi - - local pulse_lower - pulse_lower=$(echo "$pulse_config" | tr '[:upper:]' '[:lower:]') - - if [[ "$pulse_lower" == "true" ]]; then - return 0 + 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 } diff --git a/.agents/scripts/pulse-wrapper.sh b/.agents/scripts/pulse-wrapper.sh index 377f5970f..cbd568491 100755 --- a/.agents/scripts/pulse-wrapper.sh +++ b/.agents/scripts/pulse-wrapper.sh @@ -2436,22 +2436,12 @@ check_session_gate() { return 0 fi - # Config consent — the persistent gate that survives reboots - # Uses _jsonc_get from config-helper.sh (sourced via shared-constants.sh) - # which handles JSONC comment stripping, defaults merging, and jq parsing. - # Env var override has highest priority. - local pulse_config="" - - if [[ -n "${AIDEVOPS_SUPERVISOR_PULSE:-}" ]]; then - pulse_config="$AIDEVOPS_SUPERVISOR_PULSE" - elif type _jsonc_get &>/dev/null; then - pulse_config=$(_jsonc_get "orchestration.supervisor_pulse" "false") - fi - - local pulse_lower - pulse_lower=$(echo "$pulse_config" | tr '[:upper:]' '[:lower:]') - - if [[ "$pulse_lower" == "true" ]]; then + # Config consent — the persistent gate that survives reboots. + # Delegates to config_enabled from config-helper.sh (sourced via + # shared-constants.sh), which handles: env var override + # (AIDEVOPS_SUPERVISOR_PULSE) > user JSONC config > defaults. + # Single canonical implementation shared with pulse-session-helper.sh. + if type config_enabled &>/dev/null && config_enabled "orchestration.supervisor_pulse"; then return 0 fi