aidevops 3.13.95 → 3.14.1

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.
@@ -1,565 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
3
- # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
4
- # Scheduler setup orchestrator: sources sub-libraries and provides the
5
- # monitoring-tier setup functions (stats wrapper, failure miner, process
6
- # guard, memory pressure monitor, screen time snapshot).
7
- # Part of aidevops setup.sh modularization (GH#5793)
8
- #
9
- # Split from a 2754-line monolith (GH#21052) into three focused sub-libraries:
10
- # - schedulers-pulse.sh (pulse resolution, supervisor, plist, watchdog)
11
- # - schedulers-linux.sh (systemd/cron scheduler install/uninstall)
12
- # - schedulers-platform.sh (contribution watch, complexity scan, profile
13
- # README, token refresh, DB maintenance, repo
14
- # health, peer productivity monitor)
15
- #
16
- # Functions that remain here (setup_failure_miner is >100 lines; identity-key
17
- # rule from reference/large-file-split.md §3 requires it stays in this file):
18
- # setup_stats_wrapper, setup_failure_miner, setup_process_guard,
19
- # setup_memory_pressure_monitor, setup_screen_time_snapshot
20
-
21
- # Keep pulse workers alive long enough for opus-tier dispatches.
22
- PULSE_STALE_THRESHOLD_SECONDS=1800
23
-
24
- # Cron expression: top of every hour. Shared by stats-wrapper,
25
- # contribution-watch, and profile-readme schedulers — keep DRY so a
26
- # future cadence shift only touches one place.
27
- CRON_HOURLY="0 * * * *"
28
-
29
- # Cron expression: every minute. Shared by process-guard, memory-pressure
30
- # monitor, and pulse-watchdog schedulers (cron's minimum granularity).
31
- # Kept DRY for the same reason as CRON_HOURLY.
32
- CRON_EVERY_MINUTE="* * * * *"
33
-
34
- # Direct unit tests source this module without setup.sh's later
35
- # shared-constants.sh load. Provide a small fallback; the shared helper
36
- # overwrites this when setup.sh sources shared-constants.sh.
37
- if ! declare -F aidevops_launchd_sanitized_path >/dev/null 2>&1; then
38
- aidevops_launchd_sanitized_path() {
39
- local input_path="${1:-${PATH:-}}"
40
- local default_path="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
41
- local result=""
42
- local seen=""
43
- local dir=""
44
- local old_ifs="$IFS"
45
- IFS=':'
46
- for dir in $default_path:$input_path; do
47
- [[ -n "$dir" && -d "$dir" ]] || continue
48
- case ":$seen:" in
49
- *":${dir}:"*) continue ;;
50
- esac
51
- seen="${seen:+${seen}:}${dir}"
52
- result="${result:+${result}:}${dir}"
53
- done
54
- IFS="$old_ifs"
55
- printf '%s' "$result"
56
- return 0
57
- }
58
- fi
59
-
60
- # Shell safety baseline
61
- set -Eeuo pipefail
62
- IFS=$'\n\t'
63
- # shellcheck disable=SC2154 # rc is assigned by $? in the trap string
64
- trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
65
- shopt -s inherit_errexit 2>/dev/null || true
66
-
67
- # SCRIPT_DIR — resolves to the setup-modules/ directory so sub-library
68
- # source calls work regardless of the caller's working directory or any
69
- # inherited SCRIPT_DIR from parent scripts. Always derive from ${BASH_SOURCE[0]}
70
- # to ensure sub-libraries load from the correct location.
71
- _sched_orch_lib_path="${BASH_SOURCE[0]%/*}"
72
- [[ "$_sched_orch_lib_path" == "${BASH_SOURCE[0]}" ]] && _sched_orch_lib_path="."
73
- SCRIPT_DIR="$(cd "$_sched_orch_lib_path" && pwd)"
74
- unset _sched_orch_lib_path
75
-
76
- # Source sub-libraries. Each carries its own include guard so double-sourcing
77
- # is safe. SC1091 suppressed per reference/large-file-split.md §5.1 — paths
78
- # are computed at runtime via $SCRIPT_DIR and cannot be statically resolved.
79
-
80
- # shellcheck source=./schedulers-pulse.sh
81
- # shellcheck disable=SC1091 # sub-library resolved at runtime via $SCRIPT_DIR
82
- source "${SCRIPT_DIR}/schedulers-pulse.sh"
83
-
84
- # shellcheck source=./schedulers-linux.sh
85
- # shellcheck disable=SC1091 # sub-library resolved at runtime via $SCRIPT_DIR
86
- source "${SCRIPT_DIR}/schedulers-linux.sh"
87
-
88
- # shellcheck source=./schedulers-platform.sh
89
- # shellcheck disable=SC1091 # sub-library resolved at runtime via $SCRIPT_DIR
90
- source "${SCRIPT_DIR}/schedulers-platform.sh"
91
-
92
- # Setup stats-wrapper scheduler — runs quality sweep and health issue updates
93
- # separately from the pulse (t1429). Only installed when the supervisor
94
- # pulse is enabled (stats are useless without it).
95
- # macOS: launchd plist (hourly) | Linux: systemd timer or cron (hourly)
96
- # t2744: interval raised from 15 min → hourly. Stats UI is not realtime,
97
- # the four-times-an-hour cadence drove ~200-400 GraphQL points/hr of pure
98
- # overhead on multi-repo setups.
99
- setup_stats_wrapper() {
100
- local _pulse_lower="$1"
101
- # Use effective pulse state (PULSE_ENABLED) if available; fall back to consent string.
102
- # PULSE_ENABLED reflects the actual install decision (e.g., false when wrapper is missing).
103
- local _pulse_effective="${PULSE_ENABLED:-$_pulse_lower}"
104
- local stats_script="$HOME/.aidevops/agents/scripts/stats-wrapper.sh"
105
- local stats_label="com.aidevops.aidevops-stats-wrapper"
106
- local stats_systemd="aidevops-stats-wrapper"
107
- local stats_log="$HOME/.aidevops/logs/stats.log"
108
- if [[ -x "$stats_script" ]] && [[ "$_pulse_effective" == "true" ]]; then
109
- # Always regenerate to pick up config/format changes (matches pulse behavior)
110
- if [[ "$(uname -s)" == "Darwin" ]]; then
111
- local stats_plist="$HOME/Library/LaunchAgents/${stats_label}.plist"
112
-
113
- local _xml_stats_script _xml_stats_home _xml_stats_path
114
- _xml_stats_script=$(_xml_escape "$stats_script")
115
- _xml_stats_home=$(_xml_escape "$HOME")
116
- _xml_stats_path=$(_xml_escape "$(aidevops_launchd_sanitized_path "$PATH")")
117
- local stats_plist_content
118
- stats_plist_content=$(
119
- cat <<PLIST
120
- <?xml version="1.0" encoding="UTF-8"?>
121
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
122
- <plist version="1.0">
123
- <dict>
124
- <key>Label</key>
125
- <string>${stats_label}</string>
126
- <key>ProgramArguments</key>
127
- <array>
128
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
129
- <string>${_xml_stats_script}</string>
130
- </array>
131
- <key>StartInterval</key>
132
- <integer>3600</integer>
133
- <key>StandardOutPath</key>
134
- <string>${_xml_stats_home}/.aidevops/logs/stats.log</string>
135
- <key>StandardErrorPath</key>
136
- <string>${_xml_stats_home}/.aidevops/logs/stats.log</string>
137
- <key>EnvironmentVariables</key>
138
- <dict>
139
- <key>PATH</key>
140
- <string>${_xml_stats_path}</string>
141
- <key>HOME</key>
142
- <string>${_xml_stats_home}</string>
143
- </dict>
144
- <key>RunAtLoad</key>
145
- <true/>
146
- <key>KeepAlive</key>
147
- <false/>
148
- </dict>
149
- </plist>
150
- PLIST
151
- )
152
- if _launchd_install_if_changed "$stats_label" "$stats_plist" "$stats_plist_content"; then
153
- print_info "Stats wrapper enabled (launchd, every hour)"
154
- else
155
- print_warning "Failed to load stats wrapper LaunchAgent"
156
- fi
157
- else
158
- _install_scheduler_linux \
159
- "$stats_systemd" \
160
- "aidevops: stats-wrapper" \
161
- "$CRON_HOURLY" \
162
- "\"${stats_script}\"" \
163
- "3600" \
164
- "$stats_log" \
165
- "" \
166
- "Stats wrapper enabled (every hour)" \
167
- "Failed to install stats wrapper scheduler" \
168
- "true" \
169
- "false"
170
- fi
171
- elif [[ "$_pulse_effective" == "false" ]]; then
172
- # Remove stats scheduler if pulse is disabled
173
- _uninstall_scheduler \
174
- "$(uname -s)" \
175
- "$stats_label" \
176
- "$stats_systemd" \
177
- "aidevops: stats-wrapper" \
178
- "Stats wrapper disabled (pulse is off)"
179
- fi
180
- return 0
181
- }
182
-
183
- # Setup failure miner — mines GitHub CI failure notifications for systemic patterns
184
- # and auto-files root-cause issues. Runs as a pure bash script (no LLM needed).
185
- # Installed when pulse is enabled and the helper script exists.
186
- # macOS: launchd plist (hourly at :15) | Linux: systemd timer or cron (hourly at :15)
187
- #
188
- # NOTE: This function is 105 lines and must remain in this file (schedulers.sh) to
189
- # preserve its (file, fname) identity key for the function-complexity CI scanner.
190
- # Moving it to a sub-library would register it as a new violation.
191
- # See reference/large-file-split.md §3 "Identity-Key Preservation Rules".
192
- setup_failure_miner() {
193
- local _pulse_lower="$1"
194
- local _pulse_effective="${PULSE_ENABLED:-$_pulse_lower}"
195
- local miner_script="$HOME/.aidevops/agents/scripts/gh-failure-miner-helper.sh"
196
- local miner_label="sh.aidevops.routine-gh-failure-miner"
197
- local miner_systemd="aidevops-gh-failure-miner"
198
- local miner_log="$HOME/.aidevops/logs/routine-gh-failure-miner.log"
199
- if [[ ! -x "$miner_script" ]] || [[ "$_pulse_effective" != "true" ]]; then
200
- # Remove scheduler if pulse is disabled or script missing
201
- _uninstall_scheduler \
202
- "$(uname -s)" \
203
- "$miner_label" \
204
- "$miner_systemd" \
205
- "aidevops: gh-failure-miner" \
206
- "Failure miner disabled (pulse is off or script missing)"
207
- return 0
208
- fi
209
-
210
- mkdir -p "$HOME/.aidevops/logs"
211
-
212
- if [[ "$(uname -s)" == "Darwin" ]]; then
213
- local miner_plist="$HOME/Library/LaunchAgents/${miner_label}.plist"
214
-
215
- local _xml_miner_script _xml_miner_home _xml_miner_path _xml_miner_log
216
- _xml_miner_script=$(_xml_escape "$miner_script")
217
- _xml_miner_home=$(_xml_escape "$HOME")
218
- _xml_miner_path=$(_xml_escape "$(aidevops_launchd_sanitized_path "/bin:/usr/bin:/usr/local/bin:/opt/homebrew/bin:${PATH}")")
219
- _xml_miner_log=$(_xml_escape "$miner_log")
220
-
221
- local miner_plist_content
222
- miner_plist_content=$(
223
- cat <<MINER_PLIST
224
- <?xml version="1.0" encoding="UTF-8"?>
225
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
226
- <plist version="1.0">
227
- <dict>
228
- <key>Label</key>
229
- <string>${miner_label}</string>
230
- <key>ProgramArguments</key>
231
- <array>
232
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
233
- <string>${_xml_miner_script}</string>
234
- <string>create-issues</string>
235
- <string>--since-hours</string>
236
- <string>24</string>
237
- <string>--pulse-repos</string>
238
- <string>--systemic-threshold</string>
239
- <string>2</string>
240
- <string>--max-issues</string>
241
- <string>3</string>
242
- <string>--label</string>
243
- <string>auto-dispatch</string>
244
- </array>
245
- <key>EnvironmentVariables</key>
246
- <dict>
247
- <key>HOME</key>
248
- <string>${_xml_miner_home}</string>
249
- <key>PATH</key>
250
- <string>${_xml_miner_path}</string>
251
- </dict>
252
- <key>StartCalendarInterval</key>
253
- <array>
254
- <dict>
255
- <key>Minute</key>
256
- <integer>15</integer>
257
- </dict>
258
- </array>
259
- <key>StandardOutPath</key>
260
- <string>${_xml_miner_log}</string>
261
- <key>StandardErrorPath</key>
262
- <string>${_xml_miner_log}</string>
263
- <key>RunAtLoad</key>
264
- <false/>
265
- </dict>
266
- </plist>
267
- MINER_PLIST
268
- )
269
-
270
- if _launchd_install_if_changed "$miner_label" "$miner_plist" "$miner_plist_content"; then
271
- print_info "Failure miner enabled (launchd, hourly at :15)"
272
- else
273
- print_warning "Failed to load failure miner LaunchAgent"
274
- fi
275
- else
276
- _install_scheduler_linux \
277
- "$miner_systemd" \
278
- "aidevops: gh-failure-miner" \
279
- "15 * * * *" \
280
- "\"${miner_script}\" create-issues --since-hours 24 --pulse-repos --systemic-threshold 2 --max-issues 3 --label auto-dispatch" \
281
- "3600" \
282
- "$miner_log" \
283
- "" \
284
- "Failure miner enabled (hourly at :15)" \
285
- "Failed to install failure miner scheduler" \
286
- "false" \
287
- "false" \
288
- "*-*-* *:15:00"
289
- fi
290
- return 0
291
- }
292
-
293
- # Setup process guard — kills runaway AI processes (ShellCheck bloat, stuck workers)
294
- # before they exhaust memory and cause kernel panics. Always installed when the
295
- # script exists; no consent needed (safety net, not autonomous action).
296
- # macOS: launchd plist (30s interval, RunAtLoad=true) | Linux: systemd timer or cron (every minute)
297
- setup_process_guard() {
298
- local guard_script="$HOME/.aidevops/agents/scripts/process-guard-helper.sh"
299
- local guard_label="sh.aidevops.process-guard"
300
- local guard_systemd="aidevops-process-guard"
301
- local guard_log="$HOME/.aidevops/logs/process-guard.log"
302
- if [[ ! -x "$guard_script" ]]; then
303
- return 0
304
- fi
305
-
306
- mkdir -p "$HOME/.aidevops/logs"
307
-
308
- if [[ "$(uname -s)" == "Darwin" ]]; then
309
- local guard_plist="$HOME/Library/LaunchAgents/${guard_label}.plist"
310
-
311
- # XML-escape paths for safe plist embedding (prevents injection
312
- # if $HOME or paths contain &, <, > characters)
313
- local _xml_guard_script _xml_guard_home _xml_guard_path
314
- _xml_guard_script=$(_xml_escape "$guard_script")
315
- _xml_guard_home=$(_xml_escape "$HOME")
316
- _xml_guard_path=$(_xml_escape "$(aidevops_launchd_sanitized_path "$PATH")")
317
-
318
- local guard_plist_content
319
- guard_plist_content=$(
320
- cat <<GUARD_PLIST
321
- <?xml version="1.0" encoding="UTF-8"?>
322
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
323
- <plist version="1.0">
324
- <dict>
325
- <key>Label</key>
326
- <string>${guard_label}</string>
327
- <key>ProgramArguments</key>
328
- <array>
329
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
330
- <string>${_xml_guard_script}</string>
331
- <string>kill-runaways</string>
332
- </array>
333
- <key>StartInterval</key>
334
- <integer>30</integer>
335
- <key>StandardOutPath</key>
336
- <string>${_xml_guard_home}/.aidevops/logs/process-guard.log</string>
337
- <key>StandardErrorPath</key>
338
- <string>${_xml_guard_home}/.aidevops/logs/process-guard.log</string>
339
- <key>EnvironmentVariables</key>
340
- <dict>
341
- <key>PATH</key>
342
- <string>${_xml_guard_path}</string>
343
- <key>HOME</key>
344
- <string>${_xml_guard_home}</string>
345
- <key>SHELLCHECK_RSS_LIMIT_KB</key>
346
- <string>524288</string>
347
- <key>SHELLCHECK_RUNTIME_LIMIT</key>
348
- <string>120</string>
349
- <key>CHILD_RSS_LIMIT_KB</key>
350
- <string>8388608</string>
351
- <key>CHILD_RUNTIME_LIMIT</key>
352
- <string>7200</string>
353
- </dict>
354
- <key>RunAtLoad</key>
355
- <true/>
356
- <key>KeepAlive</key>
357
- <false/>
358
- </dict>
359
- </plist>
360
- GUARD_PLIST
361
- )
362
-
363
- if _launchd_install_if_changed "$guard_label" "$guard_plist" "$guard_plist_content"; then
364
- print_info "Process guard enabled (launchd, every 30s, survives reboot)"
365
- else
366
- print_warning "Failed to load process guard LaunchAgent"
367
- fi
368
- else
369
- # Linux: systemd timer (30s) or cron fallback (every minute — cron minimum granularity)
370
- _install_scheduler_linux \
371
- "$guard_systemd" \
372
- "aidevops: process-guard" \
373
- "$CRON_EVERY_MINUTE" \
374
- "\"${guard_script}\" kill-runaways" \
375
- "30" \
376
- "$guard_log" \
377
- "SHELLCHECK_RSS_LIMIT_KB=524288
378
- SHELLCHECK_RUNTIME_LIMIT=120
379
- CHILD_RSS_LIMIT_KB=8388608
380
- CHILD_RUNTIME_LIMIT=7200" \
381
- "Process guard enabled (every 30s)" \
382
- "Failed to install process guard scheduler" \
383
- "true" \
384
- "false"
385
- fi
386
- return 0
387
- }
388
-
389
- # Setup memory pressure monitor — process-focused memory watchdog (t1398.5, GH#2915).
390
- # Monitors individual process RSS, runtime, session count, and aggregate memory.
391
- # Auto-kills runaway ShellCheck (language server respawns them). Always installed
392
- # when the script exists; no consent needed (safety net, not autonomous action).
393
- # macOS: launchd plist (60s interval, RunAtLoad=true) | Linux: systemd timer or cron (every minute)
394
- setup_memory_pressure_monitor() {
395
- local monitor_script="$HOME/.aidevops/agents/scripts/memory-pressure-monitor.sh"
396
- local monitor_label="sh.aidevops.memory-pressure-monitor"
397
- local monitor_systemd="aidevops-memory-pressure-monitor"
398
- local monitor_log="$HOME/.aidevops/logs/memory-pressure-launchd.log"
399
- if [[ ! -x "$monitor_script" ]]; then
400
- return 0
401
- fi
402
-
403
- mkdir -p "$HOME/.aidevops/logs"
404
-
405
- if [[ "$(uname -s)" == "Darwin" ]]; then
406
- local monitor_plist="$HOME/Library/LaunchAgents/${monitor_label}.plist"
407
-
408
- # XML-escape paths for safe plist embedding
409
- local _xml_monitor_script _xml_monitor_home
410
- _xml_monitor_script=$(_xml_escape "$monitor_script")
411
- _xml_monitor_home=$(_xml_escape "$HOME")
412
-
413
- local monitor_plist_content
414
- monitor_plist_content=$(
415
- cat <<MONITOR_PLIST
416
- <?xml version="1.0" encoding="UTF-8"?>
417
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
418
- <plist version="1.0">
419
- <dict>
420
- <key>Label</key>
421
- <string>${monitor_label}</string>
422
- <key>ProgramArguments</key>
423
- <array>
424
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
425
- <string>${_xml_monitor_script}</string>
426
- </array>
427
- <key>StartInterval</key>
428
- <integer>60</integer>
429
- <key>StandardOutPath</key>
430
- <string>${_xml_monitor_home}/.aidevops/logs/memory-pressure-launchd.log</string>
431
- <key>StandardErrorPath</key>
432
- <string>${_xml_monitor_home}/.aidevops/logs/memory-pressure-launchd.log</string>
433
- <key>EnvironmentVariables</key>
434
- <dict>
435
- <key>PATH</key>
436
- <string>$(aidevops_launchd_sanitized_path)</string>
437
- <key>HOME</key>
438
- <string>${_xml_monitor_home}</string>
439
- </dict>
440
- <key>RunAtLoad</key>
441
- <true/>
442
- <key>KeepAlive</key>
443
- <false/>
444
- <key>ProcessType</key>
445
- <string>Background</string>
446
- <key>LowPriorityBackgroundIO</key>
447
- <true/>
448
- <key>Nice</key>
449
- <integer>10</integer>
450
- </dict>
451
- </plist>
452
- MONITOR_PLIST
453
- )
454
-
455
- if _launchd_install_if_changed "$monitor_label" "$monitor_plist" "$monitor_plist_content"; then
456
- print_info "Memory pressure monitor enabled (launchd, every 60s, survives reboot)"
457
- else
458
- print_warning "Failed to load memory pressure monitor LaunchAgent"
459
- fi
460
- else
461
- # Linux: systemd timer (60s) or cron fallback (every minute — cron minimum granularity)
462
- _install_scheduler_linux \
463
- "$monitor_systemd" \
464
- "aidevops: memory-pressure-monitor" \
465
- "$CRON_EVERY_MINUTE" \
466
- "\"${monitor_script}\"" \
467
- "60" \
468
- "$monitor_log" \
469
- "" \
470
- "Memory pressure monitor enabled (every 60s)" \
471
- "Failed to install memory pressure monitor scheduler" \
472
- "true" \
473
- "true"
474
- fi
475
- return 0
476
- }
477
-
478
- # Setup screen time snapshot — captures daily screen time for contributor stats.
479
- # Accumulates data in screen-time.jsonl (macOS Knowledge DB retains only ~28 days).
480
- # Always installed when the script exists; no consent needed (data collection only).
481
- # macOS: launchd plist (every 6h, RunAtLoad=true) | Linux: systemd timer or cron (every 6h)
482
- setup_screen_time_snapshot() {
483
- local st_script="$HOME/.aidevops/agents/scripts/screen-time-helper.sh"
484
- local st_label="sh.aidevops.screen-time-snapshot"
485
- local st_systemd="aidevops-screen-time-snapshot"
486
- local st_log="$HOME/.aidevops/.agent-workspace/logs/screen-time-snapshot.log"
487
- if [[ ! -x "$st_script" ]]; then
488
- return 0
489
- fi
490
-
491
- mkdir -p "$HOME/.aidevops/.agent-workspace/logs"
492
-
493
- if [[ "$(uname -s)" == "Darwin" ]]; then
494
- local st_plist="$HOME/Library/LaunchAgents/${st_label}.plist"
495
-
496
- # XML-escape paths for safe plist embedding
497
- local _xml_st_script _xml_st_home
498
- _xml_st_script=$(_xml_escape "$st_script")
499
- _xml_st_home=$(_xml_escape "$HOME")
500
-
501
- local st_plist_content
502
- st_plist_content=$(
503
- cat <<ST_PLIST
504
- <?xml version="1.0" encoding="UTF-8"?>
505
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
506
- <plist version="1.0">
507
- <dict>
508
- <key>Label</key>
509
- <string>${st_label}</string>
510
- <key>ProgramArguments</key>
511
- <array>
512
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
513
- <string>${_xml_st_script}</string>
514
- <string>snapshot</string>
515
- </array>
516
- <key>StartInterval</key>
517
- <integer>21600</integer>
518
- <key>StandardOutPath</key>
519
- <string>${_xml_st_home}/.aidevops/.agent-workspace/logs/screen-time-snapshot.log</string>
520
- <key>StandardErrorPath</key>
521
- <string>${_xml_st_home}/.aidevops/.agent-workspace/logs/screen-time-snapshot.log</string>
522
- <key>EnvironmentVariables</key>
523
- <dict>
524
- <key>PATH</key>
525
- <string>$(aidevops_launchd_sanitized_path)</string>
526
- <key>HOME</key>
527
- <string>${_xml_st_home}</string>
528
- </dict>
529
- <key>RunAtLoad</key>
530
- <true/>
531
- <key>KeepAlive</key>
532
- <false/>
533
- <key>ProcessType</key>
534
- <string>Background</string>
535
- <key>LowPriorityBackgroundIO</key>
536
- <true/>
537
- <key>Nice</key>
538
- <integer>10</integer>
539
- </dict>
540
- </plist>
541
- ST_PLIST
542
- )
543
-
544
- if _launchd_install_if_changed "$st_label" "$st_plist" "$st_plist_content"; then
545
- print_info "Screen time snapshot enabled (launchd, every 6h, survives reboot)"
546
- else
547
- print_warning "Failed to load screen time snapshot LaunchAgent"
548
- fi
549
- else
550
- # Linux: systemd timer (every 6h) or cron fallback
551
- _install_scheduler_linux \
552
- "$st_systemd" \
553
- "aidevops: screen-time-snapshot" \
554
- "0 */6 * * *" \
555
- "\"${st_script}\" snapshot" \
556
- "21600" \
557
- "$st_log" \
558
- "" \
559
- "Screen time snapshot enabled (every 6h)" \
560
- "Failed to install screen time snapshot scheduler" \
561
- "true" \
562
- "true"
563
- fi
564
- return 0
565
- }