aidevops 3.13.95 → 3.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1072 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
3
- # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
4
- # =============================================================================
5
- # Schedulers Platform Sub-Library -- Platform scheduler setup for contribution
6
- # watch, complexity scan, pulse merge routine, profile README, OAuth token
7
- # refresh, OpenCode DB maintenance, repo sync, repo health, and peer
8
- # productivity monitor.
9
- # =============================================================================
10
- # This sub-library is sourced by setup-modules/schedulers.sh (the orchestrator).
11
- # It covers:
12
- # - Contribution watch (t1554): passive monitoring of external issues/PRs
13
- # - Complexity scan (t2903): decoupled weekly complexity scan
14
- # - Pulse merge routine (t2862, GH#20919): fast 120s merge pass
15
- # - Draft responses (t1555): private repo + local draft storage
16
- # - Profile README: auto-create and scheduled update
17
- # - OAuth token refresh: launchd/systemd/cron/schtasks
18
- # - OpenCode DB maintenance (r913, t2183): weekly checkpoint/vacuum
19
- # - Repo sync: daily fast-forward pull
20
- # - Repo aidevops health (r914): daily drift keeper
21
- # - Peer productivity monitor (t2932): adaptive cross-runner coordination
22
- #
23
- # Usage: source "${SCRIPT_DIR}/schedulers-platform.sh"
24
- #
25
- # Dependencies:
26
- # - shared-constants.sh (print_info, print_warning, print_error)
27
- # - schedulers-linux.sh (_install_scheduler_linux, _uninstall_scheduler)
28
- # - schedulers-pulse.sh (_resolve_modern_bash)
29
- #
30
- # Part of aidevops framework: https://aidevops.sh
31
-
32
- # Apply strict mode only when executed directly (not when sourced)
33
- [[ "${BASH_SOURCE[0]}" == "${0}" ]] && set -euo pipefail
34
-
35
- # Include guard
36
- [[ -n "${_SCHEDULERS_PLATFORM_LIB_LOADED:-}" ]] && return 0
37
- _SCHEDULERS_PLATFORM_LIB_LOADED=1
38
-
39
- # SCRIPT_DIR fallback — needed when sourced from test harnesses that don't set it.
40
- if [[ -z "${SCRIPT_DIR:-}" ]]; then
41
- _sched_platform_lib_path="${BASH_SOURCE[0]%/*}"
42
- [[ "$_sched_platform_lib_path" == "${BASH_SOURCE[0]}" ]] && _sched_platform_lib_path="."
43
- SCRIPT_DIR="$(cd "$_sched_platform_lib_path" && pwd)"
44
- unset _sched_platform_lib_path
45
- fi
46
-
47
- # --- Functions ---
48
-
49
- # Resolve and validate the log directory from config for contribution watch.
50
- # Delegates resolution to _resolve_log_dir (shared-constants.sh), then applies
51
- # install-time character validation (safe for shell paths and cron lines).
52
- # Prints the resolved absolute path. Returns 1 on invalid characters.
53
- _resolve_cw_log_dir() {
54
- local _cw_log_dir
55
- _cw_log_dir=$(_resolve_log_dir)
56
- # Whitelist: only allow characters safe in shell paths and cron lines.
57
- # Reject anything outside [A-Za-z0-9_./ -] (tilde already expanded by _resolve_log_dir).
58
- # Store regex in variable — bash [[ =~ ]] requires unquoted RHS for regex,
59
- # and a variable avoids quoting issues with special chars in the pattern.
60
- local _cw_log_dir_re='^[A-Za-z0-9_./ -]+$'
61
- if ! [[ "$_cw_log_dir" =~ $_cw_log_dir_re ]]; then
62
- # Redirect to stderr so $() captures only the path result
63
- print_error "Invalid characters in paths.log_dir (only [A-Za-z0-9_./ -] allowed): $_cw_log_dir" >&2
64
- return 1
65
- fi
66
- printf '%s' "$_cw_log_dir"
67
- return 0
68
- }
69
-
70
- # Install contribution watch via launchd (macOS).
71
- # Args: $1=label, $2=script path, $3=log dir
72
- _install_cw_launchd() {
73
- local cw_label="$1"
74
- local cw_script="$2"
75
- local _cw_log_dir="$3"
76
- local cw_plist="$HOME/Library/LaunchAgents/${cw_label}.plist"
77
-
78
- local _xml_cw_script _xml_cw_home _xml_cw_log_dir
79
- _xml_cw_script=$(_xml_escape "$cw_script")
80
- _xml_cw_home=$(_xml_escape "$HOME")
81
- _xml_cw_log_dir=$(_xml_escape "$_cw_log_dir")
82
-
83
- local cw_plist_content
84
- cw_plist_content=$(
85
- cat <<CW_PLIST
86
- <?xml version="1.0" encoding="UTF-8"?>
87
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
88
- <plist version="1.0">
89
- <dict>
90
- <key>Label</key>
91
- <string>${cw_label}</string>
92
- <key>ProgramArguments</key>
93
- <array>
94
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
95
- <string>${_xml_cw_script}</string>
96
- <string>scan</string>
97
- </array>
98
- <key>StartInterval</key>
99
- <integer>3600</integer>
100
- <key>StandardOutPath</key>
101
- <string>${_xml_cw_log_dir}/contribution-watch.log</string>
102
- <key>StandardErrorPath</key>
103
- <string>${_xml_cw_log_dir}/contribution-watch.log</string>
104
- <key>EnvironmentVariables</key>
105
- <dict>
106
- <key>PATH</key>
107
- <string>$(aidevops_launchd_sanitized_path)</string>
108
- <key>HOME</key>
109
- <string>${_xml_cw_home}</string>
110
- </dict>
111
- <key>RunAtLoad</key>
112
- <false/>
113
- <key>KeepAlive</key>
114
- <false/>
115
- <key>ProcessType</key>
116
- <string>Background</string>
117
- <key>LowPriorityBackgroundIO</key>
118
- <true/>
119
- <key>Nice</key>
120
- <integer>10</integer>
121
- </dict>
122
- </plist>
123
- CW_PLIST
124
- )
125
-
126
- if _launchd_install_if_changed "$cw_label" "$cw_plist" "$cw_plist_content"; then
127
- print_info "Contribution watch enabled (launchd, hourly scan)"
128
- else
129
- print_warning "Failed to load contribution watch LaunchAgent"
130
- fi
131
- return 0
132
- }
133
-
134
- # Install contribution watch via systemd or cron (Linux).
135
- # Args: $1=script path, $2=log dir
136
- _install_cw_linux() {
137
- local cw_script="$1"
138
- local _cw_log_dir="$2"
139
- local cw_systemd="aidevops-contribution-watch"
140
- _install_scheduler_linux \
141
- "$cw_systemd" \
142
- "aidevops: contribution-watch" \
143
- "$CRON_HOURLY" \
144
- "\"${cw_script}\" scan" \
145
- "3600" \
146
- "${_cw_log_dir}/contribution-watch.log" \
147
- "" \
148
- "Contribution watch enabled (hourly scan)" \
149
- "Failed to install contribution watch scheduler" \
150
- "false" \
151
- "true"
152
- return 0
153
- }
154
-
155
- # Setup contribution watch — monitors external issues/PRs for new activity (t1554).
156
- # Auto-seeds on first run (discovers authored/commented issues/PRs), then installs
157
- # a launchd/systemd/cron job to scan periodically. Requires gh CLI authenticated.
158
- # No consent needed — this is passive monitoring (read-only notifications API),
159
- # not autonomous action. Comment bodies are never processed by LLM in automated context.
160
- # Respects config: aidevops config set orchestration.contribution_watch false
161
- setup_contribution_watch() {
162
- local cw_script="$HOME/.aidevops/agents/scripts/contribution-watch-helper.sh"
163
- local cw_label="sh.aidevops.contribution-watch"
164
- local cw_state="$HOME/.aidevops/cache/contribution-watch.json"
165
- 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
166
- return 0
167
- fi
168
-
169
- # Resolve and validate log directory
170
- local _cw_log_dir
171
- _cw_log_dir=$(_resolve_cw_log_dir) || return 1
172
- mkdir -p "$HOME/.aidevops/cache" "$_cw_log_dir"
173
-
174
- # Auto-seed on first run (populates state file with existing contributions)
175
- if [[ ! -f "$cw_state" ]]; then
176
- print_info "Discovering external contributions for contribution watch..."
177
- if bash "$cw_script" seed >/dev/null 2>&1; then
178
- print_info "Contribution watch seeded (external issues/PRs discovered)"
179
- else
180
- print_warning "Contribution watch seed failed (non-fatal, will retry on next run)"
181
- fi
182
- fi
183
-
184
- # Install/update scheduled scanner
185
- if [[ "$(uname -s)" == "Darwin" ]]; then
186
- _install_cw_launchd "$cw_label" "$cw_script" "$_cw_log_dir"
187
- else
188
- _install_cw_linux "$cw_script" "$_cw_log_dir"
189
- fi
190
- return 0
191
- }
192
-
193
- # Install complexity scan via launchd (macOS).
194
- # Args: $1=label, $2=script path, $3=log dir
195
- # (t2903) Extracted from pulse dispatch preflight — independent schedule so
196
- # the 200-470s scan never starves dispatch or downstream scanners.
197
- _install_complexity_scan_launchd() {
198
- local cs_label="$1"
199
- local cs_script="$2"
200
- local _cs_log_dir="$3"
201
- local cs_plist="$HOME/Library/LaunchAgents/${cs_label}.plist"
202
-
203
- local _xml_cs_script _xml_cs_home _xml_cs_log_dir
204
- _xml_cs_script=$(_xml_escape "$cs_script")
205
- _xml_cs_home=$(_xml_escape "$HOME")
206
- _xml_cs_log_dir=$(_xml_escape "$_cs_log_dir")
207
-
208
- local cs_plist_content
209
- cs_plist_content=$(
210
- cat <<CS_PLIST
211
- <?xml version="1.0" encoding="UTF-8"?>
212
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
213
- <plist version="1.0">
214
- <dict>
215
- <key>Label</key>
216
- <string>${cs_label}</string>
217
- <key>ProgramArguments</key>
218
- <array>
219
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
220
- <string>${_xml_cs_script}</string>
221
- <string>run</string>
222
- </array>
223
- <key>StartInterval</key>
224
- <integer>3600</integer>
225
- <key>StandardOutPath</key>
226
- <string>${_xml_cs_log_dir}/complexity-scan-runner.log</string>
227
- <key>StandardErrorPath</key>
228
- <string>${_xml_cs_log_dir}/complexity-scan-runner.log</string>
229
- <key>EnvironmentVariables</key>
230
- <dict>
231
- <key>PATH</key>
232
- <string>$(aidevops_launchd_sanitized_path)</string>
233
- <key>HOME</key>
234
- <string>${_xml_cs_home}</string>
235
- </dict>
236
- <key>RunAtLoad</key>
237
- <true/>
238
- <key>KeepAlive</key>
239
- <false/>
240
- <key>ProcessType</key>
241
- <string>Background</string>
242
- <key>LowPriorityBackgroundIO</key>
243
- <true/>
244
- <key>Nice</key>
245
- <integer>10</integer>
246
- </dict>
247
- </plist>
248
- CS_PLIST
249
- )
250
-
251
- if _launchd_install_if_changed "$cs_label" "$cs_plist" "$cs_plist_content"; then
252
- print_info "Complexity scan enabled (launchd, hourly run)"
253
- else
254
- print_warning "Failed to load complexity scan LaunchAgent"
255
- fi
256
- return 0
257
- }
258
-
259
- # Install complexity scan via systemd or cron (Linux).
260
- # Args: $1=script path, $2=log dir
261
- _install_complexity_scan_linux() {
262
- local cs_script="$1"
263
- local _cs_log_dir="$2"
264
- local cs_systemd="aidevops-complexity-scan"
265
- _install_scheduler_linux \
266
- "$cs_systemd" \
267
- "aidevops: complexity-scan" \
268
- "$CRON_HOURLY" \
269
- "\"${cs_script}\" run" \
270
- "3600" \
271
- "${_cs_log_dir}/complexity-scan-runner.log" \
272
- "" \
273
- "Complexity scan enabled (hourly run)" \
274
- "Failed to install complexity scan scheduler" \
275
- "true" \
276
- "true"
277
- return 0
278
- }
279
-
280
- # Setup complexity scan (t2903) — extracts the weekly complexity scan from
281
- # pulse dispatch preflight into its own launchd/cron schedule. The scan was
282
- # observed consuming 200-470s per pulse cycle (26%+ of the 1800s pulse stale
283
- # ceiling), starving downstream scanners. Promoting it to its own schedule
284
- # decouples it from dispatch entirely. The runner reuses run_weekly_complexity_scan
285
- # from pulse-simplification.sh, which has internal 15-min cadence gating
286
- # (COMPLEXITY_SCAN_INTERVAL=900) so hourly launchd ticks are always safe.
287
- setup_complexity_scan() {
288
- local cs_script="$HOME/.aidevops/agents/scripts/complexity-scan-runner.sh"
289
- local cs_label="sh.aidevops.complexity-scan"
290
- if ! [[ -x "$cs_script" ]]; then
291
- return 0
292
- fi
293
-
294
- # Reuse contribution-watch's log-dir resolver (same logic, same config key).
295
- local _cs_log_dir
296
- _cs_log_dir=$(_resolve_cw_log_dir) || return 1
297
- mkdir -p "$_cs_log_dir"
298
-
299
- # Install/update scheduled runner
300
- if [[ "$(uname -s)" == "Darwin" ]]; then
301
- _install_complexity_scan_launchd "$cs_label" "$cs_script" "$_cs_log_dir"
302
- else
303
- _install_complexity_scan_linux "$cs_script" "$_cs_log_dir"
304
- fi
305
- return 0
306
- }
307
-
308
- # Install the dedicated merge-pass launchd plist (macOS).
309
- # Uses the timeout-protected pulse-merge-routine.sh path (t3378). The older
310
- # t21247 pulse-wrapper.sh --merge-only path had no hard ceiling; a hung merge
311
- # pass could leave green PRs unmerged until manual intervention.
312
- # Supersedes the t2862 sh.aidevops.pulse-merge-routine label while keeping the
313
- # timeout-protected helper implementation:
314
- # - Label: com.aidevops.aidevops-supervisor-merge
315
- # - Script: pulse-merge-routine.sh run
316
- # - Interval: 60s (down from 120s) — targets <=90s PR-green-to-merged latency
317
- # - Log: pulse-merge.log (isolated from main pulse log)
318
- #
319
- # Args: $1=label $2=wrapper_script $3=log_dir
320
- _install_pulse_merge_routine_launchd() {
321
- local pmr_label="$1"
322
- local pmr_script="$2"
323
- local _pmr_log_dir="$3"
324
- local pmr_plist="$HOME/Library/LaunchAgents/${pmr_label}.plist"
325
-
326
- # One-time migration: unload and remove the legacy sh.aidevops.pulse-merge-routine
327
- # plist (installed by t2862) to avoid running two merge passes simultaneously.
328
- local _legacy_pmr_label="sh.aidevops.pulse-merge-routine"
329
- local _legacy_pmr_plist="$HOME/Library/LaunchAgents/${_legacy_pmr_label}.plist"
330
- if [[ -f "$_legacy_pmr_plist" ]]; then
331
- launchctl unload "$_legacy_pmr_plist" 2>/dev/null || true
332
- rm -f "$_legacy_pmr_plist"
333
- print_info "Removed legacy pulse-merge-routine plist (superseded by ${pmr_label})"
334
- fi
335
-
336
- local _xml_pmr_script _xml_pmr_home _xml_pmr_log_dir _xml_bash_bin
337
- _xml_pmr_script=$(_xml_escape "$pmr_script")
338
- _xml_pmr_home=$(_xml_escape "$HOME")
339
- _xml_pmr_log_dir=$(_xml_escape "$_pmr_log_dir")
340
- _xml_bash_bin=$(_xml_escape "$(_resolve_modern_bash)")
341
-
342
- local pmr_plist_content
343
- pmr_plist_content=$(
344
- cat <<PMR_PLIST
345
- <?xml version="1.0" encoding="UTF-8"?>
346
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
347
- <plist version="1.0">
348
- <dict>
349
- <key>Label</key>
350
- <string>${pmr_label}</string>
351
- <key>ProgramArguments</key>
352
- <array>
353
- <string>${_xml_bash_bin}</string>
354
- <string>${_xml_pmr_script}</string>
355
- <string>run</string>
356
- </array>
357
- <key>StartInterval</key>
358
- <integer>60</integer>
359
- <key>StandardOutPath</key>
360
- <string>${_xml_pmr_log_dir}/pulse-merge.log</string>
361
- <key>StandardErrorPath</key>
362
- <string>${_xml_pmr_log_dir}/pulse-merge.log</string>
363
- <key>EnvironmentVariables</key>
364
- <dict>
365
- <key>PATH</key>
366
- <string>$(aidevops_launchd_sanitized_path)</string>
367
- <key>HOME</key>
368
- <string>${_xml_pmr_home}</string>
369
- </dict>
370
- <key>RunAtLoad</key>
371
- <false/>
372
- <key>KeepAlive</key>
373
- <false/>
374
- <key>SoftResourceLimits</key>
375
- <dict>
376
- <key>NumberOfFiles</key>
377
- <integer>4096</integer>
378
- </dict>
379
- </dict>
380
- </plist>
381
- PMR_PLIST
382
- )
383
-
384
- if _launchd_install_if_changed "$pmr_label" "$pmr_plist" "$pmr_plist_content"; then
385
- print_info "Pulse merge pass enabled (launchd, every 60s via pulse-merge-routine.sh)"
386
- else
387
- print_warning "Failed to load pulse merge pass LaunchAgent"
388
- fi
389
- return 0
390
- }
391
-
392
- # Install the dedicated merge-pass scheduler via systemd or cron (Linux).
393
- # Uses timeout-protected pulse-merge-routine.sh (t3378).
394
- # - Command: pulse-merge-routine.sh run
395
- # - Interval: every 60s (cron * * * * *, systemd OnUnitActiveSec=60)
396
- # - Log: pulse-merge.log
397
- # Args: $1=wrapper_script $2=log_dir
398
- _install_pulse_merge_routine_linux() {
399
- local pmr_script="$1"
400
- local _pmr_log_dir="$2"
401
- local pmr_systemd="aidevops-pulse-merge"
402
-
403
- # One-time migration: remove legacy systemd/cron entry for aidevops-pulse-merge-routine.
404
- if _systemd_user_available 2>/dev/null; then
405
- systemctl --user disable --now "aidevops-pulse-merge-routine.timer" 2>/dev/null || true
406
- rm -f "$HOME/.config/systemd/user/aidevops-pulse-merge-routine.service" 2>/dev/null || true
407
- rm -f "$HOME/.config/systemd/user/aidevops-pulse-merge-routine.timer" 2>/dev/null || true
408
- systemctl --user daemon-reload 2>/dev/null || true
409
- fi
410
- if crontab -l 2>/dev/null | grep -qF "pulse-merge-routine"; then
411
- crontab -l 2>/dev/null | grep -v 'aidevops: pulse-merge-routine' | crontab - 2>/dev/null || true
412
- fi
413
-
414
- _install_scheduler_linux \
415
- "$pmr_systemd" \
416
- "aidevops: pulse-merge-routine" \
417
- "* * * * *" \
418
- "\"${pmr_script}\" run" \
419
- "60" \
420
- "${_pmr_log_dir}/pulse-merge.log" \
421
- "" \
422
- "Pulse merge pass enabled (every 60s via pulse-merge-routine.sh)" \
423
- "Failed to install pulse merge pass scheduler" \
424
- "true" \
425
- "true"
426
- return 0
427
- }
428
-
429
- # Setup the dedicated merge-pass scheduler (t21247, GH#21247).
430
- #
431
- # Supersedes t2862/GH#20919's legacy label/120s interval. The current
432
- # approach runs pulse-merge-routine.sh every 60s so green PRs merge
433
- # within <=90s of CI completion regardless of dispatch-cycle length (~23 min
434
- # average). The routine has its own hard timeout and lockdir so a hung merge
435
- # pass cannot starve the backlog.
436
- #
437
- # Requires: pulse-merge-routine.sh must be installed and executable.
438
- # The in-cycle merge pass in pulse-wrapper.sh is kept as defense-in-depth —
439
- # it short-circuits when a merge pass ran within the last 60s.
440
- setup_pulse_merge_routine() {
441
- local pmr_wrapper="$HOME/.aidevops/agents/scripts/pulse-merge-routine.sh"
442
- local pmr_label="com.aidevops.aidevops-supervisor-merge"
443
- if ! [[ -x "$pmr_wrapper" ]]; then
444
- return 0
445
- fi
446
-
447
- # Reuse contribution-watch's log-dir resolver (same logic, same config key).
448
- local _pmr_log_dir
449
- _pmr_log_dir=$(_resolve_cw_log_dir) || return 1
450
- mkdir -p "$_pmr_log_dir"
451
-
452
- # Install/update scheduled runner
453
- if [[ "$(uname -s)" == "Darwin" ]]; then
454
- _install_pulse_merge_routine_launchd "$pmr_label" "$pmr_wrapper" "$_pmr_log_dir"
455
- else
456
- _install_pulse_merge_routine_linux "$pmr_wrapper" "$_pmr_log_dir"
457
- fi
458
- return 0
459
- }
460
-
461
- # Setup draft responses — private repo + local draft storage for reviewing
462
- # AI-drafted replies to external contributions (t1555).
463
- # Respects config: aidevops config set orchestration.draft_responses false
464
- setup_draft_responses() {
465
- local dr_script="$HOME/.aidevops/agents/scripts/draft-response-helper.sh"
466
- 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
467
- mkdir -p "$HOME/.aidevops/.agent-workspace/draft-responses"
468
- if bash "$dr_script" init >/dev/null 2>&1; then
469
- print_info "Draft responses ready (private repo + local drafts)"
470
- else
471
- print_warning "Draft responses repo setup failed (non-fatal, local drafts still work)"
472
- fi
473
- fi
474
- return 0
475
- }
476
-
477
- # Setup profile README — auto-create repo and seed README if not already set up.
478
- # Requires gh CLI authenticated. Creates username/username repo, seeds README
479
- # with stat markers, registers in repos.json with priority: "profile".
480
- _profile_readme_ready() {
481
- local pr_script="$1"
482
- if ! [[ -x "$pr_script" ]]; then
483
- return 1
484
- fi
485
- if ! command -v gh &>/dev/null; then
486
- return 1
487
- fi
488
- if ! gh auth status &>/dev/null; then
489
- return 1
490
- fi
491
- return 0
492
- }
493
-
494
- _run_profile_readme_init() {
495
- local pr_script="$1"
496
- print_info "Checking GitHub profile README..."
497
- if bash "$pr_script" init; then
498
- print_info "Profile README ready."
499
- else
500
- print_warning "Profile README setup failed (non-fatal, skipping)"
501
- fi
502
- return 0
503
- }
504
-
505
- _install_profile_readme_launchd() {
506
- local pr_label="$1"
507
- local pr_script="$2"
508
- local pr_plist="$HOME/Library/LaunchAgents/${pr_label}.plist"
509
- local _xml_pr_script _xml_pr_home
510
- _xml_pr_script=$(_xml_escape "$pr_script")
511
- _xml_pr_home=$(_xml_escape "$HOME")
512
-
513
- local pr_plist_content
514
- pr_plist_content=$(
515
- cat <<PR_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>${pr_label}</string>
522
- <key>ProgramArguments</key>
523
- <array>
524
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
525
- <string>${_xml_pr_script}</string>
526
- <string>update</string>
527
- </array>
528
- <key>StartInterval</key>
529
- <integer>3600</integer>
530
- <key>StandardOutPath</key>
531
- <string>${_xml_pr_home}/.aidevops/.agent-workspace/logs/profile-readme-update.log</string>
532
- <key>StandardErrorPath</key>
533
- <string>${_xml_pr_home}/.aidevops/.agent-workspace/logs/profile-readme-update.log</string>
534
- <key>EnvironmentVariables</key>
535
- <dict>
536
- <key>PATH</key>
537
- <string>$(aidevops_launchd_sanitized_path)</string>
538
- <key>HOME</key>
539
- <string>${_xml_pr_home}</string>
540
- </dict>
541
- <key>RunAtLoad</key>
542
- <false/>
543
- <key>KeepAlive</key>
544
- <false/>
545
- <key>ProcessType</key>
546
- <string>Background</string>
547
- <key>LowPriorityBackgroundIO</key>
548
- <true/>
549
- <key>Nice</key>
550
- <integer>10</integer>
551
- </dict>
552
- </plist>
553
- PR_PLIST
554
- )
555
-
556
- if _launchd_install_if_changed "$pr_label" "$pr_plist" "$pr_plist_content"; then
557
- if _launchd_kickstart_and_recover "$pr_label" "$pr_plist"; then
558
- print_info "Profile README update kickstart verified"
559
- else
560
- print_warning "Profile README update LaunchAgent loaded but kickstart verification failed"
561
- fi
562
- print_info "Profile README update enabled (launchd, hourly)"
563
- else
564
- print_warning "Failed to load profile README update LaunchAgent"
565
- fi
566
- return 0
567
- }
568
-
569
- _install_profile_readme_scheduler() {
570
- local pr_label="$1"
571
- local pr_systemd="$2"
572
- local pr_script="$3"
573
- local pr_log="$4"
574
-
575
- if [[ "$(uname -s)" == "Darwin" ]]; then
576
- _install_profile_readme_launchd "$pr_label" "$pr_script"
577
- return 0
578
- fi
579
-
580
- _install_scheduler_linux \
581
- "$pr_systemd" \
582
- "aidevops: profile-readme-update" \
583
- "$CRON_HOURLY" \
584
- "\"${pr_script}\" update" \
585
- "3600" \
586
- "$pr_log" \
587
- "" \
588
- "Profile README update enabled (hourly)" \
589
- "Failed to install profile README update scheduler" \
590
- "false" \
591
- "true"
592
- return 0
593
- }
594
-
595
- setup_profile_readme() {
596
- local pr_script="$HOME/.aidevops/agents/scripts/profile-readme-helper.sh"
597
- local pr_label="sh.aidevops.profile-readme-update"
598
- if ! _profile_readme_ready "$pr_script"; then
599
- return 0
600
- fi
601
-
602
- # Initialize profile repo if not already set up.
603
- # Always run init — it's idempotent and handles:
604
- # - Fresh installs (no profile repo)
605
- # - Missing markers (injects them into existing README)
606
- # - Diverged history (repo deleted and recreated on GitHub)
607
- # - Already-initialized repos (returns early with no changes)
608
- _run_profile_readme_init "$pr_script"
609
-
610
- # Profile README auto-update scheduled job.
611
- # Installed whenever gh CLI is available — the update script self-heals
612
- # (discovers/creates the profile repo on first run via _resolve_profile_repo).
613
- # macOS: launchd plist (hourly) | Linux: systemd timer or cron (hourly)
614
- local pr_systemd="aidevops-profile-readme-update"
615
- local pr_log="$HOME/.aidevops/.agent-workspace/logs/profile-readme-update.log"
616
- mkdir -p "$HOME/.aidevops/.agent-workspace/logs"
617
-
618
- _install_profile_readme_scheduler "$pr_label" "$pr_systemd" "$pr_script" "$pr_log"
619
- return 0
620
- }
621
-
622
- # Detect Windows Git Bash / MINGW64 / MSYS2 environment.
623
- # WSL reports "Linux" from uname -s and uses the cron path — correct behaviour.
624
- # Returns 0 (true) on Windows Git Bash/MINGW/MSYS/Cygwin, 1 otherwise.
625
- _is_windows() {
626
- case "$(uname -s)" in
627
- MINGW* | MSYS* | CYGWIN*)
628
- return 0
629
- ;;
630
- *)
631
- return 1
632
- ;;
633
- esac
634
- }
635
-
636
- # Install OAuth token refresh via Windows Task Scheduler (schtasks).
637
- # Args: $1=tr_script (Unix path), $2=log_dir (Unix path)
638
- # Runs every 30 minutes, matching macOS launchd and Linux cron behaviour.
639
- # Uses bash.exe from Git for Windows to execute the shell script.
640
- _install_token_refresh_schtasks() {
641
- local tr_script="$1"
642
- local log_dir="$2"
643
- local task_name="aidevops-token-refresh"
644
-
645
- # Resolve bash.exe — Git for Windows ships it alongside git.exe
646
- local bash_exe
647
- bash_exe=$(command -v bash.exe 2>/dev/null || command -v bash 2>/dev/null || echo "bash")
648
-
649
- # Convert Unix paths to Windows paths for schtasks (requires cygpath from Git Bash)
650
- local tr_script_win log_dir_win bash_exe_win
651
- if command -v cygpath &>/dev/null; then
652
- tr_script_win=$(cygpath -w "$tr_script")
653
- log_dir_win=$(cygpath -w "$log_dir")
654
- bash_exe_win=$(cygpath -w "$bash_exe")
655
- else
656
- # Fallback: manual conversion (replace /c/ with C:\, forward to backslash)
657
- tr_script_win=$(echo "$tr_script" | sed 's|^/\([a-zA-Z]\)/|\1:\\|; s|/|\\|g')
658
- log_dir_win=$(echo "$log_dir" | sed 's|^/\([a-zA-Z]\)/|\1:\\|; s|/|\\|g')
659
- bash_exe_win="bash.exe"
660
- fi
661
-
662
- # Remove existing task (idempotent — ignore error if not present)
663
- schtasks /Delete /TN "$task_name" /F >/dev/null 2>&1 || true
664
-
665
- # Create scheduled task: every 30 minutes, run at logon, run whether logged on or not
666
- # /SC MINUTE /MO 30 = every 30 minutes
667
- # /RL HIGHEST = run with highest available privileges (needed for token writes)
668
- # /F = force creation (overwrite if exists)
669
- # The action runs bash.exe with -c to chain both refresh calls
670
- local action_cmd
671
- action_cmd="\"${bash_exe_win}\" -c \"'${tr_script_win}' refresh anthropic >> '${log_dir_win}\\token-refresh.log' 2>&1; '${tr_script_win}' refresh openai >> '${log_dir_win}\\token-refresh.log' 2>&1\""
672
-
673
- if schtasks /Create \
674
- /TN "$task_name" \
675
- /TR "$action_cmd" \
676
- /SC MINUTE \
677
- /MO 30 \
678
- /RL HIGHEST \
679
- /F \
680
- >/dev/null 2>&1; then
681
- print_info "OAuth token refresh enabled (schtasks, every 30 min)"
682
- # Run immediately to refresh any expired tokens
683
- schtasks /Run /TN "$task_name" >/dev/null 2>&1 || true
684
- else
685
- print_warning "Failed to create token refresh scheduled task. Run manually: schtasks /Create /TN aidevops-token-refresh /TR \"bash '${tr_script_win}' refresh anthropic\" /SC MINUTE /MO 30"
686
- fi
687
- return 0
688
- }
689
-
690
- # Remove OAuth token refresh Windows scheduled task (uninstall path).
691
- _uninstall_token_refresh_schtasks() {
692
- local task_name="aidevops-token-refresh"
693
- if schtasks /Query /TN "$task_name" >/dev/null 2>&1; then
694
- schtasks /Delete /TN "$task_name" /F >/dev/null 2>&1 || true
695
- print_info "OAuth token refresh disabled (schtasks task removed)"
696
- fi
697
- return 0
698
- }
699
-
700
- # Setup OAuth token refresh scheduled job.
701
- # Refreshes expired/expiring tokens every 30 min so sessions never hit
702
- # "invalid x-api-key". Also runs at load to catch tokens that expired
703
- # while the machine was off.
704
- # macOS: launchd plist | Linux/WSL: systemd timer or cron | Windows Git Bash: schtasks
705
- _oauth_token_refresh_ready() {
706
- local tr_script="$1"
707
- if ! [[ -x "$tr_script" ]]; then
708
- return 1
709
- fi
710
- if ! [[ -f "$HOME/.aidevops/oauth-pool.json" ]]; then
711
- return 1
712
- fi
713
- return 0
714
- }
715
-
716
- _install_token_refresh_launchd() {
717
- local tr_label="$1"
718
- local tr_script="$2"
719
- local tr_plist="$HOME/Library/LaunchAgents/${tr_label}.plist"
720
- local _xml_tr_script _xml_tr_home
721
- _xml_tr_script=$(_xml_escape "$tr_script")
722
- _xml_tr_home=$(_xml_escape "$HOME")
723
-
724
- local tr_plist_content
725
- tr_plist_content=$(
726
- cat <<TR_PLIST
727
- <?xml version="1.0" encoding="UTF-8"?>
728
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
729
- <plist version="1.0">
730
- <dict>
731
- <key>Label</key>
732
- <string>${tr_label}</string>
733
- <key>ProgramArguments</key>
734
- <array>
735
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
736
- <string>-c</string>
737
- <string>&quot;${_xml_tr_script}&quot; refresh anthropic; &quot;${_xml_tr_script}&quot; refresh openai</string>
738
- </array>
739
- <key>StartInterval</key>
740
- <integer>1800</integer>
741
- <key>StandardOutPath</key>
742
- <string>${_xml_tr_home}/.aidevops/.agent-workspace/logs/token-refresh.log</string>
743
- <key>StandardErrorPath</key>
744
- <string>${_xml_tr_home}/.aidevops/.agent-workspace/logs/token-refresh.log</string>
745
- <key>EnvironmentVariables</key>
746
- <dict>
747
- <key>PATH</key>
748
- <string>$(aidevops_launchd_sanitized_path)</string>
749
- <key>HOME</key>
750
- <string>${_xml_tr_home}</string>
751
- </dict>
752
- <key>RunAtLoad</key>
753
- <true/>
754
- <key>KeepAlive</key>
755
- <false/>
756
- <key>ProcessType</key>
757
- <string>Background</string>
758
- <key>LowPriorityBackgroundIO</key>
759
- <true/>
760
- <key>Nice</key>
761
- <integer>10</integer>
762
- </dict>
763
- </plist>
764
- TR_PLIST
765
- )
766
-
767
- if _launchd_install_if_changed "$tr_label" "$tr_plist" "$tr_plist_content"; then
768
- print_info "OAuth token refresh enabled (launchd, every 30 min)"
769
- else
770
- print_warning "Failed to load token refresh LaunchAgent"
771
- fi
772
- return 0
773
- }
774
-
775
- setup_oauth_token_refresh() {
776
- local tr_script="$HOME/.aidevops/agents/scripts/oauth-pool-helper.sh"
777
- local tr_label="sh.aidevops.token-refresh"
778
- if ! _oauth_token_refresh_ready "$tr_script"; then
779
- return 0
780
- fi
781
-
782
- local tr_log_dir="$HOME/.aidevops/.agent-workspace/logs"
783
- mkdir -p "$tr_log_dir"
784
-
785
- if [[ "$(uname -s)" == "Darwin" ]]; then
786
- _install_token_refresh_launchd "$tr_label" "$tr_script"
787
- elif _is_windows; then
788
- # Windows Git Bash / MINGW64 / MSYS2: use Task Scheduler (schtasks)
789
- _install_token_refresh_schtasks "$tr_script" "$tr_log_dir"
790
- else
791
- # Linux / WSL without systemd: systemd timer or cron fallback
792
- _install_scheduler_linux \
793
- "aidevops-token-refresh" \
794
- "aidevops: token-refresh" \
795
- "*/30 * * * *" \
796
- "\"${tr_script}\" refresh anthropic; \"${tr_script}\" refresh openai" \
797
- "1800" \
798
- "${tr_log_dir}/token-refresh.log" \
799
- "" \
800
- "OAuth token refresh enabled (every 30 min)" \
801
- "Failed to install token refresh scheduler" \
802
- "true" \
803
- "true"
804
- fi
805
- return 0
806
- }
807
-
808
- # Setup opencode DB maintenance scheduler (r913, t2183).
809
- # Runs weekly (Sun 04:00 local by default) to checkpoint/optimize/vacuum opencode.db.
810
- # The helper self-noops on missing DB, so installing unconditionally is safe —
811
- # a non-opencode machine wakes up weekly, sees no DB, exits 0 silently.
812
- #
813
- # Platform split (mirrors the pattern for token-refresh):
814
- # macOS — helper owns its plist generation via cmd_install (Approach B).
815
- # Linux — _install_scheduler_linux with cron `0 4 * * 0` + systemd
816
- # OnCalendar `Sun *-*-* 04:00:00` for accurate wall-clock firing.
817
- # Windows — TODO(t2183-followup): opencode on Windows is rare and the
818
- # helper self-noops on missing DB, so leaving unscheduled is
819
- # low-risk for this iteration.
820
- setup_opencode_db_maintenance() {
821
- local ocdbm_script="$HOME/.aidevops/agents/scripts/opencode-db-maintenance-helper.sh"
822
- if ! [[ -x "$ocdbm_script" ]]; then
823
- return 0
824
- fi
825
-
826
- local ocdbm_log_dir="$HOME/.aidevops/.agent-workspace/logs"
827
- local ocdbm_hour="${OPENCODE_DB_MAINTENANCE_HOUR:-4}"
828
- local ocdbm_minute="${OPENCODE_DB_MAINTENANCE_MINUTE:-0}"
829
- local ocdbm_mode="${OPENCODE_DB_MAINTENANCE_MODE:-auto}"
830
- local ocdbm_command="\"${ocdbm_script}\" auto"
831
- if [[ "$ocdbm_mode" == "maintenance-window" ]]; then
832
- ocdbm_command="\"${ocdbm_script}\" maintenance-window --force-opencode"
833
- fi
834
- mkdir -p "$ocdbm_log_dir"
835
-
836
- if [[ "$(uname -s)" == "Darwin" ]]; then
837
- # Helper owns its own plist generation (Approach B, like repo-sync).
838
- # Quiet the helper's multi-line output and emit one consolidated line
839
- # to match the style of setup_profile_readme / setup_oauth_token_refresh.
840
- if OPENCODE_DB_MAINTENANCE_HOUR="$ocdbm_hour" OPENCODE_DB_MAINTENANCE_MINUTE="$ocdbm_minute" OPENCODE_DB_MAINTENANCE_MODE="$ocdbm_mode" bash "$ocdbm_script" install >/dev/null 2>&1; then
841
- print_info "OpenCode DB maintenance enabled (launchd, weekly Sun ${ocdbm_hour}:${ocdbm_minute})"
842
- else
843
- print_warning "Failed to install opencode DB maintenance LaunchAgent"
844
- fi
845
- elif _is_windows; then
846
- # Windows scheduling deferred — helper self-noops on missing DB so
847
- # the cost of leaving unscheduled is ~0 until opencode lands on
848
- # Windows in quantity.
849
- return 0
850
- else
851
- # Linux / WSL: prefer systemd user timer, fall back to cron.
852
- # Weekly Sunday local time — cron: `${minute} ${hour} * * 0`; systemd OnCalendar
853
- # ensures wall-clock firing even across suspends/reboots.
854
- _install_scheduler_linux \
855
- "aidevops-opencode-db-maintenance" \
856
- "aidevops: opencode-db-maintenance" \
857
- "${ocdbm_minute} ${ocdbm_hour} * * 0" \
858
- "${ocdbm_command}" \
859
- "604800" \
860
- "${ocdbm_log_dir}/opencode-db-maintenance.log" \
861
- "" \
862
- "OpenCode DB maintenance enabled (weekly Sun ${ocdbm_hour}:${ocdbm_minute})" \
863
- "Failed to install opencode DB maintenance scheduler" \
864
- "false" \
865
- "true" \
866
- "Sun *-*-* ${ocdbm_hour}:${ocdbm_minute}:00"
867
- fi
868
- return 0
869
- }
870
-
871
- # Setup repo-sync scheduler if not already installed.
872
- # Keeps local git repos up to date with daily ff-only pulls.
873
- # Respects config: aidevops config set orchestration.repo_sync false
874
- setup_repo_sync() {
875
- local repo_sync_script="$HOME/.aidevops/agents/scripts/repo-sync-helper.sh"
876
- if ! [[ -x "$repo_sync_script" ]] || ! is_feature_enabled repo_sync 2>/dev/null; then
877
- return 0
878
- fi
879
-
880
- local _repo_sync_installed=false
881
- if _launchd_has_agent "com.aidevops.aidevops-repo-sync"; then
882
- _repo_sync_installed=true
883
- elif _launchd_has_agent "sh.aidevops.repo-sync"; then
884
- _repo_sync_installed=true
885
- elif crontab -l 2>/dev/null | grep -qF "aidevops-repo-sync"; then
886
- _repo_sync_installed=true
887
- elif command -v systemctl >/dev/null 2>&1 &&
888
- systemctl --user is-enabled "aidevops-repo-sync.timer" >/dev/null 2>&1; then
889
- _repo_sync_installed=true
890
- fi
891
- if [[ "$_repo_sync_installed" == "false" ]]; then
892
- if [[ "$NON_INTERACTIVE" == "true" ]]; then
893
- bash "$repo_sync_script" enable >/dev/null 2>&1 || true
894
- print_info "Repo sync enabled (daily). Disable: aidevops repo-sync disable"
895
- else
896
- echo ""
897
- echo "Repo sync keeps your local git repos up to date by running"
898
- echo "git pull --ff-only daily on clean repos on their default branch."
899
- echo ""
900
- setup_prompt enable_repo_sync "Enable daily repo sync? [Y/n]: " "Y"
901
- if [[ "$enable_repo_sync" =~ ^[Yy]?$ || -z "$enable_repo_sync" ]]; then
902
- bash "$repo_sync_script" enable
903
- else
904
- print_info "Skipped. Enable later: aidevops repo-sync enable"
905
- fi
906
- fi
907
- fi
908
- return 0
909
- }
910
-
911
- # Setup r914 repo-aidevops-health scheduler if not already installed.
912
- # Daily drift keeper for repos.json: bumps stale .aidevops.json versions
913
- # and surfaces missing-folder / no-init drift for human triage.
914
- # Respects config: aidevops config set orchestration.repo_aidevops_health false
915
- setup_repo_aidevops_health() {
916
- local repo_health_script="$HOME/.aidevops/agents/scripts/repo-aidevops-health-helper.sh"
917
- if ! [[ -x "$repo_health_script" ]] || ! is_feature_enabled repo_aidevops_health 2>/dev/null; then
918
- return 0
919
- fi
920
-
921
- local _repo_health_installed=false
922
- if _launchd_has_agent "sh.aidevops.repo-aidevops-health"; then
923
- _repo_health_installed=true
924
- elif crontab -l 2>/dev/null | grep -qF "aidevops-repo-aidevops-health"; then
925
- _repo_health_installed=true
926
- elif command -v systemctl >/dev/null 2>&1 &&
927
- systemctl --user is-enabled "aidevops-repo-aidevops-health.timer" >/dev/null 2>&1; then
928
- _repo_health_installed=true
929
- fi
930
- if [[ "$_repo_health_installed" == "false" ]]; then
931
- if [[ "$NON_INTERACTIVE" == "true" ]]; then
932
- bash "$repo_health_script" enable >/dev/null 2>&1 || true
933
- print_info "r914 repo-aidevops-health enabled (daily @03:30). Disable: aidevops repo-aidevops-health disable"
934
- else
935
- echo ""
936
- echo "r914 keeps \`.aidevops.json\` versions current across all registered"
937
- echo "repos and surfaces registry drift (missing folders, unregistered git"
938
- echo "repos) for human triage. Runs daily at 03:30."
939
- echo ""
940
- setup_prompt enable_repo_health "Enable daily r914 repo-aidevops-health? [Y/n]: " "Y"
941
- if [[ "$enable_repo_health" =~ ^[Yy]?$ || -z "$enable_repo_health" ]]; then
942
- bash "$repo_health_script" enable
943
- else
944
- print_info "Skipped. Enable later: aidevops repo-aidevops-health enable"
945
- fi
946
- fi
947
- fi
948
- return 0
949
- }
950
-
951
- # ============================================================================
952
- # Peer productivity monitor (t2932)
953
- # ============================================================================
954
- #
955
- # Adaptive cross-runner dispatch coordination: observes peer GitHub activity
956
- # every 30 min and updates ~/.config/aidevops/dispatch-override.conf to
957
- # `ignore` peers whose pulse is broken (claims issues but never PRs) and
958
- # back to `honour` when they recover. Self-healing across the ecosystem —
959
- # each runner observes peers independently, no central coordinator needed.
960
- # Manual entries in dispatch-override.conf above the auto-managed marker
961
- # always take precedence.
962
-
963
- # Install peer-productivity-monitor launchd plist (macOS).
964
- # Args: $1=label $2=script $3=log_dir
965
- _install_peer_productivity_monitor_launchd() {
966
- local ppm_label="$1"
967
- local ppm_script="$2"
968
- local _ppm_log_dir="$3"
969
- local ppm_plist="$HOME/Library/LaunchAgents/${ppm_label}.plist"
970
-
971
- local _xml_ppm_script _xml_ppm_home _xml_ppm_log_dir
972
- _xml_ppm_script=$(_xml_escape "$ppm_script")
973
- _xml_ppm_home=$(_xml_escape "$HOME")
974
- _xml_ppm_log_dir=$(_xml_escape "$_ppm_log_dir")
975
-
976
- local ppm_plist_content
977
- ppm_plist_content=$(
978
- cat <<PPM_PLIST
979
- <?xml version="1.0" encoding="UTF-8"?>
980
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
981
- <plist version="1.0">
982
- <dict>
983
- <key>Label</key>
984
- <string>${ppm_label}</string>
985
- <key>ProgramArguments</key>
986
- <array>
987
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
988
- <string>${_xml_ppm_script}</string>
989
- <string>observe</string>
990
- </array>
991
- <key>StartInterval</key>
992
- <integer>1800</integer>
993
- <key>StandardOutPath</key>
994
- <string>${_xml_ppm_log_dir}/peer-productivity-launchd.log</string>
995
- <key>StandardErrorPath</key>
996
- <string>${_xml_ppm_log_dir}/peer-productivity-launchd.log</string>
997
- <key>EnvironmentVariables</key>
998
- <dict>
999
- <key>PATH</key>
1000
- <string>$(aidevops_launchd_sanitized_path)</string>
1001
- <key>HOME</key>
1002
- <string>${_xml_ppm_home}</string>
1003
- </dict>
1004
- <key>RunAtLoad</key>
1005
- <true/>
1006
- <key>KeepAlive</key>
1007
- <false/>
1008
- <key>ProcessType</key>
1009
- <string>Background</string>
1010
- <key>LowPriorityBackgroundIO</key>
1011
- <true/>
1012
- <key>Nice</key>
1013
- <integer>10</integer>
1014
- </dict>
1015
- </plist>
1016
- PPM_PLIST
1017
- )
1018
-
1019
- if _launchd_install_if_changed "$ppm_label" "$ppm_plist" "$ppm_plist_content"; then
1020
- print_info "Peer productivity monitor enabled (launchd, every 30 min)"
1021
- else
1022
- print_warning "Failed to load peer-productivity-monitor LaunchAgent"
1023
- fi
1024
- return 0
1025
- }
1026
-
1027
- # Install peer-productivity-monitor via systemd or cron (Linux).
1028
- # Args: $1=script path, $2=log dir
1029
- _install_peer_productivity_monitor_linux() {
1030
- local ppm_script="$1"
1031
- local _ppm_log_dir="$2"
1032
- local ppm_systemd="aidevops-peer-productivity-monitor"
1033
- _install_scheduler_linux \
1034
- "$ppm_systemd" \
1035
- "aidevops: peer-productivity-monitor" \
1036
- "*/30 * * * *" \
1037
- "\"${ppm_script}\" observe" \
1038
- "1800" \
1039
- "${_ppm_log_dir}/peer-productivity-launchd.log" \
1040
- "" \
1041
- "Peer productivity monitor enabled (every 30 min)" \
1042
- "Failed to install peer-productivity-monitor scheduler" \
1043
- "true" \
1044
- "true"
1045
- return 0
1046
- }
1047
-
1048
- # Setup peer-productivity-monitor (t2932) — observes peer GitHub activity
1049
- # every 30 min and updates ~/.config/aidevops/dispatch-override.conf so the
1050
- # local pulse competes with broken peers and collaborates with healthy ones.
1051
- # Manual entries in dispatch-override.conf above the auto-managed marker
1052
- # always take precedence.
1053
- setup_peer_productivity_monitor() {
1054
- local ppm_script="$HOME/.aidevops/agents/scripts/peer-productivity-monitor.sh"
1055
- local ppm_label="sh.aidevops.peer-productivity-monitor"
1056
- if ! [[ -x "$ppm_script" ]]; then
1057
- return 0
1058
- fi
1059
-
1060
- # Reuse contribution-watch's log-dir resolver (same logic, same config key).
1061
- local _ppm_log_dir
1062
- _ppm_log_dir=$(_resolve_cw_log_dir) || return 1
1063
- mkdir -p "$_ppm_log_dir"
1064
-
1065
- # Install/update scheduled runner
1066
- if [[ "$(uname -s)" == "Darwin" ]]; then
1067
- _install_peer_productivity_monitor_launchd "$ppm_label" "$ppm_script" "$_ppm_log_dir"
1068
- else
1069
- _install_peer_productivity_monitor_linux "$ppm_script" "$_ppm_log_dir"
1070
- fi
1071
- return 0
1072
- }