aidevops 3.1.117 → 3.1.119

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1018 @@
1
+ #!/usr/bin/env bash
2
+ # Scheduler setup functions: supervisor pulse, stats wrapper, process guard,
3
+ # memory pressure monitor, screen time snapshot, contribution watch,
4
+ # profile README, OAuth token refresh.
5
+ # Part of aidevops setup.sh modularization (GH#5793)
6
+
7
+ # Shell safety baseline
8
+ set -Eeuo pipefail
9
+ IFS=$'\n\t'
10
+ # shellcheck disable=SC2154 # rc is assigned by $? in the trap string
11
+ trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
12
+ shopt -s inherit_errexit 2>/dev/null || true
13
+
14
+ # Setup the supervisor pulse scheduler (consent-gated autonomous orchestration).
15
+ # Uses pulse-wrapper.sh which handles dedup, orphan cleanup, and RAM-based concurrency.
16
+ # macOS: launchd plist invoking wrapper | Linux: cron entry invoking wrapper
17
+ # The plist is ALWAYS regenerated on setup.sh to pick up config changes (env vars,
18
+ # thresholds). Only the first-install prompt is gated on consent state.
19
+ setup_supervisor_pulse() {
20
+ local _os="$1"
21
+
22
+ # Ensure crontab has a global PATH= line (Linux only; macOS uses launchd env).
23
+ # Must run before any cron entries are installed so they inherit the PATH.
24
+ if [[ "$_os" != "Darwin" ]]; then
25
+ _ensure_cron_path
26
+ fi
27
+
28
+ # Consent model (GH#2926):
29
+ # - Default OFF: supervisor_pulse defaults to false in all config layers
30
+ # - Explicit consent required: user must type "y" (prompt defaults to [y/N])
31
+ # - Consent persisted: written to config.jsonc so it survives updates
32
+ # - Never silently re-enabled: if config says false, skip entirely
33
+ # - Non-interactive: only installs if config explicitly says true
34
+ local wrapper_script="$HOME/.aidevops/agents/scripts/pulse-wrapper.sh"
35
+ local pulse_label="com.aidevops.aidevops-supervisor-pulse"
36
+ # Read explicit user consent from config.jsonc (not merged defaults).
37
+ # Empty = user never configured this; "true"/"false" = explicit choice.
38
+ local _pulse_user_config=""
39
+ if type _jsonc_get_raw &>/dev/null && [[ -f "${JSONC_USER:-$HOME/.config/aidevops/config.jsonc}" ]]; then
40
+ _pulse_user_config=$(_jsonc_get_raw "${JSONC_USER:-$HOME/.config/aidevops/config.jsonc}" "orchestration.supervisor_pulse")
41
+ fi
42
+
43
+ # Also check legacy .conf user override
44
+ if [[ -z "$_pulse_user_config" && -f "${FEATURE_TOGGLES_USER:-$HOME/.config/aidevops/feature-toggles.conf}" ]]; then
45
+ local _legacy_val
46
+ # Use awk instead of grep|tail|cut — grep exits 1 on no match, which
47
+ # aborts the script under set -euo pipefail. awk always exits 0.
48
+ _legacy_val=$(awk -F= '/^supervisor_pulse=/{val=$2} END{print val}' "${FEATURE_TOGGLES_USER:-$HOME/.config/aidevops/feature-toggles.conf}")
49
+ if [[ -n "$_legacy_val" ]]; then
50
+ _pulse_user_config="$_legacy_val"
51
+ fi
52
+ fi
53
+
54
+ # Also check env var override (highest priority)
55
+ if [[ -n "${AIDEVOPS_SUPERVISOR_PULSE:-}" ]]; then
56
+ _pulse_user_config="$AIDEVOPS_SUPERVISOR_PULSE"
57
+ fi
58
+
59
+ # Determine action based on consent state
60
+ local _do_install=false
61
+ local _pulse_lower
62
+ _pulse_lower=$(echo "$_pulse_user_config" | tr '[:upper:]' '[:lower:]')
63
+
64
+ if [[ "$_pulse_lower" == "false" ]]; then
65
+ # User explicitly declined — never prompt, never install
66
+ _do_install=false
67
+ elif [[ "$_pulse_lower" == "true" ]]; then
68
+ # User explicitly consented — install/regenerate
69
+ _do_install=true
70
+ elif [[ -z "$_pulse_user_config" ]]; then
71
+ # No explicit config — fresh install or never configured
72
+ if [[ "$NON_INTERACTIVE" == "true" ]]; then
73
+ # Non-interactive: default OFF, do not install without consent
74
+ _do_install=false
75
+ elif [[ -f "$wrapper_script" ]]; then
76
+ # Interactive: prompt with default-no
77
+ echo ""
78
+ echo "The supervisor pulse enables autonomous orchestration."
79
+ echo "It will act under your GitHub identity and consume API credits:"
80
+ echo " - Dispatches AI workers to implement tasks from GitHub issues"
81
+ echo " - Creates PRs, merges passing PRs, files improvement issues"
82
+ echo " - 4-hourly strategic review (opus-tier) for queue health"
83
+ echo " - Circuit breaker pauses dispatch on consecutive failures"
84
+ echo ""
85
+ read -r -p "Enable supervisor pulse? [y/N]: " enable_pulse
86
+ if [[ "$enable_pulse" =~ ^[Yy]$ ]]; then
87
+ _do_install=true
88
+ # Record explicit consent
89
+ if type cmd_set &>/dev/null; then
90
+ cmd_set "orchestration.supervisor_pulse" "true" || true
91
+ fi
92
+ else
93
+ _do_install=false
94
+ # Record explicit decline so we never re-prompt on updates
95
+ if type cmd_set &>/dev/null; then
96
+ cmd_set "orchestration.supervisor_pulse" "false" || true
97
+ fi
98
+ print_info "Skipped. Enable later: aidevops config set orchestration.supervisor_pulse true && ./setup.sh"
99
+ fi
100
+ fi
101
+ fi
102
+
103
+ # Guard: wrapper must exist
104
+ if [[ "$_do_install" == "true" && ! -f "$wrapper_script" ]]; then
105
+ # Wrapper not deployed yet — skip (will install on next run after rsync)
106
+ _do_install=false
107
+ fi
108
+
109
+ # Detect if pulse is already installed (for upgrade messaging)
110
+ # Uses shared helper to check both launchd and cron consistently
111
+ local _pulse_installed=false
112
+ if _scheduler_detect_installed \
113
+ "Supervisor pulse" \
114
+ "$pulse_label" \
115
+ "" \
116
+ "pulse-wrapper" \
117
+ "" \
118
+ "" \
119
+ ""; then
120
+ _pulse_installed=true
121
+ fi
122
+
123
+ # Detect opencode binary location
124
+ local opencode_bin
125
+ opencode_bin=$(command -v opencode 2>/dev/null || echo "/opt/homebrew/bin/opencode")
126
+
127
+ if [[ "$_do_install" == "true" ]]; then
128
+ mkdir -p "$HOME/.aidevops/logs"
129
+
130
+ if [[ "$_os" == "Darwin" ]]; then
131
+ _install_pulse_launchd "$pulse_label" "$wrapper_script" "$opencode_bin" "$_pulse_installed"
132
+ else
133
+ _install_pulse_cron "$wrapper_script"
134
+ fi
135
+ elif [[ "$_pulse_lower" == "false" && "$_pulse_installed" == "true" ]]; then
136
+ # User explicitly disabled but pulse is still installed — clean up
137
+ _uninstall_pulse "$_os" "$pulse_label"
138
+ fi
139
+
140
+ # Export effective pulse state for setup_stats_wrapper.
141
+ # Use the actual install decision (_do_install), not just the consent string,
142
+ # so stats wrapper tracks the real scheduler state (e.g., wrapper missing → false).
143
+ PULSE_CONSENT_LOWER="$_pulse_lower"
144
+ if [[ "$_do_install" == "true" ]]; then
145
+ PULSE_ENABLED="true"
146
+ else
147
+ PULSE_ENABLED="false"
148
+ fi
149
+ return 0
150
+ }
151
+
152
+ # Install supervisor pulse via launchd (macOS)
153
+ _install_pulse_launchd() {
154
+ local pulse_label="$1"
155
+ local wrapper_script="$2"
156
+ local opencode_bin="$3"
157
+ local _pulse_installed="$4"
158
+ local pulse_plist="$HOME/Library/LaunchAgents/${pulse_label}.plist"
159
+
160
+ # Unload old plist if upgrading
161
+ if _launchd_has_agent "$pulse_label"; then
162
+ launchctl unload "$pulse_plist" || true
163
+ pkill -f 'Supervisor Pulse' 2>/dev/null || true
164
+ fi
165
+
166
+ # Also clean up old label if present
167
+ local old_plist="$HOME/Library/LaunchAgents/com.aidevops.supervisor-pulse.plist"
168
+ if [[ -f "$old_plist" ]]; then
169
+ launchctl unload "$old_plist" || true
170
+ rm -f "$old_plist"
171
+ fi
172
+
173
+ # XML-escape paths for safe plist embedding (prevents injection
174
+ # if $HOME or paths contain &, <, > characters)
175
+ local _xml_wrapper_script _xml_home _xml_opencode_bin _xml_pulse_dir _xml_path
176
+ local _headless_xml_env=""
177
+ _xml_wrapper_script=$(_xml_escape "$wrapper_script")
178
+ _xml_home=$(_xml_escape "$HOME")
179
+ _xml_opencode_bin=$(_xml_escape "$opencode_bin")
180
+ # Use neutral workspace path for PULSE_DIR so supervisor sessions
181
+ # are not associated with any specific managed repo (GH#5136).
182
+ _xml_pulse_dir=$(_xml_escape "${HOME}/.aidevops/.agent-workspace")
183
+ _xml_path=$(_xml_escape "$PATH")
184
+ if [[ -n "${AIDEVOPS_HEADLESS_MODELS:-}" ]]; then
185
+ local _xml_headless_models
186
+ _xml_headless_models=$(_xml_escape "$AIDEVOPS_HEADLESS_MODELS")
187
+ _headless_xml_env+=$'\n'
188
+ _headless_xml_env+=$'\t\t<key>AIDEVOPS_HEADLESS_MODELS</key>'
189
+ _headless_xml_env+=$'\n'
190
+ _headless_xml_env+=$'\t\t'"<string>${_xml_headless_models}</string>"
191
+ fi
192
+ if [[ -n "${AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST:-}" ]]; then
193
+ local _xml_headless_allowlist
194
+ _xml_headless_allowlist=$(_xml_escape "$AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST")
195
+ _headless_xml_env+=$'\n'
196
+ _headless_xml_env+=$'\t\t<key>AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST</key>'
197
+ _headless_xml_env+=$'\n'
198
+ _headless_xml_env+=$'\t\t'"<string>${_xml_headless_allowlist}</string>"
199
+ fi
200
+
201
+ # Write the plist (always regenerated to pick up config changes)
202
+ cat >"$pulse_plist" <<PLIST
203
+ <?xml version="1.0" encoding="UTF-8"?>
204
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
205
+ <plist version="1.0">
206
+ <dict>
207
+ <key>Label</key>
208
+ <string>${pulse_label}</string>
209
+ <key>ProgramArguments</key>
210
+ <array>
211
+ <string>/bin/bash</string>
212
+ <string>${_xml_wrapper_script}</string>
213
+ </array>
214
+ <key>StartInterval</key>
215
+ <integer>120</integer>
216
+ <key>StandardOutPath</key>
217
+ <string>${_xml_home}/.aidevops/logs/pulse-wrapper.log</string>
218
+ <key>StandardErrorPath</key>
219
+ <string>${_xml_home}/.aidevops/logs/pulse-wrapper.log</string>
220
+ <key>EnvironmentVariables</key>
221
+ <dict>
222
+ <key>PATH</key>
223
+ <string>${_xml_path}</string>
224
+ <key>HOME</key>
225
+ <string>${_xml_home}</string>
226
+ <key>OPENCODE_BIN</key>
227
+ <string>${_xml_opencode_bin}</string>
228
+ <key>PULSE_DIR</key>
229
+ <string>${_xml_pulse_dir}</string>
230
+ <key>PULSE_STALE_THRESHOLD</key>
231
+ <string>1800</string>
232
+ ${_headless_xml_env}
233
+ </dict>
234
+ <key>RunAtLoad</key>
235
+ <true/>
236
+ <key>KeepAlive</key>
237
+ <false/>
238
+ </dict>
239
+ </plist>
240
+ PLIST
241
+
242
+ if launchctl load "$pulse_plist"; then
243
+ if [[ "$_pulse_installed" == "true" ]]; then
244
+ print_info "Supervisor pulse updated (launchd config regenerated)"
245
+ else
246
+ print_info "Supervisor pulse enabled (launchd, every 2 min)"
247
+ fi
248
+ else
249
+ print_warning "Failed to load supervisor pulse LaunchAgent"
250
+ fi
251
+ return 0
252
+ }
253
+
254
+ # Install supervisor pulse via cron (Linux)
255
+ _install_pulse_cron() {
256
+ local wrapper_script="$1"
257
+ # Shell-escape all interpolated paths to prevent command injection
258
+ # via $(…) or backticks if paths contain shell metacharacters
259
+ # PATH is managed globally by _ensure_cron_path() — do NOT set inline
260
+ # PATH= here, it overrides the global line and breaks nvm/bun/cargo.
261
+ # OPENCODE_BIN removed — resolved from PATH at runtime via command -v.
262
+ # See #4099 and #4240 for history.
263
+ local _cron_pulse_dir _cron_wrapper_script _cron_headless_env=""
264
+ # Use neutral workspace path for PULSE_DIR (GH#5136)
265
+ _cron_pulse_dir=$(_cron_escape "${HOME}/.aidevops/.agent-workspace")
266
+ _cron_wrapper_script=$(_cron_escape "$wrapper_script")
267
+ if [[ -n "${AIDEVOPS_HEADLESS_MODELS:-}" ]]; then
268
+ local _cron_headless_models
269
+ _cron_headless_models=$(_cron_escape "$AIDEVOPS_HEADLESS_MODELS")
270
+ _cron_headless_env+=" AIDEVOPS_HEADLESS_MODELS=${_cron_headless_models}"
271
+ fi
272
+ if [[ -n "${AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST:-}" ]]; then
273
+ local _cron_headless_allowlist
274
+ _cron_headless_allowlist=$(_cron_escape "$AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST")
275
+ _cron_headless_env+=" AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST=${_cron_headless_allowlist}"
276
+ fi
277
+ (
278
+ crontab -l 2>/dev/null | grep -v 'aidevops: supervisor-pulse' || true
279
+ echo "*/2 * * * * PULSE_DIR=${_cron_pulse_dir}${_cron_headless_env} /bin/bash ${_cron_wrapper_script} >> \"\$HOME/.aidevops/logs/pulse-wrapper.log\" 2>&1 # aidevops: supervisor-pulse"
280
+ ) | crontab - || true
281
+ if crontab -l 2>/dev/null | grep -qF "aidevops: supervisor-pulse"; then
282
+ print_info "Supervisor pulse enabled (cron, every 2 min). Disable: crontab -e and remove the supervisor-pulse line"
283
+ else
284
+ print_warning "Failed to install supervisor pulse cron entry. See runners.md for manual setup."
285
+ fi
286
+ return 0
287
+ }
288
+
289
+ # Uninstall supervisor pulse (user explicitly disabled)
290
+ _uninstall_pulse() {
291
+ local _os="$1"
292
+ local pulse_label="$2"
293
+ if [[ "$_os" == "Darwin" ]]; then
294
+ local pulse_plist="$HOME/Library/LaunchAgents/${pulse_label}.plist"
295
+ if _launchd_has_agent "$pulse_label"; then
296
+ launchctl unload "$pulse_plist" || true
297
+ rm -f "$pulse_plist"
298
+ pkill -f 'Supervisor Pulse' 2>/dev/null || true
299
+ print_info "Supervisor pulse disabled (launchd agent removed per config)"
300
+ fi
301
+ else
302
+ if crontab -l 2>/dev/null | grep -qF "pulse-wrapper"; then
303
+ crontab -l 2>/dev/null | grep -v 'aidevops: supervisor-pulse' | crontab - || true
304
+ print_info "Supervisor pulse disabled (cron entry removed per config)"
305
+ fi
306
+ fi
307
+ return 0
308
+ }
309
+
310
+ # Setup stats-wrapper scheduler — runs quality sweep and health issue updates
311
+ # separately from the pulse (t1429). Only installed when the supervisor
312
+ # pulse is enabled (stats are useless without it).
313
+ setup_stats_wrapper() {
314
+ local _pulse_lower="$1"
315
+ # Use effective pulse state (PULSE_ENABLED) if available; fall back to consent string.
316
+ # PULSE_ENABLED reflects the actual install decision (e.g., false when wrapper is missing).
317
+ local _pulse_effective="${PULSE_ENABLED:-$_pulse_lower}"
318
+ local stats_script="$HOME/.aidevops/agents/scripts/stats-wrapper.sh"
319
+ local stats_label="com.aidevops.aidevops-stats-wrapper"
320
+ if [[ -x "$stats_script" ]] && [[ "$_pulse_effective" == "true" ]]; then
321
+ # Always regenerate to pick up config/format changes (matches pulse behavior)
322
+ if [[ "$(uname -s)" == "Darwin" ]]; then
323
+ local stats_plist="$HOME/Library/LaunchAgents/${stats_label}.plist"
324
+
325
+ local _xml_stats_script _xml_stats_home _xml_stats_path
326
+ _xml_stats_script=$(_xml_escape "$stats_script")
327
+ _xml_stats_home=$(_xml_escape "$HOME")
328
+ _xml_stats_path=$(_xml_escape "$PATH")
329
+ local stats_plist_content
330
+ stats_plist_content=$(
331
+ cat <<PLIST
332
+ <?xml version="1.0" encoding="UTF-8"?>
333
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
334
+ <plist version="1.0">
335
+ <dict>
336
+ <key>Label</key>
337
+ <string>${stats_label}</string>
338
+ <key>ProgramArguments</key>
339
+ <array>
340
+ <string>/bin/bash</string>
341
+ <string>${_xml_stats_script}</string>
342
+ </array>
343
+ <key>StartInterval</key>
344
+ <integer>900</integer>
345
+ <key>StandardOutPath</key>
346
+ <string>${_xml_stats_home}/.aidevops/logs/stats.log</string>
347
+ <key>StandardErrorPath</key>
348
+ <string>${_xml_stats_home}/.aidevops/logs/stats.log</string>
349
+ <key>EnvironmentVariables</key>
350
+ <dict>
351
+ <key>PATH</key>
352
+ <string>${_xml_stats_path}</string>
353
+ <key>HOME</key>
354
+ <string>${_xml_stats_home}</string>
355
+ </dict>
356
+ <key>RunAtLoad</key>
357
+ <true/>
358
+ <key>KeepAlive</key>
359
+ <false/>
360
+ </dict>
361
+ </plist>
362
+ PLIST
363
+ )
364
+ if _launchd_install_if_changed "$stats_label" "$stats_plist" "$stats_plist_content"; then
365
+ print_info "Stats wrapper enabled (launchd, every 15 min)"
366
+ else
367
+ print_warning "Failed to load stats wrapper LaunchAgent"
368
+ fi
369
+ else
370
+ local _cron_stats_script
371
+ _cron_stats_script=$(_cron_escape "$stats_script")
372
+ (
373
+ crontab -l 2>/dev/null | grep -v 'aidevops: stats-wrapper' || true
374
+ echo "*/15 * * * * /bin/bash ${_cron_stats_script} >> \"\$HOME/.aidevops/logs/stats.log\" 2>&1 # aidevops: stats-wrapper"
375
+ ) | crontab - || true
376
+ if crontab -l 2>/dev/null | grep -qF "aidevops: stats-wrapper"; then
377
+ print_info "Stats wrapper enabled (cron, every 15 min)"
378
+ fi
379
+ fi
380
+ elif [[ "$_pulse_effective" == "false" ]]; then
381
+ # Remove stats scheduler if pulse is disabled
382
+ if [[ "$(uname -s)" == "Darwin" ]]; then
383
+ local stats_plist="$HOME/Library/LaunchAgents/${stats_label}.plist"
384
+ if _launchd_has_agent "$stats_label"; then
385
+ launchctl unload "$stats_plist" || true
386
+ rm -f "$stats_plist"
387
+ print_info "Stats wrapper disabled (launchd agent removed — pulse is off)"
388
+ fi
389
+ else
390
+ if crontab -l 2>/dev/null | grep -qF "aidevops: stats-wrapper"; then
391
+ crontab -l 2>/dev/null | grep -v 'aidevops: stats-wrapper' | crontab - || true
392
+ print_info "Stats wrapper disabled (cron entry removed — pulse is off)"
393
+ fi
394
+ fi
395
+ fi
396
+ return 0
397
+ }
398
+
399
+ # Setup process guard — kills runaway AI processes (ShellCheck bloat, stuck workers)
400
+ # before they exhaust memory and cause kernel panics. Always installed when the
401
+ # script exists; no consent needed (safety net, not autonomous action).
402
+ # macOS: launchd plist (30s interval, RunAtLoad=true) | Linux: cron (every minute)
403
+ setup_process_guard() {
404
+ local guard_script="$HOME/.aidevops/agents/scripts/process-guard-helper.sh"
405
+ local guard_label="sh.aidevops.process-guard"
406
+ if [[ ! -x "$guard_script" ]]; then
407
+ return 0
408
+ fi
409
+
410
+ mkdir -p "$HOME/.aidevops/logs"
411
+
412
+ if [[ "$(uname -s)" == "Darwin" ]]; then
413
+ local guard_plist="$HOME/Library/LaunchAgents/${guard_label}.plist"
414
+
415
+ # XML-escape paths for safe plist embedding (prevents injection
416
+ # if $HOME or paths contain &, <, > characters)
417
+ local _xml_guard_script _xml_guard_home _xml_guard_path
418
+ _xml_guard_script=$(_xml_escape "$guard_script")
419
+ _xml_guard_home=$(_xml_escape "$HOME")
420
+ _xml_guard_path=$(_xml_escape "$PATH")
421
+
422
+ local guard_plist_content
423
+ guard_plist_content=$(
424
+ cat <<GUARD_PLIST
425
+ <?xml version="1.0" encoding="UTF-8"?>
426
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
427
+ <plist version="1.0">
428
+ <dict>
429
+ <key>Label</key>
430
+ <string>${guard_label}</string>
431
+ <key>ProgramArguments</key>
432
+ <array>
433
+ <string>/bin/bash</string>
434
+ <string>${_xml_guard_script}</string>
435
+ <string>kill-runaways</string>
436
+ </array>
437
+ <key>StartInterval</key>
438
+ <integer>30</integer>
439
+ <key>StandardOutPath</key>
440
+ <string>${_xml_guard_home}/.aidevops/logs/process-guard.log</string>
441
+ <key>StandardErrorPath</key>
442
+ <string>${_xml_guard_home}/.aidevops/logs/process-guard.log</string>
443
+ <key>EnvironmentVariables</key>
444
+ <dict>
445
+ <key>PATH</key>
446
+ <string>${_xml_guard_path}</string>
447
+ <key>HOME</key>
448
+ <string>${_xml_guard_home}</string>
449
+ <key>SHELLCHECK_RSS_LIMIT_KB</key>
450
+ <string>524288</string>
451
+ <key>SHELLCHECK_RUNTIME_LIMIT</key>
452
+ <string>120</string>
453
+ <key>CHILD_RSS_LIMIT_KB</key>
454
+ <string>8388608</string>
455
+ <key>CHILD_RUNTIME_LIMIT</key>
456
+ <string>7200</string>
457
+ </dict>
458
+ <key>RunAtLoad</key>
459
+ <true/>
460
+ <key>KeepAlive</key>
461
+ <false/>
462
+ </dict>
463
+ </plist>
464
+ GUARD_PLIST
465
+ )
466
+
467
+ if _launchd_install_if_changed "$guard_label" "$guard_plist" "$guard_plist_content"; then
468
+ print_info "Process guard enabled (launchd, every 30s, survives reboot)"
469
+ else
470
+ print_warning "Failed to load process guard LaunchAgent"
471
+ fi
472
+ else
473
+ # Linux: cron entry (every minute — cron minimum granularity)
474
+ # Always regenerate to pick up config changes (matches macOS behavior)
475
+ # Shell-escape path to prevent command injection via metacharacters
476
+ local _cron_guard_script
477
+ _cron_guard_script=$(_cron_escape "$guard_script")
478
+ (
479
+ crontab -l 2>/dev/null | grep -v 'aidevops: process-guard' || true
480
+ echo "* * * * * SHELLCHECK_RSS_LIMIT_KB=524288 SHELLCHECK_RUNTIME_LIMIT=120 CHILD_RSS_LIMIT_KB=8388608 CHILD_RUNTIME_LIMIT=7200 /bin/bash ${_cron_guard_script} kill-runaways >> \"\$HOME/.aidevops/logs/process-guard.log\" 2>&1 # aidevops: process-guard"
481
+ ) | crontab - || true
482
+ if crontab -l 2>/dev/null | grep -qF "aidevops: process-guard"; then
483
+ print_info "Process guard enabled (cron, every minute)"
484
+ else
485
+ print_warning "Failed to install process guard cron entry"
486
+ fi
487
+ fi
488
+ return 0
489
+ }
490
+
491
+ # Setup memory pressure monitor — process-focused memory watchdog (t1398.5, GH#2915).
492
+ # Monitors individual process RSS, runtime, session count, and aggregate memory.
493
+ # Auto-kills runaway ShellCheck (language server respawns them). Always installed
494
+ # when the script exists; no consent needed (safety net, not autonomous action).
495
+ # macOS: launchd plist (60s interval, RunAtLoad=true) | Linux: cron (every minute)
496
+ setup_memory_pressure_monitor() {
497
+ local monitor_script="$HOME/.aidevops/agents/scripts/memory-pressure-monitor.sh"
498
+ local monitor_label="sh.aidevops.memory-pressure-monitor"
499
+ if [[ ! -x "$monitor_script" ]]; then
500
+ return 0
501
+ fi
502
+
503
+ mkdir -p "$HOME/.aidevops/logs"
504
+
505
+ if [[ "$(uname -s)" == "Darwin" ]]; then
506
+ local monitor_plist="$HOME/Library/LaunchAgents/${monitor_label}.plist"
507
+
508
+ # XML-escape paths for safe plist embedding
509
+ local _xml_monitor_script _xml_monitor_home
510
+ _xml_monitor_script=$(_xml_escape "$monitor_script")
511
+ _xml_monitor_home=$(_xml_escape "$HOME")
512
+
513
+ local monitor_plist_content
514
+ monitor_plist_content=$(
515
+ cat <<MONITOR_PLIST
516
+ <?xml version="1.0" encoding="UTF-8"?>
517
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
518
+ <plist version="1.0">
519
+ <dict>
520
+ <key>Label</key>
521
+ <string>${monitor_label}</string>
522
+ <key>ProgramArguments</key>
523
+ <array>
524
+ <string>/bin/bash</string>
525
+ <string>${_xml_monitor_script}</string>
526
+ </array>
527
+ <key>StartInterval</key>
528
+ <integer>60</integer>
529
+ <key>StandardOutPath</key>
530
+ <string>${_xml_monitor_home}/.aidevops/logs/memory-pressure-launchd.log</string>
531
+ <key>StandardErrorPath</key>
532
+ <string>${_xml_monitor_home}/.aidevops/logs/memory-pressure-launchd.log</string>
533
+ <key>EnvironmentVariables</key>
534
+ <dict>
535
+ <key>PATH</key>
536
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
537
+ <key>HOME</key>
538
+ <string>${_xml_monitor_home}</string>
539
+ </dict>
540
+ <key>RunAtLoad</key>
541
+ <true/>
542
+ <key>KeepAlive</key>
543
+ <false/>
544
+ <key>ProcessType</key>
545
+ <string>Background</string>
546
+ <key>LowPriorityBackgroundIO</key>
547
+ <true/>
548
+ <key>Nice</key>
549
+ <integer>10</integer>
550
+ </dict>
551
+ </plist>
552
+ MONITOR_PLIST
553
+ )
554
+
555
+ if _launchd_install_if_changed "$monitor_label" "$monitor_plist" "$monitor_plist_content"; then
556
+ print_info "Memory pressure monitor enabled (launchd, every 60s, survives reboot)"
557
+ else
558
+ print_warning "Failed to load memory pressure monitor LaunchAgent"
559
+ fi
560
+ else
561
+ # Linux: cron entry (every minute — cron minimum granularity)
562
+ (
563
+ crontab -l 2>/dev/null | grep -v 'aidevops: memory-pressure-monitor' || true
564
+ echo "* * * * * /bin/bash \"${monitor_script}\" >> \"\$HOME/.aidevops/logs/memory-pressure-launchd.log\" 2>&1 # aidevops: memory-pressure-monitor"
565
+ ) | crontab - 2>/dev/null || true
566
+ if crontab -l 2>/dev/null | grep -qF "aidevops: memory-pressure-monitor" 2>/dev/null; then
567
+ print_info "Memory pressure monitor enabled (cron, every minute)"
568
+ else
569
+ print_warning "Failed to install memory pressure monitor cron entry"
570
+ fi
571
+ fi
572
+ return 0
573
+ }
574
+
575
+ # Setup screen time snapshot — captures daily screen time for contributor stats.
576
+ # Accumulates data in screen-time.jsonl (macOS Knowledge DB retains only ~28 days).
577
+ # Always installed when the script exists; no consent needed (data collection only).
578
+ # macOS: launchd plist (every 6h, RunAtLoad=true) | Linux: cron (every 6h)
579
+ setup_screen_time_snapshot() {
580
+ local st_script="$HOME/.aidevops/agents/scripts/screen-time-helper.sh"
581
+ local st_label="sh.aidevops.screen-time-snapshot"
582
+ if [[ ! -x "$st_script" ]]; then
583
+ return 0
584
+ fi
585
+
586
+ mkdir -p "$HOME/.aidevops/.agent-workspace/logs"
587
+
588
+ if [[ "$(uname -s)" == "Darwin" ]]; then
589
+ local st_plist="$HOME/Library/LaunchAgents/${st_label}.plist"
590
+
591
+ # XML-escape paths for safe plist embedding
592
+ local _xml_st_script _xml_st_home
593
+ _xml_st_script=$(_xml_escape "$st_script")
594
+ _xml_st_home=$(_xml_escape "$HOME")
595
+
596
+ local st_plist_content
597
+ st_plist_content=$(
598
+ cat <<ST_PLIST
599
+ <?xml version="1.0" encoding="UTF-8"?>
600
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
601
+ <plist version="1.0">
602
+ <dict>
603
+ <key>Label</key>
604
+ <string>${st_label}</string>
605
+ <key>ProgramArguments</key>
606
+ <array>
607
+ <string>/bin/bash</string>
608
+ <string>${_xml_st_script}</string>
609
+ <string>snapshot</string>
610
+ </array>
611
+ <key>StartInterval</key>
612
+ <integer>21600</integer>
613
+ <key>StandardOutPath</key>
614
+ <string>${_xml_st_home}/.aidevops/.agent-workspace/logs/screen-time-snapshot.log</string>
615
+ <key>StandardErrorPath</key>
616
+ <string>${_xml_st_home}/.aidevops/.agent-workspace/logs/screen-time-snapshot.log</string>
617
+ <key>EnvironmentVariables</key>
618
+ <dict>
619
+ <key>PATH</key>
620
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
621
+ <key>HOME</key>
622
+ <string>${_xml_st_home}</string>
623
+ </dict>
624
+ <key>RunAtLoad</key>
625
+ <true/>
626
+ <key>KeepAlive</key>
627
+ <false/>
628
+ <key>ProcessType</key>
629
+ <string>Background</string>
630
+ <key>LowPriorityBackgroundIO</key>
631
+ <true/>
632
+ <key>Nice</key>
633
+ <integer>10</integer>
634
+ </dict>
635
+ </plist>
636
+ ST_PLIST
637
+ )
638
+
639
+ if _launchd_install_if_changed "$st_label" "$st_plist" "$st_plist_content"; then
640
+ print_info "Screen time snapshot enabled (launchd, every 6h, survives reboot)"
641
+ else
642
+ print_warning "Failed to load screen time snapshot LaunchAgent"
643
+ fi
644
+ else
645
+ # Linux: cron entry (every 6 hours)
646
+ local _cron_st_script
647
+ _cron_st_script=$(_cron_escape "$st_script")
648
+ (
649
+ crontab -l 2>/dev/null | grep -v 'aidevops: screen-time-snapshot' || true
650
+ echo "0 */6 * * * /bin/bash ${_cron_st_script} snapshot >> \"\$HOME/.aidevops/.agent-workspace/logs/screen-time-snapshot.log\" 2>&1 # aidevops: screen-time-snapshot"
651
+ ) | crontab - 2>/dev/null || true
652
+ if crontab -l 2>/dev/null | grep -qF "aidevops: screen-time-snapshot" 2>/dev/null; then
653
+ print_info "Screen time snapshot enabled (cron, every 6h)"
654
+ else
655
+ print_warning "Failed to install screen time snapshot cron entry"
656
+ fi
657
+ fi
658
+ return 0
659
+ }
660
+
661
+ # Setup contribution watch — monitors external issues/PRs for new activity (t1554).
662
+ # Auto-seeds on first run (discovers authored/commented issues/PRs), then installs
663
+ # a launchd/cron job to scan periodically. Requires gh CLI authenticated.
664
+ # No consent needed — this is passive monitoring (read-only notifications API),
665
+ # not autonomous action. Comment bodies are never processed by LLM in automated context.
666
+ # Respects config: aidevops config set orchestration.contribution_watch false
667
+ setup_contribution_watch() {
668
+ local cw_script="$HOME/.aidevops/agents/scripts/contribution-watch-helper.sh"
669
+ local cw_label="sh.aidevops.contribution-watch"
670
+ local cw_state="$HOME/.aidevops/cache/contribution-watch.json"
671
+ if ! [[ -x "$cw_script" ]] || ! is_feature_enabled orchestration.contribution_watch 2>/dev/null || ! command -v gh &>/dev/null || ! gh auth status &>/dev/null 2>&1; then
672
+ return 0
673
+ fi
674
+
675
+ # Resolve log directory from config (paths.log_dir), expanding ~ to $HOME.
676
+ # Falls back to the default if config is unavailable or jq is missing.
677
+ # Validate before expansion to guard against shell metacharacter injection.
678
+ local _cw_log_dir
679
+ # shellcheck disable=SC2088 # Tilde is intentionally literal here; expanded below via ${/#\~/$HOME}
680
+ if type _jsonc_get &>/dev/null; then
681
+ _cw_log_dir=$(_jsonc_get "paths.log_dir" "~/.aidevops/logs")
682
+ else
683
+ _cw_log_dir="~/.aidevops/logs"
684
+ fi
685
+ # Whitelist: only allow characters safe in shell paths and cron lines.
686
+ # Reject anything outside [A-Za-z0-9_./ ~-] (tilde allowed before expansion).
687
+ # Store regex in variable — bash [[ =~ ]] requires unquoted RHS for regex,
688
+ # and a variable avoids quoting issues with special chars in the pattern.
689
+ local _cw_log_dir_re='^[A-Za-z0-9_./ ~-]+$'
690
+ if ! [[ "$_cw_log_dir" =~ $_cw_log_dir_re ]]; then
691
+ print_error "Invalid characters in paths.log_dir (only [A-Za-z0-9_./ ~-] allowed): $_cw_log_dir"
692
+ return 1
693
+ fi
694
+ _cw_log_dir="${_cw_log_dir/#\~/$HOME}"
695
+ mkdir -p "$HOME/.aidevops/cache" "$_cw_log_dir"
696
+
697
+ # Auto-seed on first run (populates state file with existing contributions)
698
+ if [[ ! -f "$cw_state" ]]; then
699
+ print_info "Discovering external contributions for contribution watch..."
700
+ if bash "$cw_script" seed >/dev/null 2>&1; then
701
+ print_info "Contribution watch seeded (external issues/PRs discovered)"
702
+ else
703
+ print_warning "Contribution watch seed failed (non-fatal, will retry on next run)"
704
+ fi
705
+ fi
706
+
707
+ # Install/update scheduled scanner
708
+ if [[ "$(uname -s)" == "Darwin" ]]; then
709
+ local cw_plist="$HOME/Library/LaunchAgents/${cw_label}.plist"
710
+
711
+ local _xml_cw_script _xml_cw_home _xml_cw_log_dir
712
+ _xml_cw_script=$(_xml_escape "$cw_script")
713
+ _xml_cw_home=$(_xml_escape "$HOME")
714
+ _xml_cw_log_dir=$(_xml_escape "$_cw_log_dir")
715
+
716
+ local cw_plist_content
717
+ cw_plist_content=$(
718
+ cat <<CW_PLIST
719
+ <?xml version="1.0" encoding="UTF-8"?>
720
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
721
+ <plist version="1.0">
722
+ <dict>
723
+ <key>Label</key>
724
+ <string>${cw_label}</string>
725
+ <key>ProgramArguments</key>
726
+ <array>
727
+ <string>/bin/bash</string>
728
+ <string>${_xml_cw_script}</string>
729
+ <string>scan</string>
730
+ </array>
731
+ <key>StartInterval</key>
732
+ <integer>3600</integer>
733
+ <key>StandardOutPath</key>
734
+ <string>${_xml_cw_log_dir}/contribution-watch.log</string>
735
+ <key>StandardErrorPath</key>
736
+ <string>${_xml_cw_log_dir}/contribution-watch.log</string>
737
+ <key>EnvironmentVariables</key>
738
+ <dict>
739
+ <key>PATH</key>
740
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
741
+ <key>HOME</key>
742
+ <string>${_xml_cw_home}</string>
743
+ </dict>
744
+ <key>RunAtLoad</key>
745
+ <false/>
746
+ <key>KeepAlive</key>
747
+ <false/>
748
+ <key>ProcessType</key>
749
+ <string>Background</string>
750
+ <key>LowPriorityBackgroundIO</key>
751
+ <true/>
752
+ <key>Nice</key>
753
+ <integer>10</integer>
754
+ </dict>
755
+ </plist>
756
+ CW_PLIST
757
+ )
758
+
759
+ if _launchd_install_if_changed "$cw_label" "$cw_plist" "$cw_plist_content"; then
760
+ print_info "Contribution watch enabled (launchd, hourly scan)"
761
+ else
762
+ print_warning "Failed to load contribution watch LaunchAgent"
763
+ fi
764
+ else
765
+ # Linux: cron entry (hourly)
766
+ local _cron_cw_script _cron_cw_log_dir
767
+ _cron_cw_script=$(_cron_escape "$cw_script")
768
+ _cron_cw_log_dir=$(_cron_escape "$_cw_log_dir")
769
+ (
770
+ crontab -l 2>/dev/null | grep -v 'aidevops: contribution-watch' || true
771
+ echo "0 * * * * /bin/bash ${_cron_cw_script} scan >> \"${_cron_cw_log_dir}/contribution-watch.log\" 2>&1 # aidevops: contribution-watch"
772
+ ) | crontab - 2>/dev/null || true
773
+ if crontab -l 2>/dev/null | grep -qF "aidevops: contribution-watch" 2>/dev/null; then
774
+ print_info "Contribution watch enabled (cron, hourly scan)"
775
+ else
776
+ print_warning "Failed to install contribution watch cron entry"
777
+ fi
778
+ fi
779
+ return 0
780
+ }
781
+
782
+ # Setup draft responses — private repo + local draft storage for reviewing
783
+ # AI-drafted replies to external contributions (t1555).
784
+ # Respects config: aidevops config set orchestration.draft_responses false
785
+ setup_draft_responses() {
786
+ local dr_script="$HOME/.aidevops/agents/scripts/draft-response-helper.sh"
787
+ if [[ -x "$dr_script" ]] && is_feature_enabled orchestration.draft_responses 2>/dev/null && is_feature_enabled orchestration.contribution_watch 2>/dev/null && command -v gh &>/dev/null && gh auth status &>/dev/null 2>&1; then
788
+ mkdir -p "$HOME/.aidevops/.agent-workspace/draft-responses"
789
+ if bash "$dr_script" init >/dev/null 2>&1; then
790
+ print_info "Draft responses ready (private repo + local drafts)"
791
+ else
792
+ print_warning "Draft responses repo setup failed (non-fatal, local drafts still work)"
793
+ fi
794
+ fi
795
+ return 0
796
+ }
797
+
798
+ # Setup profile README — auto-create repo and seed README if not already set up.
799
+ # Requires gh CLI authenticated. Creates username/username repo, seeds README
800
+ # with stat markers, registers in repos.json with priority: "profile".
801
+ setup_profile_readme() {
802
+ local pr_script="$HOME/.aidevops/agents/scripts/profile-readme-helper.sh"
803
+ local pr_label="sh.aidevops.profile-readme-update"
804
+ if ! [[ -x "$pr_script" ]] || ! command -v gh &>/dev/null || ! gh auth status &>/dev/null; then
805
+ return 0
806
+ fi
807
+
808
+ # Initialize profile repo if not already set up.
809
+ # Always run init — it's idempotent and handles:
810
+ # - Fresh installs (no profile repo)
811
+ # - Missing markers (injects them into existing README)
812
+ # - Diverged history (repo deleted and recreated on GitHub)
813
+ # - Already-initialized repos (returns early with no changes)
814
+ print_info "Checking GitHub profile README..."
815
+ if bash "$pr_script" init; then
816
+ print_info "Profile README ready."
817
+ else
818
+ print_warning "Profile README setup failed (non-fatal, skipping)"
819
+ fi
820
+
821
+ # Profile README auto-update scheduled job.
822
+ # Installed whenever gh CLI is available — the update script self-heals
823
+ # (discovers/creates the profile repo on first run via _resolve_profile_repo).
824
+ # macOS: launchd plist (hourly) | Linux: cron (hourly)
825
+ mkdir -p "$HOME/.aidevops/.agent-workspace/logs"
826
+
827
+ if [[ "$(uname -s)" == "Darwin" ]]; then
828
+ local pr_plist="$HOME/Library/LaunchAgents/${pr_label}.plist"
829
+
830
+ # XML-escape paths for safe plist embedding
831
+ local _xml_pr_script _xml_pr_home
832
+ _xml_pr_script=$(_xml_escape "$pr_script")
833
+ _xml_pr_home=$(_xml_escape "$HOME")
834
+
835
+ local pr_plist_content
836
+ pr_plist_content=$(
837
+ cat <<PR_PLIST
838
+ <?xml version="1.0" encoding="UTF-8"?>
839
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
840
+ <plist version="1.0">
841
+ <dict>
842
+ <key>Label</key>
843
+ <string>${pr_label}</string>
844
+ <key>ProgramArguments</key>
845
+ <array>
846
+ <string>/bin/bash</string>
847
+ <string>${_xml_pr_script}</string>
848
+ <string>update</string>
849
+ </array>
850
+ <key>StartInterval</key>
851
+ <integer>3600</integer>
852
+ <key>StandardOutPath</key>
853
+ <string>${_xml_pr_home}/.aidevops/.agent-workspace/logs/profile-readme-update.log</string>
854
+ <key>StandardErrorPath</key>
855
+ <string>${_xml_pr_home}/.aidevops/.agent-workspace/logs/profile-readme-update.log</string>
856
+ <key>EnvironmentVariables</key>
857
+ <dict>
858
+ <key>PATH</key>
859
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
860
+ <key>HOME</key>
861
+ <string>${_xml_pr_home}</string>
862
+ </dict>
863
+ <key>RunAtLoad</key>
864
+ <false/>
865
+ <key>KeepAlive</key>
866
+ <false/>
867
+ <key>ProcessType</key>
868
+ <string>Background</string>
869
+ <key>LowPriorityBackgroundIO</key>
870
+ <true/>
871
+ <key>Nice</key>
872
+ <integer>10</integer>
873
+ </dict>
874
+ </plist>
875
+ PR_PLIST
876
+ )
877
+
878
+ if _launchd_install_if_changed "$pr_label" "$pr_plist" "$pr_plist_content"; then
879
+ print_info "Profile README update enabled (launchd, hourly)"
880
+ else
881
+ print_warning "Failed to load profile README update LaunchAgent"
882
+ fi
883
+ else
884
+ # Linux: cron entry (hourly)
885
+ local _cron_pr_script
886
+ _cron_pr_script=$(_cron_escape "$pr_script")
887
+ (
888
+ crontab -l 2>/dev/null | grep -v 'aidevops: profile-readme-update' || true
889
+ echo "0 * * * * /bin/bash ${_cron_pr_script} update >> \"\$HOME/.aidevops/.agent-workspace/logs/profile-readme-update.log\" 2>&1 # aidevops: profile-readme-update"
890
+ ) | crontab - 2>/dev/null || true
891
+ if crontab -l 2>/dev/null | grep -qF "aidevops: profile-readme-update" 2>/dev/null; then
892
+ print_info "Profile README update enabled (cron, hourly)"
893
+ else
894
+ print_warning "Failed to install profile README update cron entry"
895
+ fi
896
+ fi
897
+ return 0
898
+ }
899
+
900
+ # Setup OAuth token refresh scheduled job.
901
+ # Refreshes expired/expiring tokens every 30 min so sessions never hit
902
+ # "invalid x-api-key". Also runs at load to catch tokens that expired
903
+ # while the machine was off.
904
+ setup_oauth_token_refresh() {
905
+ local tr_script="$HOME/.aidevops/agents/scripts/oauth-pool-helper.sh"
906
+ local tr_label="sh.aidevops.token-refresh"
907
+ if ! [[ -x "$tr_script" ]] || ! [[ -f "$HOME/.aidevops/oauth-pool.json" ]]; then
908
+ return 0
909
+ fi
910
+
911
+ mkdir -p "$HOME/.aidevops/.agent-workspace/logs"
912
+
913
+ if [[ "$(uname -s)" == "Darwin" ]]; then
914
+ local tr_plist="$HOME/Library/LaunchAgents/${tr_label}.plist"
915
+
916
+ local _xml_tr_script _xml_tr_home
917
+ _xml_tr_script=$(_xml_escape "$tr_script")
918
+ _xml_tr_home=$(_xml_escape "$HOME")
919
+
920
+ local tr_plist_content
921
+ tr_plist_content=$(
922
+ cat <<TR_PLIST
923
+ <?xml version="1.0" encoding="UTF-8"?>
924
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
925
+ <plist version="1.0">
926
+ <dict>
927
+ <key>Label</key>
928
+ <string>${tr_label}</string>
929
+ <key>ProgramArguments</key>
930
+ <array>
931
+ <string>/bin/bash</string>
932
+ <string>-c</string>
933
+ <string>&quot;${_xml_tr_script}&quot; refresh anthropic; &quot;${_xml_tr_script}&quot; refresh openai</string>
934
+ </array>
935
+ <key>StartInterval</key>
936
+ <integer>1800</integer>
937
+ <key>StandardOutPath</key>
938
+ <string>${_xml_tr_home}/.aidevops/.agent-workspace/logs/token-refresh.log</string>
939
+ <key>StandardErrorPath</key>
940
+ <string>${_xml_tr_home}/.aidevops/.agent-workspace/logs/token-refresh.log</string>
941
+ <key>EnvironmentVariables</key>
942
+ <dict>
943
+ <key>PATH</key>
944
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
945
+ <key>HOME</key>
946
+ <string>${_xml_tr_home}</string>
947
+ </dict>
948
+ <key>RunAtLoad</key>
949
+ <true/>
950
+ <key>KeepAlive</key>
951
+ <false/>
952
+ <key>ProcessType</key>
953
+ <string>Background</string>
954
+ <key>LowPriorityBackgroundIO</key>
955
+ <true/>
956
+ <key>Nice</key>
957
+ <integer>10</integer>
958
+ </dict>
959
+ </plist>
960
+ TR_PLIST
961
+ )
962
+
963
+ if _launchd_install_if_changed "$tr_label" "$tr_plist" "$tr_plist_content"; then
964
+ print_info "OAuth token refresh enabled (launchd, every 30 min)"
965
+ else
966
+ print_warning "Failed to load token refresh LaunchAgent"
967
+ fi
968
+ else
969
+ # Linux: cron entry (every 30 min)
970
+ local _cron_tr_script
971
+ _cron_tr_script=$(_cron_escape "$tr_script")
972
+ (
973
+ crontab -l 2>/dev/null | grep -v 'aidevops: token-refresh' || true
974
+ echo "*/30 * * * * /bin/bash ${_cron_tr_script} refresh anthropic >> \"\$HOME/.aidevops/.agent-workspace/logs/token-refresh.log\" 2>&1; /bin/bash ${_cron_tr_script} refresh openai >> \"\$HOME/.aidevops/.agent-workspace/logs/token-refresh.log\" 2>&1 # aidevops: token-refresh"
975
+ ) | crontab - 2>/dev/null || true
976
+ if crontab -l 2>/dev/null | grep -qF "aidevops: token-refresh" 2>/dev/null; then
977
+ print_info "OAuth token refresh enabled (cron, every 30 min)"
978
+ else
979
+ print_warning "Failed to install token refresh cron entry"
980
+ fi
981
+ fi
982
+ return 0
983
+ }
984
+
985
+ # Setup repo-sync scheduler if not already installed.
986
+ # Keeps local git repos up to date with daily ff-only pulls.
987
+ # Respects config: aidevops config set orchestration.repo_sync false
988
+ setup_repo_sync() {
989
+ local repo_sync_script="$HOME/.aidevops/agents/scripts/repo-sync-helper.sh"
990
+ if ! [[ -x "$repo_sync_script" ]] || ! is_feature_enabled repo_sync 2>/dev/null; then
991
+ return 0
992
+ fi
993
+
994
+ local _repo_sync_installed=false
995
+ if _launchd_has_agent "com.aidevops.aidevops-repo-sync"; then
996
+ _repo_sync_installed=true
997
+ elif crontab -l 2>/dev/null | grep -qF "aidevops-repo-sync"; then
998
+ _repo_sync_installed=true
999
+ fi
1000
+ if [[ "$_repo_sync_installed" == "false" ]]; then
1001
+ if [[ "$NON_INTERACTIVE" == "true" ]]; then
1002
+ bash "$repo_sync_script" enable >/dev/null 2>&1 || true
1003
+ print_info "Repo sync enabled (daily). Disable: aidevops repo-sync disable"
1004
+ else
1005
+ echo ""
1006
+ echo "Repo sync keeps your local git repos up to date by running"
1007
+ echo "git pull --ff-only daily on clean repos on their default branch."
1008
+ echo ""
1009
+ read -r -p "Enable daily repo sync? [Y/n]: " enable_repo_sync
1010
+ if [[ "$enable_repo_sync" =~ ^[Yy]?$ || -z "$enable_repo_sync" ]]; then
1011
+ bash "$repo_sync_script" enable
1012
+ else
1013
+ print_info "Skipped. Enable later: aidevops repo-sync enable"
1014
+ fi
1015
+ fi
1016
+ fi
1017
+ return 0
1018
+ }