aidevops 3.5.892 → 3.8.2
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 +96 -16
- package/VERSION +1 -1
- package/aidevops.sh +293 -34
- package/package.json +1 -1
- package/setup-modules/agent-deploy.sh +297 -669
- package/setup-modules/agent-runtime.sh +251 -0
- package/setup-modules/config.sh +83 -0
- package/setup-modules/core.sh +2 -0
- package/setup-modules/mcp-setup.sh +2 -0
- package/setup-modules/migrations.sh +267 -1
- package/setup-modules/plugins.sh +2 -0
- package/setup-modules/post-setup.sh +8 -1
- package/setup-modules/schedulers.sh +762 -336
- package/setup-modules/shell-env.sh +54 -16
- package/setup-modules/tool-beads.sh +324 -0
- package/setup-modules/tool-install.sh +6 -1
- package/setup.sh +207 -2
- package/templates/deploy-templates.sh +2 -0
- package/templates/home/.agents/README.md +3 -0
- package/templates/home/AGENTS.md +3 -0
- package/templates/home/git/.agent/README.md +3 -0
- package/templates/home/git/AGENTS.md +3 -0
- package/templates/opencode-config-agents.md +3 -0
- package/templates/standard-functions.sh +2 -0
- package/templates/wordpress-performance-workflow.md +3 -0
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
|
|
2
4
|
# Scheduler setup functions: supervisor pulse, stats wrapper, process guard,
|
|
3
5
|
# memory pressure monitor, screen time snapshot, contribution watch,
|
|
4
6
|
# profile README, OAuth token refresh.
|
|
5
7
|
# Part of aidevops setup.sh modularization (GH#5793)
|
|
6
8
|
|
|
9
|
+
# Keep pulse workers alive long enough for opus-tier dispatches.
|
|
10
|
+
PULSE_STALE_THRESHOLD_SECONDS=1800
|
|
11
|
+
|
|
7
12
|
# Shell safety baseline
|
|
8
13
|
set -Eeuo pipefail
|
|
9
14
|
IFS=$'\n\t'
|
|
@@ -105,27 +110,237 @@ _determine_pulse_install() {
|
|
|
105
110
|
return 0
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
# GH#17769: These functions are deprecated — model routing is now derived
|
|
114
|
+
# from the OAuth pool + routing table at runtime. Kept as no-ops for one
|
|
115
|
+
# release cycle in case external scripts call them.
|
|
108
116
|
_resolve_headless_models_override() {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
printf '%s' ""
|
|
118
|
+
return 0
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_resolve_pulse_model_override() {
|
|
122
|
+
printf '%s' ""
|
|
123
|
+
return 0
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_is_pulse_installed() {
|
|
127
|
+
local pulse_label="$1"
|
|
128
|
+
|
|
129
|
+
if _scheduler_detect_installed \
|
|
130
|
+
"Supervisor pulse" \
|
|
131
|
+
"$pulse_label" \
|
|
132
|
+
"" \
|
|
133
|
+
"pulse-wrapper" \
|
|
134
|
+
"" \
|
|
135
|
+
"" \
|
|
136
|
+
"" \
|
|
137
|
+
"aidevops-supervisor-pulse"; then
|
|
138
|
+
return 0
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
return 1
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_resolve_pulse_runtime_binary() {
|
|
145
|
+
# GH#18439 Bug 2: Persist the resolved binary path across setup.sh
|
|
146
|
+
# invocations. aidevops-auto-update.timer runs setup.sh under systemd's
|
|
147
|
+
# minimal PATH, so re-resolving from live `$PATH` alone yields the
|
|
148
|
+
# legacy macOS-biased `/opt/homebrew/bin/opencode` fallback on Linux.
|
|
149
|
+
# Reading from persistence first (populated during an interactive
|
|
150
|
+
# setup.sh run with a rich `$PATH`) prevents the auto-update cycle
|
|
151
|
+
# from silently degrading the service file.
|
|
152
|
+
local _persisted_file="$HOME/.config/aidevops/scheduler-runtime-bin"
|
|
153
|
+
local opencode_bin=""
|
|
154
|
+
|
|
155
|
+
# 1. Prefer persisted path if it still points at an executable file.
|
|
156
|
+
if [[ -f "$_persisted_file" ]]; then
|
|
157
|
+
local _persisted
|
|
158
|
+
_persisted=$(head -n1 "$_persisted_file" 2>/dev/null || true)
|
|
159
|
+
if [[ -n "$_persisted" ]] && [[ -x "$_persisted" ]]; then
|
|
160
|
+
printf '%s' "$_persisted"
|
|
161
|
+
return 0
|
|
114
162
|
fi
|
|
115
163
|
fi
|
|
116
|
-
|
|
164
|
+
|
|
165
|
+
# 2. Try runtime-registry lookup via live PATH.
|
|
166
|
+
if type rt_list_headless &>/dev/null; then
|
|
167
|
+
local _sched_rt_id=""
|
|
168
|
+
local _sched_bin=""
|
|
169
|
+
while IFS= read -r _sched_rt_id; do
|
|
170
|
+
_sched_bin=$(rt_binary "$_sched_rt_id") || continue
|
|
171
|
+
if [[ -n "$_sched_bin" ]] && command -v "$_sched_bin" &>/dev/null; then
|
|
172
|
+
opencode_bin=$(command -v "$_sched_bin")
|
|
173
|
+
break
|
|
174
|
+
fi
|
|
175
|
+
done < <(rt_list_headless)
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
# 3. Direct PATH lookup for the default runtime.
|
|
179
|
+
if [[ -z "$opencode_bin" ]]; then
|
|
180
|
+
opencode_bin=$(command -v opencode 2>/dev/null || true)
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
# 4. OS-aware common-install-location sweep. Used when live `$PATH` is
|
|
184
|
+
# minimal (systemd-spawned setup.sh) and persistence hasn't been
|
|
185
|
+
# seeded yet. Covers Homebrew (macOS + Linuxbrew), /usr/local, npm
|
|
186
|
+
# global, Python/uv pipx-style `.local/bin`, and bun.
|
|
187
|
+
if [[ -z "$opencode_bin" ]]; then
|
|
188
|
+
local _candidate
|
|
189
|
+
for _candidate in \
|
|
190
|
+
/opt/homebrew/bin/opencode \
|
|
191
|
+
/usr/local/bin/opencode \
|
|
192
|
+
/home/linuxbrew/.linuxbrew/bin/opencode \
|
|
193
|
+
"$HOME/.npm-global/bin/opencode" \
|
|
194
|
+
"$HOME/.local/bin/opencode" \
|
|
195
|
+
"$HOME/.bun/bin/opencode" \
|
|
196
|
+
/opt/homebrew/bin/claude \
|
|
197
|
+
/usr/local/bin/claude \
|
|
198
|
+
"$HOME/.local/bin/claude"; do
|
|
199
|
+
if [[ -x "$_candidate" ]]; then
|
|
200
|
+
opencode_bin="$_candidate"
|
|
201
|
+
break
|
|
202
|
+
fi
|
|
203
|
+
done
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
# 5. Last-resort legacy fallback (preserves pre-GH#18439 behaviour so
|
|
207
|
+
# setup.sh never exits the resolver empty-handed).
|
|
208
|
+
[[ -z "$opencode_bin" ]] && opencode_bin="/opt/homebrew/bin/opencode"
|
|
209
|
+
|
|
210
|
+
# Persist the resolved path for subsequent non-interactive invocations
|
|
211
|
+
# (auto-update timer, cron regeneration). Only write when we actually
|
|
212
|
+
# found a real executable — don't persist the legacy fallback.
|
|
213
|
+
if [[ -x "$opencode_bin" ]]; then
|
|
214
|
+
mkdir -p "$(dirname "$_persisted_file")" 2>/dev/null || true
|
|
215
|
+
printf '%s\n' "$opencode_bin" >"$_persisted_file" 2>/dev/null || true
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
printf '%s' "$opencode_bin"
|
|
117
219
|
return 0
|
|
118
220
|
}
|
|
119
221
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
222
|
+
_build_pulse_linux_env() {
|
|
223
|
+
# GH#17546/GH#17769: Model config is derived from pool + routing table at
|
|
224
|
+
# runtime. No model env vars embedded in cron/systemd.
|
|
225
|
+
local opencode_bin="${1:-}"
|
|
226
|
+
local _pulse_env="PULSE_DIR=${HOME}/.aidevops/.agent-workspace
|
|
227
|
+
PULSE_STALE_THRESHOLD=${PULSE_STALE_THRESHOLD_SECONDS}"
|
|
228
|
+
|
|
229
|
+
# GH#18439 Bug 2: embed resolved runtime binary path so pulse-wrapper.sh
|
|
230
|
+
# and headless-runtime-helper.sh find the correct binary under systemd's
|
|
231
|
+
# minimal PATH (e.g. when aidevops-auto-update.timer regenerates the
|
|
232
|
+
# service file). Mirrors the macOS launchd <OPENCODE_BIN> key.
|
|
233
|
+
if [[ -n "$opencode_bin" ]]; then
|
|
234
|
+
_pulse_env+=$'\n'"OPENCODE_BIN=${opencode_bin}"
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
printf '%s' "$_pulse_env"
|
|
238
|
+
return 0
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
# Read supervisor.pulse_interval_seconds from settings.json.
|
|
242
|
+
# Falls back to 120 if the file is missing, the key is absent, or jq is unavailable.
|
|
243
|
+
# Clamps to the validated range [30, 3600].
|
|
244
|
+
# GH#18018: previously this was hardcoded as "120" in _install_supervisor_pulse.
|
|
245
|
+
_read_pulse_interval_seconds() {
|
|
246
|
+
local _settings_file="$HOME/.config/aidevops/settings.json"
|
|
247
|
+
local _interval=120
|
|
248
|
+
|
|
249
|
+
if command -v jq >/dev/null 2>&1 && [[ -f "$_settings_file" ]]; then
|
|
250
|
+
local _raw
|
|
251
|
+
_raw=$(jq -r '.supervisor.pulse_interval_seconds // empty' "$_settings_file" 2>/dev/null) || _raw=""
|
|
252
|
+
if [[ -n "$_raw" ]] && [[ "$_raw" =~ ^[0-9]+$ ]]; then
|
|
253
|
+
_interval="$_raw"
|
|
126
254
|
fi
|
|
127
255
|
fi
|
|
128
|
-
|
|
256
|
+
|
|
257
|
+
# Clamp to validated range (mirrors settings-helper.sh validation: 30-3600)
|
|
258
|
+
if [[ "$_interval" -lt 30 ]]; then
|
|
259
|
+
_interval=30
|
|
260
|
+
elif [[ "$_interval" -gt 3600 ]]; then
|
|
261
|
+
_interval=3600
|
|
262
|
+
fi
|
|
263
|
+
|
|
264
|
+
printf '%d' "$_interval"
|
|
265
|
+
return 0
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# Convert an interval in seconds to a cron schedule expression (e.g. "*/2 * * * *").
|
|
269
|
+
# Minimum granularity is 1 minute. Intervals that don't divide evenly into minutes
|
|
270
|
+
# are rounded down to whole minutes with a warning.
|
|
271
|
+
# Args: $1 = interval_seconds
|
|
272
|
+
_seconds_to_cron_schedule() {
|
|
273
|
+
local _interval_sec="$1"
|
|
274
|
+
local _minutes=$((_interval_sec / 60))
|
|
275
|
+
local _remainder=$((_interval_sec % 60))
|
|
276
|
+
|
|
277
|
+
# Clamp to at least 1 minute
|
|
278
|
+
if [[ "$_minutes" -lt 1 ]]; then
|
|
279
|
+
_minutes=1
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
# Warn if interval doesn't divide evenly into minutes
|
|
283
|
+
if [[ "$_remainder" -ne 0 ]]; then
|
|
284
|
+
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
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
# cron step values must be 1-59; */60 is invalid. Use @hourly for exactly 60 min,
|
|
288
|
+
# clamp anything above 59 to 59 (the _read_pulse_interval_seconds cap is 3600s=60min).
|
|
289
|
+
if [[ "$_minutes" -ge 60 ]]; then
|
|
290
|
+
printf '@hourly'
|
|
291
|
+
else
|
|
292
|
+
printf '*/%d * * * *' "$_minutes"
|
|
293
|
+
fi
|
|
294
|
+
return 0
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_install_supervisor_pulse() {
|
|
298
|
+
local _os="$1"
|
|
299
|
+
local pulse_label="$2"
|
|
300
|
+
local wrapper_script="$3"
|
|
301
|
+
local opencode_bin="$4"
|
|
302
|
+
local _pulse_installed="$5"
|
|
303
|
+
|
|
304
|
+
mkdir -p "$HOME/.aidevops/logs"
|
|
305
|
+
|
|
306
|
+
if [[ "$_os" == "Darwin" ]]; then
|
|
307
|
+
_install_pulse_launchd "$pulse_label" "$wrapper_script" "$opencode_bin" "$_pulse_installed"
|
|
308
|
+
return 0
|
|
309
|
+
fi
|
|
310
|
+
|
|
311
|
+
# GH#18018: read user-configured interval instead of hardcoding 120s / */2 cron
|
|
312
|
+
local _pulse_interval_sec
|
|
313
|
+
_pulse_interval_sec=$(_read_pulse_interval_seconds)
|
|
314
|
+
local _pulse_cron_schedule
|
|
315
|
+
_pulse_cron_schedule=$(_seconds_to_cron_schedule "$_pulse_interval_sec")
|
|
316
|
+
# Build a human-readable interval label: show seconds when < 60s, minutes otherwise
|
|
317
|
+
local _pulse_interval_label
|
|
318
|
+
if [[ "$_pulse_interval_sec" -lt 60 ]]; then
|
|
319
|
+
_pulse_interval_label="${_pulse_interval_sec}s"
|
|
320
|
+
else
|
|
321
|
+
_pulse_interval_label="$((_pulse_interval_sec / 60))min"
|
|
322
|
+
fi
|
|
323
|
+
|
|
324
|
+
local _pulse_timeout_sec=$((PULSE_STALE_THRESHOLD_SECONDS + 60))
|
|
325
|
+
local _pulse_env=""
|
|
326
|
+
# GH#18439 Bug 2: thread resolved runtime binary path through to the
|
|
327
|
+
# Linux env builder so OPENCODE_BIN is embedded in the systemd service
|
|
328
|
+
# file (parity with the macOS launchd plist at line 415).
|
|
329
|
+
_pulse_env=$(_build_pulse_linux_env "$opencode_bin")
|
|
330
|
+
_install_scheduler_linux \
|
|
331
|
+
"aidevops-supervisor-pulse" \
|
|
332
|
+
"aidevops: supervisor-pulse" \
|
|
333
|
+
"${_pulse_cron_schedule}" \
|
|
334
|
+
"\"${wrapper_script}\"" \
|
|
335
|
+
"${_pulse_interval_sec}" \
|
|
336
|
+
"$HOME/.aidevops/logs/pulse-wrapper.log" \
|
|
337
|
+
"$_pulse_env" \
|
|
338
|
+
"Supervisor pulse enabled (every ${_pulse_interval_label})" \
|
|
339
|
+
"Failed to install supervisor pulse scheduler. See runners.md for manual setup." \
|
|
340
|
+
"true" \
|
|
341
|
+
"false" \
|
|
342
|
+
"" \
|
|
343
|
+
"${_pulse_timeout_sec}"
|
|
129
344
|
return 0
|
|
130
345
|
}
|
|
131
346
|
|
|
@@ -162,44 +377,18 @@ setup_supervisor_pulse() {
|
|
|
162
377
|
_pulse_lower=$(echo "$_pulse_user_config" | tr '[:upper:]' '[:lower:]')
|
|
163
378
|
|
|
164
379
|
# Detect if pulse is already installed (for upgrade messaging)
|
|
165
|
-
# Uses shared helper to check
|
|
380
|
+
# Uses shared helper to check launchd, cron, and systemd (GH#17381)
|
|
166
381
|
local _pulse_installed=false
|
|
167
|
-
if
|
|
168
|
-
"Supervisor pulse" \
|
|
169
|
-
"$pulse_label" \
|
|
170
|
-
"" \
|
|
171
|
-
"pulse-wrapper" \
|
|
172
|
-
"" \
|
|
173
|
-
"" \
|
|
174
|
-
""; then
|
|
382
|
+
if _is_pulse_installed "$pulse_label"; then
|
|
175
383
|
_pulse_installed=true
|
|
176
384
|
fi
|
|
177
385
|
|
|
178
386
|
# Detect dispatch backend binary location (t1665.5 — registry-driven)
|
|
179
|
-
local opencode_bin
|
|
180
|
-
|
|
181
|
-
local _sched_rt_id _sched_bin
|
|
182
|
-
while IFS= read -r _sched_rt_id; do
|
|
183
|
-
_sched_bin=$(rt_binary "$_sched_rt_id") || continue
|
|
184
|
-
if [[ -n "$_sched_bin" ]] && command -v "$_sched_bin" &>/dev/null; then
|
|
185
|
-
opencode_bin=$(command -v "$_sched_bin")
|
|
186
|
-
break
|
|
187
|
-
fi
|
|
188
|
-
done < <(rt_list_headless)
|
|
189
|
-
fi
|
|
190
|
-
# Fallback if registry not loaded or no runtime found
|
|
191
|
-
opencode_bin="${opencode_bin:-$(command -v opencode 2>/dev/null || echo "/opt/homebrew/bin/opencode")}"
|
|
387
|
+
local opencode_bin=""
|
|
388
|
+
opencode_bin=$(_resolve_pulse_runtime_binary)
|
|
192
389
|
|
|
193
390
|
if [[ "$_do_install" == "true" ]]; then
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if [[ "$_os" == "Darwin" ]]; then
|
|
197
|
-
_install_pulse_launchd "$pulse_label" "$wrapper_script" "$opencode_bin" "$_pulse_installed"
|
|
198
|
-
elif _systemd_user_available; then
|
|
199
|
-
_install_pulse_systemd "aidevops-supervisor-pulse" "$wrapper_script"
|
|
200
|
-
else
|
|
201
|
-
_install_pulse_cron "$wrapper_script"
|
|
202
|
-
fi
|
|
391
|
+
_install_supervisor_pulse "$_os" "$pulse_label" "$wrapper_script" "$opencode_bin" "$_pulse_installed"
|
|
203
392
|
elif [[ "$_pulse_lower" == "false" && "$_pulse_installed" == "true" ]]; then
|
|
204
393
|
# User explicitly disabled but pulse is still installed — clean up
|
|
205
394
|
_uninstall_pulse "$_os" "$pulse_label"
|
|
@@ -239,40 +428,12 @@ _cleanup_old_pulse_plists() {
|
|
|
239
428
|
}
|
|
240
429
|
|
|
241
430
|
# Build XML environment variable fragment for headless model overrides.
|
|
242
|
-
#
|
|
243
|
-
#
|
|
431
|
+
# GH#17546: Model config was removed from plist embedding.
|
|
432
|
+
# GH#17769: Model routing is now derived from pool + routing table at runtime.
|
|
433
|
+
# No env vars needed — pulse-wrapper.sh reads the routing table directly.
|
|
244
434
|
_build_pulse_headless_env_xml() {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
_configured_headless_models=$(_resolve_headless_models_override)
|
|
248
|
-
_configured_pulse_model=$(_resolve_pulse_model_override)
|
|
249
|
-
|
|
250
|
-
if [[ -n "$_configured_headless_models" ]]; then
|
|
251
|
-
local _xml_headless_models
|
|
252
|
-
_xml_headless_models=$(_xml_escape "$_configured_headless_models")
|
|
253
|
-
_headless_xml_env+=$'\n'
|
|
254
|
-
_headless_xml_env+=$'\t\t<key>AIDEVOPS_HEADLESS_MODELS</key>'
|
|
255
|
-
_headless_xml_env+=$'\n'
|
|
256
|
-
_headless_xml_env+=$'\t\t'"<string>${_xml_headless_models}</string>"
|
|
257
|
-
fi
|
|
258
|
-
if [[ -n "$_configured_pulse_model" ]]; then
|
|
259
|
-
local _xml_pulse_model
|
|
260
|
-
_xml_pulse_model=$(_xml_escape "$_configured_pulse_model")
|
|
261
|
-
_headless_xml_env+=$'\n'
|
|
262
|
-
_headless_xml_env+=$'\t\t<key>PULSE_MODEL</key>'
|
|
263
|
-
_headless_xml_env+=$'\n'
|
|
264
|
-
_headless_xml_env+=$'\t\t'"<string>${_xml_pulse_model}</string>"
|
|
265
|
-
fi
|
|
266
|
-
if [[ -n "${AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST:-}" ]]; then
|
|
267
|
-
local _xml_headless_allowlist
|
|
268
|
-
_xml_headless_allowlist=$(_xml_escape "$AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST")
|
|
269
|
-
_headless_xml_env+=$'\n'
|
|
270
|
-
_headless_xml_env+=$'\t\t<key>AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST</key>'
|
|
271
|
-
_headless_xml_env+=$'\n'
|
|
272
|
-
_headless_xml_env+=$'\t\t'"<string>${_xml_headless_allowlist}</string>"
|
|
273
|
-
fi
|
|
274
|
-
|
|
275
|
-
printf '%s' "$_headless_xml_env"
|
|
435
|
+
# Intentionally empty — model config read from credentials.sh at runtime.
|
|
436
|
+
printf '%s' ""
|
|
276
437
|
return 0
|
|
277
438
|
}
|
|
278
439
|
|
|
@@ -327,7 +488,7 @@ _generate_pulse_plist_content() {
|
|
|
327
488
|
<key>PULSE_DIR</key>
|
|
328
489
|
<string>${_xml_pulse_dir}</string>
|
|
329
490
|
<key>PULSE_STALE_THRESHOLD</key>
|
|
330
|
-
<string
|
|
491
|
+
<string>${PULSE_STALE_THRESHOLD_SECONDS}</string>
|
|
331
492
|
${_headless_xml_env}
|
|
332
493
|
</dict>
|
|
333
494
|
<key>RunAtLoad</key>
|
|
@@ -365,49 +526,6 @@ _install_pulse_launchd() {
|
|
|
365
526
|
return 0
|
|
366
527
|
}
|
|
367
528
|
|
|
368
|
-
# Install supervisor pulse via cron (Linux)
|
|
369
|
-
_install_pulse_cron() {
|
|
370
|
-
local wrapper_script="$1"
|
|
371
|
-
# Shell-escape all interpolated paths to prevent command injection
|
|
372
|
-
# via $(…) or backticks if paths contain shell metacharacters
|
|
373
|
-
# PATH is managed globally by _ensure_cron_path() — do NOT set inline
|
|
374
|
-
# PATH= here, it overrides the global line and breaks nvm/bun/cargo.
|
|
375
|
-
# OPENCODE_BIN removed — resolved from PATH at runtime via command -v.
|
|
376
|
-
# See #4099 and #4240 for history.
|
|
377
|
-
local _cron_pulse_dir _cron_wrapper_script _cron_headless_env=""
|
|
378
|
-
local _configured_headless_models _configured_pulse_model
|
|
379
|
-
_configured_headless_models=$(_resolve_headless_models_override)
|
|
380
|
-
_configured_pulse_model=$(_resolve_pulse_model_override)
|
|
381
|
-
# Use neutral workspace path for PULSE_DIR (GH#5136)
|
|
382
|
-
_cron_pulse_dir=$(_cron_escape "${HOME}/.aidevops/.agent-workspace")
|
|
383
|
-
_cron_wrapper_script=$(_cron_escape "$wrapper_script")
|
|
384
|
-
if [[ -n "$_configured_headless_models" ]]; then
|
|
385
|
-
local _cron_headless_models
|
|
386
|
-
_cron_headless_models=$(_cron_escape "$_configured_headless_models")
|
|
387
|
-
_cron_headless_env+=" AIDEVOPS_HEADLESS_MODELS=${_cron_headless_models}"
|
|
388
|
-
fi
|
|
389
|
-
if [[ -n "$_configured_pulse_model" ]]; then
|
|
390
|
-
local _cron_pulse_model
|
|
391
|
-
_cron_pulse_model=$(_cron_escape "$_configured_pulse_model")
|
|
392
|
-
_cron_headless_env+=" PULSE_MODEL=${_cron_pulse_model}"
|
|
393
|
-
fi
|
|
394
|
-
if [[ -n "${AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST:-}" ]]; then
|
|
395
|
-
local _cron_headless_allowlist
|
|
396
|
-
_cron_headless_allowlist=$(_cron_escape "$AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST")
|
|
397
|
-
_cron_headless_env+=" AIDEVOPS_HEADLESS_PROVIDER_ALLOWLIST=${_cron_headless_allowlist}"
|
|
398
|
-
fi
|
|
399
|
-
(
|
|
400
|
-
crontab -l 2>/dev/null | grep -v 'aidevops: supervisor-pulse' || true
|
|
401
|
-
echo "*/2 * * * * PULSE_DIR=${_cron_pulse_dir}${_cron_headless_env} /bin/bash ${_cron_wrapper_script} >> \"\$HOME/.aidevops/logs/pulse-wrapper.log\" 2>&1 # aidevops: supervisor-pulse"
|
|
402
|
-
) | crontab - || true
|
|
403
|
-
if crontab -l 2>/dev/null | grep -qF "aidevops: supervisor-pulse"; then
|
|
404
|
-
print_info "Supervisor pulse enabled (cron, every 2 min). Disable: crontab -e and remove the supervisor-pulse line"
|
|
405
|
-
else
|
|
406
|
-
print_warning "Failed to install supervisor pulse cron entry. See runners.md for manual setup."
|
|
407
|
-
fi
|
|
408
|
-
return 0
|
|
409
|
-
}
|
|
410
|
-
|
|
411
529
|
# Check if systemd user services are available on this Linux system.
|
|
412
530
|
# Returns 0 if systemd --user is functional, 1 otherwise.
|
|
413
531
|
_systemd_user_available() {
|
|
@@ -416,54 +534,153 @@ _systemd_user_available() {
|
|
|
416
534
|
return 0
|
|
417
535
|
}
|
|
418
536
|
|
|
419
|
-
#
|
|
420
|
-
#
|
|
421
|
-
|
|
537
|
+
# Escape a value for safe embedding in a systemd unit Environment= directive.
|
|
538
|
+
# systemd interprets % as specifiers (%h, %n, %t, etc.) and spaces as
|
|
539
|
+
# key-value separators. This helper:
|
|
540
|
+
# 1. Escapes \ → \\ (must be first to avoid double-escaping)
|
|
541
|
+
# 2. Doubles % → %% (escape specifiers)
|
|
542
|
+
# 3. Escapes embedded " → \"
|
|
543
|
+
# 4. Wraps the result in "..." (handles spaces and other shell metacharacters)
|
|
544
|
+
# Usage: escaped=$(_systemd_escape "$value")
|
|
545
|
+
_systemd_escape() {
|
|
546
|
+
local _val="$1"
|
|
547
|
+
# Step 1: escape backslashes
|
|
548
|
+
_val="${_val//\\/\\\\}"
|
|
549
|
+
# Step 2: escape % specifiers
|
|
550
|
+
_val="${_val//%/%%}"
|
|
551
|
+
# Step 3: escape embedded double-quotes
|
|
552
|
+
_val="${_val//\"/\\\"}"
|
|
553
|
+
# Step 4: wrap in double-quotes
|
|
554
|
+
printf '"%s"' "$_val"
|
|
555
|
+
return 0
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
# Build systemd Environment= lines from newline-separated KEY=VALUE pairs.
|
|
559
|
+
# Always appends HOME and PATH for parity with launchd and cron execution.
|
|
560
|
+
_scheduler_systemd_env_lines() {
|
|
561
|
+
local env_vars="$1"
|
|
562
|
+
local _env_lines=""
|
|
563
|
+
|
|
564
|
+
if [[ -n "$env_vars" ]]; then
|
|
565
|
+
while IFS= read -r _kv; do
|
|
566
|
+
[[ -z "$_kv" ]] && continue
|
|
567
|
+
local _key="${_kv%%=*}"
|
|
568
|
+
local _raw_val="${_kv#*=}"
|
|
569
|
+
local _escaped_val
|
|
570
|
+
_escaped_val=$(_systemd_escape "$_raw_val")
|
|
571
|
+
_env_lines+="Environment=${_key}=${_escaped_val}"$'\n'
|
|
572
|
+
done <<<"$env_vars"
|
|
573
|
+
fi
|
|
574
|
+
|
|
575
|
+
_env_lines+="Environment=HOME=$(_systemd_escape "$HOME")"$'\n'
|
|
576
|
+
_env_lines+="Environment=PATH=$(_systemd_escape "$PATH")"$'\n'
|
|
577
|
+
printf '%s' "$_env_lines"
|
|
578
|
+
return 0
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
# Build inline cron environment assignments from newline-separated KEY=VALUE pairs.
|
|
582
|
+
_scheduler_cron_env_prefix() {
|
|
583
|
+
local env_vars="$1"
|
|
584
|
+
local _env_prefix=""
|
|
585
|
+
|
|
586
|
+
if [[ -n "$env_vars" ]]; then
|
|
587
|
+
while IFS= read -r _kv; do
|
|
588
|
+
[[ -z "$_kv" ]] && continue
|
|
589
|
+
local _key="${_kv%%=*}"
|
|
590
|
+
local _raw_val="${_kv#*=}"
|
|
591
|
+
local _escaped_val
|
|
592
|
+
_escaped_val=$(_cron_escape "$_raw_val")
|
|
593
|
+
_env_prefix+="${_key}=${_escaped_val} "
|
|
594
|
+
done <<<"$env_vars"
|
|
595
|
+
fi
|
|
596
|
+
|
|
597
|
+
printf '%s' "$_env_prefix"
|
|
598
|
+
return 0
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
# Install a generic scheduler via systemd user timer (Linux with systemd).
|
|
602
|
+
# Args:
|
|
603
|
+
# $1 = service_name (e.g. "aidevops-stats-wrapper")
|
|
604
|
+
# $2 = exec_command (shell command run via /bin/bash -lc)
|
|
605
|
+
# $3 = interval_sec (OnUnitActiveSec interval in seconds; may be empty for calendar-only)
|
|
606
|
+
# $4 = log_file (absolute path to log file)
|
|
607
|
+
# $5 = env_vars (newline-separated KEY=VALUE pairs, may be empty)
|
|
608
|
+
# $6 = run_at_load ("true" or "false")
|
|
609
|
+
# $7 = low_priority ("true" or "false")
|
|
610
|
+
# $8 = on_calendar (optional systemd OnCalendar spec)
|
|
611
|
+
# $9 = timeout_sec (optional TimeoutStartSec; defaults to interval_sec)
|
|
612
|
+
# Returns 0 on success, 1 if systemd enable fails (caller should fall back to cron).
|
|
613
|
+
_install_scheduler_systemd() {
|
|
422
614
|
local service_name="$1"
|
|
423
|
-
local
|
|
615
|
+
local exec_command="$2"
|
|
616
|
+
local interval_sec="$3"
|
|
617
|
+
local log_file="$4"
|
|
618
|
+
local env_vars="$5"
|
|
619
|
+
local run_at_load="$6"
|
|
620
|
+
local low_priority="$7"
|
|
621
|
+
local on_calendar="$8"
|
|
622
|
+
local timeout_sec="$9"
|
|
424
623
|
local service_dir="$HOME/.config/systemd/user"
|
|
425
624
|
local service_file="${service_dir}/${service_name}.service"
|
|
426
625
|
local timer_file="${service_dir}/${service_name}.timer"
|
|
427
626
|
|
|
428
627
|
mkdir -p "$service_dir"
|
|
429
628
|
|
|
430
|
-
#
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
629
|
+
# GH#18439 Bug 1: command substitution strips trailing newlines, which
|
|
630
|
+
# would run the final Environment=PATH=... into the following
|
|
631
|
+
# StandardOutput=... directive on the same line. Use a sentinel ('x')
|
|
632
|
+
# to preserve the trailing newline that _scheduler_systemd_env_lines
|
|
633
|
+
# always emits.
|
|
634
|
+
local _env_lines
|
|
635
|
+
_env_lines=$(
|
|
636
|
+
_scheduler_systemd_env_lines "$env_vars"
|
|
637
|
+
printf 'x'
|
|
638
|
+
)
|
|
639
|
+
_env_lines="${_env_lines%x}"
|
|
640
|
+
|
|
641
|
+
if [[ -z "$timeout_sec" ]]; then
|
|
642
|
+
timeout_sec="$interval_sec"
|
|
437
643
|
fi
|
|
438
|
-
if [[ -
|
|
439
|
-
|
|
644
|
+
if [[ -z "$timeout_sec" ]]; then
|
|
645
|
+
timeout_sec="3600"
|
|
440
646
|
fi
|
|
441
|
-
|
|
442
|
-
|
|
647
|
+
|
|
648
|
+
local _service_extra=""
|
|
649
|
+
if [[ "$low_priority" == "true" ]]; then
|
|
650
|
+
_service_extra+="Nice=10"$'\n'
|
|
651
|
+
_service_extra+="IOSchedulingClass=idle"$'\n'
|
|
443
652
|
fi
|
|
444
|
-
_env_lines+="Environment=PULSE_DIR=${HOME}/.aidevops/.agent-workspace\n"
|
|
445
|
-
_env_lines+="Environment=HOME=${HOME}\n"
|
|
446
653
|
|
|
447
|
-
# Write the service unit
|
|
448
654
|
printf '%s' "[Unit]
|
|
449
|
-
Description=aidevops
|
|
655
|
+
Description=aidevops ${service_name}
|
|
450
656
|
After=network.target
|
|
451
657
|
|
|
452
658
|
[Service]
|
|
453
659
|
Type=oneshot
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
660
|
+
KillMode=process
|
|
661
|
+
ExecStart=/bin/bash -lc $(_systemd_escape "$exec_command")
|
|
662
|
+
TimeoutStartSec=${timeout_sec}
|
|
663
|
+
${_service_extra}${_env_lines}StandardOutput=$(_systemd_escape "append:${log_file}")
|
|
664
|
+
StandardError=$(_systemd_escape "append:${log_file}")
|
|
457
665
|
" >"$service_file"
|
|
458
666
|
|
|
459
|
-
|
|
667
|
+
local _timer_lines=""
|
|
668
|
+
if [[ "$run_at_load" == "true" ]]; then
|
|
669
|
+
_timer_lines+="OnActiveSec=10s"$'\n'
|
|
670
|
+
fi
|
|
671
|
+
if [[ -n "$interval_sec" ]]; then
|
|
672
|
+
_timer_lines+="OnBootSec=${interval_sec}"$'\n'
|
|
673
|
+
_timer_lines+="OnUnitActiveSec=${interval_sec}"$'\n'
|
|
674
|
+
fi
|
|
675
|
+
if [[ -n "$on_calendar" ]]; then
|
|
676
|
+
_timer_lines+="OnCalendar=${on_calendar}"$'\n'
|
|
677
|
+
fi
|
|
678
|
+
|
|
460
679
|
printf '%s' "[Unit]
|
|
461
|
-
Description=aidevops
|
|
680
|
+
Description=aidevops ${service_name} Timer
|
|
462
681
|
|
|
463
682
|
[Timer]
|
|
464
|
-
|
|
465
|
-
OnUnitActiveSec=2min
|
|
466
|
-
Persistent=true
|
|
683
|
+
${_timer_lines}Persistent=true
|
|
467
684
|
|
|
468
685
|
[Install]
|
|
469
686
|
WantedBy=timers.target
|
|
@@ -471,11 +688,148 @@ WantedBy=timers.target
|
|
|
471
688
|
|
|
472
689
|
systemctl --user daemon-reload 2>/dev/null || true
|
|
473
690
|
if systemctl --user enable --now "${service_name}.timer" 2>/dev/null; then
|
|
474
|
-
|
|
475
|
-
|
|
691
|
+
return 0
|
|
692
|
+
fi
|
|
693
|
+
return 1
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
# Install a generic cron entry.
|
|
697
|
+
# Args: $1=cron_tag, $2=cron_schedule, $3=exec_command, $4=log_file, $5=env_vars
|
|
698
|
+
_install_scheduler_cron() {
|
|
699
|
+
local cron_tag="$1"
|
|
700
|
+
local cron_schedule="$2"
|
|
701
|
+
local exec_command="$3"
|
|
702
|
+
local log_file="$4"
|
|
703
|
+
local env_vars="$5"
|
|
704
|
+
local _cron_exec
|
|
705
|
+
local _cron_log
|
|
706
|
+
local _env_prefix
|
|
707
|
+
|
|
708
|
+
_env_prefix=$(_scheduler_cron_env_prefix "$env_vars")
|
|
709
|
+
_cron_exec=$(_cron_escape "$exec_command")
|
|
710
|
+
_cron_log=$(_cron_escape "$log_file")
|
|
711
|
+
|
|
712
|
+
(
|
|
713
|
+
crontab -l 2>/dev/null | grep -vF "${cron_tag}" || true
|
|
714
|
+
echo "${cron_schedule} ${_env_prefix}/bin/bash -lc ${_cron_exec} >> ${_cron_log} 2>&1 # ${cron_tag}"
|
|
715
|
+
) | crontab - 2>/dev/null || true
|
|
716
|
+
return 0
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
# Dispatcher: install a scheduler on Linux, preferring systemd over cron.
|
|
720
|
+
# Args:
|
|
721
|
+
# $1 = service_name (systemd service name, e.g. "aidevops-stats-wrapper")
|
|
722
|
+
# $2 = cron_tag (comment tag for cron line, e.g. "aidevops: stats-wrapper")
|
|
723
|
+
# $3 = cron_schedule (cron schedule expression, e.g. "*/15 * * * *")
|
|
724
|
+
# $4 = exec_command (shell command run via /bin/bash -lc)
|
|
725
|
+
# $5 = interval_sec (systemd OnUnitActiveSec in seconds; may be empty for calendar-only)
|
|
726
|
+
# $6 = log_file (absolute path to log file)
|
|
727
|
+
# $7 = env_vars (newline-separated KEY=VALUE pairs for systemd/cron, may be empty)
|
|
728
|
+
# $8 = success_msg (message to print on success)
|
|
729
|
+
# $9 = fail_msg (message to print on failure)
|
|
730
|
+
# $10 = run_at_load ("true" or "false")
|
|
731
|
+
# $11 = low_priority ("true" or "false")
|
|
732
|
+
# $12 = on_calendar (optional systemd OnCalendar spec)
|
|
733
|
+
# $13 = timeout_sec (optional TimeoutStartSec)
|
|
734
|
+
# Returns 0 always (failures are warnings, not fatal).
|
|
735
|
+
_install_scheduler_linux() {
|
|
736
|
+
local service_name="$1"
|
|
737
|
+
local cron_tag="$2"
|
|
738
|
+
local cron_schedule="$3"
|
|
739
|
+
local exec_command="$4"
|
|
740
|
+
local interval_sec="$5"
|
|
741
|
+
local log_file="$6"
|
|
742
|
+
local env_vars="$7"
|
|
743
|
+
local success_msg="$8"
|
|
744
|
+
local fail_msg="$9"
|
|
745
|
+
local run_at_load="${10}"
|
|
746
|
+
local low_priority="${11}"
|
|
747
|
+
local on_calendar="${12:-}"
|
|
748
|
+
local timeout_sec="${13:-}"
|
|
749
|
+
|
|
750
|
+
if _systemd_user_available; then
|
|
751
|
+
if _install_scheduler_systemd \
|
|
752
|
+
"$service_name" \
|
|
753
|
+
"$exec_command" \
|
|
754
|
+
"$interval_sec" \
|
|
755
|
+
"$log_file" \
|
|
756
|
+
"$env_vars" \
|
|
757
|
+
"$run_at_load" \
|
|
758
|
+
"$low_priority" \
|
|
759
|
+
"$on_calendar" \
|
|
760
|
+
"$timeout_sec"; then
|
|
761
|
+
print_info "${success_msg} (systemd user timer)"
|
|
762
|
+
# After systemd install succeeds, remove any pre-existing cron entry
|
|
763
|
+
# to prevent dual-execution (GH#17695 Finding A)
|
|
764
|
+
if command -v crontab >/dev/null 2>&1; then
|
|
765
|
+
local current_cron
|
|
766
|
+
current_cron=$(crontab -l 2>/dev/null) || current_cron=""
|
|
767
|
+
if [[ -n "$current_cron" ]] && echo "$current_cron" | grep -qF "$cron_tag"; then
|
|
768
|
+
echo "$current_cron" | grep -vF "$cron_tag" | crontab -
|
|
769
|
+
echo "[schedulers] Removed pre-existing cron entry for $cron_tag (migrated to systemd)"
|
|
770
|
+
fi
|
|
771
|
+
fi
|
|
772
|
+
else
|
|
773
|
+
print_warning "systemd enable failed for ${service_name} — falling back to cron"
|
|
774
|
+
_install_scheduler_cron "$cron_tag" "$cron_schedule" "$exec_command" "$log_file" "$env_vars"
|
|
775
|
+
if crontab -l 2>/dev/null | grep -qF "${cron_tag}" 2>/dev/null; then
|
|
776
|
+
print_info "${success_msg} (cron fallback)"
|
|
777
|
+
else
|
|
778
|
+
print_warning "${fail_msg}"
|
|
779
|
+
fi
|
|
780
|
+
fi
|
|
781
|
+
else
|
|
782
|
+
_install_scheduler_cron "$cron_tag" "$cron_schedule" "$exec_command" "$log_file" "$env_vars"
|
|
783
|
+
if crontab -l 2>/dev/null | grep -qF "${cron_tag}" 2>/dev/null; then
|
|
784
|
+
print_info "${success_msg} (cron)"
|
|
785
|
+
else
|
|
786
|
+
print_warning "${fail_msg}"
|
|
787
|
+
fi
|
|
788
|
+
fi
|
|
789
|
+
return 0
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
# Uninstall a scheduler across all backends (launchd/systemd/cron).
|
|
793
|
+
# Args:
|
|
794
|
+
# $1 = os (output of uname -s)
|
|
795
|
+
# $2 = launchd_label (e.g. "sh.aidevops.stats-wrapper")
|
|
796
|
+
# $3 = systemd_name (e.g. "aidevops-stats-wrapper")
|
|
797
|
+
# $4 = cron_tag (grep pattern for cron line, e.g. "aidevops: stats-wrapper")
|
|
798
|
+
# $5 = success_msg (message to print on removal)
|
|
799
|
+
# Returns 0 always.
|
|
800
|
+
_uninstall_scheduler() {
|
|
801
|
+
local _os="$1"
|
|
802
|
+
local launchd_label="$2"
|
|
803
|
+
local systemd_name="$3"
|
|
804
|
+
local cron_tag="$4"
|
|
805
|
+
local success_msg="$5"
|
|
806
|
+
|
|
807
|
+
if [[ "$_os" == "Darwin" ]]; then
|
|
808
|
+
local _plist="$HOME/Library/LaunchAgents/${launchd_label}.plist"
|
|
809
|
+
if _launchd_has_agent "$launchd_label"; then
|
|
810
|
+
launchctl unload "$_plist" 2>/dev/null || true
|
|
811
|
+
rm -f "$_plist"
|
|
812
|
+
print_info "${success_msg} (launchd agent removed)"
|
|
813
|
+
fi
|
|
476
814
|
else
|
|
477
|
-
|
|
478
|
-
|
|
815
|
+
# Check and remove from ALL backends sequentially, not just the first
|
|
816
|
+
# match. Prevents orphan entries when migrating between systemd and cron
|
|
817
|
+
# (GH#17695 Finding A).
|
|
818
|
+
if _systemd_user_available && systemctl --user is-enabled "${systemd_name}.timer" >/dev/null 2>&1; then
|
|
819
|
+
systemctl --user disable --now "${systemd_name}.timer" 2>/dev/null || true
|
|
820
|
+
rm -f "$HOME/.config/systemd/user/${systemd_name}.service"
|
|
821
|
+
rm -f "$HOME/.config/systemd/user/${systemd_name}.timer"
|
|
822
|
+
systemctl --user daemon-reload 2>/dev/null || true
|
|
823
|
+
print_info "${success_msg} (systemd timer removed)"
|
|
824
|
+
fi
|
|
825
|
+
if command -v crontab >/dev/null 2>&1; then
|
|
826
|
+
local current_cron
|
|
827
|
+
current_cron=$(crontab -l 2>/dev/null) || current_cron=""
|
|
828
|
+
if [[ -n "$current_cron" ]] && echo "$current_cron" | grep -qF "${cron_tag}"; then
|
|
829
|
+
echo "$current_cron" | grep -vF "${cron_tag}" | crontab - 2>/dev/null || true
|
|
830
|
+
print_info "${success_msg} (cron entry removed)"
|
|
831
|
+
fi
|
|
832
|
+
fi
|
|
479
833
|
fi
|
|
480
834
|
return 0
|
|
481
835
|
}
|
|
@@ -513,6 +867,7 @@ _uninstall_pulse() {
|
|
|
513
867
|
# Setup stats-wrapper scheduler — runs quality sweep and health issue updates
|
|
514
868
|
# separately from the pulse (t1429). Only installed when the supervisor
|
|
515
869
|
# pulse is enabled (stats are useless without it).
|
|
870
|
+
# macOS: launchd plist (every 15 min) | Linux: systemd timer or cron (every 15 min)
|
|
516
871
|
setup_stats_wrapper() {
|
|
517
872
|
local _pulse_lower="$1"
|
|
518
873
|
# Use effective pulse state (PULSE_ENABLED) if available; fall back to consent string.
|
|
@@ -520,6 +875,8 @@ setup_stats_wrapper() {
|
|
|
520
875
|
local _pulse_effective="${PULSE_ENABLED:-$_pulse_lower}"
|
|
521
876
|
local stats_script="$HOME/.aidevops/agents/scripts/stats-wrapper.sh"
|
|
522
877
|
local stats_label="com.aidevops.aidevops-stats-wrapper"
|
|
878
|
+
local stats_systemd="aidevops-stats-wrapper"
|
|
879
|
+
local stats_log="$HOME/.aidevops/logs/stats.log"
|
|
523
880
|
if [[ -x "$stats_script" ]] && [[ "$_pulse_effective" == "true" ]]; then
|
|
524
881
|
# Always regenerate to pick up config/format changes (matches pulse behavior)
|
|
525
882
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
@@ -570,31 +927,27 @@ PLIST
|
|
|
570
927
|
print_warning "Failed to load stats wrapper LaunchAgent"
|
|
571
928
|
fi
|
|
572
929
|
else
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
930
|
+
_install_scheduler_linux \
|
|
931
|
+
"$stats_systemd" \
|
|
932
|
+
"aidevops: stats-wrapper" \
|
|
933
|
+
"*/15 * * * *" \
|
|
934
|
+
"\"${stats_script}\"" \
|
|
935
|
+
"900" \
|
|
936
|
+
"$stats_log" \
|
|
937
|
+
"" \
|
|
938
|
+
"Stats wrapper enabled (every 15 min)" \
|
|
939
|
+
"Failed to install stats wrapper scheduler" \
|
|
940
|
+
"true" \
|
|
941
|
+
"false"
|
|
582
942
|
fi
|
|
583
943
|
elif [[ "$_pulse_effective" == "false" ]]; then
|
|
584
944
|
# Remove stats scheduler if pulse is disabled
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
fi
|
|
592
|
-
else
|
|
593
|
-
if crontab -l 2>/dev/null | grep -qF "aidevops: stats-wrapper"; then
|
|
594
|
-
crontab -l 2>/dev/null | grep -v 'aidevops: stats-wrapper' | crontab - || true
|
|
595
|
-
print_info "Stats wrapper disabled (cron entry removed — pulse is off)"
|
|
596
|
-
fi
|
|
597
|
-
fi
|
|
945
|
+
_uninstall_scheduler \
|
|
946
|
+
"$(uname -s)" \
|
|
947
|
+
"$stats_label" \
|
|
948
|
+
"$stats_systemd" \
|
|
949
|
+
"aidevops: stats-wrapper" \
|
|
950
|
+
"Stats wrapper disabled (pulse is off)"
|
|
598
951
|
fi
|
|
599
952
|
return 0
|
|
600
953
|
}
|
|
@@ -602,27 +955,22 @@ PLIST
|
|
|
602
955
|
# Setup failure miner — mines GitHub CI failure notifications for systemic patterns
|
|
603
956
|
# and auto-files root-cause issues. Runs as a pure bash script (no LLM needed).
|
|
604
957
|
# Installed when pulse is enabled and the helper script exists.
|
|
605
|
-
# macOS: launchd plist (hourly at :15) | Linux: cron (hourly at :15)
|
|
958
|
+
# macOS: launchd plist (hourly at :15) | Linux: systemd timer or cron (hourly at :15)
|
|
606
959
|
setup_failure_miner() {
|
|
607
960
|
local _pulse_lower="$1"
|
|
608
961
|
local _pulse_effective="${PULSE_ENABLED:-$_pulse_lower}"
|
|
609
962
|
local miner_script="$HOME/.aidevops/agents/scripts/gh-failure-miner-helper.sh"
|
|
610
963
|
local miner_label="sh.aidevops.routine-gh-failure-miner"
|
|
964
|
+
local miner_systemd="aidevops-gh-failure-miner"
|
|
965
|
+
local miner_log="$HOME/.aidevops/logs/routine-gh-failure-miner.log"
|
|
611
966
|
if [[ ! -x "$miner_script" ]] || [[ "$_pulse_effective" != "true" ]]; then
|
|
612
967
|
# Remove scheduler if pulse is disabled or script missing
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
fi
|
|
620
|
-
else
|
|
621
|
-
if crontab -l 2>/dev/null | grep -qF "aidevops: gh-failure-miner"; then
|
|
622
|
-
crontab -l 2>/dev/null | grep -v 'aidevops: gh-failure-miner' | crontab - || true
|
|
623
|
-
print_info "Failure miner disabled (cron entry removed)"
|
|
624
|
-
fi
|
|
625
|
-
fi
|
|
968
|
+
_uninstall_scheduler \
|
|
969
|
+
"$(uname -s)" \
|
|
970
|
+
"$miner_label" \
|
|
971
|
+
"$miner_systemd" \
|
|
972
|
+
"aidevops: gh-failure-miner" \
|
|
973
|
+
"Failure miner disabled (pulse is off or script missing)"
|
|
626
974
|
return 0
|
|
627
975
|
fi
|
|
628
976
|
|
|
@@ -635,7 +983,7 @@ setup_failure_miner() {
|
|
|
635
983
|
_xml_miner_script=$(_xml_escape "$miner_script")
|
|
636
984
|
_xml_miner_home=$(_xml_escape "$HOME")
|
|
637
985
|
_xml_miner_path=$(_xml_escape "/bin:/usr/bin:/usr/local/bin:/opt/homebrew/bin:${PATH}")
|
|
638
|
-
_xml_miner_log=$(_xml_escape "$
|
|
986
|
+
_xml_miner_log=$(_xml_escape "$miner_log")
|
|
639
987
|
|
|
640
988
|
local miner_plist_content
|
|
641
989
|
miner_plist_content=$(
|
|
@@ -692,15 +1040,19 @@ MINER_PLIST
|
|
|
692
1040
|
print_warning "Failed to load failure miner LaunchAgent"
|
|
693
1041
|
fi
|
|
694
1042
|
else
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
1043
|
+
_install_scheduler_linux \
|
|
1044
|
+
"$miner_systemd" \
|
|
1045
|
+
"aidevops: gh-failure-miner" \
|
|
1046
|
+
"15 * * * *" \
|
|
1047
|
+
"\"${miner_script}\" create-issues --since-hours 24 --pulse-repos --systemic-threshold 2 --max-issues 3 --label auto-dispatch" \
|
|
1048
|
+
"3600" \
|
|
1049
|
+
"$miner_log" \
|
|
1050
|
+
"" \
|
|
1051
|
+
"Failure miner enabled (hourly at :15)" \
|
|
1052
|
+
"Failed to install failure miner scheduler" \
|
|
1053
|
+
"false" \
|
|
1054
|
+
"false" \
|
|
1055
|
+
"*-*-* *:15:00"
|
|
704
1056
|
fi
|
|
705
1057
|
return 0
|
|
706
1058
|
}
|
|
@@ -708,10 +1060,12 @@ MINER_PLIST
|
|
|
708
1060
|
# Setup process guard — kills runaway AI processes (ShellCheck bloat, stuck workers)
|
|
709
1061
|
# before they exhaust memory and cause kernel panics. Always installed when the
|
|
710
1062
|
# script exists; no consent needed (safety net, not autonomous action).
|
|
711
|
-
# macOS: launchd plist (30s interval, RunAtLoad=true) | Linux: cron (every minute)
|
|
1063
|
+
# macOS: launchd plist (30s interval, RunAtLoad=true) | Linux: systemd timer or cron (every minute)
|
|
712
1064
|
setup_process_guard() {
|
|
713
1065
|
local guard_script="$HOME/.aidevops/agents/scripts/process-guard-helper.sh"
|
|
714
1066
|
local guard_label="sh.aidevops.process-guard"
|
|
1067
|
+
local guard_systemd="aidevops-process-guard"
|
|
1068
|
+
local guard_log="$HOME/.aidevops/logs/process-guard.log"
|
|
715
1069
|
if [[ ! -x "$guard_script" ]]; then
|
|
716
1070
|
return 0
|
|
717
1071
|
fi
|
|
@@ -779,20 +1133,22 @@ GUARD_PLIST
|
|
|
779
1133
|
print_warning "Failed to load process guard LaunchAgent"
|
|
780
1134
|
fi
|
|
781
1135
|
else
|
|
782
|
-
# Linux: cron
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1136
|
+
# Linux: systemd timer (30s) or cron fallback (every minute — cron minimum granularity)
|
|
1137
|
+
_install_scheduler_linux \
|
|
1138
|
+
"$guard_systemd" \
|
|
1139
|
+
"aidevops: process-guard" \
|
|
1140
|
+
"* * * * *" \
|
|
1141
|
+
"\"${guard_script}\" kill-runaways" \
|
|
1142
|
+
"30" \
|
|
1143
|
+
"$guard_log" \
|
|
1144
|
+
"SHELLCHECK_RSS_LIMIT_KB=524288
|
|
1145
|
+
SHELLCHECK_RUNTIME_LIMIT=120
|
|
1146
|
+
CHILD_RSS_LIMIT_KB=8388608
|
|
1147
|
+
CHILD_RUNTIME_LIMIT=7200" \
|
|
1148
|
+
"Process guard enabled (every 30s)" \
|
|
1149
|
+
"Failed to install process guard scheduler" \
|
|
1150
|
+
"true" \
|
|
1151
|
+
"false"
|
|
796
1152
|
fi
|
|
797
1153
|
return 0
|
|
798
1154
|
}
|
|
@@ -801,10 +1157,12 @@ GUARD_PLIST
|
|
|
801
1157
|
# Monitors individual process RSS, runtime, session count, and aggregate memory.
|
|
802
1158
|
# Auto-kills runaway ShellCheck (language server respawns them). Always installed
|
|
803
1159
|
# when the script exists; no consent needed (safety net, not autonomous action).
|
|
804
|
-
# macOS: launchd plist (60s interval, RunAtLoad=true) | Linux: cron (every minute)
|
|
1160
|
+
# macOS: launchd plist (60s interval, RunAtLoad=true) | Linux: systemd timer or cron (every minute)
|
|
805
1161
|
setup_memory_pressure_monitor() {
|
|
806
1162
|
local monitor_script="$HOME/.aidevops/agents/scripts/memory-pressure-monitor.sh"
|
|
807
1163
|
local monitor_label="sh.aidevops.memory-pressure-monitor"
|
|
1164
|
+
local monitor_systemd="aidevops-memory-pressure-monitor"
|
|
1165
|
+
local monitor_log="$HOME/.aidevops/logs/memory-pressure-launchd.log"
|
|
808
1166
|
if [[ ! -x "$monitor_script" ]]; then
|
|
809
1167
|
return 0
|
|
810
1168
|
fi
|
|
@@ -867,16 +1225,19 @@ MONITOR_PLIST
|
|
|
867
1225
|
print_warning "Failed to load memory pressure monitor LaunchAgent"
|
|
868
1226
|
fi
|
|
869
1227
|
else
|
|
870
|
-
# Linux: cron
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
1228
|
+
# Linux: systemd timer (60s) or cron fallback (every minute — cron minimum granularity)
|
|
1229
|
+
_install_scheduler_linux \
|
|
1230
|
+
"$monitor_systemd" \
|
|
1231
|
+
"aidevops: memory-pressure-monitor" \
|
|
1232
|
+
"* * * * *" \
|
|
1233
|
+
"\"${monitor_script}\"" \
|
|
1234
|
+
"60" \
|
|
1235
|
+
"$monitor_log" \
|
|
1236
|
+
"" \
|
|
1237
|
+
"Memory pressure monitor enabled (every 60s)" \
|
|
1238
|
+
"Failed to install memory pressure monitor scheduler" \
|
|
1239
|
+
"true" \
|
|
1240
|
+
"true"
|
|
880
1241
|
fi
|
|
881
1242
|
return 0
|
|
882
1243
|
}
|
|
@@ -884,10 +1245,12 @@ MONITOR_PLIST
|
|
|
884
1245
|
# Setup screen time snapshot — captures daily screen time for contributor stats.
|
|
885
1246
|
# Accumulates data in screen-time.jsonl (macOS Knowledge DB retains only ~28 days).
|
|
886
1247
|
# Always installed when the script exists; no consent needed (data collection only).
|
|
887
|
-
# macOS: launchd plist (every 6h, RunAtLoad=true) | Linux: cron (every 6h)
|
|
1248
|
+
# macOS: launchd plist (every 6h, RunAtLoad=true) | Linux: systemd timer or cron (every 6h)
|
|
888
1249
|
setup_screen_time_snapshot() {
|
|
889
1250
|
local st_script="$HOME/.aidevops/agents/scripts/screen-time-helper.sh"
|
|
890
1251
|
local st_label="sh.aidevops.screen-time-snapshot"
|
|
1252
|
+
local st_systemd="aidevops-screen-time-snapshot"
|
|
1253
|
+
local st_log="$HOME/.aidevops/.agent-workspace/logs/screen-time-snapshot.log"
|
|
891
1254
|
if [[ ! -x "$st_script" ]]; then
|
|
892
1255
|
return 0
|
|
893
1256
|
fi
|
|
@@ -951,18 +1314,19 @@ ST_PLIST
|
|
|
951
1314
|
print_warning "Failed to load screen time snapshot LaunchAgent"
|
|
952
1315
|
fi
|
|
953
1316
|
else
|
|
954
|
-
# Linux:
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1317
|
+
# Linux: systemd timer (every 6h) or cron fallback
|
|
1318
|
+
_install_scheduler_linux \
|
|
1319
|
+
"$st_systemd" \
|
|
1320
|
+
"aidevops: screen-time-snapshot" \
|
|
1321
|
+
"0 */6 * * *" \
|
|
1322
|
+
"\"${st_script}\" snapshot" \
|
|
1323
|
+
"21600" \
|
|
1324
|
+
"$st_log" \
|
|
1325
|
+
"" \
|
|
1326
|
+
"Screen time snapshot enabled (every 6h)" \
|
|
1327
|
+
"Failed to install screen time snapshot scheduler" \
|
|
1328
|
+
"true" \
|
|
1329
|
+
"true"
|
|
966
1330
|
fi
|
|
967
1331
|
return 0
|
|
968
1332
|
}
|
|
@@ -1057,29 +1421,30 @@ CW_PLIST
|
|
|
1057
1421
|
return 0
|
|
1058
1422
|
}
|
|
1059
1423
|
|
|
1060
|
-
# Install contribution watch via cron (Linux).
|
|
1424
|
+
# Install contribution watch via systemd or cron (Linux).
|
|
1061
1425
|
# Args: $1=script path, $2=log dir
|
|
1062
|
-
|
|
1426
|
+
_install_cw_linux() {
|
|
1063
1427
|
local cw_script="$1"
|
|
1064
1428
|
local _cw_log_dir="$2"
|
|
1065
|
-
local
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1429
|
+
local cw_systemd="aidevops-contribution-watch"
|
|
1430
|
+
_install_scheduler_linux \
|
|
1431
|
+
"$cw_systemd" \
|
|
1432
|
+
"aidevops: contribution-watch" \
|
|
1433
|
+
"0 * * * *" \
|
|
1434
|
+
"\"${cw_script}\" scan" \
|
|
1435
|
+
"3600" \
|
|
1436
|
+
"${_cw_log_dir}/contribution-watch.log" \
|
|
1437
|
+
"" \
|
|
1438
|
+
"Contribution watch enabled (hourly scan)" \
|
|
1439
|
+
"Failed to install contribution watch scheduler" \
|
|
1440
|
+
"false" \
|
|
1441
|
+
"true"
|
|
1077
1442
|
return 0
|
|
1078
1443
|
}
|
|
1079
1444
|
|
|
1080
1445
|
# Setup contribution watch — monitors external issues/PRs for new activity (t1554).
|
|
1081
1446
|
# Auto-seeds on first run (discovers authored/commented issues/PRs), then installs
|
|
1082
|
-
# a launchd/cron job to scan periodically. Requires gh CLI authenticated.
|
|
1447
|
+
# a launchd/systemd/cron job to scan periodically. Requires gh CLI authenticated.
|
|
1083
1448
|
# No consent needed — this is passive monitoring (read-only notifications API),
|
|
1084
1449
|
# not autonomous action. Comment bodies are never processed by LLM in automated context.
|
|
1085
1450
|
# Respects config: aidevops config set orchestration.contribution_watch false
|
|
@@ -1110,7 +1475,7 @@ setup_contribution_watch() {
|
|
|
1110
1475
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
1111
1476
|
_install_cw_launchd "$cw_label" "$cw_script" "$_cw_log_dir"
|
|
1112
1477
|
else
|
|
1113
|
-
|
|
1478
|
+
_install_cw_linux "$cw_script" "$_cw_log_dir"
|
|
1114
1479
|
fi
|
|
1115
1480
|
return 0
|
|
1116
1481
|
}
|
|
@@ -1134,43 +1499,42 @@ setup_draft_responses() {
|
|
|
1134
1499
|
# Setup profile README — auto-create repo and seed README if not already set up.
|
|
1135
1500
|
# Requires gh CLI authenticated. Creates username/username repo, seeds README
|
|
1136
1501
|
# with stat markers, registers in repos.json with priority: "profile".
|
|
1137
|
-
|
|
1138
|
-
local pr_script="$
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
return 0
|
|
1502
|
+
_profile_readme_ready() {
|
|
1503
|
+
local pr_script="$1"
|
|
1504
|
+
if ! [[ -x "$pr_script" ]]; then
|
|
1505
|
+
return 1
|
|
1142
1506
|
fi
|
|
1507
|
+
if ! command -v gh &>/dev/null; then
|
|
1508
|
+
return 1
|
|
1509
|
+
fi
|
|
1510
|
+
if ! gh auth status &>/dev/null; then
|
|
1511
|
+
return 1
|
|
1512
|
+
fi
|
|
1513
|
+
return 0
|
|
1514
|
+
}
|
|
1143
1515
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
# - Fresh installs (no profile repo)
|
|
1147
|
-
# - Missing markers (injects them into existing README)
|
|
1148
|
-
# - Diverged history (repo deleted and recreated on GitHub)
|
|
1149
|
-
# - Already-initialized repos (returns early with no changes)
|
|
1516
|
+
_run_profile_readme_init() {
|
|
1517
|
+
local pr_script="$1"
|
|
1150
1518
|
print_info "Checking GitHub profile README..."
|
|
1151
1519
|
if bash "$pr_script" init; then
|
|
1152
1520
|
print_info "Profile README ready."
|
|
1153
1521
|
else
|
|
1154
1522
|
print_warning "Profile README setup failed (non-fatal, skipping)"
|
|
1155
1523
|
fi
|
|
1524
|
+
return 0
|
|
1525
|
+
}
|
|
1156
1526
|
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
_xml_pr_script=$(_xml_escape "$pr_script")
|
|
1169
|
-
_xml_pr_home=$(_xml_escape "$HOME")
|
|
1170
|
-
|
|
1171
|
-
local pr_plist_content
|
|
1172
|
-
pr_plist_content=$(
|
|
1173
|
-
cat <<PR_PLIST
|
|
1527
|
+
_install_profile_readme_launchd() {
|
|
1528
|
+
local pr_label="$1"
|
|
1529
|
+
local pr_script="$2"
|
|
1530
|
+
local pr_plist="$HOME/Library/LaunchAgents/${pr_label}.plist"
|
|
1531
|
+
local _xml_pr_script _xml_pr_home
|
|
1532
|
+
_xml_pr_script=$(_xml_escape "$pr_script")
|
|
1533
|
+
_xml_pr_home=$(_xml_escape "$HOME")
|
|
1534
|
+
|
|
1535
|
+
local pr_plist_content
|
|
1536
|
+
pr_plist_content=$(
|
|
1537
|
+
cat <<PR_PLIST
|
|
1174
1538
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
1175
1539
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1176
1540
|
<plist version="1.0">
|
|
@@ -1209,27 +1573,66 @@ setup_profile_readme() {
|
|
|
1209
1573
|
</dict>
|
|
1210
1574
|
</plist>
|
|
1211
1575
|
PR_PLIST
|
|
1212
|
-
|
|
1576
|
+
)
|
|
1213
1577
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
else
|
|
1217
|
-
print_warning "Failed to load profile README update LaunchAgent"
|
|
1218
|
-
fi
|
|
1578
|
+
if _launchd_install_if_changed "$pr_label" "$pr_plist" "$pr_plist_content"; then
|
|
1579
|
+
print_info "Profile README update enabled (launchd, hourly)"
|
|
1219
1580
|
else
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1581
|
+
print_warning "Failed to load profile README update LaunchAgent"
|
|
1582
|
+
fi
|
|
1583
|
+
return 0
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
_install_profile_readme_scheduler() {
|
|
1587
|
+
local pr_label="$1"
|
|
1588
|
+
local pr_systemd="$2"
|
|
1589
|
+
local pr_script="$3"
|
|
1590
|
+
local pr_log="$4"
|
|
1591
|
+
|
|
1592
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
1593
|
+
_install_profile_readme_launchd "$pr_label" "$pr_script"
|
|
1594
|
+
return 0
|
|
1595
|
+
fi
|
|
1596
|
+
|
|
1597
|
+
_install_scheduler_linux \
|
|
1598
|
+
"$pr_systemd" \
|
|
1599
|
+
"aidevops: profile-readme-update" \
|
|
1600
|
+
"0 * * * *" \
|
|
1601
|
+
"\"${pr_script}\" update" \
|
|
1602
|
+
"3600" \
|
|
1603
|
+
"$pr_log" \
|
|
1604
|
+
"" \
|
|
1605
|
+
"Profile README update enabled (hourly)" \
|
|
1606
|
+
"Failed to install profile README update scheduler" \
|
|
1607
|
+
"false" \
|
|
1608
|
+
"true"
|
|
1609
|
+
return 0
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
setup_profile_readme() {
|
|
1613
|
+
local pr_script="$HOME/.aidevops/agents/scripts/profile-readme-helper.sh"
|
|
1614
|
+
local pr_label="sh.aidevops.profile-readme-update"
|
|
1615
|
+
if ! _profile_readme_ready "$pr_script"; then
|
|
1616
|
+
return 0
|
|
1232
1617
|
fi
|
|
1618
|
+
|
|
1619
|
+
# Initialize profile repo if not already set up.
|
|
1620
|
+
# Always run init — it's idempotent and handles:
|
|
1621
|
+
# - Fresh installs (no profile repo)
|
|
1622
|
+
# - Missing markers (injects them into existing README)
|
|
1623
|
+
# - Diverged history (repo deleted and recreated on GitHub)
|
|
1624
|
+
# - Already-initialized repos (returns early with no changes)
|
|
1625
|
+
_run_profile_readme_init "$pr_script"
|
|
1626
|
+
|
|
1627
|
+
# Profile README auto-update scheduled job.
|
|
1628
|
+
# Installed whenever gh CLI is available — the update script self-heals
|
|
1629
|
+
# (discovers/creates the profile repo on first run via _resolve_profile_repo).
|
|
1630
|
+
# macOS: launchd plist (hourly) | Linux: systemd timer or cron (hourly)
|
|
1631
|
+
local pr_systemd="aidevops-profile-readme-update"
|
|
1632
|
+
local pr_log="$HOME/.aidevops/.agent-workspace/logs/profile-readme-update.log"
|
|
1633
|
+
mkdir -p "$HOME/.aidevops/.agent-workspace/logs"
|
|
1634
|
+
|
|
1635
|
+
_install_profile_readme_scheduler "$pr_label" "$pr_systemd" "$pr_script" "$pr_log"
|
|
1233
1636
|
return 0
|
|
1234
1637
|
}
|
|
1235
1638
|
|
|
@@ -1315,27 +1718,29 @@ _uninstall_token_refresh_schtasks() {
|
|
|
1315
1718
|
# Refreshes expired/expiring tokens every 30 min so sessions never hit
|
|
1316
1719
|
# "invalid x-api-key". Also runs at load to catch tokens that expired
|
|
1317
1720
|
# while the machine was off.
|
|
1318
|
-
# macOS: launchd plist | Linux/WSL: cron | Windows Git Bash: schtasks
|
|
1319
|
-
|
|
1320
|
-
local tr_script="$
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
return 0
|
|
1721
|
+
# macOS: launchd plist | Linux/WSL: systemd timer or cron | Windows Git Bash: schtasks
|
|
1722
|
+
_oauth_token_refresh_ready() {
|
|
1723
|
+
local tr_script="$1"
|
|
1724
|
+
if ! [[ -x "$tr_script" ]]; then
|
|
1725
|
+
return 1
|
|
1324
1726
|
fi
|
|
1727
|
+
if ! [[ -f "$HOME/.aidevops/oauth-pool.json" ]]; then
|
|
1728
|
+
return 1
|
|
1729
|
+
fi
|
|
1730
|
+
return 0
|
|
1731
|
+
}
|
|
1325
1732
|
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
tr_plist_content=$(
|
|
1338
|
-
cat <<TR_PLIST
|
|
1733
|
+
_install_token_refresh_launchd() {
|
|
1734
|
+
local tr_label="$1"
|
|
1735
|
+
local tr_script="$2"
|
|
1736
|
+
local tr_plist="$HOME/Library/LaunchAgents/${tr_label}.plist"
|
|
1737
|
+
local _xml_tr_script _xml_tr_home
|
|
1738
|
+
_xml_tr_script=$(_xml_escape "$tr_script")
|
|
1739
|
+
_xml_tr_home=$(_xml_escape "$HOME")
|
|
1740
|
+
|
|
1741
|
+
local tr_plist_content
|
|
1742
|
+
tr_plist_content=$(
|
|
1743
|
+
cat <<TR_PLIST
|
|
1339
1744
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
1340
1745
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1341
1746
|
<plist version="1.0">
|
|
@@ -1374,29 +1779,45 @@ setup_oauth_token_refresh() {
|
|
|
1374
1779
|
</dict>
|
|
1375
1780
|
</plist>
|
|
1376
1781
|
TR_PLIST
|
|
1377
|
-
|
|
1782
|
+
)
|
|
1378
1783
|
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1784
|
+
if _launchd_install_if_changed "$tr_label" "$tr_plist" "$tr_plist_content"; then
|
|
1785
|
+
print_info "OAuth token refresh enabled (launchd, every 30 min)"
|
|
1786
|
+
else
|
|
1787
|
+
print_warning "Failed to load token refresh LaunchAgent"
|
|
1788
|
+
fi
|
|
1789
|
+
return 0
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
setup_oauth_token_refresh() {
|
|
1793
|
+
local tr_script="$HOME/.aidevops/agents/scripts/oauth-pool-helper.sh"
|
|
1794
|
+
local tr_label="sh.aidevops.token-refresh"
|
|
1795
|
+
if ! _oauth_token_refresh_ready "$tr_script"; then
|
|
1796
|
+
return 0
|
|
1797
|
+
fi
|
|
1798
|
+
|
|
1799
|
+
local tr_log_dir="$HOME/.aidevops/.agent-workspace/logs"
|
|
1800
|
+
mkdir -p "$tr_log_dir"
|
|
1801
|
+
|
|
1802
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
1803
|
+
_install_token_refresh_launchd "$tr_label" "$tr_script"
|
|
1384
1804
|
elif _is_windows; then
|
|
1385
1805
|
# Windows Git Bash / MINGW64 / MSYS2: use Task Scheduler (schtasks)
|
|
1386
1806
|
_install_token_refresh_schtasks "$tr_script" "$tr_log_dir"
|
|
1387
1807
|
else
|
|
1388
|
-
# Linux / WSL:
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1808
|
+
# Linux / WSL without systemd: systemd timer or cron fallback
|
|
1809
|
+
_install_scheduler_linux \
|
|
1810
|
+
"aidevops-token-refresh" \
|
|
1811
|
+
"aidevops: token-refresh" \
|
|
1812
|
+
"*/30 * * * *" \
|
|
1813
|
+
"\"${tr_script}\" refresh anthropic; \"${tr_script}\" refresh openai" \
|
|
1814
|
+
"1800" \
|
|
1815
|
+
"${tr_log_dir}/token-refresh.log" \
|
|
1816
|
+
"" \
|
|
1817
|
+
"OAuth token refresh enabled (every 30 min)" \
|
|
1818
|
+
"Failed to install token refresh scheduler" \
|
|
1819
|
+
"true" \
|
|
1820
|
+
"true"
|
|
1400
1821
|
fi
|
|
1401
1822
|
return 0
|
|
1402
1823
|
}
|
|
@@ -1413,8 +1834,13 @@ setup_repo_sync() {
|
|
|
1413
1834
|
local _repo_sync_installed=false
|
|
1414
1835
|
if _launchd_has_agent "com.aidevops.aidevops-repo-sync"; then
|
|
1415
1836
|
_repo_sync_installed=true
|
|
1837
|
+
elif _launchd_has_agent "sh.aidevops.repo-sync"; then
|
|
1838
|
+
_repo_sync_installed=true
|
|
1416
1839
|
elif crontab -l 2>/dev/null | grep -qF "aidevops-repo-sync"; then
|
|
1417
1840
|
_repo_sync_installed=true
|
|
1841
|
+
elif command -v systemctl >/dev/null 2>&1 &&
|
|
1842
|
+
systemctl --user is-enabled "aidevops-repo-sync.timer" >/dev/null 2>&1; then
|
|
1843
|
+
_repo_sync_installed=true
|
|
1418
1844
|
fi
|
|
1419
1845
|
if [[ "$_repo_sync_installed" == "false" ]]; then
|
|
1420
1846
|
if [[ "$NON_INTERACTIVE" == "true" ]]; then
|