aidevops 3.12.0 → 3.13.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.
- package/README.md +17 -0
- package/VERSION +1 -1
- package/aidevops.sh +185 -5
- package/package.json +1 -1
- package/setup-modules/schedulers-linux.sh +386 -0
- package/setup-modules/schedulers-platform.sh +1024 -0
- package/setup-modules/schedulers-pulse.sh +978 -0
- package/setup-modules/schedulers.sh +345 -2330
- package/setup-modules/tool-install.sh +89 -0
- package/setup.sh +139 -48
|
@@ -1,2212 +1,415 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
|
|
4
|
-
# Scheduler setup
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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
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
|
|
8
20
|
|
|
9
|
-
# Keep pulse workers alive long enough for opus-tier dispatches.
|
|
10
|
-
PULSE_STALE_THRESHOLD_SECONDS=1800
|
|
11
|
-
|
|
12
|
-
# Cron expression: top of every hour. Shared by stats-wrapper,
|
|
13
|
-
# contribution-watch, and profile-readme schedulers — keep DRY so a
|
|
14
|
-
# future cadence shift only touches one place.
|
|
15
|
-
CRON_HOURLY="0 * * * *"
|
|
16
|
-
|
|
17
|
-
# Resolve the modern bash binary path for use in launchd ProgramArguments.
|
|
18
|
-
# Launchd bypasses the shebang when ProgramArguments specifies an explicit
|
|
19
|
-
# interpreter, so we must resolve the path at plist generation time.
|
|
20
|
-
# Falls back to /bin/bash if no modern bash is available (the re-exec guard
|
|
21
|
-
# in shared-constants.sh provides defense-in-depth). (GH#19632 / t2176)
|
|
22
|
-
_resolve_modern_bash() {
|
|
23
|
-
local candidate
|
|
24
|
-
for candidate in /opt/homebrew/bin/bash /usr/local/bin/bash /home/linuxbrew/.linuxbrew/bin/bash; do
|
|
25
|
-
if [[ -x "$candidate" ]]; then
|
|
26
|
-
# Verify it's actually bash 4+
|
|
27
|
-
local ver
|
|
28
|
-
ver=$("$candidate" -c 'echo "${BASH_VERSINFO[0]}"' 2>/dev/null) || continue
|
|
29
|
-
if [[ "${ver:-0}" -ge 4 ]]; then
|
|
30
|
-
printf '%s' "$candidate"
|
|
31
|
-
return 0
|
|
32
|
-
fi
|
|
33
|
-
fi
|
|
34
|
-
done
|
|
35
|
-
# No modern bash found — fall back to /bin/bash. The re-exec guard in
|
|
36
|
-
# shared-constants.sh handles this case at runtime.
|
|
37
|
-
printf '%s' "/bin/bash"
|
|
38
|
-
return 0
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
# Shell safety baseline
|
|
42
|
-
set -Eeuo pipefail
|
|
43
|
-
IFS=$'\n\t'
|
|
44
|
-
# shellcheck disable=SC2154 # rc is assigned by $? in the trap string
|
|
45
|
-
trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
|
|
46
|
-
shopt -s inherit_errexit 2>/dev/null || true
|
|
47
|
-
|
|
48
|
-
# Resolve the user's pulse consent setting from all config layers.
|
|
49
|
-
# Priority: env var > jsonc config > legacy .conf. Prints the raw value
|
|
50
|
-
# (may be empty if never configured, or "true"/"false").
|
|
51
|
-
_resolve_pulse_consent() {
|
|
52
|
-
local _pulse_user_config=""
|
|
53
|
-
|
|
54
|
-
# Read explicit user consent from config.jsonc (not merged defaults).
|
|
55
|
-
# Empty = user never configured this; "true"/"false" = explicit choice.
|
|
56
|
-
if type _jsonc_get_raw &>/dev/null && [[ -f "${JSONC_USER:-$HOME/.config/aidevops/config.jsonc}" ]]; then
|
|
57
|
-
_pulse_user_config=$(_jsonc_get_raw "${JSONC_USER:-$HOME/.config/aidevops/config.jsonc}" "orchestration.supervisor_pulse")
|
|
58
|
-
fi
|
|
59
|
-
|
|
60
|
-
# Also check legacy .conf user override
|
|
61
|
-
if [[ -z "$_pulse_user_config" && -f "${FEATURE_TOGGLES_USER:-$HOME/.config/aidevops/feature-toggles.conf}" ]]; then
|
|
62
|
-
local _legacy_val
|
|
63
|
-
# Use awk instead of grep|tail|cut — grep exits 1 on no match, which
|
|
64
|
-
# aborts the script under set -euo pipefail. awk always exits 0.
|
|
65
|
-
_legacy_val=$(awk -F= '/^supervisor_pulse=/{val=$2} END{print val}' "${FEATURE_TOGGLES_USER:-$HOME/.config/aidevops/feature-toggles.conf}")
|
|
66
|
-
if [[ -n "$_legacy_val" ]]; then
|
|
67
|
-
_pulse_user_config="$_legacy_val"
|
|
68
|
-
fi
|
|
69
|
-
fi
|
|
70
|
-
|
|
71
|
-
# Also check env var override (highest priority)
|
|
72
|
-
if [[ -n "${AIDEVOPS_SUPERVISOR_PULSE:-}" ]]; then
|
|
73
|
-
_pulse_user_config="$AIDEVOPS_SUPERVISOR_PULSE"
|
|
74
|
-
fi
|
|
75
|
-
|
|
76
|
-
printf '%s' "$_pulse_user_config"
|
|
77
|
-
return 0
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
# Determine whether to install the pulse based on consent state.
|
|
81
|
-
# Handles interactive prompting and persisting the user's choice.
|
|
82
|
-
# Args: $1=pulse_user_config (raw), $2=wrapper_script path
|
|
83
|
-
# Prints "true" or "false".
|
|
84
|
-
_determine_pulse_install() {
|
|
85
|
-
local _pulse_user_config="$1"
|
|
86
|
-
local wrapper_script="$2"
|
|
87
|
-
local _do_install=false
|
|
88
|
-
local _pulse_lower
|
|
89
|
-
_pulse_lower=$(echo "$_pulse_user_config" | tr '[:upper:]' '[:lower:]')
|
|
90
|
-
|
|
91
|
-
if [[ "$_pulse_lower" == "false" ]]; then
|
|
92
|
-
# User explicitly declined — never prompt, never install
|
|
93
|
-
_do_install=false
|
|
94
|
-
elif [[ "$_pulse_lower" == "true" ]]; then
|
|
95
|
-
# User explicitly consented — install/regenerate
|
|
96
|
-
_do_install=true
|
|
97
|
-
elif [[ -z "$_pulse_user_config" ]]; then
|
|
98
|
-
# No explicit config — fresh install or never configured
|
|
99
|
-
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
100
|
-
# Non-interactive: default OFF, do not install without consent
|
|
101
|
-
_do_install=false
|
|
102
|
-
elif [[ -f "$wrapper_script" ]]; then
|
|
103
|
-
# Interactive: prompt with default-no
|
|
104
|
-
# All user-facing output goes to stderr so $() captures only the result
|
|
105
|
-
local enable_pulse=""
|
|
106
|
-
echo "" >&2
|
|
107
|
-
echo "The supervisor pulse enables autonomous orchestration." >&2
|
|
108
|
-
echo "It will act under your GitHub identity and consume API credits:" >&2
|
|
109
|
-
echo " - Dispatches AI workers to implement tasks from GitHub issues" >&2
|
|
110
|
-
echo " - Creates PRs, merges passing PRs, files improvement issues" >&2
|
|
111
|
-
echo " - 4-hourly strategic review (opus-tier) for queue health" >&2
|
|
112
|
-
echo " - Circuit breaker pauses dispatch on consecutive failures" >&2
|
|
113
|
-
echo "" >&2
|
|
114
|
-
setup_prompt enable_pulse "Enable supervisor pulse? [y/N]: " "n"
|
|
115
|
-
if [[ "$enable_pulse" =~ ^[Yy]$ ]]; then
|
|
116
|
-
_do_install=true
|
|
117
|
-
# Record explicit consent
|
|
118
|
-
if type cmd_set &>/dev/null; then
|
|
119
|
-
cmd_set "orchestration.supervisor_pulse" "true" || true
|
|
120
|
-
fi
|
|
121
|
-
else
|
|
122
|
-
_do_install=false
|
|
123
|
-
# Record explicit decline so we never re-prompt on updates
|
|
124
|
-
if type cmd_set &>/dev/null; then
|
|
125
|
-
cmd_set "orchestration.supervisor_pulse" "false" || true
|
|
126
|
-
fi
|
|
127
|
-
print_info "Skipped. Enable later: aidevops config set orchestration.supervisor_pulse true && ./setup.sh" >&2
|
|
128
|
-
fi
|
|
129
|
-
fi
|
|
130
|
-
fi
|
|
131
|
-
|
|
132
|
-
# Guard: wrapper must exist
|
|
133
|
-
if [[ "$_do_install" == "true" && ! -f "$wrapper_script" ]]; then
|
|
134
|
-
# Wrapper not deployed yet — skip (will install on next run after rsync)
|
|
135
|
-
_do_install=false
|
|
136
|
-
fi
|
|
137
|
-
|
|
138
|
-
printf '%s' "$_do_install"
|
|
139
|
-
return 0
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
# GH#17769: These functions are deprecated — model routing is now derived
|
|
143
|
-
# from the OAuth pool + routing table at runtime. Kept as no-ops for one
|
|
144
|
-
# release cycle in case external scripts call them.
|
|
145
|
-
_resolve_headless_models_override() {
|
|
146
|
-
printf '%s' ""
|
|
147
|
-
return 0
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
_resolve_pulse_model_override() {
|
|
151
|
-
printf '%s' ""
|
|
152
|
-
return 0
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
_is_pulse_installed() {
|
|
156
|
-
local pulse_label="$1"
|
|
157
|
-
|
|
158
|
-
if _scheduler_detect_installed \
|
|
159
|
-
"Supervisor pulse" \
|
|
160
|
-
"$pulse_label" \
|
|
161
|
-
"" \
|
|
162
|
-
"pulse-wrapper" \
|
|
163
|
-
"" \
|
|
164
|
-
"" \
|
|
165
|
-
"" \
|
|
166
|
-
"aidevops-supervisor-pulse"; then
|
|
167
|
-
return 0
|
|
168
|
-
fi
|
|
169
|
-
|
|
170
|
-
return 1
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
_resolve_pulse_runtime_binary() {
|
|
174
|
-
# GH#18439 Bug 2: Persist the resolved binary path across setup.sh
|
|
175
|
-
# invocations. aidevops-auto-update.timer runs setup.sh under systemd's
|
|
176
|
-
# minimal PATH, so re-resolving from live `$PATH` alone yields the
|
|
177
|
-
# legacy macOS-biased `/opt/homebrew/bin/opencode` fallback on Linux.
|
|
178
|
-
# Reading from persistence first (populated during an interactive
|
|
179
|
-
# setup.sh run with a rich `$PATH`) prevents the auto-update cycle
|
|
180
|
-
# from silently degrading the service file.
|
|
181
|
-
local _persisted_file="$HOME/.config/aidevops/scheduler-runtime-bin"
|
|
182
|
-
local opencode_bin=""
|
|
183
|
-
|
|
184
|
-
# 1. Prefer persisted path if it still points at an executable file.
|
|
185
|
-
if [[ -f "$_persisted_file" ]]; then
|
|
186
|
-
local _persisted
|
|
187
|
-
_persisted=$(head -n1 "$_persisted_file" 2>/dev/null || true)
|
|
188
|
-
if [[ -n "$_persisted" ]] && [[ -x "$_persisted" ]]; then
|
|
189
|
-
printf '%s' "$_persisted"
|
|
190
|
-
return 0
|
|
191
|
-
fi
|
|
192
|
-
fi
|
|
193
|
-
|
|
194
|
-
# 2. Try runtime-registry lookup via live PATH.
|
|
195
|
-
if type rt_list_headless &>/dev/null; then
|
|
196
|
-
local _sched_rt_id=""
|
|
197
|
-
local _sched_bin=""
|
|
198
|
-
while IFS= read -r _sched_rt_id; do
|
|
199
|
-
_sched_bin=$(rt_binary "$_sched_rt_id") || continue
|
|
200
|
-
if [[ -n "$_sched_bin" ]] && command -v "$_sched_bin" &>/dev/null; then
|
|
201
|
-
opencode_bin=$(command -v "$_sched_bin")
|
|
202
|
-
break
|
|
203
|
-
fi
|
|
204
|
-
done < <(rt_list_headless)
|
|
205
|
-
fi
|
|
206
|
-
|
|
207
|
-
# 3. Direct PATH lookup for the default runtime.
|
|
208
|
-
if [[ -z "$opencode_bin" ]]; then
|
|
209
|
-
opencode_bin=$(command -v opencode 2>/dev/null || true)
|
|
210
|
-
fi
|
|
211
|
-
|
|
212
|
-
# 4. OS-aware common-install-location sweep. Used when live `$PATH` is
|
|
213
|
-
# minimal (systemd-spawned setup.sh) and persistence hasn't been
|
|
214
|
-
# seeded yet. Covers Homebrew (macOS + Linuxbrew), /usr/local, npm
|
|
215
|
-
# global, Python/uv pipx-style `.local/bin`, and bun.
|
|
216
|
-
if [[ -z "$opencode_bin" ]]; then
|
|
217
|
-
local _candidate
|
|
218
|
-
for _candidate in \
|
|
219
|
-
/opt/homebrew/bin/opencode \
|
|
220
|
-
/usr/local/bin/opencode \
|
|
221
|
-
/home/linuxbrew/.linuxbrew/bin/opencode \
|
|
222
|
-
"$HOME/.npm-global/bin/opencode" \
|
|
223
|
-
"$HOME/.local/bin/opencode" \
|
|
224
|
-
"$HOME/.bun/bin/opencode" \
|
|
225
|
-
/opt/homebrew/bin/claude \
|
|
226
|
-
/usr/local/bin/claude \
|
|
227
|
-
"$HOME/.local/bin/claude"; do
|
|
228
|
-
if [[ -x "$_candidate" ]]; then
|
|
229
|
-
opencode_bin="$_candidate"
|
|
230
|
-
break
|
|
231
|
-
fi
|
|
232
|
-
done
|
|
233
|
-
fi
|
|
234
|
-
|
|
235
|
-
# 5. Last-resort legacy fallback (preserves pre-GH#18439 behaviour so
|
|
236
|
-
# setup.sh never exits the resolver empty-handed).
|
|
237
|
-
[[ -z "$opencode_bin" ]] && opencode_bin="/opt/homebrew/bin/opencode"
|
|
238
|
-
|
|
239
|
-
# Persist the resolved path for subsequent non-interactive invocations
|
|
240
|
-
# (auto-update timer, cron regeneration). Only write when we actually
|
|
241
|
-
# found a real executable — don't persist the legacy fallback.
|
|
242
|
-
if [[ -x "$opencode_bin" ]]; then
|
|
243
|
-
mkdir -p "$(dirname "$_persisted_file")" 2>/dev/null || true
|
|
244
|
-
printf '%s\n' "$opencode_bin" >"$_persisted_file" 2>/dev/null || true
|
|
245
|
-
fi
|
|
246
|
-
|
|
247
|
-
printf '%s' "$opencode_bin"
|
|
248
|
-
return 0
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
_build_pulse_linux_env() {
|
|
252
|
-
# GH#17546/GH#17769: Model config is derived from pool + routing table at
|
|
253
|
-
# runtime. No model env vars embedded in cron/systemd.
|
|
254
|
-
local opencode_bin="${1:-}"
|
|
255
|
-
local _pulse_env="PULSE_DIR=${HOME}/.aidevops/.agent-workspace
|
|
256
|
-
PULSE_STALE_THRESHOLD=${PULSE_STALE_THRESHOLD_SECONDS}"
|
|
257
|
-
|
|
258
|
-
# GH#18439 Bug 2: embed resolved runtime binary path so pulse-wrapper.sh
|
|
259
|
-
# and headless-runtime-helper.sh find the correct binary under systemd's
|
|
260
|
-
# minimal PATH (e.g. when aidevops-auto-update.timer regenerates the
|
|
261
|
-
# service file). Mirrors the macOS launchd <OPENCODE_BIN> key.
|
|
262
|
-
if [[ -n "$opencode_bin" ]]; then
|
|
263
|
-
_pulse_env+=$'\n'"OPENCODE_BIN=${opencode_bin}"
|
|
264
|
-
fi
|
|
265
|
-
|
|
266
|
-
printf '%s' "$_pulse_env"
|
|
267
|
-
return 0
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
# Read supervisor.pulse_interval_seconds from settings.json.
|
|
271
|
-
# Falls back to 180 if the file is missing, the key is absent, or jq is unavailable.
|
|
272
|
-
# Clamps to the validated range [30, 3600].
|
|
273
|
-
# GH#18018: previously this was hardcoded as "120" in _install_supervisor_pulse.
|
|
274
|
-
# t2744: default raised 120 → 180 to reduce GraphQL pressure (33% fewer cycles)
|
|
275
|
-
# on multi-repo setups where per-cycle cost chronically exceeds 5000/hr.
|
|
276
|
-
_read_pulse_interval_seconds() {
|
|
277
|
-
local _settings_file="$HOME/.config/aidevops/settings.json"
|
|
278
|
-
local _interval=180
|
|
279
|
-
|
|
280
|
-
if command -v jq >/dev/null 2>&1 && [[ -f "$_settings_file" ]]; then
|
|
281
|
-
local _raw
|
|
282
|
-
_raw=$(jq -r '.supervisor.pulse_interval_seconds // empty' "$_settings_file" 2>/dev/null) || _raw=""
|
|
283
|
-
if [[ -n "$_raw" ]] && [[ "$_raw" =~ ^[0-9]+$ ]]; then
|
|
284
|
-
_interval="$_raw"
|
|
285
|
-
fi
|
|
286
|
-
fi
|
|
287
|
-
|
|
288
|
-
# Clamp to validated range (mirrors settings-helper.sh validation: 30-3600)
|
|
289
|
-
if [[ "$_interval" -lt 30 ]]; then
|
|
290
|
-
_interval=30
|
|
291
|
-
elif [[ "$_interval" -gt 3600 ]]; then
|
|
292
|
-
_interval=3600
|
|
293
|
-
fi
|
|
294
|
-
|
|
295
|
-
printf '%d' "$_interval"
|
|
296
|
-
return 0
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
# Convert an interval in seconds to a cron schedule expression (e.g. "*/2 * * * *").
|
|
300
|
-
# Minimum granularity is 1 minute. Intervals that don't divide evenly into minutes
|
|
301
|
-
# are rounded down to whole minutes with a warning.
|
|
302
|
-
# Args: $1 = interval_seconds
|
|
303
|
-
_seconds_to_cron_schedule() {
|
|
304
|
-
local _interval_sec="$1"
|
|
305
|
-
local _minutes=$((_interval_sec / 60))
|
|
306
|
-
local _remainder=$((_interval_sec % 60))
|
|
307
|
-
|
|
308
|
-
# Clamp to at least 1 minute
|
|
309
|
-
if [[ "$_minutes" -lt 1 ]]; then
|
|
310
|
-
_minutes=1
|
|
311
|
-
fi
|
|
312
|
-
|
|
313
|
-
# Warn if interval doesn't divide evenly into minutes
|
|
314
|
-
if [[ "$_remainder" -ne 0 ]]; then
|
|
315
|
-
echo "[schedulers] Warning: pulse_interval_seconds=${_interval_sec} does not divide evenly into minutes; rounding down to ${_minutes}min for cron schedule (systemd uses exact seconds)" >&2
|
|
316
|
-
fi
|
|
317
|
-
|
|
318
|
-
# cron step values must be 1-59; */60 is invalid. Use @hourly for exactly 60 min,
|
|
319
|
-
# clamp anything above 59 to 59 (the _read_pulse_interval_seconds cap is 3600s=60min).
|
|
320
|
-
if [[ "$_minutes" -ge 60 ]]; then
|
|
321
|
-
printf '@hourly'
|
|
322
|
-
else
|
|
323
|
-
printf '*/%d * * * *' "$_minutes"
|
|
324
|
-
fi
|
|
325
|
-
return 0
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
_install_supervisor_pulse() {
|
|
329
|
-
local _os="$1"
|
|
330
|
-
local pulse_label="$2"
|
|
331
|
-
local wrapper_script="$3"
|
|
332
|
-
local opencode_bin="$4"
|
|
333
|
-
local _pulse_installed="$5"
|
|
334
|
-
|
|
335
|
-
mkdir -p "$HOME/.aidevops/logs"
|
|
336
|
-
|
|
337
|
-
if [[ "$_os" == "Darwin" ]]; then
|
|
338
|
-
_install_pulse_launchd "$pulse_label" "$wrapper_script" "$opencode_bin" "$_pulse_installed"
|
|
339
|
-
return 0
|
|
340
|
-
fi
|
|
341
|
-
|
|
342
|
-
# GH#18018: read user-configured interval instead of hardcoding 120s / */2 cron
|
|
343
|
-
local _pulse_interval_sec
|
|
344
|
-
_pulse_interval_sec=$(_read_pulse_interval_seconds)
|
|
345
|
-
local _pulse_cron_schedule
|
|
346
|
-
_pulse_cron_schedule=$(_seconds_to_cron_schedule "$_pulse_interval_sec")
|
|
347
|
-
# Build a human-readable interval label: show minutes for exact multiples of 60, seconds otherwise
|
|
348
|
-
local _pulse_interval_label
|
|
349
|
-
if (( _pulse_interval_sec % 60 == 0 )); then
|
|
350
|
-
_pulse_interval_label="$((_pulse_interval_sec / 60)) min"
|
|
351
|
-
else
|
|
352
|
-
_pulse_interval_label="${_pulse_interval_sec}s"
|
|
353
|
-
fi
|
|
354
|
-
|
|
355
|
-
local _pulse_timeout_sec=$((PULSE_STALE_THRESHOLD_SECONDS + 60))
|
|
356
|
-
local _pulse_env=""
|
|
357
|
-
# GH#18439 Bug 2: thread resolved runtime binary path through to the
|
|
358
|
-
# Linux env builder so OPENCODE_BIN is embedded in the systemd service
|
|
359
|
-
# file (parity with the macOS launchd plist at line 415).
|
|
360
|
-
_pulse_env=$(_build_pulse_linux_env "$opencode_bin")
|
|
361
|
-
_install_scheduler_linux \
|
|
362
|
-
"aidevops-supervisor-pulse" \
|
|
363
|
-
"aidevops: supervisor-pulse" \
|
|
364
|
-
"${_pulse_cron_schedule}" \
|
|
365
|
-
"\"${wrapper_script}\"" \
|
|
366
|
-
"${_pulse_interval_sec}" \
|
|
367
|
-
"$HOME/.aidevops/logs/pulse-wrapper.log" \
|
|
368
|
-
"$_pulse_env" \
|
|
369
|
-
"Supervisor pulse enabled (every ${_pulse_interval_label})" \
|
|
370
|
-
"Failed to install supervisor pulse scheduler. See runners.md for manual setup." \
|
|
371
|
-
"true" \
|
|
372
|
-
"false" \
|
|
373
|
-
"" \
|
|
374
|
-
"${_pulse_timeout_sec}"
|
|
375
|
-
return 0
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
# Setup the supervisor pulse scheduler (consent-gated autonomous orchestration).
|
|
379
|
-
# Uses pulse-wrapper.sh which handles dedup, orphan cleanup, and RAM-based concurrency.
|
|
380
|
-
# macOS: launchd plist invoking wrapper | Linux: cron entry invoking wrapper
|
|
381
|
-
# The plist is ALWAYS regenerated on setup.sh to pick up config changes (env vars,
|
|
382
|
-
# thresholds). Only the first-install prompt is gated on consent state.
|
|
383
|
-
#######################################
|
|
384
|
-
# t2119: Record the schedulers.sh template hash to the shared state
|
|
385
|
-
# directory. auto-update-helper.sh's check_launchd_plist_drift compares
|
|
386
|
-
# this against the current hash on every update cycle — whenever
|
|
387
|
-
# schedulers.sh changes without a VERSION bump (PR #19079 scenario),
|
|
388
|
-
# drift is detected and setup.sh --non-interactive is re-run to
|
|
389
|
-
# regenerate the installed plists.
|
|
390
|
-
#
|
|
391
|
-
# Called from setup_supervisor_pulse unconditionally so the hash is
|
|
392
|
-
# kept current on every setup.sh run, whether pulse is installed,
|
|
393
|
-
# upgraded, or disabled. Whole-file hash is the simplest signal that
|
|
394
|
-
# any plist-generating change has occurred.
|
|
395
|
-
#######################################
|
|
396
|
-
_schedulers_record_template_hash() {
|
|
397
|
-
local state_dir="$HOME/.aidevops/.agent-workspace/tmp"
|
|
398
|
-
mkdir -p "$state_dir" 2>/dev/null || return 0
|
|
399
|
-
local hash_file="$state_dir/schedulers-template-hash.state"
|
|
400
|
-
local schedulers_src="${BASH_SOURCE[0]:-}"
|
|
401
|
-
[[ -f "$schedulers_src" ]] || return 0
|
|
402
|
-
if command -v shasum >/dev/null 2>&1; then
|
|
403
|
-
shasum -a 256 "$schedulers_src" 2>/dev/null | awk '{print $1}' >"$hash_file" 2>/dev/null || true
|
|
404
|
-
elif command -v sha256sum >/dev/null 2>&1; then
|
|
405
|
-
sha256sum "$schedulers_src" 2>/dev/null | awk '{print $1}' >"$hash_file" 2>/dev/null || true
|
|
406
|
-
fi
|
|
407
|
-
return 0
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
setup_supervisor_pulse() {
|
|
411
|
-
local _os="$1"
|
|
412
|
-
|
|
413
|
-
# Record template hash so auto-update can detect drift between
|
|
414
|
-
# schedulers.sh and the installed plists on macOS (t2119).
|
|
415
|
-
_schedulers_record_template_hash
|
|
416
|
-
|
|
417
|
-
# Ensure crontab has a global PATH= line (Linux only; macOS uses launchd env).
|
|
418
|
-
# Must run before any cron entries are installed so they inherit the PATH.
|
|
419
|
-
if [[ "$_os" != "Darwin" ]]; then
|
|
420
|
-
_ensure_cron_path
|
|
421
|
-
fi
|
|
422
|
-
|
|
423
|
-
# Consent model (GH#2926):
|
|
424
|
-
# - Default OFF: supervisor_pulse defaults to false in all config layers
|
|
425
|
-
# - Explicit consent required: user must type "y" (prompt defaults to [y/N])
|
|
426
|
-
# - Consent persisted: written to config.jsonc so it survives updates
|
|
427
|
-
# - Never silently re-enabled: if config says false, skip entirely
|
|
428
|
-
# - Non-interactive: only installs if config explicitly says true
|
|
429
|
-
local wrapper_script="$HOME/.aidevops/agents/scripts/pulse-wrapper.sh"
|
|
430
|
-
local pulse_label="com.aidevops.aidevops-supervisor-pulse"
|
|
431
|
-
|
|
432
|
-
local _pulse_user_config
|
|
433
|
-
_pulse_user_config=$(_resolve_pulse_consent)
|
|
434
|
-
|
|
435
|
-
local _do_install
|
|
436
|
-
_do_install=$(_determine_pulse_install "$_pulse_user_config" "$wrapper_script")
|
|
437
|
-
|
|
438
|
-
local _pulse_lower
|
|
439
|
-
_pulse_lower=$(echo "$_pulse_user_config" | tr '[:upper:]' '[:lower:]')
|
|
440
|
-
|
|
441
|
-
# Detect if pulse is already installed (for upgrade messaging)
|
|
442
|
-
# Uses shared helper to check launchd, cron, and systemd (GH#17381)
|
|
443
|
-
local _pulse_installed=false
|
|
444
|
-
if _is_pulse_installed "$pulse_label"; then
|
|
445
|
-
_pulse_installed=true
|
|
446
|
-
fi
|
|
447
|
-
|
|
448
|
-
# Detect dispatch backend binary location (t1665.5 — registry-driven)
|
|
449
|
-
local opencode_bin=""
|
|
450
|
-
opencode_bin=$(_resolve_pulse_runtime_binary)
|
|
451
|
-
|
|
452
|
-
if [[ "$_do_install" == "true" ]]; then
|
|
453
|
-
_install_supervisor_pulse "$_os" "$pulse_label" "$wrapper_script" "$opencode_bin" "$_pulse_installed"
|
|
454
|
-
elif [[ "$_pulse_lower" == "false" && "$_pulse_installed" == "true" ]]; then
|
|
455
|
-
# User explicitly disabled but pulse is still installed — clean up
|
|
456
|
-
_uninstall_pulse "$_os" "$pulse_label"
|
|
457
|
-
fi
|
|
458
|
-
|
|
459
|
-
# Export effective pulse state for setup_stats_wrapper.
|
|
460
|
-
# Use the actual install decision (_do_install), not just the consent string,
|
|
461
|
-
# so stats wrapper tracks the real scheduler state (e.g., wrapper missing → false).
|
|
462
|
-
PULSE_CONSENT_LOWER="$_pulse_lower"
|
|
463
|
-
if [[ "$_do_install" == "true" ]]; then
|
|
464
|
-
PULSE_ENABLED="true"
|
|
465
|
-
else
|
|
466
|
-
PULSE_ENABLED="false"
|
|
467
|
-
fi
|
|
468
|
-
return 0
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
# Clean up old/legacy pulse launchd plists before reinstalling.
|
|
472
|
-
# Args: $1=pulse_label, $2=pulse_plist path
|
|
473
|
-
_cleanup_old_pulse_plists() {
|
|
474
|
-
local pulse_label="$1"
|
|
475
|
-
local pulse_plist="$2"
|
|
476
|
-
|
|
477
|
-
# Unload old plist if upgrading
|
|
478
|
-
if _launchd_has_agent "$pulse_label"; then
|
|
479
|
-
launchctl unload "$pulse_plist" || true
|
|
480
|
-
pkill -f 'Supervisor Pulse' 2>/dev/null || true
|
|
481
|
-
fi
|
|
482
|
-
|
|
483
|
-
# Also clean up old label if present
|
|
484
|
-
local old_plist="$HOME/Library/LaunchAgents/com.aidevops.supervisor-pulse.plist"
|
|
485
|
-
if [[ -f "$old_plist" ]]; then
|
|
486
|
-
launchctl unload "$old_plist" || true
|
|
487
|
-
rm -f "$old_plist"
|
|
488
|
-
fi
|
|
489
|
-
return 0
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
# Build XML environment variable fragment for headless model overrides.
|
|
493
|
-
# GH#17546: Model config was removed from plist embedding.
|
|
494
|
-
# GH#17769: Model routing is now derived from pool + routing table at runtime.
|
|
495
|
-
# No env vars needed — pulse-wrapper.sh reads the routing table directly.
|
|
496
|
-
_build_pulse_headless_env_xml() {
|
|
497
|
-
# Intentionally empty — model config read from credentials.sh at runtime.
|
|
498
|
-
printf '%s' ""
|
|
499
|
-
return 0
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
# Read user-owned plist env override file and emit XML key/string pairs
|
|
503
|
-
# for the matching label's env vars. Keys prefixed with _ are skipped
|
|
504
|
-
# (used as comments in the JSON template).
|
|
505
|
-
#
|
|
506
|
-
# Args: $1=plist_label (e.g. "com.aidevops.aidevops-supervisor-pulse")
|
|
507
|
-
# $2=override_file (absolute path; default ~/.agents/configs/plist-env-overrides.json)
|
|
508
|
-
# $3=indent (string to prepend each line; default "\t\t")
|
|
509
|
-
#
|
|
510
|
-
# Returns 0 on success (including empty result when label not found).
|
|
511
|
-
# Prints WARN to stderr and returns 0 when file is present but malformed.
|
|
512
|
-
# Emits nothing when file is absent.
|
|
513
|
-
_build_plist_env_overrides_xml() {
|
|
514
|
-
local _label="$1"
|
|
515
|
-
local _override_file="${2:-$HOME/.aidevops/agents/configs/plist-env-overrides.json}"
|
|
516
|
-
local _indent="${3:- }"
|
|
517
|
-
|
|
518
|
-
# Missing file is the normal case (user has not created the override file yet)
|
|
519
|
-
[[ -f "$_override_file" ]] || return 0
|
|
520
|
-
|
|
521
|
-
# Require jq — without it we cannot parse JSON safely
|
|
522
|
-
if ! command -v jq >/dev/null 2>&1; then
|
|
523
|
-
echo "[schedulers] WARN: jq not found; skipping plist-env-overrides.json injection" >&2
|
|
524
|
-
return 0
|
|
525
|
-
fi
|
|
526
|
-
|
|
527
|
-
# Validate JSON
|
|
528
|
-
if ! jq empty "$_override_file" 2>/dev/null; then
|
|
529
|
-
echo "[schedulers] WARN: plist-env-overrides.json is malformed; skipping injection (file: $_override_file)" >&2
|
|
530
|
-
return 0
|
|
531
|
-
fi
|
|
532
|
-
|
|
533
|
-
# Extract key=value pairs for the matching label; skip _ prefixed keys
|
|
534
|
-
local _pairs
|
|
535
|
-
_pairs=$(jq -r --arg label "$_label" '
|
|
536
|
-
.[$label] // {} |
|
|
537
|
-
to_entries[] |
|
|
538
|
-
select(.key | startswith("_") | not) |
|
|
539
|
-
"\(.key)=\(.value)"
|
|
540
|
-
' "$_override_file" 2>/dev/null) || return 0
|
|
541
|
-
|
|
542
|
-
[[ -z "$_pairs" ]] && return 0
|
|
543
|
-
|
|
544
|
-
local _line _key _val _xml_key _xml_val
|
|
545
|
-
while IFS= read -r _line; do
|
|
546
|
-
[[ -z "$_line" ]] && continue
|
|
547
|
-
_key="${_line%%=*}"
|
|
548
|
-
_val="${_line#*=}"
|
|
549
|
-
_xml_key=$(_xml_escape "$_key")
|
|
550
|
-
_xml_val=$(_xml_escape "$_val")
|
|
551
|
-
printf '%s<key>%s</key>\n%s<string>%s</string>\n' \
|
|
552
|
-
"$_indent" "$_xml_key" "$_indent" "$_xml_val"
|
|
553
|
-
done <<<"$_pairs"
|
|
554
|
-
|
|
555
|
-
return 0
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
# Log which env var overrides were injected from plist-env-overrides.json for a label.
|
|
559
|
-
# Prints to stdout (setup.sh output). No-op when file absent or label not found.
|
|
560
|
-
# Args: $1=plist_label, $2=override_file (optional)
|
|
561
|
-
_log_plist_env_overrides() {
|
|
562
|
-
local _label="$1"
|
|
563
|
-
local _override_file="${2:-$HOME/.aidevops/agents/configs/plist-env-overrides.json}"
|
|
564
|
-
|
|
565
|
-
[[ -f "$_override_file" ]] || return 0
|
|
566
|
-
command -v jq >/dev/null 2>&1 || return 0
|
|
567
|
-
jq empty "$_override_file" 2>/dev/null || return 0
|
|
568
|
-
|
|
569
|
-
local _keys
|
|
570
|
-
_keys=$(jq -r --arg label "$_label" '
|
|
571
|
-
.[$label] // {} |
|
|
572
|
-
keys[] |
|
|
573
|
-
select(startswith("_") | not)
|
|
574
|
-
' "$_override_file" 2>/dev/null) || return 0
|
|
575
|
-
|
|
576
|
-
[[ -z "$_keys" ]] && return 0
|
|
577
|
-
|
|
578
|
-
local _count
|
|
579
|
-
_count=$(echo "$_keys" | wc -l | tr -d ' ')
|
|
580
|
-
local _keys_inline
|
|
581
|
-
_keys_inline=$(echo "$_keys" | tr '\n' ' ' | sed 's/ $//')
|
|
582
|
-
print_info " plist-env-overrides: injected ${_count} var(s) into ${_label}: ${_keys_inline}"
|
|
583
|
-
return 0
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
# Generate the full pulse launchd plist XML content.
|
|
587
|
-
# Args: $1=pulse_label, $2=wrapper_script, $3=opencode_bin
|
|
588
|
-
# Prints the complete plist XML to stdout.
|
|
589
|
-
#
|
|
590
|
-
# StartInterval is read from supervisor.pulse_interval_seconds in
|
|
591
|
-
# settings.json via _read_pulse_interval_seconds (default 180 — t2744).
|
|
592
|
-
# Previously this was hardcoded as 120, meaning macOS users could not
|
|
593
|
-
# tune the pulse cadence via settings (Linux/cron path always honoured
|
|
594
|
-
# the setting). The hardcoding is now removed; the macOS path matches
|
|
595
|
-
# the Linux path's behaviour.
|
|
596
|
-
_generate_pulse_plist_content() {
|
|
597
|
-
local pulse_label="$1"
|
|
598
|
-
local wrapper_script="$2"
|
|
599
|
-
local opencode_bin="$3"
|
|
600
|
-
|
|
601
|
-
# XML-escape paths for safe plist embedding (prevents injection
|
|
602
|
-
# if $HOME or paths contain &, <, > characters)
|
|
603
|
-
local _xml_wrapper_script _xml_home _xml_opencode_bin _xml_pulse_dir _xml_path
|
|
604
|
-
_xml_wrapper_script=$(_xml_escape "$wrapper_script")
|
|
605
|
-
_xml_home=$(_xml_escape "$HOME")
|
|
606
|
-
_xml_opencode_bin=$(_xml_escape "$opencode_bin")
|
|
607
|
-
# Use neutral workspace path for PULSE_DIR so supervisor sessions
|
|
608
|
-
# are not associated with any specific managed repo (GH#5136).
|
|
609
|
-
_xml_pulse_dir=$(_xml_escape "${HOME}/.aidevops/.agent-workspace")
|
|
610
|
-
_xml_path=$(_xml_escape "$PATH")
|
|
611
|
-
|
|
612
|
-
local _headless_xml_env
|
|
613
|
-
_headless_xml_env=$(_build_pulse_headless_env_xml)
|
|
614
|
-
|
|
615
|
-
# Resolve modern bash for ProgramArguments — launchd bypasses shebangs
|
|
616
|
-
# when an explicit interpreter is specified. (GH#19632 / t2176)
|
|
617
|
-
local _xml_bash_bin
|
|
618
|
-
_xml_bash_bin=$(_xml_escape "$(_resolve_modern_bash)")
|
|
619
|
-
|
|
620
|
-
# Resolve the configured pulse interval (settings.json, with default).
|
|
621
|
-
# Already validated to [30, 3600] inside _read_pulse_interval_seconds.
|
|
622
|
-
local _pulse_interval_sec
|
|
623
|
-
_pulse_interval_sec=$(_read_pulse_interval_seconds)
|
|
624
|
-
|
|
625
|
-
# Inject user-owned plist env overrides (GH#20563 / t2759).
|
|
626
|
-
# Reads ~/.aidevops/agents/configs/plist-env-overrides.json when present.
|
|
627
|
-
# Missing file or label not found → emits nothing (no-op, safe default).
|
|
628
|
-
local _env_overrides_xml
|
|
629
|
-
_env_overrides_xml=$(_build_plist_env_overrides_xml "$pulse_label")
|
|
630
|
-
|
|
631
|
-
cat <<PLIST
|
|
632
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
633
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
634
|
-
<plist version="1.0">
|
|
635
|
-
<dict>
|
|
636
|
-
<key>Label</key>
|
|
637
|
-
<string>${pulse_label}</string>
|
|
638
|
-
<key>ProgramArguments</key>
|
|
639
|
-
<array>
|
|
640
|
-
<string>${_xml_bash_bin}</string>
|
|
641
|
-
<string>${_xml_wrapper_script}</string>
|
|
642
|
-
</array>
|
|
643
|
-
<key>StartInterval</key>
|
|
644
|
-
<integer>${_pulse_interval_sec}</integer>
|
|
645
|
-
<key>StandardOutPath</key>
|
|
646
|
-
<string>${_xml_home}/.aidevops/logs/pulse-wrapper.log</string>
|
|
647
|
-
<key>StandardErrorPath</key>
|
|
648
|
-
<string>${_xml_home}/.aidevops/logs/pulse-wrapper.log</string>
|
|
649
|
-
<key>EnvironmentVariables</key>
|
|
650
|
-
<dict>
|
|
651
|
-
<key>PATH</key>
|
|
652
|
-
<string>${_xml_path}</string>
|
|
653
|
-
<key>HOME</key>
|
|
654
|
-
<string>${_xml_home}</string>
|
|
655
|
-
<key>OPENCODE_BIN</key>
|
|
656
|
-
<string>${_xml_opencode_bin}</string>
|
|
657
|
-
<key>PULSE_DIR</key>
|
|
658
|
-
<string>${_xml_pulse_dir}</string>
|
|
659
|
-
<key>PULSE_STALE_THRESHOLD</key>
|
|
660
|
-
<string>${PULSE_STALE_THRESHOLD_SECONDS}</string>
|
|
661
|
-
${_headless_xml_env}
|
|
662
|
-
${_env_overrides_xml} </dict>
|
|
663
|
-
<key>SoftResourceLimits</key>
|
|
664
|
-
<dict>
|
|
665
|
-
<key>NumberOfFiles</key>
|
|
666
|
-
<integer>4096</integer>
|
|
667
|
-
</dict>
|
|
668
|
-
<key>RunAtLoad</key>
|
|
669
|
-
<true/>
|
|
670
|
-
<key>KeepAlive</key>
|
|
671
|
-
<false/>
|
|
672
|
-
</dict>
|
|
673
|
-
</plist>
|
|
674
|
-
PLIST
|
|
675
|
-
return 0
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
# Install supervisor pulse via launchd (macOS)
|
|
679
|
-
_install_pulse_launchd() {
|
|
680
|
-
local pulse_label="$1"
|
|
681
|
-
local wrapper_script="$2"
|
|
682
|
-
local opencode_bin="$3"
|
|
683
|
-
local _pulse_installed="$4"
|
|
684
|
-
local pulse_plist="$HOME/Library/LaunchAgents/${pulse_label}.plist"
|
|
685
|
-
|
|
686
|
-
# Capture plist content before touching the existing file.
|
|
687
|
-
# This avoids the "unload old, then write fails" window that leaves a 0-byte plist.
|
|
688
|
-
local pulse_plist_content
|
|
689
|
-
pulse_plist_content=$(_generate_pulse_plist_content "$pulse_label" "$wrapper_script" "$opencode_bin")
|
|
690
|
-
|
|
691
|
-
# Defensive: if generation produced empty content, refuse to touch the existing plist.
|
|
692
|
-
if [[ -z "$pulse_plist_content" ]]; then
|
|
693
|
-
print_warning "Pulse plist generation produced empty content — leaving existing plist untouched"
|
|
694
|
-
return 1
|
|
695
|
-
fi
|
|
696
|
-
|
|
697
|
-
# Resolve interval for the user-facing message (matches what the plist contains).
|
|
698
|
-
local _interval_sec _interval_label
|
|
699
|
-
_interval_sec=$(_read_pulse_interval_seconds)
|
|
700
|
-
if (( _interval_sec % 60 == 0 )); then
|
|
701
|
-
_interval_label="$((_interval_sec / 60)) min"
|
|
702
|
-
else
|
|
703
|
-
_interval_label="${_interval_sec}s"
|
|
704
|
-
fi
|
|
705
|
-
|
|
706
|
-
# One-time legacy cleanup: unload and remove the old-label plist if present.
|
|
707
|
-
# Users on stale installs may have com.aidevops.supervisor-pulse (legacy) and
|
|
708
|
-
# com.aidevops.aidevops-supervisor-pulse (current) both loaded, causing 2x
|
|
709
|
-
# dispatch. Only targets the hardcoded legacy path; idempotent — no-op when
|
|
710
|
-
# the legacy file is absent.
|
|
711
|
-
local _legacy_plist="$HOME/Library/LaunchAgents/com.aidevops.supervisor-pulse.plist"
|
|
712
|
-
if [[ -f "$_legacy_plist" ]]; then
|
|
713
|
-
launchctl unload "$_legacy_plist" 2>/dev/null || true
|
|
714
|
-
rm -f "$_legacy_plist"
|
|
715
|
-
fi
|
|
716
|
-
|
|
717
|
-
# _launchd_install_if_changed handles unload-before-replace only when content
|
|
718
|
-
# has changed, and writes atomically via tmp+rename (see setup.sh).
|
|
719
|
-
# shell-portability: ignore next — _install_pulse_launchd is macOS-only (launchd)
|
|
720
|
-
if _launchd_install_if_changed "$pulse_label" "$pulse_plist" "$pulse_plist_content"; then
|
|
721
|
-
if [[ "$_pulse_installed" == "true" ]]; then
|
|
722
|
-
print_info "Supervisor pulse updated (launchd config regenerated, every ${_interval_label})"
|
|
723
|
-
else
|
|
724
|
-
print_info "Supervisor pulse enabled (launchd, every ${_interval_label})"
|
|
725
|
-
fi
|
|
726
|
-
# Log any user-provided env var overrides that were injected (GH#20563 / t2759)
|
|
727
|
-
_log_plist_env_overrides "$pulse_label"
|
|
728
|
-
else
|
|
729
|
-
print_warning "Failed to load supervisor pulse LaunchAgent"
|
|
730
|
-
fi
|
|
731
|
-
return 0
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
# Check if systemd user services are available on this Linux system.
|
|
735
|
-
# Returns 0 if systemd --user is functional, 1 otherwise.
|
|
736
|
-
_systemd_user_available() {
|
|
737
|
-
command -v systemctl >/dev/null 2>&1 || return 1
|
|
738
|
-
systemctl --user status >/dev/null 2>&1 || return 1
|
|
739
|
-
return 0
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
# Escape a value for safe embedding in a systemd unit Environment= or ExecStart=
|
|
743
|
-
# directive. systemd interprets % as specifiers (%h, %n, %t, etc.) and spaces
|
|
744
|
-
# as key-value separators. This helper:
|
|
745
|
-
# 1. Escapes \ → \\ (must be first to avoid double-escaping)
|
|
746
|
-
# 2. Doubles % → %% (escape specifiers)
|
|
747
|
-
# 3. Escapes embedded " → \"
|
|
748
|
-
# 4. Wraps the result in "..." (handles spaces and other shell metacharacters)
|
|
749
|
-
# Usage: escaped=$(_systemd_escape "$value")
|
|
750
|
-
#
|
|
751
|
-
# WARNING: Do NOT use for StandardOutput= or StandardError= directives.
|
|
752
|
-
# systemd does not strip outer quotes from those values — "append:/path" is
|
|
753
|
-
# treated as a literal filename with quote characters, failing silently.
|
|
754
|
-
# Use bare values for StandardOutput=/StandardError=:
|
|
755
|
-
# StandardOutput=append:${log_file} ← correct
|
|
756
|
-
# StandardOutput=$(_systemd_escape "append:${log_file}") ← WRONG
|
|
757
|
-
_systemd_escape() {
|
|
758
|
-
local _val="$1"
|
|
759
|
-
# Step 1: escape backslashes
|
|
760
|
-
_val="${_val//\\/\\\\}"
|
|
761
|
-
# Step 2: escape % specifiers
|
|
762
|
-
_val="${_val//%/%%}"
|
|
763
|
-
# Step 3: escape embedded double-quotes
|
|
764
|
-
_val="${_val//\"/\\\"}"
|
|
765
|
-
# Step 4: wrap in double-quotes
|
|
766
|
-
printf '"%s"' "$_val"
|
|
767
|
-
return 0
|
|
768
|
-
}
|
|
769
|
-
|
|
770
|
-
# Build systemd Environment= lines from newline-separated KEY=VALUE pairs.
|
|
771
|
-
# Always appends HOME and PATH for parity with launchd and cron execution.
|
|
772
|
-
_scheduler_systemd_env_lines() {
|
|
773
|
-
local env_vars="$1"
|
|
774
|
-
local _env_lines=""
|
|
775
|
-
|
|
776
|
-
if [[ -n "$env_vars" ]]; then
|
|
777
|
-
while IFS= read -r _kv; do
|
|
778
|
-
[[ -z "$_kv" ]] && continue
|
|
779
|
-
local _key="${_kv%%=*}"
|
|
780
|
-
local _raw_val="${_kv#*=}"
|
|
781
|
-
local _escaped_val
|
|
782
|
-
_escaped_val=$(_systemd_escape "$_raw_val")
|
|
783
|
-
_env_lines+="Environment=${_key}=${_escaped_val}"$'\n'
|
|
784
|
-
done <<<"$env_vars"
|
|
785
|
-
fi
|
|
786
|
-
|
|
787
|
-
_env_lines+="Environment=HOME=$(_systemd_escape "$HOME")"$'\n'
|
|
788
|
-
_env_lines+="Environment=PATH=$(_systemd_escape "$PATH")"$'\n'
|
|
789
|
-
printf '%s' "$_env_lines"
|
|
790
|
-
return 0
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
# Build inline cron environment assignments from newline-separated KEY=VALUE pairs.
|
|
794
|
-
_scheduler_cron_env_prefix() {
|
|
795
|
-
local env_vars="$1"
|
|
796
|
-
local _env_prefix=""
|
|
797
|
-
|
|
798
|
-
if [[ -n "$env_vars" ]]; then
|
|
799
|
-
while IFS= read -r _kv; do
|
|
800
|
-
[[ -z "$_kv" ]] && continue
|
|
801
|
-
local _key="${_kv%%=*}"
|
|
802
|
-
local _raw_val="${_kv#*=}"
|
|
803
|
-
local _escaped_val
|
|
804
|
-
_escaped_val=$(_cron_escape "$_raw_val")
|
|
805
|
-
_env_prefix+="${_key}=${_escaped_val} "
|
|
806
|
-
done <<<"$env_vars"
|
|
807
|
-
fi
|
|
808
|
-
|
|
809
|
-
printf '%s' "$_env_prefix"
|
|
810
|
-
return 0
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
# Install a generic scheduler via systemd user timer (Linux with systemd).
|
|
814
|
-
# Args:
|
|
815
|
-
# $1 = service_name (e.g. "aidevops-stats-wrapper")
|
|
816
|
-
# $2 = exec_command (shell command run via /bin/bash -lc)
|
|
817
|
-
# $3 = interval_sec (OnUnitActiveSec interval in seconds; may be empty for calendar-only)
|
|
818
|
-
# $4 = log_file (absolute path to log file)
|
|
819
|
-
# $5 = env_vars (newline-separated KEY=VALUE pairs, may be empty)
|
|
820
|
-
# $6 = run_at_load ("true" or "false")
|
|
821
|
-
# $7 = low_priority ("true" or "false")
|
|
822
|
-
# $8 = on_calendar (optional systemd OnCalendar spec)
|
|
823
|
-
# $9 = timeout_sec (optional TimeoutStartSec; defaults to interval_sec)
|
|
824
|
-
# Returns 0 on success, 1 if systemd enable fails (caller should fall back to cron).
|
|
825
|
-
_install_scheduler_systemd() {
|
|
826
|
-
local service_name="$1"
|
|
827
|
-
local exec_command="$2"
|
|
828
|
-
local interval_sec="$3"
|
|
829
|
-
local log_file="$4"
|
|
830
|
-
local env_vars="$5"
|
|
831
|
-
local run_at_load="$6"
|
|
832
|
-
local low_priority="$7"
|
|
833
|
-
local on_calendar="$8"
|
|
834
|
-
local timeout_sec="$9"
|
|
835
|
-
local service_dir="$HOME/.config/systemd/user"
|
|
836
|
-
local service_file="${service_dir}/${service_name}.service"
|
|
837
|
-
local timer_file="${service_dir}/${service_name}.timer"
|
|
838
|
-
|
|
839
|
-
mkdir -p "$service_dir"
|
|
840
|
-
|
|
841
|
-
# GH#18439 Bug 1: command substitution strips trailing newlines, which
|
|
842
|
-
# would run the final Environment=PATH=... into the following
|
|
843
|
-
# StandardOutput=... directive on the same line. Use a sentinel ('x')
|
|
844
|
-
# to preserve the trailing newline that _scheduler_systemd_env_lines
|
|
845
|
-
# always emits.
|
|
846
|
-
local _env_lines
|
|
847
|
-
_env_lines=$(
|
|
848
|
-
_scheduler_systemd_env_lines "$env_vars"
|
|
849
|
-
printf 'x'
|
|
850
|
-
)
|
|
851
|
-
_env_lines="${_env_lines%x}"
|
|
852
|
-
|
|
853
|
-
if [[ -z "$timeout_sec" ]]; then
|
|
854
|
-
timeout_sec="$interval_sec"
|
|
855
|
-
fi
|
|
856
|
-
if [[ -z "$timeout_sec" ]]; then
|
|
857
|
-
timeout_sec="3600"
|
|
858
|
-
fi
|
|
859
|
-
|
|
860
|
-
local _service_extra=""
|
|
861
|
-
if [[ "$low_priority" == "true" ]]; then
|
|
862
|
-
_service_extra+="Nice=10"$'\n'
|
|
863
|
-
_service_extra+="IOSchedulingClass=idle"$'\n'
|
|
864
|
-
fi
|
|
865
|
-
|
|
866
|
-
printf '%s' "[Unit]
|
|
867
|
-
Description=aidevops ${service_name}
|
|
868
|
-
After=network.target
|
|
869
|
-
|
|
870
|
-
[Service]
|
|
871
|
-
Type=oneshot
|
|
872
|
-
KillMode=process
|
|
873
|
-
ExecStart=/bin/bash -lc $(_systemd_escape "$exec_command")
|
|
874
|
-
TimeoutStartSec=${timeout_sec}
|
|
875
|
-
${_service_extra}${_env_lines}StandardOutput=append:${log_file}
|
|
876
|
-
StandardError=append:${log_file}
|
|
877
|
-
" >"$service_file"
|
|
878
|
-
|
|
879
|
-
local _timer_lines=""
|
|
880
|
-
if [[ "$run_at_load" == "true" ]]; then
|
|
881
|
-
_timer_lines+="OnActiveSec=10s"$'\n'
|
|
882
|
-
fi
|
|
883
|
-
if [[ -n "$interval_sec" ]]; then
|
|
884
|
-
_timer_lines+="OnBootSec=${interval_sec}"$'\n'
|
|
885
|
-
_timer_lines+="OnUnitActiveSec=${interval_sec}"$'\n'
|
|
886
|
-
fi
|
|
887
|
-
if [[ -n "$on_calendar" ]]; then
|
|
888
|
-
_timer_lines+="OnCalendar=${on_calendar}"$'\n'
|
|
889
|
-
fi
|
|
890
|
-
|
|
891
|
-
printf '%s' "[Unit]
|
|
892
|
-
Description=aidevops ${service_name} Timer
|
|
893
|
-
|
|
894
|
-
[Timer]
|
|
895
|
-
${_timer_lines}Persistent=true
|
|
896
|
-
|
|
897
|
-
[Install]
|
|
898
|
-
WantedBy=timers.target
|
|
899
|
-
" >"$timer_file"
|
|
900
|
-
|
|
901
|
-
systemctl --user daemon-reload 2>/dev/null || true
|
|
902
|
-
if systemctl --user enable --now "${service_name}.timer" 2>/dev/null; then
|
|
903
|
-
return 0
|
|
904
|
-
fi
|
|
905
|
-
return 1
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
# Install a generic cron entry.
|
|
909
|
-
# Args: $1=cron_tag, $2=cron_schedule, $3=exec_command, $4=log_file, $5=env_vars
|
|
910
|
-
_install_scheduler_cron() {
|
|
911
|
-
local cron_tag="$1"
|
|
912
|
-
local cron_schedule="$2"
|
|
913
|
-
local exec_command="$3"
|
|
914
|
-
local log_file="$4"
|
|
915
|
-
local env_vars="$5"
|
|
916
|
-
local _cron_exec
|
|
917
|
-
local _cron_log
|
|
918
|
-
local _env_prefix
|
|
919
|
-
|
|
920
|
-
_env_prefix=$(_scheduler_cron_env_prefix "$env_vars")
|
|
921
|
-
_cron_exec=$(_cron_escape "$exec_command")
|
|
922
|
-
_cron_log=$(_cron_escape "$log_file")
|
|
923
|
-
|
|
924
|
-
(
|
|
925
|
-
crontab -l 2>/dev/null | grep -vF "${cron_tag}" || true
|
|
926
|
-
echo "${cron_schedule} ${_env_prefix}/bin/bash -lc ${_cron_exec} >> ${_cron_log} 2>&1 # ${cron_tag}"
|
|
927
|
-
) | crontab - 2>/dev/null || true
|
|
928
|
-
return 0
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
# Dispatcher: install a scheduler on Linux, preferring systemd over cron.
|
|
932
|
-
# Args:
|
|
933
|
-
# $1 = service_name (systemd service name, e.g. "aidevops-stats-wrapper")
|
|
934
|
-
# $2 = cron_tag (comment tag for cron line, e.g. "aidevops: stats-wrapper")
|
|
935
|
-
# $3 = cron_schedule (cron schedule expression, e.g. "*/15 * * * *")
|
|
936
|
-
# $4 = exec_command (shell command run via /bin/bash -lc)
|
|
937
|
-
# $5 = interval_sec (systemd OnUnitActiveSec in seconds; may be empty for calendar-only)
|
|
938
|
-
# $6 = log_file (absolute path to log file)
|
|
939
|
-
# $7 = env_vars (newline-separated KEY=VALUE pairs for systemd/cron, may be empty)
|
|
940
|
-
# $8 = success_msg (message to print on success)
|
|
941
|
-
# $9 = fail_msg (message to print on failure)
|
|
942
|
-
# $10 = run_at_load ("true" or "false")
|
|
943
|
-
# $11 = low_priority ("true" or "false")
|
|
944
|
-
# $12 = on_calendar (optional systemd OnCalendar spec)
|
|
945
|
-
# $13 = timeout_sec (optional TimeoutStartSec)
|
|
946
|
-
# Returns 0 always (failures are warnings, not fatal).
|
|
947
|
-
_install_scheduler_linux() {
|
|
948
|
-
local service_name="$1"
|
|
949
|
-
local cron_tag="$2"
|
|
950
|
-
local cron_schedule="$3"
|
|
951
|
-
local exec_command="$4"
|
|
952
|
-
local interval_sec="$5"
|
|
953
|
-
local log_file="$6"
|
|
954
|
-
local env_vars="$7"
|
|
955
|
-
local success_msg="$8"
|
|
956
|
-
local fail_msg="$9"
|
|
957
|
-
local run_at_load="${10}"
|
|
958
|
-
local low_priority="${11}"
|
|
959
|
-
local on_calendar="${12:-}"
|
|
960
|
-
local timeout_sec="${13:-}"
|
|
961
|
-
|
|
962
|
-
if _systemd_user_available; then
|
|
963
|
-
if _install_scheduler_systemd \
|
|
964
|
-
"$service_name" \
|
|
965
|
-
"$exec_command" \
|
|
966
|
-
"$interval_sec" \
|
|
967
|
-
"$log_file" \
|
|
968
|
-
"$env_vars" \
|
|
969
|
-
"$run_at_load" \
|
|
970
|
-
"$low_priority" \
|
|
971
|
-
"$on_calendar" \
|
|
972
|
-
"$timeout_sec"; then
|
|
973
|
-
print_info "${success_msg} (systemd user timer)"
|
|
974
|
-
# After systemd install succeeds, remove any pre-existing cron entry
|
|
975
|
-
# to prevent dual-execution (GH#17695 Finding A)
|
|
976
|
-
if command -v crontab >/dev/null 2>&1; then
|
|
977
|
-
local current_cron
|
|
978
|
-
current_cron=$(crontab -l 2>/dev/null) || current_cron=""
|
|
979
|
-
if [[ -n "$current_cron" ]] && echo "$current_cron" | grep -qF "$cron_tag"; then
|
|
980
|
-
echo "$current_cron" | grep -vF "$cron_tag" | crontab -
|
|
981
|
-
echo "[schedulers] Removed pre-existing cron entry for $cron_tag (migrated to systemd)"
|
|
982
|
-
fi
|
|
983
|
-
fi
|
|
984
|
-
else
|
|
985
|
-
print_warning "systemd enable failed for ${service_name} — falling back to cron"
|
|
986
|
-
_install_scheduler_cron "$cron_tag" "$cron_schedule" "$exec_command" "$log_file" "$env_vars"
|
|
987
|
-
if crontab -l 2>/dev/null | grep -qF "${cron_tag}" 2>/dev/null; then
|
|
988
|
-
print_info "${success_msg} (cron fallback)"
|
|
989
|
-
else
|
|
990
|
-
print_warning "${fail_msg}"
|
|
991
|
-
fi
|
|
992
|
-
fi
|
|
993
|
-
else
|
|
994
|
-
_install_scheduler_cron "$cron_tag" "$cron_schedule" "$exec_command" "$log_file" "$env_vars"
|
|
995
|
-
if crontab -l 2>/dev/null | grep -qF "${cron_tag}" 2>/dev/null; then
|
|
996
|
-
print_info "${success_msg} (cron)"
|
|
997
|
-
else
|
|
998
|
-
print_warning "${fail_msg}"
|
|
999
|
-
fi
|
|
1000
|
-
fi
|
|
1001
|
-
return 0
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
# Uninstall a scheduler across all backends (launchd/systemd/cron).
|
|
1005
|
-
# Args:
|
|
1006
|
-
# $1 = os (output of uname -s)
|
|
1007
|
-
# $2 = launchd_label (e.g. "sh.aidevops.stats-wrapper")
|
|
1008
|
-
# $3 = systemd_name (e.g. "aidevops-stats-wrapper")
|
|
1009
|
-
# $4 = cron_tag (grep pattern for cron line, e.g. "aidevops: stats-wrapper")
|
|
1010
|
-
# $5 = success_msg (message to print on removal)
|
|
1011
|
-
# Returns 0 always.
|
|
1012
|
-
_uninstall_scheduler() {
|
|
1013
|
-
local _os="$1"
|
|
1014
|
-
local launchd_label="$2"
|
|
1015
|
-
local systemd_name="$3"
|
|
1016
|
-
local cron_tag="$4"
|
|
1017
|
-
local success_msg="$5"
|
|
1018
|
-
|
|
1019
|
-
if [[ "$_os" == "Darwin" ]]; then
|
|
1020
|
-
local _plist="$HOME/Library/LaunchAgents/${launchd_label}.plist"
|
|
1021
|
-
if _launchd_has_agent "$launchd_label"; then
|
|
1022
|
-
launchctl unload "$_plist" 2>/dev/null || true
|
|
1023
|
-
rm -f "$_plist"
|
|
1024
|
-
print_info "${success_msg} (launchd agent removed)"
|
|
1025
|
-
fi
|
|
1026
|
-
else
|
|
1027
|
-
# Check and remove from ALL backends sequentially, not just the first
|
|
1028
|
-
# match. Prevents orphan entries when migrating between systemd and cron
|
|
1029
|
-
# (GH#17695 Finding A).
|
|
1030
|
-
if _systemd_user_available && systemctl --user is-enabled "${systemd_name}.timer" >/dev/null 2>&1; then
|
|
1031
|
-
systemctl --user disable --now "${systemd_name}.timer" 2>/dev/null || true
|
|
1032
|
-
rm -f "$HOME/.config/systemd/user/${systemd_name}.service"
|
|
1033
|
-
rm -f "$HOME/.config/systemd/user/${systemd_name}.timer"
|
|
1034
|
-
systemctl --user daemon-reload 2>/dev/null || true
|
|
1035
|
-
print_info "${success_msg} (systemd timer removed)"
|
|
1036
|
-
fi
|
|
1037
|
-
if command -v crontab >/dev/null 2>&1; then
|
|
1038
|
-
local current_cron
|
|
1039
|
-
current_cron=$(crontab -l 2>/dev/null) || current_cron=""
|
|
1040
|
-
if [[ -n "$current_cron" ]] && echo "$current_cron" | grep -qF "${cron_tag}"; then
|
|
1041
|
-
echo "$current_cron" | grep -vF "${cron_tag}" | crontab - 2>/dev/null || true
|
|
1042
|
-
print_info "${success_msg} (cron entry removed)"
|
|
1043
|
-
fi
|
|
1044
|
-
fi
|
|
1045
|
-
fi
|
|
1046
|
-
return 0
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
# Uninstall supervisor pulse (user explicitly disabled)
|
|
1050
|
-
_uninstall_pulse() {
|
|
1051
|
-
local _os="$1"
|
|
1052
|
-
local pulse_label="$2"
|
|
1053
|
-
if [[ "$_os" == "Darwin" ]]; then
|
|
1054
|
-
local pulse_plist="$HOME/Library/LaunchAgents/${pulse_label}.plist"
|
|
1055
|
-
if _launchd_has_agent "$pulse_label"; then
|
|
1056
|
-
launchctl unload "$pulse_plist" || true
|
|
1057
|
-
rm -f "$pulse_plist"
|
|
1058
|
-
pkill -f 'Supervisor Pulse' 2>/dev/null || true
|
|
1059
|
-
print_info "Supervisor pulse disabled (launchd agent removed per config)"
|
|
1060
|
-
fi
|
|
1061
|
-
elif _systemd_user_available; then
|
|
1062
|
-
local service_name="aidevops-supervisor-pulse"
|
|
1063
|
-
if systemctl --user is-enabled "${service_name}.timer" >/dev/null 2>&1; then
|
|
1064
|
-
systemctl --user disable --now "${service_name}.timer" 2>/dev/null || true
|
|
1065
|
-
rm -f "$HOME/.config/systemd/user/${service_name}.service"
|
|
1066
|
-
rm -f "$HOME/.config/systemd/user/${service_name}.timer"
|
|
1067
|
-
systemctl --user daemon-reload 2>/dev/null || true
|
|
1068
|
-
print_info "Supervisor pulse disabled (systemd timer removed per config)"
|
|
1069
|
-
fi
|
|
1070
|
-
else
|
|
1071
|
-
if crontab -l 2>/dev/null | grep -qF "pulse-wrapper"; then
|
|
1072
|
-
crontab -l 2>/dev/null | grep -v 'aidevops: supervisor-pulse' | crontab - || true
|
|
1073
|
-
print_info "Supervisor pulse disabled (cron entry removed per config)"
|
|
1074
|
-
fi
|
|
1075
|
-
fi
|
|
1076
|
-
return 0
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
# Setup stats-wrapper scheduler — runs quality sweep and health issue updates
|
|
1080
|
-
# separately from the pulse (t1429). Only installed when the supervisor
|
|
1081
|
-
# pulse is enabled (stats are useless without it).
|
|
1082
|
-
# macOS: launchd plist (hourly) | Linux: systemd timer or cron (hourly)
|
|
1083
|
-
# t2744: interval raised from 15 min → hourly. Stats UI is not realtime,
|
|
1084
|
-
# the four-times-an-hour cadence drove ~200-400 GraphQL points/hr of pure
|
|
1085
|
-
# overhead on multi-repo setups.
|
|
1086
|
-
setup_stats_wrapper() {
|
|
1087
|
-
local _pulse_lower="$1"
|
|
1088
|
-
# Use effective pulse state (PULSE_ENABLED) if available; fall back to consent string.
|
|
1089
|
-
# PULSE_ENABLED reflects the actual install decision (e.g., false when wrapper is missing).
|
|
1090
|
-
local _pulse_effective="${PULSE_ENABLED:-$_pulse_lower}"
|
|
1091
|
-
local stats_script="$HOME/.aidevops/agents/scripts/stats-wrapper.sh"
|
|
1092
|
-
local stats_label="com.aidevops.aidevops-stats-wrapper"
|
|
1093
|
-
local stats_systemd="aidevops-stats-wrapper"
|
|
1094
|
-
local stats_log="$HOME/.aidevops/logs/stats.log"
|
|
1095
|
-
if [[ -x "$stats_script" ]] && [[ "$_pulse_effective" == "true" ]]; then
|
|
1096
|
-
# Always regenerate to pick up config/format changes (matches pulse behavior)
|
|
1097
|
-
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
1098
|
-
local stats_plist="$HOME/Library/LaunchAgents/${stats_label}.plist"
|
|
1099
|
-
|
|
1100
|
-
local _xml_stats_script _xml_stats_home _xml_stats_path
|
|
1101
|
-
_xml_stats_script=$(_xml_escape "$stats_script")
|
|
1102
|
-
_xml_stats_home=$(_xml_escape "$HOME")
|
|
1103
|
-
_xml_stats_path=$(_xml_escape "$PATH")
|
|
1104
|
-
local stats_plist_content
|
|
1105
|
-
stats_plist_content=$(
|
|
1106
|
-
cat <<PLIST
|
|
1107
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
1108
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1109
|
-
<plist version="1.0">
|
|
1110
|
-
<dict>
|
|
1111
|
-
<key>Label</key>
|
|
1112
|
-
<string>${stats_label}</string>
|
|
1113
|
-
<key>ProgramArguments</key>
|
|
1114
|
-
<array>
|
|
1115
|
-
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
1116
|
-
<string>${_xml_stats_script}</string>
|
|
1117
|
-
</array>
|
|
1118
|
-
<key>StartInterval</key>
|
|
1119
|
-
<integer>3600</integer>
|
|
1120
|
-
<key>StandardOutPath</key>
|
|
1121
|
-
<string>${_xml_stats_home}/.aidevops/logs/stats.log</string>
|
|
1122
|
-
<key>StandardErrorPath</key>
|
|
1123
|
-
<string>${_xml_stats_home}/.aidevops/logs/stats.log</string>
|
|
1124
|
-
<key>EnvironmentVariables</key>
|
|
1125
|
-
<dict>
|
|
1126
|
-
<key>PATH</key>
|
|
1127
|
-
<string>${_xml_stats_path}</string>
|
|
1128
|
-
<key>HOME</key>
|
|
1129
|
-
<string>${_xml_stats_home}</string>
|
|
1130
|
-
</dict>
|
|
1131
|
-
<key>RunAtLoad</key>
|
|
1132
|
-
<true/>
|
|
1133
|
-
<key>KeepAlive</key>
|
|
1134
|
-
<false/>
|
|
1135
|
-
</dict>
|
|
1136
|
-
</plist>
|
|
1137
|
-
PLIST
|
|
1138
|
-
)
|
|
1139
|
-
if _launchd_install_if_changed "$stats_label" "$stats_plist" "$stats_plist_content"; then
|
|
1140
|
-
print_info "Stats wrapper enabled (launchd, every hour)"
|
|
1141
|
-
else
|
|
1142
|
-
print_warning "Failed to load stats wrapper LaunchAgent"
|
|
1143
|
-
fi
|
|
1144
|
-
else
|
|
1145
|
-
_install_scheduler_linux \
|
|
1146
|
-
"$stats_systemd" \
|
|
1147
|
-
"aidevops: stats-wrapper" \
|
|
1148
|
-
"$CRON_HOURLY" \
|
|
1149
|
-
"\"${stats_script}\"" \
|
|
1150
|
-
"3600" \
|
|
1151
|
-
"$stats_log" \
|
|
1152
|
-
"" \
|
|
1153
|
-
"Stats wrapper enabled (every hour)" \
|
|
1154
|
-
"Failed to install stats wrapper scheduler" \
|
|
1155
|
-
"true" \
|
|
1156
|
-
"false"
|
|
1157
|
-
fi
|
|
1158
|
-
elif [[ "$_pulse_effective" == "false" ]]; then
|
|
1159
|
-
# Remove stats scheduler if pulse is disabled
|
|
1160
|
-
_uninstall_scheduler \
|
|
1161
|
-
"$(uname -s)" \
|
|
1162
|
-
"$stats_label" \
|
|
1163
|
-
"$stats_systemd" \
|
|
1164
|
-
"aidevops: stats-wrapper" \
|
|
1165
|
-
"Stats wrapper disabled (pulse is off)"
|
|
1166
|
-
fi
|
|
1167
|
-
return 0
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
# Setup failure miner — mines GitHub CI failure notifications for systemic patterns
|
|
1171
|
-
# and auto-files root-cause issues. Runs as a pure bash script (no LLM needed).
|
|
1172
|
-
# Installed when pulse is enabled and the helper script exists.
|
|
1173
|
-
# macOS: launchd plist (hourly at :15) | Linux: systemd timer or cron (hourly at :15)
|
|
1174
|
-
setup_failure_miner() {
|
|
1175
|
-
local _pulse_lower="$1"
|
|
1176
|
-
local _pulse_effective="${PULSE_ENABLED:-$_pulse_lower}"
|
|
1177
|
-
local miner_script="$HOME/.aidevops/agents/scripts/gh-failure-miner-helper.sh"
|
|
1178
|
-
local miner_label="sh.aidevops.routine-gh-failure-miner"
|
|
1179
|
-
local miner_systemd="aidevops-gh-failure-miner"
|
|
1180
|
-
local miner_log="$HOME/.aidevops/logs/routine-gh-failure-miner.log"
|
|
1181
|
-
if [[ ! -x "$miner_script" ]] || [[ "$_pulse_effective" != "true" ]]; then
|
|
1182
|
-
# Remove scheduler if pulse is disabled or script missing
|
|
1183
|
-
_uninstall_scheduler \
|
|
1184
|
-
"$(uname -s)" \
|
|
1185
|
-
"$miner_label" \
|
|
1186
|
-
"$miner_systemd" \
|
|
1187
|
-
"aidevops: gh-failure-miner" \
|
|
1188
|
-
"Failure miner disabled (pulse is off or script missing)"
|
|
1189
|
-
return 0
|
|
1190
|
-
fi
|
|
1191
|
-
|
|
1192
|
-
mkdir -p "$HOME/.aidevops/logs"
|
|
1193
|
-
|
|
1194
|
-
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
1195
|
-
local miner_plist="$HOME/Library/LaunchAgents/${miner_label}.plist"
|
|
1196
|
-
|
|
1197
|
-
local _xml_miner_script _xml_miner_home _xml_miner_path _xml_miner_log
|
|
1198
|
-
_xml_miner_script=$(_xml_escape "$miner_script")
|
|
1199
|
-
_xml_miner_home=$(_xml_escape "$HOME")
|
|
1200
|
-
_xml_miner_path=$(_xml_escape "/bin:/usr/bin:/usr/local/bin:/opt/homebrew/bin:${PATH}")
|
|
1201
|
-
_xml_miner_log=$(_xml_escape "$miner_log")
|
|
1202
|
-
|
|
1203
|
-
local miner_plist_content
|
|
1204
|
-
miner_plist_content=$(
|
|
1205
|
-
cat <<MINER_PLIST
|
|
1206
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
1207
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1208
|
-
<plist version="1.0">
|
|
1209
|
-
<dict>
|
|
1210
|
-
<key>Label</key>
|
|
1211
|
-
<string>${miner_label}</string>
|
|
1212
|
-
<key>ProgramArguments</key>
|
|
1213
|
-
<array>
|
|
1214
|
-
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
1215
|
-
<string>${_xml_miner_script}</string>
|
|
1216
|
-
<string>create-issues</string>
|
|
1217
|
-
<string>--since-hours</string>
|
|
1218
|
-
<string>24</string>
|
|
1219
|
-
<string>--pulse-repos</string>
|
|
1220
|
-
<string>--systemic-threshold</string>
|
|
1221
|
-
<string>2</string>
|
|
1222
|
-
<string>--max-issues</string>
|
|
1223
|
-
<string>3</string>
|
|
1224
|
-
<string>--label</string>
|
|
1225
|
-
<string>auto-dispatch</string>
|
|
1226
|
-
</array>
|
|
1227
|
-
<key>EnvironmentVariables</key>
|
|
1228
|
-
<dict>
|
|
1229
|
-
<key>HOME</key>
|
|
1230
|
-
<string>${_xml_miner_home}</string>
|
|
1231
|
-
<key>PATH</key>
|
|
1232
|
-
<string>${_xml_miner_path}</string>
|
|
1233
|
-
</dict>
|
|
1234
|
-
<key>StartCalendarInterval</key>
|
|
1235
|
-
<array>
|
|
1236
|
-
<dict>
|
|
1237
|
-
<key>Minute</key>
|
|
1238
|
-
<integer>15</integer>
|
|
1239
|
-
</dict>
|
|
1240
|
-
</array>
|
|
1241
|
-
<key>StandardOutPath</key>
|
|
1242
|
-
<string>${_xml_miner_log}</string>
|
|
1243
|
-
<key>StandardErrorPath</key>
|
|
1244
|
-
<string>${_xml_miner_log}</string>
|
|
1245
|
-
<key>RunAtLoad</key>
|
|
1246
|
-
<false/>
|
|
1247
|
-
</dict>
|
|
1248
|
-
</plist>
|
|
1249
|
-
MINER_PLIST
|
|
1250
|
-
)
|
|
1251
|
-
|
|
1252
|
-
if _launchd_install_if_changed "$miner_label" "$miner_plist" "$miner_plist_content"; then
|
|
1253
|
-
print_info "Failure miner enabled (launchd, hourly at :15)"
|
|
1254
|
-
else
|
|
1255
|
-
print_warning "Failed to load failure miner LaunchAgent"
|
|
1256
|
-
fi
|
|
1257
|
-
else
|
|
1258
|
-
_install_scheduler_linux \
|
|
1259
|
-
"$miner_systemd" \
|
|
1260
|
-
"aidevops: gh-failure-miner" \
|
|
1261
|
-
"15 * * * *" \
|
|
1262
|
-
"\"${miner_script}\" create-issues --since-hours 24 --pulse-repos --systemic-threshold 2 --max-issues 3 --label auto-dispatch" \
|
|
1263
|
-
"3600" \
|
|
1264
|
-
"$miner_log" \
|
|
1265
|
-
"" \
|
|
1266
|
-
"Failure miner enabled (hourly at :15)" \
|
|
1267
|
-
"Failed to install failure miner scheduler" \
|
|
1268
|
-
"false" \
|
|
1269
|
-
"false" \
|
|
1270
|
-
"*-*-* *:15:00"
|
|
1271
|
-
fi
|
|
1272
|
-
return 0
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
# Setup process guard — kills runaway AI processes (ShellCheck bloat, stuck workers)
|
|
1276
|
-
# before they exhaust memory and cause kernel panics. Always installed when the
|
|
1277
|
-
# script exists; no consent needed (safety net, not autonomous action).
|
|
1278
|
-
# macOS: launchd plist (30s interval, RunAtLoad=true) | Linux: systemd timer or cron (every minute)
|
|
1279
|
-
setup_process_guard() {
|
|
1280
|
-
local guard_script="$HOME/.aidevops/agents/scripts/process-guard-helper.sh"
|
|
1281
|
-
local guard_label="sh.aidevops.process-guard"
|
|
1282
|
-
local guard_systemd="aidevops-process-guard"
|
|
1283
|
-
local guard_log="$HOME/.aidevops/logs/process-guard.log"
|
|
1284
|
-
if [[ ! -x "$guard_script" ]]; then
|
|
1285
|
-
return 0
|
|
1286
|
-
fi
|
|
1287
|
-
|
|
1288
|
-
mkdir -p "$HOME/.aidevops/logs"
|
|
1289
|
-
|
|
1290
|
-
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
1291
|
-
local guard_plist="$HOME/Library/LaunchAgents/${guard_label}.plist"
|
|
1292
|
-
|
|
1293
|
-
# XML-escape paths for safe plist embedding (prevents injection
|
|
1294
|
-
# if $HOME or paths contain &, <, > characters)
|
|
1295
|
-
local _xml_guard_script _xml_guard_home _xml_guard_path
|
|
1296
|
-
_xml_guard_script=$(_xml_escape "$guard_script")
|
|
1297
|
-
_xml_guard_home=$(_xml_escape "$HOME")
|
|
1298
|
-
_xml_guard_path=$(_xml_escape "$PATH")
|
|
1299
|
-
|
|
1300
|
-
local guard_plist_content
|
|
1301
|
-
guard_plist_content=$(
|
|
1302
|
-
cat <<GUARD_PLIST
|
|
1303
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
1304
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1305
|
-
<plist version="1.0">
|
|
1306
|
-
<dict>
|
|
1307
|
-
<key>Label</key>
|
|
1308
|
-
<string>${guard_label}</string>
|
|
1309
|
-
<key>ProgramArguments</key>
|
|
1310
|
-
<array>
|
|
1311
|
-
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
1312
|
-
<string>${_xml_guard_script}</string>
|
|
1313
|
-
<string>kill-runaways</string>
|
|
1314
|
-
</array>
|
|
1315
|
-
<key>StartInterval</key>
|
|
1316
|
-
<integer>30</integer>
|
|
1317
|
-
<key>StandardOutPath</key>
|
|
1318
|
-
<string>${_xml_guard_home}/.aidevops/logs/process-guard.log</string>
|
|
1319
|
-
<key>StandardErrorPath</key>
|
|
1320
|
-
<string>${_xml_guard_home}/.aidevops/logs/process-guard.log</string>
|
|
1321
|
-
<key>EnvironmentVariables</key>
|
|
1322
|
-
<dict>
|
|
1323
|
-
<key>PATH</key>
|
|
1324
|
-
<string>${_xml_guard_path}</string>
|
|
1325
|
-
<key>HOME</key>
|
|
1326
|
-
<string>${_xml_guard_home}</string>
|
|
1327
|
-
<key>SHELLCHECK_RSS_LIMIT_KB</key>
|
|
1328
|
-
<string>524288</string>
|
|
1329
|
-
<key>SHELLCHECK_RUNTIME_LIMIT</key>
|
|
1330
|
-
<string>120</string>
|
|
1331
|
-
<key>CHILD_RSS_LIMIT_KB</key>
|
|
1332
|
-
<string>8388608</string>
|
|
1333
|
-
<key>CHILD_RUNTIME_LIMIT</key>
|
|
1334
|
-
<string>7200</string>
|
|
1335
|
-
</dict>
|
|
1336
|
-
<key>RunAtLoad</key>
|
|
1337
|
-
<true/>
|
|
1338
|
-
<key>KeepAlive</key>
|
|
1339
|
-
<false/>
|
|
1340
|
-
</dict>
|
|
1341
|
-
</plist>
|
|
1342
|
-
GUARD_PLIST
|
|
1343
|
-
)
|
|
1344
|
-
|
|
1345
|
-
if _launchd_install_if_changed "$guard_label" "$guard_plist" "$guard_plist_content"; then
|
|
1346
|
-
print_info "Process guard enabled (launchd, every 30s, survives reboot)"
|
|
1347
|
-
else
|
|
1348
|
-
print_warning "Failed to load process guard LaunchAgent"
|
|
1349
|
-
fi
|
|
1350
|
-
else
|
|
1351
|
-
# Linux: systemd timer (30s) or cron fallback (every minute — cron minimum granularity)
|
|
1352
|
-
_install_scheduler_linux \
|
|
1353
|
-
"$guard_systemd" \
|
|
1354
|
-
"aidevops: process-guard" \
|
|
1355
|
-
"* * * * *" \
|
|
1356
|
-
"\"${guard_script}\" kill-runaways" \
|
|
1357
|
-
"30" \
|
|
1358
|
-
"$guard_log" \
|
|
1359
|
-
"SHELLCHECK_RSS_LIMIT_KB=524288
|
|
1360
|
-
SHELLCHECK_RUNTIME_LIMIT=120
|
|
1361
|
-
CHILD_RSS_LIMIT_KB=8388608
|
|
1362
|
-
CHILD_RUNTIME_LIMIT=7200" \
|
|
1363
|
-
"Process guard enabled (every 30s)" \
|
|
1364
|
-
"Failed to install process guard scheduler" \
|
|
1365
|
-
"true" \
|
|
1366
|
-
"false"
|
|
1367
|
-
fi
|
|
1368
|
-
return 0
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
# Setup memory pressure monitor — process-focused memory watchdog (t1398.5, GH#2915).
|
|
1372
|
-
# Monitors individual process RSS, runtime, session count, and aggregate memory.
|
|
1373
|
-
# Auto-kills runaway ShellCheck (language server respawns them). Always installed
|
|
1374
|
-
# when the script exists; no consent needed (safety net, not autonomous action).
|
|
1375
|
-
# macOS: launchd plist (60s interval, RunAtLoad=true) | Linux: systemd timer or cron (every minute)
|
|
1376
|
-
setup_memory_pressure_monitor() {
|
|
1377
|
-
local monitor_script="$HOME/.aidevops/agents/scripts/memory-pressure-monitor.sh"
|
|
1378
|
-
local monitor_label="sh.aidevops.memory-pressure-monitor"
|
|
1379
|
-
local monitor_systemd="aidevops-memory-pressure-monitor"
|
|
1380
|
-
local monitor_log="$HOME/.aidevops/logs/memory-pressure-launchd.log"
|
|
1381
|
-
if [[ ! -x "$monitor_script" ]]; then
|
|
1382
|
-
return 0
|
|
1383
|
-
fi
|
|
1384
|
-
|
|
1385
|
-
mkdir -p "$HOME/.aidevops/logs"
|
|
1386
|
-
|
|
1387
|
-
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
1388
|
-
local monitor_plist="$HOME/Library/LaunchAgents/${monitor_label}.plist"
|
|
1389
|
-
|
|
1390
|
-
# XML-escape paths for safe plist embedding
|
|
1391
|
-
local _xml_monitor_script _xml_monitor_home
|
|
1392
|
-
_xml_monitor_script=$(_xml_escape "$monitor_script")
|
|
1393
|
-
_xml_monitor_home=$(_xml_escape "$HOME")
|
|
1394
|
-
|
|
1395
|
-
local monitor_plist_content
|
|
1396
|
-
monitor_plist_content=$(
|
|
1397
|
-
cat <<MONITOR_PLIST
|
|
1398
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
1399
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1400
|
-
<plist version="1.0">
|
|
1401
|
-
<dict>
|
|
1402
|
-
<key>Label</key>
|
|
1403
|
-
<string>${monitor_label}</string>
|
|
1404
|
-
<key>ProgramArguments</key>
|
|
1405
|
-
<array>
|
|
1406
|
-
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
1407
|
-
<string>${_xml_monitor_script}</string>
|
|
1408
|
-
</array>
|
|
1409
|
-
<key>StartInterval</key>
|
|
1410
|
-
<integer>60</integer>
|
|
1411
|
-
<key>StandardOutPath</key>
|
|
1412
|
-
<string>${_xml_monitor_home}/.aidevops/logs/memory-pressure-launchd.log</string>
|
|
1413
|
-
<key>StandardErrorPath</key>
|
|
1414
|
-
<string>${_xml_monitor_home}/.aidevops/logs/memory-pressure-launchd.log</string>
|
|
1415
|
-
<key>EnvironmentVariables</key>
|
|
1416
|
-
<dict>
|
|
1417
|
-
<key>PATH</key>
|
|
1418
|
-
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
1419
|
-
<key>HOME</key>
|
|
1420
|
-
<string>${_xml_monitor_home}</string>
|
|
1421
|
-
</dict>
|
|
1422
|
-
<key>RunAtLoad</key>
|
|
1423
|
-
<true/>
|
|
1424
|
-
<key>KeepAlive</key>
|
|
1425
|
-
<false/>
|
|
1426
|
-
<key>ProcessType</key>
|
|
1427
|
-
<string>Background</string>
|
|
1428
|
-
<key>LowPriorityBackgroundIO</key>
|
|
1429
|
-
<true/>
|
|
1430
|
-
<key>Nice</key>
|
|
1431
|
-
<integer>10</integer>
|
|
1432
|
-
</dict>
|
|
1433
|
-
</plist>
|
|
1434
|
-
MONITOR_PLIST
|
|
1435
|
-
)
|
|
1436
|
-
|
|
1437
|
-
if _launchd_install_if_changed "$monitor_label" "$monitor_plist" "$monitor_plist_content"; then
|
|
1438
|
-
print_info "Memory pressure monitor enabled (launchd, every 60s, survives reboot)"
|
|
1439
|
-
else
|
|
1440
|
-
print_warning "Failed to load memory pressure monitor LaunchAgent"
|
|
1441
|
-
fi
|
|
1442
|
-
else
|
|
1443
|
-
# Linux: systemd timer (60s) or cron fallback (every minute — cron minimum granularity)
|
|
1444
|
-
_install_scheduler_linux \
|
|
1445
|
-
"$monitor_systemd" \
|
|
1446
|
-
"aidevops: memory-pressure-monitor" \
|
|
1447
|
-
"* * * * *" \
|
|
1448
|
-
"\"${monitor_script}\"" \
|
|
1449
|
-
"60" \
|
|
1450
|
-
"$monitor_log" \
|
|
1451
|
-
"" \
|
|
1452
|
-
"Memory pressure monitor enabled (every 60s)" \
|
|
1453
|
-
"Failed to install memory pressure monitor scheduler" \
|
|
1454
|
-
"true" \
|
|
1455
|
-
"true"
|
|
1456
|
-
fi
|
|
1457
|
-
return 0
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
# Setup screen time snapshot — captures daily screen time for contributor stats.
|
|
1461
|
-
# Accumulates data in screen-time.jsonl (macOS Knowledge DB retains only ~28 days).
|
|
1462
|
-
# Always installed when the script exists; no consent needed (data collection only).
|
|
1463
|
-
# macOS: launchd plist (every 6h, RunAtLoad=true) | Linux: systemd timer or cron (every 6h)
|
|
1464
|
-
setup_screen_time_snapshot() {
|
|
1465
|
-
local st_script="$HOME/.aidevops/agents/scripts/screen-time-helper.sh"
|
|
1466
|
-
local st_label="sh.aidevops.screen-time-snapshot"
|
|
1467
|
-
local st_systemd="aidevops-screen-time-snapshot"
|
|
1468
|
-
local st_log="$HOME/.aidevops/.agent-workspace/logs/screen-time-snapshot.log"
|
|
1469
|
-
if [[ ! -x "$st_script" ]]; then
|
|
1470
|
-
return 0
|
|
1471
|
-
fi
|
|
1472
|
-
|
|
1473
|
-
mkdir -p "$HOME/.aidevops/.agent-workspace/logs"
|
|
1474
|
-
|
|
1475
|
-
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
1476
|
-
local st_plist="$HOME/Library/LaunchAgents/${st_label}.plist"
|
|
1477
|
-
|
|
1478
|
-
# XML-escape paths for safe plist embedding
|
|
1479
|
-
local _xml_st_script _xml_st_home
|
|
1480
|
-
_xml_st_script=$(_xml_escape "$st_script")
|
|
1481
|
-
_xml_st_home=$(_xml_escape "$HOME")
|
|
1482
|
-
|
|
1483
|
-
local st_plist_content
|
|
1484
|
-
st_plist_content=$(
|
|
1485
|
-
cat <<ST_PLIST
|
|
1486
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
1487
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1488
|
-
<plist version="1.0">
|
|
1489
|
-
<dict>
|
|
1490
|
-
<key>Label</key>
|
|
1491
|
-
<string>${st_label}</string>
|
|
1492
|
-
<key>ProgramArguments</key>
|
|
1493
|
-
<array>
|
|
1494
|
-
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
1495
|
-
<string>${_xml_st_script}</string>
|
|
1496
|
-
<string>snapshot</string>
|
|
1497
|
-
</array>
|
|
1498
|
-
<key>StartInterval</key>
|
|
1499
|
-
<integer>21600</integer>
|
|
1500
|
-
<key>StandardOutPath</key>
|
|
1501
|
-
<string>${_xml_st_home}/.aidevops/.agent-workspace/logs/screen-time-snapshot.log</string>
|
|
1502
|
-
<key>StandardErrorPath</key>
|
|
1503
|
-
<string>${_xml_st_home}/.aidevops/.agent-workspace/logs/screen-time-snapshot.log</string>
|
|
1504
|
-
<key>EnvironmentVariables</key>
|
|
1505
|
-
<dict>
|
|
1506
|
-
<key>PATH</key>
|
|
1507
|
-
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
1508
|
-
<key>HOME</key>
|
|
1509
|
-
<string>${_xml_st_home}</string>
|
|
1510
|
-
</dict>
|
|
1511
|
-
<key>RunAtLoad</key>
|
|
1512
|
-
<true/>
|
|
1513
|
-
<key>KeepAlive</key>
|
|
1514
|
-
<false/>
|
|
1515
|
-
<key>ProcessType</key>
|
|
1516
|
-
<string>Background</string>
|
|
1517
|
-
<key>LowPriorityBackgroundIO</key>
|
|
1518
|
-
<true/>
|
|
1519
|
-
<key>Nice</key>
|
|
1520
|
-
<integer>10</integer>
|
|
1521
|
-
</dict>
|
|
1522
|
-
</plist>
|
|
1523
|
-
ST_PLIST
|
|
1524
|
-
)
|
|
1525
|
-
|
|
1526
|
-
if _launchd_install_if_changed "$st_label" "$st_plist" "$st_plist_content"; then
|
|
1527
|
-
print_info "Screen time snapshot enabled (launchd, every 6h, survives reboot)"
|
|
1528
|
-
else
|
|
1529
|
-
print_warning "Failed to load screen time snapshot LaunchAgent"
|
|
1530
|
-
fi
|
|
1531
|
-
else
|
|
1532
|
-
# Linux: systemd timer (every 6h) or cron fallback
|
|
1533
|
-
_install_scheduler_linux \
|
|
1534
|
-
"$st_systemd" \
|
|
1535
|
-
"aidevops: screen-time-snapshot" \
|
|
1536
|
-
"0 */6 * * *" \
|
|
1537
|
-
"\"${st_script}\" snapshot" \
|
|
1538
|
-
"21600" \
|
|
1539
|
-
"$st_log" \
|
|
1540
|
-
"" \
|
|
1541
|
-
"Screen time snapshot enabled (every 6h)" \
|
|
1542
|
-
"Failed to install screen time snapshot scheduler" \
|
|
1543
|
-
"true" \
|
|
1544
|
-
"true"
|
|
1545
|
-
fi
|
|
1546
|
-
return 0
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
# Resolve and validate the log directory from config for contribution watch.
|
|
1550
|
-
# Reads paths.log_dir from jsonc config, validates characters, expands tilde.
|
|
1551
|
-
# Prints the resolved absolute path. Returns 1 on invalid characters.
|
|
1552
|
-
_resolve_cw_log_dir() {
|
|
1553
|
-
local _cw_log_dir
|
|
1554
|
-
# shellcheck disable=SC2088 # Tilde is intentionally literal here; expanded below via ${/#\~/$HOME}
|
|
1555
|
-
if type _jsonc_get &>/dev/null; then
|
|
1556
|
-
_cw_log_dir=$(_jsonc_get "paths.log_dir" "~/.aidevops/logs")
|
|
1557
|
-
else
|
|
1558
|
-
_cw_log_dir="~/.aidevops/logs"
|
|
1559
|
-
fi
|
|
1560
|
-
# Whitelist: only allow characters safe in shell paths and cron lines.
|
|
1561
|
-
# Reject anything outside [A-Za-z0-9_./ ~-] (tilde allowed before expansion).
|
|
1562
|
-
# Store regex in variable — bash [[ =~ ]] requires unquoted RHS for regex,
|
|
1563
|
-
# and a variable avoids quoting issues with special chars in the pattern.
|
|
1564
|
-
local _cw_log_dir_re='^[A-Za-z0-9_./ ~-]+$'
|
|
1565
|
-
if ! [[ "$_cw_log_dir" =~ $_cw_log_dir_re ]]; then
|
|
1566
|
-
# Redirect to stderr so $() captures only the path result
|
|
1567
|
-
print_error "Invalid characters in paths.log_dir (only [A-Za-z0-9_./ ~-] allowed): $_cw_log_dir" >&2
|
|
1568
|
-
return 1
|
|
1569
|
-
fi
|
|
1570
|
-
_cw_log_dir="${_cw_log_dir/#\~/$HOME}"
|
|
1571
|
-
printf '%s' "$_cw_log_dir"
|
|
1572
|
-
return 0
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
# Install contribution watch via launchd (macOS).
|
|
1576
|
-
# Args: $1=label, $2=script path, $3=log dir
|
|
1577
|
-
_install_cw_launchd() {
|
|
1578
|
-
local cw_label="$1"
|
|
1579
|
-
local cw_script="$2"
|
|
1580
|
-
local _cw_log_dir="$3"
|
|
1581
|
-
local cw_plist="$HOME/Library/LaunchAgents/${cw_label}.plist"
|
|
1582
|
-
|
|
1583
|
-
local _xml_cw_script _xml_cw_home _xml_cw_log_dir
|
|
1584
|
-
_xml_cw_script=$(_xml_escape "$cw_script")
|
|
1585
|
-
_xml_cw_home=$(_xml_escape "$HOME")
|
|
1586
|
-
_xml_cw_log_dir=$(_xml_escape "$_cw_log_dir")
|
|
21
|
+
# Keep pulse workers alive long enough for opus-tier dispatches.
|
|
22
|
+
PULSE_STALE_THRESHOLD_SECONDS=1800
|
|
1587
23
|
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1593
|
-
<plist version="1.0">
|
|
1594
|
-
<dict>
|
|
1595
|
-
<key>Label</key>
|
|
1596
|
-
<string>${cw_label}</string>
|
|
1597
|
-
<key>ProgramArguments</key>
|
|
1598
|
-
<array>
|
|
1599
|
-
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
1600
|
-
<string>${_xml_cw_script}</string>
|
|
1601
|
-
<string>scan</string>
|
|
1602
|
-
</array>
|
|
1603
|
-
<key>StartInterval</key>
|
|
1604
|
-
<integer>3600</integer>
|
|
1605
|
-
<key>StandardOutPath</key>
|
|
1606
|
-
<string>${_xml_cw_log_dir}/contribution-watch.log</string>
|
|
1607
|
-
<key>StandardErrorPath</key>
|
|
1608
|
-
<string>${_xml_cw_log_dir}/contribution-watch.log</string>
|
|
1609
|
-
<key>EnvironmentVariables</key>
|
|
1610
|
-
<dict>
|
|
1611
|
-
<key>PATH</key>
|
|
1612
|
-
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
1613
|
-
<key>HOME</key>
|
|
1614
|
-
<string>${_xml_cw_home}</string>
|
|
1615
|
-
</dict>
|
|
1616
|
-
<key>RunAtLoad</key>
|
|
1617
|
-
<false/>
|
|
1618
|
-
<key>KeepAlive</key>
|
|
1619
|
-
<false/>
|
|
1620
|
-
<key>ProcessType</key>
|
|
1621
|
-
<string>Background</string>
|
|
1622
|
-
<key>LowPriorityBackgroundIO</key>
|
|
1623
|
-
<true/>
|
|
1624
|
-
<key>Nice</key>
|
|
1625
|
-
<integer>10</integer>
|
|
1626
|
-
</dict>
|
|
1627
|
-
</plist>
|
|
1628
|
-
CW_PLIST
|
|
1629
|
-
)
|
|
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 * * * *"
|
|
1630
28
|
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
fi
|
|
1636
|
-
return 0
|
|
1637
|
-
}
|
|
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="* * * * *"
|
|
1638
33
|
|
|
1639
|
-
#
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
_install_scheduler_linux \
|
|
1646
|
-
"$cw_systemd" \
|
|
1647
|
-
"aidevops: contribution-watch" \
|
|
1648
|
-
"$CRON_HOURLY" \
|
|
1649
|
-
"\"${cw_script}\" scan" \
|
|
1650
|
-
"3600" \
|
|
1651
|
-
"${_cw_log_dir}/contribution-watch.log" \
|
|
1652
|
-
"" \
|
|
1653
|
-
"Contribution watch enabled (hourly scan)" \
|
|
1654
|
-
"Failed to install contribution watch scheduler" \
|
|
1655
|
-
"false" \
|
|
1656
|
-
"true"
|
|
1657
|
-
return 0
|
|
1658
|
-
}
|
|
34
|
+
# Shell safety baseline
|
|
35
|
+
set -Eeuo pipefail
|
|
36
|
+
IFS=$'\n\t'
|
|
37
|
+
# shellcheck disable=SC2154 # rc is assigned by $? in the trap string
|
|
38
|
+
trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
|
|
39
|
+
shopt -s inherit_errexit 2>/dev/null || true
|
|
1659
40
|
|
|
1660
|
-
#
|
|
1661
|
-
#
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
local cw_label="sh.aidevops.contribution-watch"
|
|
1669
|
-
local cw_state="$HOME/.aidevops/cache/contribution-watch.json"
|
|
1670
|
-
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
|
|
1671
|
-
return 0
|
|
1672
|
-
fi
|
|
41
|
+
# SCRIPT_DIR — resolves to the setup-modules/ directory so sub-library
|
|
42
|
+
# source calls work regardless of the caller's working directory.
|
|
43
|
+
if [[ -z "${SCRIPT_DIR:-}" ]]; then
|
|
44
|
+
_sched_orch_lib_path="${BASH_SOURCE[0]%/*}"
|
|
45
|
+
[[ "$_sched_orch_lib_path" == "${BASH_SOURCE[0]}" ]] && _sched_orch_lib_path="."
|
|
46
|
+
SCRIPT_DIR="$(cd "$_sched_orch_lib_path" && pwd)"
|
|
47
|
+
unset _sched_orch_lib_path
|
|
48
|
+
fi
|
|
1673
49
|
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
mkdir -p "$HOME/.aidevops/cache" "$_cw_log_dir"
|
|
50
|
+
# Source sub-libraries. Each carries its own include guard so double-sourcing
|
|
51
|
+
# is safe. SC1091 suppressed per reference/large-file-split.md §5.1 — paths
|
|
52
|
+
# are computed at runtime via $SCRIPT_DIR and cannot be statically resolved.
|
|
1678
53
|
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
if bash "$cw_script" seed >/dev/null 2>&1; then
|
|
1683
|
-
print_info "Contribution watch seeded (external issues/PRs discovered)"
|
|
1684
|
-
else
|
|
1685
|
-
print_warning "Contribution watch seed failed (non-fatal, will retry on next run)"
|
|
1686
|
-
fi
|
|
1687
|
-
fi
|
|
54
|
+
# shellcheck source=./schedulers-pulse.sh
|
|
55
|
+
# shellcheck disable=SC1091 # sub-library resolved at runtime via $SCRIPT_DIR
|
|
56
|
+
source "${SCRIPT_DIR}/schedulers-pulse.sh"
|
|
1688
57
|
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
else
|
|
1693
|
-
_install_cw_linux "$cw_script" "$_cw_log_dir"
|
|
1694
|
-
fi
|
|
1695
|
-
return 0
|
|
1696
|
-
}
|
|
58
|
+
# shellcheck source=./schedulers-linux.sh
|
|
59
|
+
# shellcheck disable=SC1091 # sub-library resolved at runtime via $SCRIPT_DIR
|
|
60
|
+
source "${SCRIPT_DIR}/schedulers-linux.sh"
|
|
1697
61
|
|
|
1698
|
-
#
|
|
1699
|
-
#
|
|
1700
|
-
|
|
1701
|
-
# the 200-470s scan never starves dispatch or downstream scanners.
|
|
1702
|
-
_install_complexity_scan_launchd() {
|
|
1703
|
-
local cs_label="$1"
|
|
1704
|
-
local cs_script="$2"
|
|
1705
|
-
local _cs_log_dir="$3"
|
|
1706
|
-
local cs_plist="$HOME/Library/LaunchAgents/${cs_label}.plist"
|
|
62
|
+
# shellcheck source=./schedulers-platform.sh
|
|
63
|
+
# shellcheck disable=SC1091 # sub-library resolved at runtime via $SCRIPT_DIR
|
|
64
|
+
source "${SCRIPT_DIR}/schedulers-platform.sh"
|
|
1707
65
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
66
|
+
# Setup stats-wrapper scheduler — runs quality sweep and health issue updates
|
|
67
|
+
# separately from the pulse (t1429). Only installed when the supervisor
|
|
68
|
+
# pulse is enabled (stats are useless without it).
|
|
69
|
+
# macOS: launchd plist (hourly) | Linux: systemd timer or cron (hourly)
|
|
70
|
+
# t2744: interval raised from 15 min → hourly. Stats UI is not realtime,
|
|
71
|
+
# the four-times-an-hour cadence drove ~200-400 GraphQL points/hr of pure
|
|
72
|
+
# overhead on multi-repo setups.
|
|
73
|
+
setup_stats_wrapper() {
|
|
74
|
+
local _pulse_lower="$1"
|
|
75
|
+
# Use effective pulse state (PULSE_ENABLED) if available; fall back to consent string.
|
|
76
|
+
# PULSE_ENABLED reflects the actual install decision (e.g., false when wrapper is missing).
|
|
77
|
+
local _pulse_effective="${PULSE_ENABLED:-$_pulse_lower}"
|
|
78
|
+
local stats_script="$HOME/.aidevops/agents/scripts/stats-wrapper.sh"
|
|
79
|
+
local stats_label="com.aidevops.aidevops-stats-wrapper"
|
|
80
|
+
local stats_systemd="aidevops-stats-wrapper"
|
|
81
|
+
local stats_log="$HOME/.aidevops/logs/stats.log"
|
|
82
|
+
if [[ -x "$stats_script" ]] && [[ "$_pulse_effective" == "true" ]]; then
|
|
83
|
+
# Always regenerate to pick up config/format changes (matches pulse behavior)
|
|
84
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
85
|
+
local stats_plist="$HOME/Library/LaunchAgents/${stats_label}.plist"
|
|
1712
86
|
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
87
|
+
local _xml_stats_script _xml_stats_home _xml_stats_path
|
|
88
|
+
_xml_stats_script=$(_xml_escape "$stats_script")
|
|
89
|
+
_xml_stats_home=$(_xml_escape "$HOME")
|
|
90
|
+
_xml_stats_path=$(_xml_escape "$PATH")
|
|
91
|
+
local stats_plist_content
|
|
92
|
+
stats_plist_content=$(
|
|
93
|
+
cat <<PLIST
|
|
1716
94
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
1717
95
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1718
96
|
<plist version="1.0">
|
|
1719
97
|
<dict>
|
|
1720
98
|
<key>Label</key>
|
|
1721
|
-
<string>${
|
|
99
|
+
<string>${stats_label}</string>
|
|
1722
100
|
<key>ProgramArguments</key>
|
|
1723
101
|
<array>
|
|
1724
102
|
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
1725
|
-
<string>${
|
|
1726
|
-
<string>run</string>
|
|
103
|
+
<string>${_xml_stats_script}</string>
|
|
1727
104
|
</array>
|
|
1728
105
|
<key>StartInterval</key>
|
|
1729
106
|
<integer>3600</integer>
|
|
1730
107
|
<key>StandardOutPath</key>
|
|
1731
|
-
<string>${
|
|
108
|
+
<string>${_xml_stats_home}/.aidevops/logs/stats.log</string>
|
|
1732
109
|
<key>StandardErrorPath</key>
|
|
1733
|
-
<string>${
|
|
110
|
+
<string>${_xml_stats_home}/.aidevops/logs/stats.log</string>
|
|
1734
111
|
<key>EnvironmentVariables</key>
|
|
1735
112
|
<dict>
|
|
1736
113
|
<key>PATH</key>
|
|
1737
|
-
<string
|
|
114
|
+
<string>${_xml_stats_path}</string>
|
|
1738
115
|
<key>HOME</key>
|
|
1739
|
-
<string>${
|
|
116
|
+
<string>${_xml_stats_home}</string>
|
|
1740
117
|
</dict>
|
|
1741
118
|
<key>RunAtLoad</key>
|
|
1742
119
|
<true/>
|
|
1743
120
|
<key>KeepAlive</key>
|
|
1744
121
|
<false/>
|
|
1745
|
-
<key>ProcessType</key>
|
|
1746
|
-
<string>Background</string>
|
|
1747
|
-
<key>LowPriorityBackgroundIO</key>
|
|
1748
|
-
<true/>
|
|
1749
|
-
<key>Nice</key>
|
|
1750
|
-
<integer>10</integer>
|
|
1751
122
|
</dict>
|
|
1752
123
|
</plist>
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
124
|
+
PLIST
|
|
125
|
+
)
|
|
126
|
+
if _launchd_install_if_changed "$stats_label" "$stats_plist" "$stats_plist_content"; then
|
|
127
|
+
print_info "Stats wrapper enabled (launchd, every hour)"
|
|
128
|
+
else
|
|
129
|
+
print_warning "Failed to load stats wrapper LaunchAgent"
|
|
130
|
+
fi
|
|
131
|
+
else
|
|
132
|
+
_install_scheduler_linux \
|
|
133
|
+
"$stats_systemd" \
|
|
134
|
+
"aidevops: stats-wrapper" \
|
|
135
|
+
"$CRON_HOURLY" \
|
|
136
|
+
"\"${stats_script}\"" \
|
|
137
|
+
"3600" \
|
|
138
|
+
"$stats_log" \
|
|
139
|
+
"" \
|
|
140
|
+
"Stats wrapper enabled (every hour)" \
|
|
141
|
+
"Failed to install stats wrapper scheduler" \
|
|
142
|
+
"true" \
|
|
143
|
+
"false"
|
|
144
|
+
fi
|
|
145
|
+
elif [[ "$_pulse_effective" == "false" ]]; then
|
|
146
|
+
# Remove stats scheduler if pulse is disabled
|
|
147
|
+
_uninstall_scheduler \
|
|
148
|
+
"$(uname -s)" \
|
|
149
|
+
"$stats_label" \
|
|
150
|
+
"$stats_systemd" \
|
|
151
|
+
"aidevops: stats-wrapper" \
|
|
152
|
+
"Stats wrapper disabled (pulse is off)"
|
|
1760
153
|
fi
|
|
1761
154
|
return 0
|
|
1762
155
|
}
|
|
1763
156
|
|
|
1764
|
-
#
|
|
1765
|
-
#
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
# ceiling), starving downstream scanners. Promoting it to its own schedule
|
|
1789
|
-
# decouples it from dispatch entirely. The runner reuses run_weekly_complexity_scan
|
|
1790
|
-
# from pulse-simplification.sh, which has internal 15-min cadence gating
|
|
1791
|
-
# (COMPLEXITY_SCAN_INTERVAL=900) so hourly launchd ticks are always safe.
|
|
1792
|
-
setup_complexity_scan() {
|
|
1793
|
-
local cs_script="$HOME/.aidevops/agents/scripts/complexity-scan-runner.sh"
|
|
1794
|
-
local cs_label="sh.aidevops.complexity-scan"
|
|
1795
|
-
if ! [[ -x "$cs_script" ]]; then
|
|
157
|
+
# Setup failure miner — mines GitHub CI failure notifications for systemic patterns
|
|
158
|
+
# and auto-files root-cause issues. Runs as a pure bash script (no LLM needed).
|
|
159
|
+
# Installed when pulse is enabled and the helper script exists.
|
|
160
|
+
# macOS: launchd plist (hourly at :15) | Linux: systemd timer or cron (hourly at :15)
|
|
161
|
+
#
|
|
162
|
+
# NOTE: This function is 105 lines and must remain in this file (schedulers.sh) to
|
|
163
|
+
# preserve its (file, fname) identity key for the function-complexity CI scanner.
|
|
164
|
+
# Moving it to a sub-library would register it as a new violation.
|
|
165
|
+
# See reference/large-file-split.md §3 "Identity-Key Preservation Rules".
|
|
166
|
+
setup_failure_miner() {
|
|
167
|
+
local _pulse_lower="$1"
|
|
168
|
+
local _pulse_effective="${PULSE_ENABLED:-$_pulse_lower}"
|
|
169
|
+
local miner_script="$HOME/.aidevops/agents/scripts/gh-failure-miner-helper.sh"
|
|
170
|
+
local miner_label="sh.aidevops.routine-gh-failure-miner"
|
|
171
|
+
local miner_systemd="aidevops-gh-failure-miner"
|
|
172
|
+
local miner_log="$HOME/.aidevops/logs/routine-gh-failure-miner.log"
|
|
173
|
+
if [[ ! -x "$miner_script" ]] || [[ "$_pulse_effective" != "true" ]]; then
|
|
174
|
+
# Remove scheduler if pulse is disabled or script missing
|
|
175
|
+
_uninstall_scheduler \
|
|
176
|
+
"$(uname -s)" \
|
|
177
|
+
"$miner_label" \
|
|
178
|
+
"$miner_systemd" \
|
|
179
|
+
"aidevops: gh-failure-miner" \
|
|
180
|
+
"Failure miner disabled (pulse is off or script missing)"
|
|
1796
181
|
return 0
|
|
1797
182
|
fi
|
|
1798
183
|
|
|
1799
|
-
|
|
1800
|
-
local _cs_log_dir
|
|
1801
|
-
_cs_log_dir=$(_resolve_cw_log_dir) || return 1
|
|
1802
|
-
mkdir -p "$_cs_log_dir"
|
|
184
|
+
mkdir -p "$HOME/.aidevops/logs"
|
|
1803
185
|
|
|
1804
|
-
# Install/update scheduled runner
|
|
1805
186
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
1806
|
-
|
|
1807
|
-
else
|
|
1808
|
-
_install_complexity_scan_linux "$cs_script" "$_cs_log_dir"
|
|
1809
|
-
fi
|
|
1810
|
-
return 0
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
# Install pulse-merge-routine launchd plist (macOS).
|
|
1814
|
-
# Args: $1=label $2=script $3=log_dir
|
|
1815
|
-
_install_pulse_merge_routine_launchd() {
|
|
1816
|
-
local pmr_label="$1"
|
|
1817
|
-
local pmr_script="$2"
|
|
1818
|
-
local _pmr_log_dir="$3"
|
|
1819
|
-
local pmr_plist="$HOME/Library/LaunchAgents/${pmr_label}.plist"
|
|
187
|
+
local miner_plist="$HOME/Library/LaunchAgents/${miner_label}.plist"
|
|
1820
188
|
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
189
|
+
local _xml_miner_script _xml_miner_home _xml_miner_path _xml_miner_log
|
|
190
|
+
_xml_miner_script=$(_xml_escape "$miner_script")
|
|
191
|
+
_xml_miner_home=$(_xml_escape "$HOME")
|
|
192
|
+
_xml_miner_path=$(_xml_escape "/bin:/usr/bin:/usr/local/bin:/opt/homebrew/bin:${PATH}")
|
|
193
|
+
_xml_miner_log=$(_xml_escape "$miner_log")
|
|
1825
194
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
195
|
+
local miner_plist_content
|
|
196
|
+
miner_plist_content=$(
|
|
197
|
+
cat <<MINER_PLIST
|
|
1829
198
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
1830
199
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1831
200
|
<plist version="1.0">
|
|
1832
201
|
<dict>
|
|
1833
202
|
<key>Label</key>
|
|
1834
|
-
<string>${
|
|
203
|
+
<string>${miner_label}</string>
|
|
1835
204
|
<key>ProgramArguments</key>
|
|
1836
205
|
<array>
|
|
1837
206
|
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
1838
|
-
<string>${
|
|
1839
|
-
<string>
|
|
207
|
+
<string>${_xml_miner_script}</string>
|
|
208
|
+
<string>create-issues</string>
|
|
209
|
+
<string>--since-hours</string>
|
|
210
|
+
<string>24</string>
|
|
211
|
+
<string>--pulse-repos</string>
|
|
212
|
+
<string>--systemic-threshold</string>
|
|
213
|
+
<string>2</string>
|
|
214
|
+
<string>--max-issues</string>
|
|
215
|
+
<string>3</string>
|
|
216
|
+
<string>--label</string>
|
|
217
|
+
<string>auto-dispatch</string>
|
|
1840
218
|
</array>
|
|
1841
|
-
<key>StartInterval</key>
|
|
1842
|
-
<integer>120</integer>
|
|
1843
|
-
<key>StandardOutPath</key>
|
|
1844
|
-
<string>${_xml_pmr_log_dir}/pulse-merge-routine.log</string>
|
|
1845
|
-
<key>StandardErrorPath</key>
|
|
1846
|
-
<string>${_xml_pmr_log_dir}/pulse-merge-routine.log</string>
|
|
1847
219
|
<key>EnvironmentVariables</key>
|
|
1848
220
|
<dict>
|
|
1849
|
-
<key>PATH</key>
|
|
1850
|
-
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
1851
221
|
<key>HOME</key>
|
|
1852
|
-
<string>${
|
|
222
|
+
<string>${_xml_miner_home}</string>
|
|
223
|
+
<key>PATH</key>
|
|
224
|
+
<string>${_xml_miner_path}</string>
|
|
1853
225
|
</dict>
|
|
226
|
+
<key>StartCalendarInterval</key>
|
|
227
|
+
<array>
|
|
228
|
+
<dict>
|
|
229
|
+
<key>Minute</key>
|
|
230
|
+
<integer>15</integer>
|
|
231
|
+
</dict>
|
|
232
|
+
</array>
|
|
233
|
+
<key>StandardOutPath</key>
|
|
234
|
+
<string>${_xml_miner_log}</string>
|
|
235
|
+
<key>StandardErrorPath</key>
|
|
236
|
+
<string>${_xml_miner_log}</string>
|
|
1854
237
|
<key>RunAtLoad</key>
|
|
1855
|
-
<true/>
|
|
1856
|
-
<key>KeepAlive</key>
|
|
1857
238
|
<false/>
|
|
1858
|
-
<key>ProcessType</key>
|
|
1859
|
-
<string>Background</string>
|
|
1860
|
-
<key>LowPriorityBackgroundIO</key>
|
|
1861
|
-
<true/>
|
|
1862
|
-
<key>Nice</key>
|
|
1863
|
-
<integer>10</integer>
|
|
1864
239
|
</dict>
|
|
1865
240
|
</plist>
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
if _launchd_install_if_changed "$pmr_label" "$pmr_plist" "$pmr_plist_content"; then
|
|
1870
|
-
print_info "Pulse merge routine enabled (launchd, every 2 min)"
|
|
1871
|
-
else
|
|
1872
|
-
print_warning "Failed to load pulse merge routine LaunchAgent"
|
|
1873
|
-
fi
|
|
1874
|
-
return 0
|
|
1875
|
-
}
|
|
1876
|
-
|
|
1877
|
-
# Install pulse-merge-routine via systemd or cron (Linux).
|
|
1878
|
-
# Args: $1=script path, $2=log dir
|
|
1879
|
-
_install_pulse_merge_routine_linux() {
|
|
1880
|
-
local pmr_script="$1"
|
|
1881
|
-
local _pmr_log_dir="$2"
|
|
1882
|
-
local pmr_systemd="aidevops-pulse-merge-routine"
|
|
1883
|
-
_install_scheduler_linux \
|
|
1884
|
-
"$pmr_systemd" \
|
|
1885
|
-
"aidevops: pulse-merge-routine" \
|
|
1886
|
-
"*/2 * * * *" \
|
|
1887
|
-
"\"${pmr_script}\" run" \
|
|
1888
|
-
"120" \
|
|
1889
|
-
"${_pmr_log_dir}/pulse-merge-routine.log" \
|
|
1890
|
-
"" \
|
|
1891
|
-
"Pulse merge routine enabled (every 2 min)" \
|
|
1892
|
-
"Failed to install pulse merge routine scheduler" \
|
|
1893
|
-
"true" \
|
|
1894
|
-
"true"
|
|
1895
|
-
return 0
|
|
1896
|
-
}
|
|
1897
|
-
|
|
1898
|
-
# Setup pulse merge routine (t2862, GH#20919) — runs merge_ready_prs_all_repos()
|
|
1899
|
-
# as a fast 120s standalone routine, decoupled from the monolithic pulse cycle.
|
|
1900
|
-
# The pulse cycle's preflight stack (60-470s) meant the merge pass ran only ~7
|
|
1901
|
-
# times/24h despite ~40+ cycles. This routine ensures green PRs merge within ~3
|
|
1902
|
-
# min of CI completion. The in-cycle merge call in pulse-wrapper.sh is kept as
|
|
1903
|
-
# defense-in-depth but short-circuits when this routine ran within the last 60s.
|
|
1904
|
-
setup_pulse_merge_routine() {
|
|
1905
|
-
local pmr_script="$HOME/.aidevops/agents/scripts/pulse-merge-routine.sh"
|
|
1906
|
-
local pmr_label="sh.aidevops.pulse-merge-routine"
|
|
1907
|
-
if ! [[ -x "$pmr_script" ]]; then
|
|
1908
|
-
return 0
|
|
1909
|
-
fi
|
|
1910
|
-
|
|
1911
|
-
# Reuse contribution-watch's log-dir resolver (same logic, same config key).
|
|
1912
|
-
local _pmr_log_dir
|
|
1913
|
-
_pmr_log_dir=$(_resolve_cw_log_dir) || return 1
|
|
1914
|
-
mkdir -p "$_pmr_log_dir"
|
|
1915
|
-
|
|
1916
|
-
# Install/update scheduled runner
|
|
1917
|
-
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
1918
|
-
_install_pulse_merge_routine_launchd "$pmr_label" "$pmr_script" "$_pmr_log_dir"
|
|
1919
|
-
else
|
|
1920
|
-
_install_pulse_merge_routine_linux "$pmr_script" "$_pmr_log_dir"
|
|
1921
|
-
fi
|
|
1922
|
-
return 0
|
|
1923
|
-
}
|
|
241
|
+
MINER_PLIST
|
|
242
|
+
)
|
|
1924
243
|
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
# Respects config: aidevops config set orchestration.draft_responses false
|
|
1928
|
-
setup_draft_responses() {
|
|
1929
|
-
local dr_script="$HOME/.aidevops/agents/scripts/draft-response-helper.sh"
|
|
1930
|
-
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
|
|
1931
|
-
mkdir -p "$HOME/.aidevops/.agent-workspace/draft-responses"
|
|
1932
|
-
if bash "$dr_script" init >/dev/null 2>&1; then
|
|
1933
|
-
print_info "Draft responses ready (private repo + local drafts)"
|
|
244
|
+
if _launchd_install_if_changed "$miner_label" "$miner_plist" "$miner_plist_content"; then
|
|
245
|
+
print_info "Failure miner enabled (launchd, hourly at :15)"
|
|
1934
246
|
else
|
|
1935
|
-
print_warning "
|
|
247
|
+
print_warning "Failed to load failure miner LaunchAgent"
|
|
1936
248
|
fi
|
|
249
|
+
else
|
|
250
|
+
_install_scheduler_linux \
|
|
251
|
+
"$miner_systemd" \
|
|
252
|
+
"aidevops: gh-failure-miner" \
|
|
253
|
+
"15 * * * *" \
|
|
254
|
+
"\"${miner_script}\" create-issues --since-hours 24 --pulse-repos --systemic-threshold 2 --max-issues 3 --label auto-dispatch" \
|
|
255
|
+
"3600" \
|
|
256
|
+
"$miner_log" \
|
|
257
|
+
"" \
|
|
258
|
+
"Failure miner enabled (hourly at :15)" \
|
|
259
|
+
"Failed to install failure miner scheduler" \
|
|
260
|
+
"false" \
|
|
261
|
+
"false" \
|
|
262
|
+
"*-*-* *:15:00"
|
|
1937
263
|
fi
|
|
1938
264
|
return 0
|
|
1939
265
|
}
|
|
1940
266
|
|
|
1941
|
-
# Setup
|
|
1942
|
-
#
|
|
1943
|
-
#
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
if ! gh auth status &>/dev/null; then
|
|
1953
|
-
return 1
|
|
267
|
+
# Setup process guard — kills runaway AI processes (ShellCheck bloat, stuck workers)
|
|
268
|
+
# before they exhaust memory and cause kernel panics. Always installed when the
|
|
269
|
+
# script exists; no consent needed (safety net, not autonomous action).
|
|
270
|
+
# macOS: launchd plist (30s interval, RunAtLoad=true) | Linux: systemd timer or cron (every minute)
|
|
271
|
+
setup_process_guard() {
|
|
272
|
+
local guard_script="$HOME/.aidevops/agents/scripts/process-guard-helper.sh"
|
|
273
|
+
local guard_label="sh.aidevops.process-guard"
|
|
274
|
+
local guard_systemd="aidevops-process-guard"
|
|
275
|
+
local guard_log="$HOME/.aidevops/logs/process-guard.log"
|
|
276
|
+
if [[ ! -x "$guard_script" ]]; then
|
|
277
|
+
return 0
|
|
1954
278
|
fi
|
|
1955
|
-
return 0
|
|
1956
|
-
}
|
|
1957
279
|
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
print_info "Profile README ready."
|
|
1963
|
-
else
|
|
1964
|
-
print_warning "Profile README setup failed (non-fatal, skipping)"
|
|
1965
|
-
fi
|
|
1966
|
-
return 0
|
|
1967
|
-
}
|
|
280
|
+
mkdir -p "$HOME/.aidevops/logs"
|
|
281
|
+
|
|
282
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
283
|
+
local guard_plist="$HOME/Library/LaunchAgents/${guard_label}.plist"
|
|
1968
284
|
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
_xml_pr_home=$(_xml_escape "$HOME")
|
|
285
|
+
# XML-escape paths for safe plist embedding (prevents injection
|
|
286
|
+
# if $HOME or paths contain &, <, > characters)
|
|
287
|
+
local _xml_guard_script _xml_guard_home _xml_guard_path
|
|
288
|
+
_xml_guard_script=$(_xml_escape "$guard_script")
|
|
289
|
+
_xml_guard_home=$(_xml_escape "$HOME")
|
|
290
|
+
_xml_guard_path=$(_xml_escape "$PATH")
|
|
1976
291
|
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
292
|
+
local guard_plist_content
|
|
293
|
+
guard_plist_content=$(
|
|
294
|
+
cat <<GUARD_PLIST
|
|
1980
295
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
1981
296
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1982
297
|
<plist version="1.0">
|
|
1983
298
|
<dict>
|
|
1984
299
|
<key>Label</key>
|
|
1985
|
-
<string>${
|
|
300
|
+
<string>${guard_label}</string>
|
|
1986
301
|
<key>ProgramArguments</key>
|
|
1987
302
|
<array>
|
|
1988
303
|
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
1989
|
-
<string>${
|
|
1990
|
-
<string>
|
|
304
|
+
<string>${_xml_guard_script}</string>
|
|
305
|
+
<string>kill-runaways</string>
|
|
1991
306
|
</array>
|
|
1992
307
|
<key>StartInterval</key>
|
|
1993
|
-
<integer>
|
|
308
|
+
<integer>30</integer>
|
|
1994
309
|
<key>StandardOutPath</key>
|
|
1995
|
-
<string>${
|
|
310
|
+
<string>${_xml_guard_home}/.aidevops/logs/process-guard.log</string>
|
|
1996
311
|
<key>StandardErrorPath</key>
|
|
1997
|
-
<string>${
|
|
312
|
+
<string>${_xml_guard_home}/.aidevops/logs/process-guard.log</string>
|
|
1998
313
|
<key>EnvironmentVariables</key>
|
|
1999
314
|
<dict>
|
|
2000
315
|
<key>PATH</key>
|
|
2001
|
-
<string
|
|
316
|
+
<string>${_xml_guard_path}</string>
|
|
2002
317
|
<key>HOME</key>
|
|
2003
|
-
<string>${
|
|
318
|
+
<string>${_xml_guard_home}</string>
|
|
319
|
+
<key>SHELLCHECK_RSS_LIMIT_KB</key>
|
|
320
|
+
<string>524288</string>
|
|
321
|
+
<key>SHELLCHECK_RUNTIME_LIMIT</key>
|
|
322
|
+
<string>120</string>
|
|
323
|
+
<key>CHILD_RSS_LIMIT_KB</key>
|
|
324
|
+
<string>8388608</string>
|
|
325
|
+
<key>CHILD_RUNTIME_LIMIT</key>
|
|
326
|
+
<string>7200</string>
|
|
2004
327
|
</dict>
|
|
2005
328
|
<key>RunAtLoad</key>
|
|
2006
|
-
<
|
|
329
|
+
<true/>
|
|
2007
330
|
<key>KeepAlive</key>
|
|
2008
331
|
<false/>
|
|
2009
|
-
<key>ProcessType</key>
|
|
2010
|
-
<string>Background</string>
|
|
2011
|
-
<key>LowPriorityBackgroundIO</key>
|
|
2012
|
-
<true/>
|
|
2013
|
-
<key>Nice</key>
|
|
2014
|
-
<integer>10</integer>
|
|
2015
332
|
</dict>
|
|
2016
333
|
</plist>
|
|
2017
|
-
|
|
2018
|
-
|
|
334
|
+
GUARD_PLIST
|
|
335
|
+
)
|
|
2019
336
|
|
|
2020
|
-
|
|
2021
|
-
|
|
337
|
+
if _launchd_install_if_changed "$guard_label" "$guard_plist" "$guard_plist_content"; then
|
|
338
|
+
print_info "Process guard enabled (launchd, every 30s, survives reboot)"
|
|
339
|
+
else
|
|
340
|
+
print_warning "Failed to load process guard LaunchAgent"
|
|
341
|
+
fi
|
|
2022
342
|
else
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
_install_scheduler_linux \
|
|
2040
|
-
"$pr_systemd" \
|
|
2041
|
-
"aidevops: profile-readme-update" \
|
|
2042
|
-
"$CRON_HOURLY" \
|
|
2043
|
-
"\"${pr_script}\" update" \
|
|
2044
|
-
"3600" \
|
|
2045
|
-
"$pr_log" \
|
|
2046
|
-
"" \
|
|
2047
|
-
"Profile README update enabled (hourly)" \
|
|
2048
|
-
"Failed to install profile README update scheduler" \
|
|
2049
|
-
"false" \
|
|
2050
|
-
"true"
|
|
2051
|
-
return 0
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
|
-
setup_profile_readme() {
|
|
2055
|
-
local pr_script="$HOME/.aidevops/agents/scripts/profile-readme-helper.sh"
|
|
2056
|
-
local pr_label="sh.aidevops.profile-readme-update"
|
|
2057
|
-
if ! _profile_readme_ready "$pr_script"; then
|
|
2058
|
-
return 0
|
|
343
|
+
# Linux: systemd timer (30s) or cron fallback (every minute — cron minimum granularity)
|
|
344
|
+
_install_scheduler_linux \
|
|
345
|
+
"$guard_systemd" \
|
|
346
|
+
"aidevops: process-guard" \
|
|
347
|
+
"$CRON_EVERY_MINUTE" \
|
|
348
|
+
"\"${guard_script}\" kill-runaways" \
|
|
349
|
+
"30" \
|
|
350
|
+
"$guard_log" \
|
|
351
|
+
"SHELLCHECK_RSS_LIMIT_KB=524288
|
|
352
|
+
SHELLCHECK_RUNTIME_LIMIT=120
|
|
353
|
+
CHILD_RSS_LIMIT_KB=8388608
|
|
354
|
+
CHILD_RUNTIME_LIMIT=7200" \
|
|
355
|
+
"Process guard enabled (every 30s)" \
|
|
356
|
+
"Failed to install process guard scheduler" \
|
|
357
|
+
"true" \
|
|
358
|
+
"false"
|
|
2059
359
|
fi
|
|
2060
|
-
|
|
2061
|
-
# Initialize profile repo if not already set up.
|
|
2062
|
-
# Always run init — it's idempotent and handles:
|
|
2063
|
-
# - Fresh installs (no profile repo)
|
|
2064
|
-
# - Missing markers (injects them into existing README)
|
|
2065
|
-
# - Diverged history (repo deleted and recreated on GitHub)
|
|
2066
|
-
# - Already-initialized repos (returns early with no changes)
|
|
2067
|
-
_run_profile_readme_init "$pr_script"
|
|
2068
|
-
|
|
2069
|
-
# Profile README auto-update scheduled job.
|
|
2070
|
-
# Installed whenever gh CLI is available — the update script self-heals
|
|
2071
|
-
# (discovers/creates the profile repo on first run via _resolve_profile_repo).
|
|
2072
|
-
# macOS: launchd plist (hourly) | Linux: systemd timer or cron (hourly)
|
|
2073
|
-
local pr_systemd="aidevops-profile-readme-update"
|
|
2074
|
-
local pr_log="$HOME/.aidevops/.agent-workspace/logs/profile-readme-update.log"
|
|
2075
|
-
mkdir -p "$HOME/.aidevops/.agent-workspace/logs"
|
|
2076
|
-
|
|
2077
|
-
_install_profile_readme_scheduler "$pr_label" "$pr_systemd" "$pr_script" "$pr_log"
|
|
2078
360
|
return 0
|
|
2079
361
|
}
|
|
2080
362
|
|
|
2081
|
-
#
|
|
2082
|
-
#
|
|
2083
|
-
#
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
363
|
+
# Setup memory pressure monitor — process-focused memory watchdog (t1398.5, GH#2915).
|
|
364
|
+
# Monitors individual process RSS, runtime, session count, and aggregate memory.
|
|
365
|
+
# Auto-kills runaway ShellCheck (language server respawns them). Always installed
|
|
366
|
+
# when the script exists; no consent needed (safety net, not autonomous action).
|
|
367
|
+
# macOS: launchd plist (60s interval, RunAtLoad=true) | Linux: systemd timer or cron (every minute)
|
|
368
|
+
setup_memory_pressure_monitor() {
|
|
369
|
+
local monitor_script="$HOME/.aidevops/agents/scripts/memory-pressure-monitor.sh"
|
|
370
|
+
local monitor_label="sh.aidevops.memory-pressure-monitor"
|
|
371
|
+
local monitor_systemd="aidevops-memory-pressure-monitor"
|
|
372
|
+
local monitor_log="$HOME/.aidevops/logs/memory-pressure-launchd.log"
|
|
373
|
+
if [[ ! -x "$monitor_script" ]]; then
|
|
2087
374
|
return 0
|
|
2088
|
-
;;
|
|
2089
|
-
*)
|
|
2090
|
-
return 1
|
|
2091
|
-
;;
|
|
2092
|
-
esac
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
# Install OAuth token refresh via Windows Task Scheduler (schtasks).
|
|
2096
|
-
# Args: $1=tr_script (Unix path), $2=log_dir (Unix path)
|
|
2097
|
-
# Runs every 30 minutes, matching macOS launchd and Linux cron behaviour.
|
|
2098
|
-
# Uses bash.exe from Git for Windows to execute the shell script.
|
|
2099
|
-
_install_token_refresh_schtasks() {
|
|
2100
|
-
local tr_script="$1"
|
|
2101
|
-
local log_dir="$2"
|
|
2102
|
-
local task_name="aidevops-token-refresh"
|
|
2103
|
-
|
|
2104
|
-
# Resolve bash.exe — Git for Windows ships it alongside git.exe
|
|
2105
|
-
local bash_exe
|
|
2106
|
-
bash_exe=$(command -v bash.exe 2>/dev/null || command -v bash 2>/dev/null || echo "bash")
|
|
2107
|
-
|
|
2108
|
-
# Convert Unix paths to Windows paths for schtasks (requires cygpath from Git Bash)
|
|
2109
|
-
local tr_script_win log_dir_win bash_exe_win
|
|
2110
|
-
if command -v cygpath &>/dev/null; then
|
|
2111
|
-
tr_script_win=$(cygpath -w "$tr_script")
|
|
2112
|
-
log_dir_win=$(cygpath -w "$log_dir")
|
|
2113
|
-
bash_exe_win=$(cygpath -w "$bash_exe")
|
|
2114
|
-
else
|
|
2115
|
-
# Fallback: manual conversion (replace /c/ with C:\, forward to backslash)
|
|
2116
|
-
tr_script_win=$(echo "$tr_script" | sed 's|^/\([a-zA-Z]\)/|\1:\\|; s|/|\\|g')
|
|
2117
|
-
log_dir_win=$(echo "$log_dir" | sed 's|^/\([a-zA-Z]\)/|\1:\\|; s|/|\\|g')
|
|
2118
|
-
bash_exe_win="bash.exe"
|
|
2119
|
-
fi
|
|
2120
|
-
|
|
2121
|
-
# Remove existing task (idempotent — ignore error if not present)
|
|
2122
|
-
schtasks /Delete /TN "$task_name" /F >/dev/null 2>&1 || true
|
|
2123
|
-
|
|
2124
|
-
# Create scheduled task: every 30 minutes, run at logon, run whether logged on or not
|
|
2125
|
-
# /SC MINUTE /MO 30 = every 30 minutes
|
|
2126
|
-
# /RL HIGHEST = run with highest available privileges (needed for token writes)
|
|
2127
|
-
# /F = force creation (overwrite if exists)
|
|
2128
|
-
# The action runs bash.exe with -c to chain both refresh calls
|
|
2129
|
-
local action_cmd
|
|
2130
|
-
action_cmd="\"${bash_exe_win}\" -c \"'${tr_script_win}' refresh anthropic >> '${log_dir_win}\\token-refresh.log' 2>&1; '${tr_script_win}' refresh openai >> '${log_dir_win}\\token-refresh.log' 2>&1\""
|
|
2131
|
-
|
|
2132
|
-
if schtasks /Create \
|
|
2133
|
-
/TN "$task_name" \
|
|
2134
|
-
/TR "$action_cmd" \
|
|
2135
|
-
/SC MINUTE \
|
|
2136
|
-
/MO 30 \
|
|
2137
|
-
/RL HIGHEST \
|
|
2138
|
-
/F \
|
|
2139
|
-
>/dev/null 2>&1; then
|
|
2140
|
-
print_info "OAuth token refresh enabled (schtasks, every 30 min)"
|
|
2141
|
-
# Run immediately to refresh any expired tokens
|
|
2142
|
-
schtasks /Run /TN "$task_name" >/dev/null 2>&1 || true
|
|
2143
|
-
else
|
|
2144
|
-
print_warning "Failed to create token refresh scheduled task. Run manually: schtasks /Create /TN aidevops-token-refresh /TR \"bash '${tr_script_win}' refresh anthropic\" /SC MINUTE /MO 30"
|
|
2145
375
|
fi
|
|
2146
|
-
return 0
|
|
2147
|
-
}
|
|
2148
376
|
|
|
2149
|
-
|
|
2150
|
-
_uninstall_token_refresh_schtasks() {
|
|
2151
|
-
local task_name="aidevops-token-refresh"
|
|
2152
|
-
if schtasks /Query /TN "$task_name" >/dev/null 2>&1; then
|
|
2153
|
-
schtasks /Delete /TN "$task_name" /F >/dev/null 2>&1 || true
|
|
2154
|
-
print_info "OAuth token refresh disabled (schtasks task removed)"
|
|
2155
|
-
fi
|
|
2156
|
-
return 0
|
|
2157
|
-
}
|
|
377
|
+
mkdir -p "$HOME/.aidevops/logs"
|
|
2158
378
|
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
# "invalid x-api-key". Also runs at load to catch tokens that expired
|
|
2162
|
-
# while the machine was off.
|
|
2163
|
-
# macOS: launchd plist | Linux/WSL: systemd timer or cron | Windows Git Bash: schtasks
|
|
2164
|
-
_oauth_token_refresh_ready() {
|
|
2165
|
-
local tr_script="$1"
|
|
2166
|
-
if ! [[ -x "$tr_script" ]]; then
|
|
2167
|
-
return 1
|
|
2168
|
-
fi
|
|
2169
|
-
if ! [[ -f "$HOME/.aidevops/oauth-pool.json" ]]; then
|
|
2170
|
-
return 1
|
|
2171
|
-
fi
|
|
2172
|
-
return 0
|
|
2173
|
-
}
|
|
379
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
380
|
+
local monitor_plist="$HOME/Library/LaunchAgents/${monitor_label}.plist"
|
|
2174
381
|
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
local _xml_tr_script _xml_tr_home
|
|
2180
|
-
_xml_tr_script=$(_xml_escape "$tr_script")
|
|
2181
|
-
_xml_tr_home=$(_xml_escape "$HOME")
|
|
382
|
+
# XML-escape paths for safe plist embedding
|
|
383
|
+
local _xml_monitor_script _xml_monitor_home
|
|
384
|
+
_xml_monitor_script=$(_xml_escape "$monitor_script")
|
|
385
|
+
_xml_monitor_home=$(_xml_escape "$HOME")
|
|
2182
386
|
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
387
|
+
local monitor_plist_content
|
|
388
|
+
monitor_plist_content=$(
|
|
389
|
+
cat <<MONITOR_PLIST
|
|
2186
390
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2187
391
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2188
392
|
<plist version="1.0">
|
|
2189
393
|
<dict>
|
|
2190
394
|
<key>Label</key>
|
|
2191
|
-
<string>${
|
|
395
|
+
<string>${monitor_label}</string>
|
|
2192
396
|
<key>ProgramArguments</key>
|
|
2193
397
|
<array>
|
|
2194
398
|
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
2195
|
-
<string
|
|
2196
|
-
<string>"${_xml_tr_script}" refresh anthropic; "${_xml_tr_script}" refresh openai</string>
|
|
399
|
+
<string>${_xml_monitor_script}</string>
|
|
2197
400
|
</array>
|
|
2198
401
|
<key>StartInterval</key>
|
|
2199
|
-
<integer>
|
|
402
|
+
<integer>60</integer>
|
|
2200
403
|
<key>StandardOutPath</key>
|
|
2201
|
-
<string>${
|
|
404
|
+
<string>${_xml_monitor_home}/.aidevops/logs/memory-pressure-launchd.log</string>
|
|
2202
405
|
<key>StandardErrorPath</key>
|
|
2203
|
-
<string>${
|
|
406
|
+
<string>${_xml_monitor_home}/.aidevops/logs/memory-pressure-launchd.log</string>
|
|
2204
407
|
<key>EnvironmentVariables</key>
|
|
2205
408
|
<dict>
|
|
2206
409
|
<key>PATH</key>
|
|
2207
410
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
2208
411
|
<key>HOME</key>
|
|
2209
|
-
<string>${
|
|
412
|
+
<string>${_xml_monitor_home}</string>
|
|
2210
413
|
</dict>
|
|
2211
414
|
<key>RunAtLoad</key>
|
|
2212
415
|
<true/>
|
|
@@ -2220,238 +423,82 @@ _install_token_refresh_launchd() {
|
|
|
2220
423
|
<integer>10</integer>
|
|
2221
424
|
</dict>
|
|
2222
425
|
</plist>
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
if _launchd_install_if_changed "$tr_label" "$tr_plist" "$tr_plist_content"; then
|
|
2227
|
-
print_info "OAuth token refresh enabled (launchd, every 30 min)"
|
|
2228
|
-
else
|
|
2229
|
-
print_warning "Failed to load token refresh LaunchAgent"
|
|
2230
|
-
fi
|
|
2231
|
-
return 0
|
|
2232
|
-
}
|
|
2233
|
-
|
|
2234
|
-
setup_oauth_token_refresh() {
|
|
2235
|
-
local tr_script="$HOME/.aidevops/agents/scripts/oauth-pool-helper.sh"
|
|
2236
|
-
local tr_label="sh.aidevops.token-refresh"
|
|
2237
|
-
if ! _oauth_token_refresh_ready "$tr_script"; then
|
|
2238
|
-
return 0
|
|
2239
|
-
fi
|
|
2240
|
-
|
|
2241
|
-
local tr_log_dir="$HOME/.aidevops/.agent-workspace/logs"
|
|
2242
|
-
mkdir -p "$tr_log_dir"
|
|
2243
|
-
|
|
2244
|
-
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
2245
|
-
_install_token_refresh_launchd "$tr_label" "$tr_script"
|
|
2246
|
-
elif _is_windows; then
|
|
2247
|
-
# Windows Git Bash / MINGW64 / MSYS2: use Task Scheduler (schtasks)
|
|
2248
|
-
_install_token_refresh_schtasks "$tr_script" "$tr_log_dir"
|
|
2249
|
-
else
|
|
2250
|
-
# Linux / WSL without systemd: systemd timer or cron fallback
|
|
2251
|
-
_install_scheduler_linux \
|
|
2252
|
-
"aidevops-token-refresh" \
|
|
2253
|
-
"aidevops: token-refresh" \
|
|
2254
|
-
"*/30 * * * *" \
|
|
2255
|
-
"\"${tr_script}\" refresh anthropic; \"${tr_script}\" refresh openai" \
|
|
2256
|
-
"1800" \
|
|
2257
|
-
"${tr_log_dir}/token-refresh.log" \
|
|
2258
|
-
"" \
|
|
2259
|
-
"OAuth token refresh enabled (every 30 min)" \
|
|
2260
|
-
"Failed to install token refresh scheduler" \
|
|
2261
|
-
"true" \
|
|
2262
|
-
"true"
|
|
2263
|
-
fi
|
|
2264
|
-
return 0
|
|
2265
|
-
}
|
|
2266
|
-
|
|
2267
|
-
# Setup opencode DB maintenance scheduler (r913, t2183).
|
|
2268
|
-
# Runs weekly (Sun 04:00 local) to checkpoint/optimize/vacuum opencode.db.
|
|
2269
|
-
# The helper self-noops on missing DB, so installing unconditionally is safe —
|
|
2270
|
-
# a non-opencode machine wakes up weekly, sees no DB, exits 0 silently.
|
|
2271
|
-
#
|
|
2272
|
-
# Platform split (mirrors the pattern for token-refresh):
|
|
2273
|
-
# macOS — helper owns its plist generation via cmd_install (Approach B).
|
|
2274
|
-
# Linux — _install_scheduler_linux with cron `0 4 * * 0` + systemd
|
|
2275
|
-
# OnCalendar `Sun *-*-* 04:00:00` for accurate wall-clock firing.
|
|
2276
|
-
# Windows — TODO(t2183-followup): opencode on Windows is rare and the
|
|
2277
|
-
# helper self-noops on missing DB, so leaving unscheduled is
|
|
2278
|
-
# low-risk for this iteration.
|
|
2279
|
-
setup_opencode_db_maintenance() {
|
|
2280
|
-
local ocdbm_script="$HOME/.aidevops/agents/scripts/opencode-db-maintenance-helper.sh"
|
|
2281
|
-
if ! [[ -x "$ocdbm_script" ]]; then
|
|
2282
|
-
return 0
|
|
2283
|
-
fi
|
|
2284
|
-
|
|
2285
|
-
local ocdbm_log_dir="$HOME/.aidevops/.agent-workspace/logs"
|
|
2286
|
-
mkdir -p "$ocdbm_log_dir"
|
|
426
|
+
MONITOR_PLIST
|
|
427
|
+
)
|
|
2287
428
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
# Quiet the helper's multi-line output and emit one consolidated line
|
|
2291
|
-
# to match the style of setup_profile_readme / setup_oauth_token_refresh.
|
|
2292
|
-
if bash "$ocdbm_script" install >/dev/null 2>&1; then
|
|
2293
|
-
print_info "OpenCode DB maintenance enabled (launchd, weekly Sun 04:00)"
|
|
429
|
+
if _launchd_install_if_changed "$monitor_label" "$monitor_plist" "$monitor_plist_content"; then
|
|
430
|
+
print_info "Memory pressure monitor enabled (launchd, every 60s, survives reboot)"
|
|
2294
431
|
else
|
|
2295
|
-
print_warning "Failed to
|
|
432
|
+
print_warning "Failed to load memory pressure monitor LaunchAgent"
|
|
2296
433
|
fi
|
|
2297
|
-
elif _is_windows; then
|
|
2298
|
-
# Windows scheduling deferred — helper self-noops on missing DB so
|
|
2299
|
-
# the cost of leaving unscheduled is ~0 until opencode lands on
|
|
2300
|
-
# Windows in quantity.
|
|
2301
|
-
return 0
|
|
2302
434
|
else
|
|
2303
|
-
# Linux
|
|
2304
|
-
# Weekly Sunday 04:00 local — cron: `0 4 * * 0`; systemd OnCalendar
|
|
2305
|
-
# ensures wall-clock firing even across suspends/reboots.
|
|
435
|
+
# Linux: systemd timer (60s) or cron fallback (every minute — cron minimum granularity)
|
|
2306
436
|
_install_scheduler_linux \
|
|
2307
|
-
"
|
|
2308
|
-
"aidevops:
|
|
2309
|
-
"
|
|
2310
|
-
"\"${
|
|
2311
|
-
"
|
|
2312
|
-
"$
|
|
437
|
+
"$monitor_systemd" \
|
|
438
|
+
"aidevops: memory-pressure-monitor" \
|
|
439
|
+
"$CRON_EVERY_MINUTE" \
|
|
440
|
+
"\"${monitor_script}\"" \
|
|
441
|
+
"60" \
|
|
442
|
+
"$monitor_log" \
|
|
2313
443
|
"" \
|
|
2314
|
-
"
|
|
2315
|
-
"Failed to install
|
|
2316
|
-
"false" \
|
|
444
|
+
"Memory pressure monitor enabled (every 60s)" \
|
|
445
|
+
"Failed to install memory pressure monitor scheduler" \
|
|
2317
446
|
"true" \
|
|
2318
|
-
"
|
|
2319
|
-
fi
|
|
2320
|
-
return 0
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
# Setup repo-sync scheduler if not already installed.
|
|
2324
|
-
# Keeps local git repos up to date with daily ff-only pulls.
|
|
2325
|
-
# Respects config: aidevops config set orchestration.repo_sync false
|
|
2326
|
-
setup_repo_sync() {
|
|
2327
|
-
local repo_sync_script="$HOME/.aidevops/agents/scripts/repo-sync-helper.sh"
|
|
2328
|
-
if ! [[ -x "$repo_sync_script" ]] || ! is_feature_enabled repo_sync 2>/dev/null; then
|
|
2329
|
-
return 0
|
|
2330
|
-
fi
|
|
2331
|
-
|
|
2332
|
-
local _repo_sync_installed=false
|
|
2333
|
-
if _launchd_has_agent "com.aidevops.aidevops-repo-sync"; then
|
|
2334
|
-
_repo_sync_installed=true
|
|
2335
|
-
elif _launchd_has_agent "sh.aidevops.repo-sync"; then
|
|
2336
|
-
_repo_sync_installed=true
|
|
2337
|
-
elif crontab -l 2>/dev/null | grep -qF "aidevops-repo-sync"; then
|
|
2338
|
-
_repo_sync_installed=true
|
|
2339
|
-
elif command -v systemctl >/dev/null 2>&1 &&
|
|
2340
|
-
systemctl --user is-enabled "aidevops-repo-sync.timer" >/dev/null 2>&1; then
|
|
2341
|
-
_repo_sync_installed=true
|
|
2342
|
-
fi
|
|
2343
|
-
if [[ "$_repo_sync_installed" == "false" ]]; then
|
|
2344
|
-
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
2345
|
-
bash "$repo_sync_script" enable >/dev/null 2>&1 || true
|
|
2346
|
-
print_info "Repo sync enabled (daily). Disable: aidevops repo-sync disable"
|
|
2347
|
-
else
|
|
2348
|
-
echo ""
|
|
2349
|
-
echo "Repo sync keeps your local git repos up to date by running"
|
|
2350
|
-
echo "git pull --ff-only daily on clean repos on their default branch."
|
|
2351
|
-
echo ""
|
|
2352
|
-
setup_prompt enable_repo_sync "Enable daily repo sync? [Y/n]: " "Y"
|
|
2353
|
-
if [[ "$enable_repo_sync" =~ ^[Yy]?$ || -z "$enable_repo_sync" ]]; then
|
|
2354
|
-
bash "$repo_sync_script" enable
|
|
2355
|
-
else
|
|
2356
|
-
print_info "Skipped. Enable later: aidevops repo-sync enable"
|
|
2357
|
-
fi
|
|
2358
|
-
fi
|
|
447
|
+
"true"
|
|
2359
448
|
fi
|
|
2360
449
|
return 0
|
|
2361
450
|
}
|
|
2362
451
|
|
|
2363
|
-
# Setup
|
|
2364
|
-
#
|
|
2365
|
-
#
|
|
2366
|
-
#
|
|
2367
|
-
|
|
2368
|
-
local
|
|
2369
|
-
|
|
452
|
+
# Setup screen time snapshot — captures daily screen time for contributor stats.
|
|
453
|
+
# Accumulates data in screen-time.jsonl (macOS Knowledge DB retains only ~28 days).
|
|
454
|
+
# Always installed when the script exists; no consent needed (data collection only).
|
|
455
|
+
# macOS: launchd plist (every 6h, RunAtLoad=true) | Linux: systemd timer or cron (every 6h)
|
|
456
|
+
setup_screen_time_snapshot() {
|
|
457
|
+
local st_script="$HOME/.aidevops/agents/scripts/screen-time-helper.sh"
|
|
458
|
+
local st_label="sh.aidevops.screen-time-snapshot"
|
|
459
|
+
local st_systemd="aidevops-screen-time-snapshot"
|
|
460
|
+
local st_log="$HOME/.aidevops/.agent-workspace/logs/screen-time-snapshot.log"
|
|
461
|
+
if [[ ! -x "$st_script" ]]; then
|
|
2370
462
|
return 0
|
|
2371
463
|
fi
|
|
2372
464
|
|
|
2373
|
-
|
|
2374
|
-
if _launchd_has_agent "sh.aidevops.repo-aidevops-health"; then
|
|
2375
|
-
_repo_health_installed=true
|
|
2376
|
-
elif crontab -l 2>/dev/null | grep -qF "aidevops-repo-aidevops-health"; then
|
|
2377
|
-
_repo_health_installed=true
|
|
2378
|
-
elif command -v systemctl >/dev/null 2>&1 &&
|
|
2379
|
-
systemctl --user is-enabled "aidevops-repo-aidevops-health.timer" >/dev/null 2>&1; then
|
|
2380
|
-
_repo_health_installed=true
|
|
2381
|
-
fi
|
|
2382
|
-
if [[ "$_repo_health_installed" == "false" ]]; then
|
|
2383
|
-
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|
|
2384
|
-
bash "$repo_health_script" enable >/dev/null 2>&1 || true
|
|
2385
|
-
print_info "r914 repo-aidevops-health enabled (daily @03:30). Disable: aidevops repo-aidevops-health disable"
|
|
2386
|
-
else
|
|
2387
|
-
echo ""
|
|
2388
|
-
echo "r914 keeps \`.aidevops.json\` versions current across all registered"
|
|
2389
|
-
echo "repos and surfaces registry drift (missing folders, unregistered git"
|
|
2390
|
-
echo "repos) for human triage. Runs daily at 03:30."
|
|
2391
|
-
echo ""
|
|
2392
|
-
setup_prompt enable_repo_health "Enable daily r914 repo-aidevops-health? [Y/n]: " "Y"
|
|
2393
|
-
if [[ "$enable_repo_health" =~ ^[Yy]?$ || -z "$enable_repo_health" ]]; then
|
|
2394
|
-
bash "$repo_health_script" enable
|
|
2395
|
-
else
|
|
2396
|
-
print_info "Skipped. Enable later: aidevops repo-aidevops-health enable"
|
|
2397
|
-
fi
|
|
2398
|
-
fi
|
|
2399
|
-
fi
|
|
2400
|
-
return 0
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
# ============================================================================
|
|
2404
|
-
# Peer productivity monitor (t2932)
|
|
2405
|
-
# ============================================================================
|
|
2406
|
-
#
|
|
2407
|
-
# Adaptive cross-runner dispatch coordination: observes peer GitHub activity
|
|
2408
|
-
# every 30 min and updates ~/.config/aidevops/dispatch-override.conf to
|
|
2409
|
-
# `ignore` peers whose pulse is broken (claims issues but never PRs) and
|
|
2410
|
-
# back to `honour` when they recover. Self-healing across the ecosystem —
|
|
2411
|
-
# each runner observes peers independently, no central coordinator needed.
|
|
2412
|
-
# Manual entries in dispatch-override.conf above the auto-managed marker
|
|
2413
|
-
# always take precedence.
|
|
465
|
+
mkdir -p "$HOME/.aidevops/.agent-workspace/logs"
|
|
2414
466
|
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
_install_peer_productivity_monitor_launchd() {
|
|
2418
|
-
local ppm_label="$1"
|
|
2419
|
-
local ppm_script="$2"
|
|
2420
|
-
local _ppm_log_dir="$3"
|
|
2421
|
-
local ppm_plist="$HOME/Library/LaunchAgents/${ppm_label}.plist"
|
|
467
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
468
|
+
local st_plist="$HOME/Library/LaunchAgents/${st_label}.plist"
|
|
2422
469
|
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
470
|
+
# XML-escape paths for safe plist embedding
|
|
471
|
+
local _xml_st_script _xml_st_home
|
|
472
|
+
_xml_st_script=$(_xml_escape "$st_script")
|
|
473
|
+
_xml_st_home=$(_xml_escape "$HOME")
|
|
2427
474
|
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
475
|
+
local st_plist_content
|
|
476
|
+
st_plist_content=$(
|
|
477
|
+
cat <<ST_PLIST
|
|
2431
478
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2432
479
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
2433
480
|
<plist version="1.0">
|
|
2434
481
|
<dict>
|
|
2435
482
|
<key>Label</key>
|
|
2436
|
-
<string>${
|
|
483
|
+
<string>${st_label}</string>
|
|
2437
484
|
<key>ProgramArguments</key>
|
|
2438
485
|
<array>
|
|
2439
486
|
<string>$(_xml_escape "$(_resolve_modern_bash)")</string>
|
|
2440
|
-
<string>${
|
|
2441
|
-
<string>
|
|
487
|
+
<string>${_xml_st_script}</string>
|
|
488
|
+
<string>snapshot</string>
|
|
2442
489
|
</array>
|
|
2443
490
|
<key>StartInterval</key>
|
|
2444
|
-
<integer>
|
|
491
|
+
<integer>21600</integer>
|
|
2445
492
|
<key>StandardOutPath</key>
|
|
2446
|
-
<string>${
|
|
493
|
+
<string>${_xml_st_home}/.aidevops/.agent-workspace/logs/screen-time-snapshot.log</string>
|
|
2447
494
|
<key>StandardErrorPath</key>
|
|
2448
|
-
<string>${
|
|
495
|
+
<string>${_xml_st_home}/.aidevops/.agent-workspace/logs/screen-time-snapshot.log</string>
|
|
2449
496
|
<key>EnvironmentVariables</key>
|
|
2450
497
|
<dict>
|
|
2451
498
|
<key>PATH</key>
|
|
2452
499
|
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
|
2453
500
|
<key>HOME</key>
|
|
2454
|
-
<string>${
|
|
501
|
+
<string>${_xml_st_home}</string>
|
|
2455
502
|
</dict>
|
|
2456
503
|
<key>RunAtLoad</key>
|
|
2457
504
|
<true/>
|
|
@@ -2465,60 +512,28 @@ _install_peer_productivity_monitor_launchd() {
|
|
|
2465
512
|
<integer>10</integer>
|
|
2466
513
|
</dict>
|
|
2467
514
|
</plist>
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
if _launchd_install_if_changed "$ppm_label" "$ppm_plist" "$ppm_plist_content"; then
|
|
2472
|
-
print_info "Peer productivity monitor enabled (launchd, every 30 min)"
|
|
2473
|
-
else
|
|
2474
|
-
print_warning "Failed to load peer-productivity-monitor LaunchAgent"
|
|
2475
|
-
fi
|
|
2476
|
-
return 0
|
|
2477
|
-
}
|
|
2478
|
-
|
|
2479
|
-
# Install peer-productivity-monitor via systemd or cron (Linux).
|
|
2480
|
-
# Args: $1=script path, $2=log dir
|
|
2481
|
-
_install_peer_productivity_monitor_linux() {
|
|
2482
|
-
local ppm_script="$1"
|
|
2483
|
-
local _ppm_log_dir="$2"
|
|
2484
|
-
local ppm_systemd="aidevops-peer-productivity-monitor"
|
|
2485
|
-
_install_scheduler_linux \
|
|
2486
|
-
"$ppm_systemd" \
|
|
2487
|
-
"aidevops: peer-productivity-monitor" \
|
|
2488
|
-
"*/30 * * * *" \
|
|
2489
|
-
"\"${ppm_script}\" observe" \
|
|
2490
|
-
"1800" \
|
|
2491
|
-
"${_ppm_log_dir}/peer-productivity-launchd.log" \
|
|
2492
|
-
"" \
|
|
2493
|
-
"Peer productivity monitor enabled (every 30 min)" \
|
|
2494
|
-
"Failed to install peer-productivity-monitor scheduler" \
|
|
2495
|
-
"true" \
|
|
2496
|
-
"true"
|
|
2497
|
-
return 0
|
|
2498
|
-
}
|
|
2499
|
-
|
|
2500
|
-
# Setup peer-productivity-monitor (t2932) — observes peer GitHub activity
|
|
2501
|
-
# every 30 min and updates ~/.config/aidevops/dispatch-override.conf so the
|
|
2502
|
-
# local pulse competes with broken peers and collaborates with healthy ones.
|
|
2503
|
-
# Manual entries in dispatch-override.conf above the auto-managed marker
|
|
2504
|
-
# always take precedence.
|
|
2505
|
-
setup_peer_productivity_monitor() {
|
|
2506
|
-
local ppm_script="$HOME/.aidevops/agents/scripts/peer-productivity-monitor.sh"
|
|
2507
|
-
local ppm_label="sh.aidevops.peer-productivity-monitor"
|
|
2508
|
-
if ! [[ -x "$ppm_script" ]]; then
|
|
2509
|
-
return 0
|
|
2510
|
-
fi
|
|
2511
|
-
|
|
2512
|
-
# Reuse contribution-watch's log-dir resolver (same logic, same config key).
|
|
2513
|
-
local _ppm_log_dir
|
|
2514
|
-
_ppm_log_dir=$(_resolve_cw_log_dir) || return 1
|
|
2515
|
-
mkdir -p "$_ppm_log_dir"
|
|
515
|
+
ST_PLIST
|
|
516
|
+
)
|
|
2516
517
|
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
518
|
+
if _launchd_install_if_changed "$st_label" "$st_plist" "$st_plist_content"; then
|
|
519
|
+
print_info "Screen time snapshot enabled (launchd, every 6h, survives reboot)"
|
|
520
|
+
else
|
|
521
|
+
print_warning "Failed to load screen time snapshot LaunchAgent"
|
|
522
|
+
fi
|
|
2520
523
|
else
|
|
2521
|
-
|
|
524
|
+
# Linux: systemd timer (every 6h) or cron fallback
|
|
525
|
+
_install_scheduler_linux \
|
|
526
|
+
"$st_systemd" \
|
|
527
|
+
"aidevops: screen-time-snapshot" \
|
|
528
|
+
"0 */6 * * *" \
|
|
529
|
+
"\"${st_script}\" snapshot" \
|
|
530
|
+
"21600" \
|
|
531
|
+
"$st_log" \
|
|
532
|
+
"" \
|
|
533
|
+
"Screen time snapshot enabled (every 6h)" \
|
|
534
|
+
"Failed to install screen time snapshot scheduler" \
|
|
535
|
+
"true" \
|
|
536
|
+
"true"
|
|
2522
537
|
fi
|
|
2523
538
|
return 0
|
|
2524
539
|
}
|