aidevops 3.0.12 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.0.12
1
+ 3.1.1
package/aidevops.sh CHANGED
@@ -3,7 +3,7 @@
3
3
  # AI DevOps Framework CLI
4
4
  # Usage: aidevops <command> [options]
5
5
  #
6
- # Version: 3.0.12
6
+ # Version: 3.1.1
7
7
 
8
8
  set -euo pipefail
9
9
 
@@ -105,7 +105,9 @@ check_file() {
105
105
  # Ensure file ends with a trailing newline (prevents malformed appends)
106
106
  ensure_trailing_newline() {
107
107
  local file="$1"
108
- [[ -s "$file" && $(tail -c1 "$file" | wc -l) -eq 0 ]] && printf '\n' >>"$file"
108
+ local last
109
+ last="$(tail -c 1 "$file"; printf x)"
110
+ [[ -s "$file" ]] && [[ "$last" != $'\n'x ]] && printf '\n' >>"$file"
109
111
  }
110
112
 
111
113
  # Initialize repos.json if it doesn't exist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.0.12",
3
+ "version": "3.1.1",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,13 @@ trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
10
10
  shopt -s inherit_errexit 2>/dev/null || true
11
11
 
12
12
  install_mcp_packages() {
13
+ # Check prerequisites before announcing setup (GH#5240)
14
+ if ! command -v bun &>/dev/null && ! command -v npm &>/dev/null; then
15
+ print_skip "MCP packages" "neither bun nor npm found" "Install bun: brew install oven-sh/bun/bun (or npm via Node.js)"
16
+ setup_track_deferred "MCP packages" "Install bun or npm"
17
+ return 0
18
+ fi
19
+
13
20
  print_info "Installing MCP server packages globally (eliminates npx startup delay)..."
14
21
 
15
22
  # Security note: MCP servers run as persistent processes with access to conversation
@@ -27,12 +34,6 @@ install_mcp_packages() {
27
34
  "@steipete/claude-code-mcp"
28
35
  )
29
36
 
30
- if ! command -v bun &>/dev/null && ! command -v npm &>/dev/null; then
31
- print_warning "Neither bun nor npm found - cannot install MCP packages"
32
- print_info "Install bun (recommended): npm install -g bun OR brew install oven-sh/bun/bun"
33
- return 0
34
- fi
35
-
36
37
  local installer="npm"
37
38
  command -v bun &>/dev/null && installer="bun"
38
39
  print_info "Using $installer to install/update Node.js MCP packages..."
@@ -205,36 +206,33 @@ update_mcp_paths_in_opencode() {
205
206
  }
206
207
 
207
208
  setup_localwp_mcp() {
208
- print_info "Setting up LocalWP MCP server..."
209
-
210
- # Check if LocalWP is installed
209
+ # Check prerequisites before announcing setup (GH#5240)
211
210
  local localwp_found=false
212
211
  if [[ -d "/Applications/Local.app" ]] || [[ -d "$HOME/Applications/Local.app" ]]; then
213
212
  localwp_found=true
214
213
  fi
215
214
 
216
215
  if [[ "$localwp_found" != "true" ]]; then
217
- print_info "LocalWP not found - skipping MCP server setup"
218
- print_info "Install LocalWP from: https://localwp.com/"
216
+ print_skip "LocalWP MCP" "LocalWP not installed" "Install from https://localwp.com/ then re-run setup"
217
+ setup_track_skipped "LocalWP MCP" "LocalWP not installed"
219
218
  return 0
220
219
  fi
221
220
 
222
- print_success "LocalWP found"
223
-
224
- # Check if npm is available
225
221
  if ! command -v npm &>/dev/null; then
226
- print_warning "npm not found - cannot install LocalWP MCP server"
227
- print_info "Install Node.js and npm first"
222
+ print_skip "LocalWP MCP" "npm not found" "Install Node.js and npm first"
223
+ setup_track_deferred "LocalWP MCP" "Install Node.js/npm, then re-run setup"
228
224
  return 0
229
225
  fi
230
226
 
231
- # Check if mcp-local-wp is already installed
227
+ # Prerequisites met proceed with setup
228
+ print_info "Setting up LocalWP MCP server..."
229
+
232
230
  if command -v mcp-local-wp &>/dev/null; then
233
231
  print_success "LocalWP MCP server already installed"
232
+ setup_track_configured "LocalWP MCP"
234
233
  return 0
235
234
  fi
236
235
 
237
- # Offer to install mcp-local-wp
238
236
  print_info "LocalWP MCP server enables AI assistants to query WordPress databases"
239
237
  read -r -p "Install LocalWP MCP server (@verygoodplugins/mcp-local-wp)? [Y/n]: " install_mcp
240
238
 
@@ -242,6 +240,7 @@ setup_localwp_mcp() {
242
240
  if run_with_spinner "Installing LocalWP MCP server" npm_global_install "@verygoodplugins/mcp-local-wp"; then
243
241
  print_info "Start with: ~/.aidevops/agents/scripts/localhost-helper.sh start-mcp"
244
242
  print_info "Or configure in OpenCode MCP settings for auto-start"
243
+ setup_track_configured "LocalWP MCP"
245
244
  else
246
245
  print_info "Try manually: sudo npm install -g @verygoodplugins/mcp-local-wp"
247
246
  fi
@@ -254,48 +253,47 @@ setup_localwp_mcp() {
254
253
  }
255
254
 
256
255
  setup_augment_context_engine() {
257
- print_info "Setting up Augment Context Engine MCP..."
258
-
259
- # Check Node.js version (requires 22+)
256
+ # Check prerequisites before announcing setup (GH#5240)
260
257
  if ! command -v node &>/dev/null; then
261
- print_warning "Node.js not found - Augment Context Engine setup skipped"
262
- print_info "Install Node.js 22+ to enable Augment Context Engine"
263
- return
258
+ print_skip "Augment Context Engine" "Node.js not installed" "Install Node.js 22+: brew install node@22 (macOS) or nvm install 22"
259
+ setup_track_deferred "Augment Context Engine" "Install Node.js 22+"
260
+ return 0
264
261
  fi
265
262
 
266
263
  local node_version
267
264
  node_version=$(node --version 2>/dev/null | cut -d'v' -f2 | cut -d'.' -f1)
268
265
  if [[ -z "$node_version" ]] || ! [[ "$node_version" =~ ^[0-9]+$ ]]; then
269
- print_warning "Could not determine Node.js version - Augment Context Engine setup skipped"
270
- return
266
+ print_skip "Augment Context Engine" "could not determine Node.js version"
267
+ setup_track_skipped "Augment Context Engine" "Node.js version unknown"
268
+ return 0
271
269
  fi
272
270
  if [[ "$node_version" -lt 22 ]]; then
273
- print_warning "Node.js 22+ required for Augment Context Engine, found v$node_version"
274
- print_info "Install: brew install node@22 (macOS) or nvm install 22"
275
- return
271
+ print_skip "Augment Context Engine" "requires Node.js 22+, found v$node_version" "Upgrade: brew install node@22 (macOS) or nvm install 22"
272
+ setup_track_deferred "Augment Context Engine" "Upgrade Node.js to 22+ (currently v$node_version)"
273
+ return 0
276
274
  fi
277
275
 
278
- # Check if auggie is installed
279
276
  if ! command -v auggie &>/dev/null; then
280
- print_warning "Auggie CLI not found"
281
- print_info "Install with: npm install -g @augmentcode/auggie@prerelease"
282
- print_info "Then run: auggie login"
283
- return
277
+ print_skip "Augment Context Engine" "Auggie CLI not installed" "Install: npm install -g @augmentcode/auggie@prerelease && auggie login"
278
+ setup_track_deferred "Augment Context Engine" "Install Auggie CLI: npm install -g @augmentcode/auggie@prerelease"
279
+ return 0
284
280
  fi
285
281
 
286
- # Check if logged in
287
282
  if [[ ! -f "$HOME/.augment/session.json" ]]; then
288
- print_warning "Auggie not logged in"
289
- print_info "Run: auggie login"
290
- return
283
+ print_skip "Augment Context Engine" "Auggie not logged in" "Run: auggie login"
284
+ setup_track_deferred "Augment Context Engine" "Run: auggie login"
285
+ return 0
291
286
  fi
292
287
 
288
+ # Prerequisites met — proceed with setup
289
+ print_info "Setting up Augment Context Engine MCP..."
293
290
  print_success "Auggie CLI found and authenticated"
294
291
 
295
292
  # MCP configuration is handled by generate-opencode-agents.sh for OpenCode
296
293
 
297
294
  print_info "Augment Context Engine available as MCP in OpenCode"
298
295
  print_info "Verification: 'What is this project? Please use codebase retrieval tool.'"
296
+ setup_track_configured "Augment Context Engine"
299
297
 
300
298
  return 0
301
299
  }
@@ -479,34 +477,35 @@ add_opencode_plugin() {
479
477
  }
480
478
 
481
479
  setup_opencode_plugins() {
482
- print_info "Setting up OpenCode plugins..."
483
-
484
- # Check if OpenCode is installed
480
+ # Check prerequisites before announcing setup (GH#5240)
485
481
  if ! command -v opencode &>/dev/null; then
486
- print_warning "OpenCode not found - plugin setup skipped"
487
- print_info "Install OpenCode first: https://opencode.ai"
482
+ print_skip "OpenCode plugins" "OpenCode not installed" "Install from https://opencode.ai"
483
+ setup_track_skipped "OpenCode plugins" "OpenCode not installed"
488
484
  return 0
489
485
  fi
490
486
 
491
- # Check if config exists
492
487
  local opencode_config
493
488
  if ! opencode_config=$(find_opencode_config); then
494
- print_warning "OpenCode config not found - plugin setup skipped"
489
+ print_skip "OpenCode plugins" "OpenCode config not found" "Run 'opencode' once to create config, then re-run setup"
490
+ setup_track_deferred "OpenCode plugins" "Run 'opencode' once to create config"
495
491
  return 0
496
492
  fi
497
493
 
498
- # Check if jq is available
499
494
  if ! command -v jq &>/dev/null; then
500
- print_warning "jq not found - cannot update OpenCode config"
495
+ print_skip "OpenCode plugins" "jq not installed" "Install jq: brew install jq (macOS) or apt install jq"
496
+ setup_track_deferred "OpenCode plugins" "Install jq"
501
497
  return 0
502
498
  fi
503
499
 
500
+ # Prerequisites met — proceed with setup
501
+ print_info "Setting up OpenCode plugins..."
502
+
504
503
  # Setup aidevops compaction plugin (local file plugin)
505
504
  local aidevops_plugin_path="$HOME/.aidevops/agents/plugins/opencode-aidevops/index.mjs"
506
505
  if [[ -f "$aidevops_plugin_path" ]]; then
507
- print_info "Setting up aidevops compaction plugin..."
508
506
  add_opencode_plugin "file://$HOME/.aidevops" "file://${aidevops_plugin_path}" "$opencode_config"
509
507
  print_success "aidevops compaction plugin registered (preserves context across compaction)"
508
+ setup_track_configured "OpenCode plugins"
510
509
  fi
511
510
 
512
511
  # Note: opencode-anthropic-auth is built into OpenCode v1.1.36+
@@ -514,7 +513,7 @@ setup_opencode_plugins() {
514
513
  # Removed in v2.90.0 - see PR #230.
515
514
 
516
515
  print_info "After setup, authenticate with: opencode auth login"
517
- print_info " For Claude OAuth: Select 'Anthropic' 'Claude Pro/Max' (built-in)"
516
+ print_info " - For Claude OAuth: Select 'Anthropic' -> 'Claude Pro/Max' (built-in)"
518
517
 
519
518
  return 0
520
519
  }
@@ -566,32 +565,31 @@ setup_seo_mcps() {
566
565
  }
567
566
 
568
567
  setup_google_analytics_mcp() {
569
- print_info "Setting up Google Analytics MCP..."
570
-
571
568
  local gsc_creds="$HOME/.config/aidevops/gsc-credentials.json"
572
569
 
573
- # Check if opencode.json exists
570
+ # Check prerequisites before announcing setup (GH#5240)
574
571
  local opencode_config
575
572
  if ! opencode_config=$(find_opencode_config); then
576
- print_warning "OpenCode config not found - skipping Google Analytics MCP"
573
+ print_skip "Google Analytics MCP" "OpenCode config not found" "Run 'opencode' once to create config, then re-run setup"
574
+ setup_track_skipped "Google Analytics MCP" "OpenCode config not found"
577
575
  return 0
578
576
  fi
579
577
 
580
- # Check if jq is available
581
578
  if ! command -v jq &>/dev/null; then
582
- print_warning "jq not found - cannot add Google Analytics MCP to config"
583
- print_info "Install jq and re-run setup, or manually add the MCP config"
579
+ print_skip "Google Analytics MCP" "jq not installed" "Install jq: brew install jq (macOS) or apt install jq"
580
+ setup_track_deferred "Google Analytics MCP" "Install jq"
584
581
  return 0
585
582
  fi
586
583
 
587
- # Check if pipx is available
588
584
  if ! command -v pipx &>/dev/null; then
589
- print_warning "pipx not found - Google Analytics MCP requires pipx"
590
- print_info "Install pipx: brew install pipx (macOS) or pip install pipx"
591
- print_info "Then re-run setup to add Google Analytics MCP"
585
+ print_skip "Google Analytics MCP" "pipx not installed" "Install pipx: brew install pipx (macOS) or pip install pipx"
586
+ setup_track_deferred "Google Analytics MCP" "Install pipx"
592
587
  return 0
593
588
  fi
594
589
 
590
+ # Prerequisites met — proceed with setup
591
+ print_info "Setting up Google Analytics MCP..."
592
+
595
593
  # Auto-detect credentials from shared GSC service account
596
594
  local creds_path=""
597
595
  local project_id=""
@@ -672,19 +670,20 @@ setup_google_analytics_mcp() {
672
670
  }
673
671
 
674
672
  setup_quickfile_mcp() {
675
- print_info "Setting up QuickFile MCP server..."
676
-
677
673
  local quickfile_dir="$HOME/Git/quickfile-mcp"
678
674
  local credentials_dir="$HOME/.config/.quickfile-mcp"
679
675
  local credentials_file="$credentials_dir/credentials.json"
680
676
 
681
- # Check if Node.js is available
677
+ # Check prerequisites before announcing setup (GH#5240)
682
678
  if ! command -v node &>/dev/null; then
683
- print_warning "Node.js not found - QuickFile MCP setup skipped"
684
- print_info "Install Node.js 18+ to enable QuickFile MCP"
679
+ print_skip "QuickFile MCP" "Node.js not installed" "Install Node.js 18+: brew install node (macOS) or nvm install 18"
680
+ setup_track_deferred "QuickFile MCP" "Install Node.js 18+"
685
681
  return 0
686
682
  fi
687
683
 
684
+ # Prerequisites met — proceed with setup
685
+ print_info "Setting up QuickFile MCP server..."
686
+
688
687
  # Check if already cloned and built
689
688
  if [[ -f "$quickfile_dir/dist/index.js" ]]; then
690
689
  print_success "QuickFile MCP already installed at $quickfile_dir"
@@ -37,6 +37,22 @@ cleanup_deprecated_paths() {
37
37
  "$agents_dir/youtube"
38
38
  # osgrep removed — disproportionate CPU/disk cost vs rg + LLM comprehension
39
39
  "$agents_dir/tools/context/osgrep.md"
40
+ # GH#5155: scripts archived upstream but orphaned in deployed installs
41
+ # (rsync only adds/overwrites, doesn't delete removed files)
42
+ "$agents_dir/scripts/pattern-tracker-helper.sh"
43
+ "$agents_dir/scripts/quality-sweep-helper.sh"
44
+ "$agents_dir/scripts/quality-loop-helper.sh"
45
+ "$agents_dir/scripts/review-pulse-helper.sh"
46
+ "$agents_dir/scripts/self-improve-helper.sh"
47
+ "$agents_dir/scripts/coderabbit-pulse-helper.sh"
48
+ "$agents_dir/scripts/coderabbit-task-creator-helper.sh"
49
+ "$agents_dir/scripts/audit-task-creator-helper.sh"
50
+ "$agents_dir/scripts/batch-cleanup-helper.sh"
51
+ "$agents_dir/scripts/coordinator-helper.sh"
52
+ "$agents_dir/scripts/finding-to-task-helper.sh"
53
+ "$agents_dir/scripts/objective-runner-helper.sh"
54
+ "$agents_dir/scripts/ralph-loop-helper.sh"
55
+ "$agents_dir/scripts/stale-pr-helper.sh"
40
56
  )
41
57
 
42
58
  for path in "${deprecated_paths[@]}"; do
@@ -963,3 +979,88 @@ migrate_pulse_repos_to_repos_json() {
963
979
 
964
980
  return 0
965
981
  }
982
+
983
+ # Migrate orphaned supervisor-helper.sh and supervisor/ modules (GH#5147)
984
+ # After the supervisor-to-pulse-wrapper migration (PR #2291, PR #2475), the
985
+ # upstream repo moved supervisor files to supervisor-archived/. But aidevops
986
+ # update (rsync) only adds/overwrites — it doesn't delete files that no longer
987
+ # exist in the source. Users who installed before the migration retain:
988
+ # - ~/.aidevops/agents/scripts/supervisor-helper.sh (old entry point)
989
+ # - ~/.aidevops/agents/scripts/supervisor/ (old module directory)
990
+ # - cron/launchd entries invoking supervisor-helper.sh pulse
991
+ # These orphaned files shadow the new pulse-wrapper.sh architecture.
992
+ # This migration removes the orphaned files and rewrites scheduler entries.
993
+ migrate_orphaned_supervisor() {
994
+ local agents_dir="$HOME/.aidevops/agents"
995
+ local scripts_dir="$agents_dir/scripts"
996
+ local cleaned=0
997
+
998
+ # 1. Remove orphaned supervisor-helper.sh from deployed scripts
999
+ # The canonical location is now supervisor-archived/supervisor-helper.sh
1000
+ # (shipped for reference/tests only, not as an active entry point)
1001
+ if [[ -f "$scripts_dir/supervisor-helper.sh" ]]; then
1002
+ rm -f "$scripts_dir/supervisor-helper.sh"
1003
+ print_info "Removed orphaned supervisor-helper.sh from deployed scripts"
1004
+ ((++cleaned))
1005
+ fi
1006
+
1007
+ # 2. Remove orphaned supervisor/ module directory
1008
+ # The canonical location is now supervisor-archived/ (shipped by rsync)
1009
+ # Only remove if it's the old modules dir (contains pulse.sh, dispatch.sh, etc.)
1010
+ # Do NOT remove supervisor-archived/ — that's the intentional archive
1011
+ if [[ -d "$scripts_dir/supervisor" && ! -L "$scripts_dir/supervisor" ]]; then
1012
+ # Verify it's the old module directory (not something user-created)
1013
+ if [[ -f "$scripts_dir/supervisor/pulse.sh" ]] ||
1014
+ [[ -f "$scripts_dir/supervisor/dispatch.sh" ]] ||
1015
+ [[ -f "$scripts_dir/supervisor/_common.sh" ]]; then
1016
+ rm -rf "$scripts_dir/supervisor"
1017
+ print_info "Removed orphaned supervisor/ module directory from deployed scripts"
1018
+ ((++cleaned))
1019
+ fi
1020
+ fi
1021
+
1022
+ # 3. Migrate cron entries from supervisor-helper.sh to pulse-wrapper.sh
1023
+ # Old pattern: */2 * * * * ... supervisor-helper.sh pulse ...
1024
+ # New pattern: already installed by setup.sh's pulse section
1025
+ # Strategy: remove old entries; setup.sh will install the new one if pulse is enabled
1026
+ local current_crontab
1027
+ current_crontab=$(crontab -l 2>/dev/null) || current_crontab=""
1028
+ if echo "$current_crontab" | grep -qF "supervisor-helper.sh"; then
1029
+ # Remove all cron lines referencing supervisor-helper.sh
1030
+ local new_crontab
1031
+ new_crontab=$(echo "$current_crontab" | grep -v "supervisor-helper.sh")
1032
+ if [[ -n "$new_crontab" ]]; then
1033
+ printf '%s\n' "$new_crontab" | crontab - || true
1034
+ else
1035
+ # All entries were supervisor-helper.sh — remove crontab entirely
1036
+ crontab -r || true
1037
+ fi
1038
+ print_info "Removed orphaned supervisor-helper.sh cron entries"
1039
+ print_info " pulse-wrapper.sh will be installed by setup.sh if supervisor pulse is enabled"
1040
+ ((++cleaned))
1041
+ fi
1042
+
1043
+ # 4. Migrate launchd entries from old supervisor label (macOS only)
1044
+ # Old label: com.aidevops.supervisor-pulse (from cron.sh/launchd.sh)
1045
+ # New label: com.aidevops.aidevops-supervisor-pulse (from setup.sh)
1046
+ # setup.sh already handles the new label cleanup at line ~1000, but
1047
+ # the old label from cron.sh may also be present
1048
+ if [[ "$(uname -s)" == "Darwin" ]]; then
1049
+ local old_label="com.aidevops.supervisor-pulse"
1050
+ local old_plist="$HOME/Library/LaunchAgents/${old_label}.plist"
1051
+ if _launchd_has_agent "$old_label" || [[ -f "$old_plist" ]]; then
1052
+ # Use launchctl remove by label — works even when the plist file is
1053
+ # missing (orphaned agent loaded without a backing file on disk)
1054
+ launchctl remove "$old_label" || true
1055
+ rm -f "$old_plist"
1056
+ print_info "Removed orphaned supervisor-pulse LaunchAgent ($old_label)"
1057
+ ((++cleaned))
1058
+ fi
1059
+ fi
1060
+
1061
+ if [[ $cleaned -gt 0 ]]; then
1062
+ print_success "Cleaned up $cleaned orphaned supervisor artifact(s) — pulse-wrapper.sh is the active system"
1063
+ fi
1064
+
1065
+ return 0
1066
+ }
@@ -117,12 +117,13 @@ deploy_plugins() {
117
117
  local target_dir="$1"
118
118
  local plugins_file="$2"
119
119
 
120
- # Skip if no plugins.json or no jq
120
+ # Skip if no plugins.json or no jq (GH#5240: clear skip messages)
121
121
  if [[ ! -f "$plugins_file" ]]; then
122
122
  return 0
123
123
  fi
124
124
  if ! command -v jq &>/dev/null; then
125
- print_warning "jq not found; skipping plugin deployment"
125
+ print_skip "Plugin deployment" "jq not installed" "Install jq: brew install jq (macOS) or apt install jq"
126
+ setup_track_deferred "Plugin deployment" "Install jq"
126
127
  return 0
127
128
  fi
128
129
 
@@ -401,15 +402,18 @@ check_skill_updates() {
401
402
  }
402
403
 
403
404
  scan_imported_skills() {
404
- print_info "Running security scan on imported skills..."
405
-
405
+ # Check prerequisites before announcing setup (GH#5240)
406
406
  local security_helper="$HOME/.aidevops/agents/scripts/security-helper.sh"
407
407
 
408
408
  if [[ ! -f "$security_helper" ]]; then
409
- print_warning "security-helper.sh not found - skipping skill scan"
409
+ print_skip "Skill security scan" "security-helper.sh not found" "Deploy agents first (setup.sh), then re-run"
410
+ setup_track_skipped "Skill security scan" "security-helper.sh not found"
410
411
  return 0
411
412
  fi
412
413
 
414
+ # Prerequisites met — proceed with setup
415
+ print_info "Running security scan on imported skills..."
416
+
413
417
  # Install skill-scanner if not present
414
418
  # Pre-check: cisco-ai-skill-scanner requires Python >= 3.10
415
419
  # Fallback chain: uv -> pipx -> venv+symlink -> pip3 --user (legacy)
@@ -484,8 +488,7 @@ scan_imported_skills() {
484
488
  }
485
489
 
486
490
  setup_multi_tenant_credentials() {
487
- print_info "Multi-tenant credential storage..."
488
-
491
+ # Check prerequisites before announcing setup (GH#5240)
489
492
  local credential_helper="$HOME/.aidevops/agents/scripts/credential-helper.sh"
490
493
 
491
494
  if [[ ! -f "$credential_helper" ]]; then
@@ -494,10 +497,14 @@ setup_multi_tenant_credentials() {
494
497
  fi
495
498
 
496
499
  if [[ ! -f "$credential_helper" ]]; then
497
- print_warning "credential-helper.sh not found - skipping"
500
+ print_skip "Multi-tenant credentials" "credential-helper.sh not found" "Deploy agents first (setup.sh), then re-run"
501
+ setup_track_skipped "Multi-tenant credentials" "credential-helper.sh not found"
498
502
  return 0
499
503
  fi
500
504
 
505
+ # Prerequisites met — proceed with setup
506
+ print_info "Multi-tenant credential storage..."
507
+
501
508
  # Check if already initialized
502
509
  if [[ -d "$HOME/.config/aidevops/tenants" ]]; then
503
510
  local tenant_count
@@ -547,8 +554,7 @@ setup_multi_tenant_credentials() {
547
554
  }
548
555
 
549
556
  check_tool_updates() {
550
- print_info "Checking for tool updates..."
551
-
557
+ # Check prerequisites before announcing setup (GH#5240)
552
558
  local tool_check_script="$HOME/.aidevops/agents/scripts/tool-version-check.sh"
553
559
 
554
560
  if [[ ! -f "$tool_check_script" ]]; then
@@ -557,10 +563,14 @@ check_tool_updates() {
557
563
  fi
558
564
 
559
565
  if [[ ! -f "$tool_check_script" ]]; then
560
- print_warning "Tool version check script not found - skipping update check"
566
+ print_skip "Tool updates" "version check script not found" "Deploy agents first (setup.sh), then re-run"
567
+ setup_track_skipped "Tool updates" "version check script not found"
561
568
  return 0
562
569
  fi
563
570
 
571
+ # Prerequisites met — proceed with setup
572
+ print_info "Checking for tool updates..."
573
+
564
574
  # Run the check in quiet mode first to see if there are updates
565
575
  # Capture both output and exit code
566
576
  local outdated_output
@@ -94,24 +94,25 @@ get_all_shell_rcs() {
94
94
 
95
95
  # Offer to install Oh My Zsh if zsh is the default shell and OMZ is not present
96
96
  setup_oh_my_zsh() {
97
- # Only relevant if zsh is available
97
+ # Check prerequisites before announcing setup (GH#5240)
98
98
  if ! command -v zsh >/dev/null 2>&1; then
99
- print_info "zsh not found - skipping Oh My Zsh setup"
99
+ print_skip "Oh My Zsh" "zsh not installed" "Install zsh first, then re-run setup"
100
+ setup_track_skipped "Oh My Zsh" "zsh not installed"
100
101
  return 0
101
102
  fi
102
103
 
103
- # Check if Oh My Zsh is already installed
104
104
  if [[ -d "$HOME/.oh-my-zsh" ]]; then
105
105
  print_success "Oh My Zsh already installed"
106
+ setup_track_configured "Oh My Zsh"
106
107
  return 0
107
108
  fi
108
109
 
109
110
  local default_shell
110
111
  default_shell=$(detect_default_shell)
111
112
 
112
- # Only offer if zsh is the default shell (or on macOS where it's the system default)
113
113
  if [[ "$default_shell" != "zsh" && "$(uname)" != "Darwin" ]]; then
114
- print_info "Default shell is $default_shell (not zsh) - skipping Oh My Zsh"
114
+ print_skip "Oh My Zsh" "default shell is $default_shell (not zsh)" "Change default shell to zsh: chsh -s \$(which zsh)"
115
+ setup_track_skipped "Oh My Zsh" "default shell is $default_shell"
115
116
  return 0
116
117
  fi
117
118
 
@@ -386,6 +387,10 @@ check_optional_deps() {
386
387
  print_info "Checking optional dependencies..."
387
388
 
388
389
  local missing_optional=()
390
+ local recommended_python_formula
391
+ recommended_python_formula=$(get_recommended_python_formula)
392
+ local python_required_major="${PYTHON_REQUIRED_MAJOR:-3}"
393
+ local python_required_minor="${PYTHON_REQUIRED_MINOR:-10}"
389
394
 
390
395
  if ! command -v sshpass >/dev/null 2>&1; then
391
396
  missing_optional+=("sshpass")
@@ -393,6 +398,26 @@ check_optional_deps() {
393
398
  print_success "sshpass found"
394
399
  fi
395
400
 
401
+ local python3_bin=""
402
+ if python3_bin=$(find_python3); then
403
+ local python_version
404
+ python_version=$("$python3_bin" -c 'import sys; print("{}.{}.{}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]))' 2>/dev/null || true)
405
+ local python_major
406
+ python_major=$(echo "$python_version" | cut -d. -f1)
407
+ local python_minor
408
+ python_minor=$(echo "$python_version" | cut -d. -f2)
409
+
410
+ if [[ "$python_major" =~ ^[0-9]+$ ]] && [[ "$python_minor" =~ ^[0-9]+$ ]] && { ((python_major > python_required_major)) || { ((python_major == python_required_major)) && ((python_minor >= python_required_minor)); }; }; then
411
+ print_success "Python $python_version found ($python_required_major.$python_required_minor+ required)"
412
+ else
413
+ print_warning "Python $python_version found, but $python_required_major.$python_required_minor+ is recommended for all skills/tools"
414
+ offer_python_brew_install "upgrade" "$recommended_python_formula" || true
415
+ fi
416
+ else
417
+ print_warning "Python 3 not found"
418
+ offer_python_brew_install "install" "$recommended_python_formula" || true
419
+ fi
420
+
396
421
  if [[ ${#missing_optional[@]} -gt 0 ]]; then
397
422
  print_warning "Missing optional dependencies: ${missing_optional[*]}"
398
423
  echo " sshpass - needed for password-based SSH (like Hostinger)"
@@ -822,12 +847,12 @@ ALIASES
822
847
 
823
848
  # Install terminal title integration that syncs tab titles with git repo/branch
824
849
  setup_terminal_title() {
825
- print_info "Setting up terminal title integration..."
826
-
850
+ # Check prerequisites before announcing setup (GH#5240)
827
851
  local setup_script=".agents/scripts/terminal-title-setup.sh"
828
852
 
829
853
  if [[ ! -f "$setup_script" ]]; then
830
- print_warning "Terminal title setup script not found - skipping"
854
+ print_skip "Terminal title" "setup script not found" "Deploy agents first (setup.sh), then re-run"
855
+ setup_track_skipped "Terminal title" "setup script not found"
831
856
  return 0
832
857
  fi
833
858
 
@@ -843,10 +868,14 @@ setup_terminal_title() {
843
868
  done < <(get_all_shell_rcs)
844
869
 
845
870
  if [[ "$title_configured" == "true" ]]; then
846
- print_info "Terminal title integration already configured - Skipping"
871
+ print_success "Terminal title integration already configured"
872
+ setup_track_configured "Terminal title"
847
873
  return 0
848
874
  fi
849
875
 
876
+ # Prerequisites met — proceed with setup
877
+ print_info "Setting up terminal title integration..."
878
+
850
879
  # Show current status before asking
851
880
  echo ""
852
881
  print_info "Terminal title integration syncs your terminal tab with git repo/branch"
@@ -57,7 +57,7 @@ setup_git_clis() {
57
57
  echo "📋 Next steps - authenticate each CLI:"
58
58
  for pkg in "${missing_packages[@]}"; do
59
59
  case "$pkg" in
60
- gh) echo " • gh auth login" ;;
60
+ gh) echo " • gh auth login -s workflow (workflow scope required for CI PRs)" ;;
61
61
  glab) echo " • glab auth login" ;;
62
62
  esac
63
63
  done
@@ -1001,6 +1001,92 @@ setup_ssh_key() {
1001
1001
  return 0
1002
1002
  }
1003
1003
 
1004
+ # Check installed Python version against latest stable available from package manager.
1005
+ # Warns if an upgrade is available but never auto-upgrades (GH#5237).
1006
+ # Works on macOS (Homebrew) and Linux (apt/dnf).
1007
+ check_python_version() {
1008
+ print_info "Checking Python version..."
1009
+
1010
+ # 1. Check currently installed Python
1011
+ local python3_bin
1012
+ if ! python3_bin=$(find_python3); then
1013
+ print_warning "Python 3 not found"
1014
+ echo ""
1015
+ echo " Install options:"
1016
+ if [[ "$PLATFORM_MACOS" == "true" ]]; then
1017
+ echo " brew install python3"
1018
+ elif command -v apt-get >/dev/null 2>&1; then
1019
+ echo " sudo apt install python3"
1020
+ elif command -v dnf >/dev/null 2>&1; then
1021
+ echo " sudo dnf install python3"
1022
+ else
1023
+ echo " Install Python 3 via your system package manager"
1024
+ fi
1025
+ echo ""
1026
+ return 0
1027
+ fi
1028
+
1029
+ local installed_version
1030
+ installed_version=$("$python3_bin" --version 2>&1 | cut -d' ' -f2)
1031
+ local installed_major installed_minor
1032
+ installed_major=$(echo "$installed_version" | cut -d. -f1)
1033
+ installed_minor=$(echo "$installed_version" | cut -d. -f2)
1034
+
1035
+ # 2. Determine latest stable version from package manager
1036
+ local latest_version=""
1037
+
1038
+ if [[ "$PLATFORM_MACOS" == "true" ]] && command -v brew >/dev/null 2>&1; then
1039
+ # Homebrew: `brew info python3` outputs "python@3.X: 3.X.Y" on the first line
1040
+ latest_version=$(brew info --json=v2 python3 2>/dev/null |
1041
+ python3 -c "import sys,json; d=json.load(sys.stdin); print(d['formulae'][0]['versions']['stable'])" 2>/dev/null) || latest_version=""
1042
+ elif command -v apt-cache >/dev/null 2>&1; then
1043
+ # Debian/Ubuntu: get candidate version from apt-cache
1044
+ latest_version=$(apt-cache policy python3 2>/dev/null |
1045
+ awk '/Candidate:/{print $2}' |
1046
+ grep -oE '[0-9]+\.[0-9]+\.[0-9]+') || latest_version=""
1047
+ elif command -v dnf >/dev/null 2>&1; then
1048
+ # Fedora/RHEL: get available version from dnf
1049
+ latest_version=$(dnf info python3 2>/dev/null |
1050
+ awk '/^Version/{print $3}') || latest_version=""
1051
+ fi
1052
+
1053
+ # 3. Compare versions and advise
1054
+ if [[ -z "$latest_version" ]]; then
1055
+ # Could not determine latest — just report installed version
1056
+ print_success "Python $installed_version found"
1057
+ return 0
1058
+ fi
1059
+
1060
+ local latest_major latest_minor
1061
+ latest_major=$(echo "$latest_version" | cut -d. -f1)
1062
+ latest_minor=$(echo "$latest_version" | cut -d. -f2)
1063
+
1064
+ # Compare major.minor (patch differences are not worth warning about)
1065
+ if [[ "$installed_major" -lt "$latest_major" ]] ||
1066
+ { [[ "$installed_major" -eq "$latest_major" ]] && [[ "$installed_minor" -lt "$latest_minor" ]]; }; then
1067
+ print_warning "Python $installed_version installed, but $latest_version is available"
1068
+ echo ""
1069
+ echo " Some tools and skills require Python 3.10+."
1070
+ echo " Upgrade is recommended but not required."
1071
+ echo ""
1072
+ if [[ "$PLATFORM_MACOS" == "true" ]]; then
1073
+ echo " Upgrade command:"
1074
+ echo " brew upgrade python3"
1075
+ elif command -v apt-get >/dev/null 2>&1; then
1076
+ echo " Upgrade command:"
1077
+ echo " sudo apt update && sudo apt install python3"
1078
+ elif command -v dnf >/dev/null 2>&1; then
1079
+ echo " Upgrade command:"
1080
+ echo " sudo dnf upgrade python3"
1081
+ fi
1082
+ echo ""
1083
+ else
1084
+ print_success "Python $installed_version found (latest stable: $latest_version)"
1085
+ fi
1086
+
1087
+ return 0
1088
+ }
1089
+
1004
1090
  setup_python_env() {
1005
1091
  print_info "Setting up Python environment for DSPy..."
1006
1092
 
@@ -1370,36 +1456,32 @@ setup_ai_orchestration() {
1370
1456
  print_info "Setting up AI orchestration frameworks..."
1371
1457
 
1372
1458
  local has_python=false
1373
-
1374
- # Check Python (prefer Homebrew/pyenv over system)
1459
+ local python_required_major="${PYTHON_REQUIRED_MAJOR:-3}"
1460
+ local python_required_minor="${PYTHON_REQUIRED_MINOR:-10}"
1461
+ local recommended_python_formula
1462
+ recommended_python_formula=$(get_recommended_python_formula)
1463
+
1464
+ # Check Python (prefer Homebrew/pyenv over system) — uses shared helpers
1465
+ # from _common.sh (get_recommended_python_formula, find_python3,
1466
+ # offer_python_brew_install) to avoid duplicating version-check logic.
1375
1467
  local python3_bin
1376
1468
  if python3_bin=$(find_python3); then
1377
1469
  local python_version
1378
- python_version=$("$python3_bin" --version 2>&1 | cut -d' ' -f2)
1470
+ python_version=$("$python3_bin" -c 'import sys; print("{}.{}.{}".format(sys.version_info[0], sys.version_info[1], sys.version_info[2]))' 2>/dev/null || true)
1379
1471
  local major minor
1380
1472
  major=$(echo "$python_version" | cut -d. -f1)
1381
1473
  minor=$(echo "$python_version" | cut -d. -f2)
1382
1474
 
1383
- if [[ $major -ge 3 ]] && [[ $minor -ge 10 ]]; then
1475
+ if [[ "$major" =~ ^[0-9]+$ ]] && [[ "$minor" =~ ^[0-9]+$ ]] && { ((major > python_required_major)) || { ((major == python_required_major)) && ((minor >= python_required_minor)); }; }; then
1384
1476
  has_python=true
1385
- print_success "Python $python_version found (3.10+ required)"
1477
+ print_success "Python $python_version found ($python_required_major.$python_required_minor+ required)"
1386
1478
  else
1387
- print_warning "Python 3.10+ required for AI orchestration, found $python_version"
1388
- echo ""
1389
- echo " Upgrade options:"
1390
- echo " macOS (Homebrew): brew install python@3.12"
1391
- echo " macOS (pyenv): pyenv install 3.12 && pyenv global 3.12"
1392
- echo " Ubuntu/Debian: sudo apt install python3.12"
1393
- echo " Fedora: sudo dnf install python3.12"
1394
- echo ""
1479
+ print_warning "Python $python_required_major.$python_required_minor+ required for AI orchestration, found $python_version"
1480
+ offer_python_brew_install "upgrade" "$recommended_python_formula" || true
1395
1481
  fi
1396
1482
  else
1397
1483
  print_warning "Python 3 not found - AI orchestration frameworks unavailable"
1398
- echo ""
1399
- echo " Install options:"
1400
- echo " macOS: brew install python@3.12"
1401
- echo " Linux: sudo apt install python3 (or dnf/pacman)"
1402
- echo ""
1484
+ offer_python_brew_install "install" "$recommended_python_formula" || true
1403
1485
  return 0
1404
1486
  fi
1405
1487
 
package/setup.sh CHANGED
@@ -10,7 +10,7 @@ shopt -s inherit_errexit 2>/dev/null || true
10
10
  # AI Assistant Server Access Framework Setup Script
11
11
  # Helps developers set up the framework for their infrastructure
12
12
  #
13
- # Version: 3.0.12
13
+ # Version: 3.1.1
14
14
  #
15
15
  # Quick Install:
16
16
  # npm install -g aidevops && aidevops update (recommended)
@@ -22,6 +22,7 @@ GREEN='\033[0;32m'
22
22
  BLUE='\033[0;34m'
23
23
  YELLOW='\033[1;33m'
24
24
  RED='\033[0;31m'
25
+ GRAY='\033[0;90m'
25
26
  NC='\033[0m' # No Color
26
27
 
27
28
  # Global flags
@@ -29,6 +30,11 @@ CLEAN_MODE=false
29
30
  INTERACTIVE_MODE=false
30
31
  NON_INTERACTIVE="${AIDEVOPS_NON_INTERACTIVE:-false}"
31
32
  UPDATE_TOOLS_MODE=false
33
+ # Python compatibility floor used by setup checks and skill/tool gating.
34
+ # Keep in sync with setup-modules/plugins.sh requirements.
35
+ PYTHON_REQUIRED_MAJOR=3
36
+ PYTHON_REQUIRED_MINOR=10
37
+ export PYTHON_REQUIRED_MAJOR PYTHON_REQUIRED_MINOR
32
38
  # Platform constants — exported for sourced setup-modules (shell-env.sh,
33
39
  # tool-install.sh) that reference them at runtime.
34
40
  PLATFORM_MACOS=$([[ "$(uname -s)" == "Darwin" ]] && echo true || echo false)
@@ -362,26 +368,8 @@ find_opencode_config() {
362
368
  return 1
363
369
  }
364
370
 
365
- # Find best python3 binary (prefer Homebrew/pyenv over system)
366
- find_python3() {
367
- local candidates=(
368
- "/opt/homebrew/bin/python3"
369
- "/usr/local/bin/python3"
370
- "$HOME/.pyenv/shims/python3"
371
- )
372
- for candidate in "${candidates[@]}"; do
373
- if [[ -x "$candidate" ]]; then
374
- echo "$candidate"
375
- return 0
376
- fi
377
- done
378
- # Fallback to PATH
379
- if command -v python3 &>/dev/null; then
380
- command -v python3
381
- return 0
382
- fi
383
- return 1
384
- }
371
+ # get_latest_homebrew_python_formula() and find_python3() are defined in
372
+ # _common.sh (sourced above). Not duplicated here — see GH#5239 review.
385
373
 
386
374
  # Install a package globally via npm, with sudo when needed on Linux.
387
375
  # Usage: npm_global_install "package-name" OR npm_global_install "package@version"
@@ -709,6 +697,7 @@ main() {
709
697
  print_info "Non-interactive mode: deploying agents and running safe migrations only"
710
698
  verify_location
711
699
  check_requirements
700
+ check_python_version
712
701
  set_permissions
713
702
  migrate_old_backups
714
703
  migrate_loop_state_directories
@@ -716,6 +705,7 @@ main() {
716
705
  migrate_mcp_env_to_credentials
717
706
  migrate_pulse_repos_to_repos_json
718
707
  cleanup_deprecated_paths
708
+ migrate_orphaned_supervisor
719
709
  cleanup_deprecated_mcps
720
710
  cleanup_stale_bun_opencode
721
711
  validate_opencode_config
@@ -772,6 +762,7 @@ main() {
772
762
 
773
763
  # Optional steps with confirmation in interactive mode
774
764
  confirm_step "Check optional dependencies (bun, node, python)" && check_optional_deps
765
+ confirm_step "Check Python version (recommend upgrade if outdated)" && check_python_version
775
766
  confirm_step "Setup recommended tools (Tabby, Zed, etc.)" && setup_recommended_tools
776
767
  confirm_step "Setup MiniSim (iOS/Android emulator launcher)" && setup_minisim
777
768
  confirm_step "Setup Git CLIs (gh, glab, tea)" && setup_git_clis
@@ -795,6 +786,7 @@ main() {
795
786
  confirm_step "Migrate mcp-env.sh -> credentials.sh" && migrate_mcp_env_to_credentials
796
787
  confirm_step "Migrate pulse-repos.json into repos.json" && migrate_pulse_repos_to_repos_json
797
788
  confirm_step "Cleanup deprecated agent paths" && cleanup_deprecated_paths
789
+ confirm_step "Migrate orphaned supervisor to pulse-wrapper" && migrate_orphaned_supervisor
798
790
  confirm_step "Cleanup deprecated MCP entries (hetzner, serper, etc.)" && cleanup_deprecated_mcps
799
791
  confirm_step "Cleanup stale bun opencode install" && cleanup_stale_bun_opencode
800
792
  confirm_step "Validate and repair OpenCode config schema" && validate_opencode_config
@@ -833,8 +825,11 @@ main() {
833
825
  confirm_step "Disable on-demand MCPs globally" && disable_ondemand_mcps
834
826
  fi
835
827
 
828
+ # Print setup summary before final success message (GH#5240)
829
+ print_setup_summary
830
+
836
831
  echo ""
837
- print_success "🎉 Setup complete!"
832
+ print_success "Setup complete!"
838
833
 
839
834
  # Enable auto-update if not already enabled
840
835
  # Check both launchd (macOS) and cron (Linux) for existing installation
@@ -892,10 +887,6 @@ main() {
892
887
  # - Non-interactive: only installs if config explicitly says true
893
888
  local wrapper_script="$HOME/.aidevops/agents/scripts/pulse-wrapper.sh"
894
889
  local pulse_label="com.aidevops.aidevops-supervisor-pulse"
895
- local _aidevops_dir _pulse_repo_dir
896
- _aidevops_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
897
- _pulse_repo_dir=$(_resolve_main_worktree_dir "$_aidevops_dir")
898
-
899
890
  # Read explicit user consent from config.jsonc (not merged defaults).
900
891
  # Empty = user never configured this; "true"/"false" = explicit choice.
901
892
  local _pulse_user_config=""
@@ -1009,12 +1000,14 @@ main() {
1009
1000
 
1010
1001
  # XML-escape paths for safe plist embedding (prevents injection
1011
1002
  # if $HOME or paths contain &, <, > characters)
1012
- local _xml_wrapper_script _xml_home _xml_opencode_bin _xml_aidevops_dir _xml_path
1003
+ local _xml_wrapper_script _xml_home _xml_opencode_bin _xml_pulse_dir _xml_path
1013
1004
  local _headless_xml_env=""
1014
1005
  _xml_wrapper_script=$(_xml_escape "$wrapper_script")
1015
1006
  _xml_home=$(_xml_escape "$HOME")
1016
1007
  _xml_opencode_bin=$(_xml_escape "$opencode_bin")
1017
- _xml_aidevops_dir=$(_xml_escape "$_pulse_repo_dir")
1008
+ # Use neutral workspace path for PULSE_DIR so supervisor sessions
1009
+ # are not associated with any specific managed repo (GH#5136).
1010
+ _xml_pulse_dir=$(_xml_escape "${HOME}/.aidevops/.agent-workspace")
1018
1011
  _xml_path=$(_xml_escape "$PATH")
1019
1012
  if [[ -n "${AIDEVOPS_HEADLESS_MODELS:-}" ]]; then
1020
1013
  local _xml_headless_models
@@ -1061,7 +1054,7 @@ main() {
1061
1054
  <key>OPENCODE_BIN</key>
1062
1055
  <string>${_xml_opencode_bin}</string>
1063
1056
  <key>PULSE_DIR</key>
1064
- <string>${_xml_aidevops_dir}</string>
1057
+ <string>${_xml_pulse_dir}</string>
1065
1058
  <key>PULSE_STALE_THRESHOLD</key>
1066
1059
  <string>1800</string>
1067
1060
  ${_headless_xml_env}
@@ -1092,8 +1085,9 @@ PLIST
1092
1085
  # PATH= here, it overrides the global line and breaks nvm/bun/cargo.
1093
1086
  # OPENCODE_BIN removed — resolved from PATH at runtime via command -v.
1094
1087
  # See #4099 and #4240 for history.
1095
- local _cron_aidevops_dir _cron_wrapper_script _cron_headless_env=""
1096
- _cron_aidevops_dir=$(_cron_escape "$_pulse_repo_dir")
1088
+ local _cron_pulse_dir _cron_wrapper_script _cron_headless_env=""
1089
+ # Use neutral workspace path for PULSE_DIR (GH#5136)
1090
+ _cron_pulse_dir=$(_cron_escape "${HOME}/.aidevops/.agent-workspace")
1097
1091
  _cron_wrapper_script=$(_cron_escape "$wrapper_script")
1098
1092
  if [[ -n "${AIDEVOPS_HEADLESS_MODELS:-}" ]]; then
1099
1093
  local _cron_headless_models
@@ -1107,7 +1101,7 @@ PLIST
1107
1101
  fi
1108
1102
  (
1109
1103
  crontab -l 2>/dev/null | grep -v 'aidevops: supervisor-pulse'
1110
- echo "*/2 * * * * PULSE_DIR=${_cron_aidevops_dir}${_cron_headless_env} /bin/bash ${_cron_wrapper_script} >> \"\$HOME/.aidevops/logs/pulse-wrapper.log\" 2>&1 # aidevops: supervisor-pulse"
1104
+ 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"
1111
1105
  ) | crontab - || true
1112
1106
  if crontab -l 2>/dev/null | grep -qF "aidevops: supervisor-pulse"; then
1113
1107
  print_info "Supervisor pulse enabled (cron, every 2 min). Disable: crontab -e and remove the supervisor-pulse line"