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