aidevops 3.8.70 → 3.8.72

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.8.70
1
+ 3.8.72
package/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.8.70
8
+ # Version: 3.8.72
9
9
 
10
10
  set -euo pipefail
11
11
 
@@ -61,11 +61,31 @@ _timeout_cmd() {
61
61
  fi
62
62
  }
63
63
 
64
- print_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
65
- print_success() { echo -e "${GREEN}[OK]${NC} $1"; }
66
- print_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
67
- print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
68
- print_header() { echo -e "${BOLD}${CYAN}$1${NC}"; }
64
+ print_info() {
65
+ local msg="$1"
66
+ echo -e "${BLUE}[INFO]${NC} $msg"
67
+ return 0
68
+ }
69
+ print_success() {
70
+ local msg="$1"
71
+ echo -e "${GREEN}[OK]${NC} $msg"
72
+ return 0
73
+ }
74
+ print_warning() {
75
+ local msg="$1"
76
+ echo -e "${YELLOW}[WARN]${NC} $msg"
77
+ return 0
78
+ }
79
+ print_error() {
80
+ local msg="$1"
81
+ echo -e "${RED}[ERROR]${NC} $msg"
82
+ return 0
83
+ }
84
+ print_header() {
85
+ local msg="$1"
86
+ echo -e "${BOLD}${CYAN}$msg${NC}"
87
+ return 0
88
+ }
69
89
 
70
90
  # Get current version
71
91
  get_version() {
@@ -106,17 +126,20 @@ get_public_release_tag() {
106
126
 
107
127
  # Check if a command exists
108
128
  check_cmd() {
109
- command -v "$1" >/dev/null 2>&1
129
+ local cmd="$1"
130
+ command -v "$cmd" >/dev/null 2>&1
110
131
  }
111
132
 
112
133
  # Check if a directory exists
113
134
  check_dir() {
114
- [[ -d "$1" ]]
135
+ local dir="$1"
136
+ [[ -d "$dir" ]]
115
137
  }
116
138
 
117
139
  # Check if a file exists
118
140
  check_file() {
119
- [[ -f "$1" ]]
141
+ local file="$1"
142
+ [[ -f "$file" ]]
120
143
  }
121
144
 
122
145
  # Ensure file ends with a trailing newline (prevents malformed appends)
@@ -268,6 +291,61 @@ _compute_repo_registration_defaults() {
268
291
  return 0
269
292
  }
270
293
 
294
+ # Resolve a worktree path to its canonical main-worktree path, if applicable.
295
+ # Usage: resolve_canonical_repo_path <path>
296
+ # Prints the canonical path to stdout. If the input is already the main
297
+ # worktree, a non-git path, or git is unavailable, prints the input unchanged.
298
+ #
299
+ # Why this exists: `find ~/Git -name .aidevops.json` in auto-discovery and
300
+ # similar scans pick up .aidevops.json files that exist in linked worktrees
301
+ # (because worktrees inherit the working tree contents), and without this
302
+ # guard each worktree gets registered as a separate repo. That's what caused
303
+ # tabby-profile-sync to emit a profile for a worktree directory.
304
+ resolve_canonical_repo_path() {
305
+ local input_path="$1"
306
+ local common_dir
307
+ common_dir=$(git -C "$input_path" rev-parse --git-common-dir 2>/dev/null) || {
308
+ printf '%s\n' "$input_path"
309
+ return 0
310
+ }
311
+ local own_git_dir
312
+ own_git_dir=$(git -C "$input_path" rev-parse --git-dir 2>/dev/null) || {
313
+ printf '%s\n' "$input_path"
314
+ return 0
315
+ }
316
+
317
+ # Resolve both to absolute paths for a reliable comparison.
318
+ # git -C <path> returns paths relative to <path> when they are relative.
319
+ local common_abs own_abs
320
+ if [[ "$common_dir" = /* ]]; then
321
+ common_abs=$(cd "$common_dir" 2>/dev/null && pwd -P)
322
+ else
323
+ common_abs=$(cd "$input_path/$common_dir" 2>/dev/null && pwd -P)
324
+ fi
325
+ if [[ "$own_git_dir" = /* ]]; then
326
+ own_abs=$(cd "$own_git_dir" 2>/dev/null && pwd -P)
327
+ else
328
+ own_abs=$(cd "$input_path/$own_git_dir" 2>/dev/null && pwd -P)
329
+ fi
330
+
331
+ if [[ -z "$common_abs" || -z "$own_abs" || "$common_abs" == "$own_abs" ]]; then
332
+ # Main worktree or degraded resolution — pass through.
333
+ printf '%s\n' "$input_path"
334
+ return 0
335
+ fi
336
+
337
+ # Linked worktree — ask git for the main worktree's working tree path.
338
+ local main_path
339
+ main_path=$(git -C "$input_path" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')
340
+ if [[ -n "$main_path" && "$main_path" != "$input_path" && -d "$main_path" ]]; then
341
+ printf '%s\n' "$main_path"
342
+ return 0
343
+ fi
344
+
345
+ printf '%s\n' "$input_path"
346
+ return 0
347
+ }
348
+
271
349
  # Register a repo in repos.json
272
350
  # Usage: register_repo <path> <version> <features>
273
351
  register_repo() {
@@ -283,6 +361,20 @@ register_repo() {
283
361
  return 1
284
362
  fi
285
363
 
364
+ # Resolve linked worktrees to their canonical main-worktree path.
365
+ # Every registration path (cmd_init, auto-discovery, scan) runs through
366
+ # register_repo, so the guard here catches all of them — not just the
367
+ # cmd_init path that previously checked only when WORKTREE_PATH was set.
368
+ local canonical_path
369
+ canonical_path=$(resolve_canonical_repo_path "$repo_path")
370
+ if [[ -n "$canonical_path" && "$canonical_path" != "$repo_path" ]]; then
371
+ print_info "Resolved worktree to canonical repo: $repo_path → $canonical_path"
372
+ if ! repo_path=$(cd "$canonical_path" 2>/dev/null && pwd -P); then
373
+ print_warning "Cannot access canonical path: $canonical_path"
374
+ return 1
375
+ fi
376
+ fi
377
+
286
378
  if ! command -v jq &>/dev/null; then
287
379
  print_warning "jq not installed - repo tracking disabled"
288
380
  return 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.8.70",
3
+ "version": "3.8.72",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -308,6 +308,62 @@ cleanup_stale_bun_opencode() {
308
308
  # by the 4-layer role resolution in stats-functions.sh.
309
309
  #
310
310
  # Gated by a flag file so it runs exactly once per install.
311
+ cleanup_worktree_entries_in_repos_json() {
312
+ # t2250: `find ~/Git -name .aidevops.json` during auto-discovery picks up
313
+ # files that exist inside linked worktrees (because worktrees inherit the
314
+ # working tree). Before the register_repo guard, each worktree ended up as
315
+ # a separate entry in repos.json — confusing tabby-profile-sync, pulse,
316
+ # cross-repo tooling, and anything that enumerates `initialized_repos`.
317
+ #
318
+ # One-shot migration: scan `initialized_repos[].path`, detect entries that
319
+ # are linked worktrees (git rev-parse --git-dir != --git-common-dir), and
320
+ # remove them. Safe to re-run; a flag file suppresses re-execution once
321
+ # the cleanup has been done on this machine.
322
+ local flag_file="${HOME}/.aidevops/logs/.migrated-worktree-repos-json-t2250"
323
+ [[ -f "$flag_file" ]] && return 0
324
+
325
+ local repos_json="${HOME}/.config/aidevops/repos.json"
326
+ [[ -f "$repos_json" ]] || return 0
327
+
328
+ command -v jq &>/dev/null || return 0
329
+ command -v git &>/dev/null || return 0
330
+
331
+ local stale_paths=()
332
+ local path git_dir common_dir
333
+
334
+ while IFS= read -r path; do
335
+ [[ -n "$path" && -d "$path" ]] || continue
336
+ git_dir=$(git -C "$path" rev-parse --git-dir 2>/dev/null) || continue
337
+ common_dir=$(git -C "$path" rev-parse --git-common-dir 2>/dev/null) || continue
338
+ # Normalise to absolute paths for comparison.
339
+ [[ "$git_dir" = /* ]] || git_dir="$path/$git_dir"
340
+ [[ "$common_dir" = /* ]] || common_dir="$path/$common_dir"
341
+ git_dir=$(cd "$git_dir" 2>/dev/null && pwd -P) || git_dir=""
342
+ common_dir=$(cd "$common_dir" 2>/dev/null && pwd -P) || common_dir=""
343
+ if [[ -n "$git_dir" && -n "$common_dir" && "$git_dir" != "$common_dir" ]]; then
344
+ stale_paths+=("$path")
345
+ fi
346
+ done < <(jq -r '.initialized_repos[].path // empty' "$repos_json" 2>/dev/null)
347
+
348
+ if [[ ${#stale_paths[@]} -gt 0 ]]; then
349
+ local temp_file="${repos_json}.tmp"
350
+ local paths_json
351
+ paths_json=$(printf '%s\n' "${stale_paths[@]}" | jq -R . | jq -s .)
352
+ jq --argjson stale "$paths_json" \
353
+ '.initialized_repos |= map(select(.path as $p | ($stale | index($p)) | not))' \
354
+ "$repos_json" >"$temp_file" && mv "$temp_file" "$repos_json"
355
+ print_info "Removed ${#stale_paths[@]} worktree entry/entries from repos.json (t2250):"
356
+ local p
357
+ for p in "${stale_paths[@]}"; do
358
+ print_info " - $p"
359
+ done
360
+ fi
361
+
362
+ mkdir -p "$(dirname "$flag_file")"
363
+ date -u +"%Y-%m-%dT%H:%M:%SZ" >"$flag_file"
364
+ return 0
365
+ }
366
+
311
367
  cleanup_stale_health_issue_caches() {
312
368
  local flag_file="${HOME}/.aidevops/logs/.migrated-health-issue-caches-t1929"
313
369
  [[ -f "$flag_file" ]] && return 0
@@ -9,6 +9,30 @@
9
9
  # Keep pulse workers alive long enough for opus-tier dispatches.
10
10
  PULSE_STALE_THRESHOLD_SECONDS=1800
11
11
 
12
+ # Resolve the modern bash binary path for use in launchd ProgramArguments.
13
+ # Launchd bypasses the shebang when ProgramArguments specifies an explicit
14
+ # interpreter, so we must resolve the path at plist generation time.
15
+ # Falls back to /bin/bash if no modern bash is available (the re-exec guard
16
+ # in shared-constants.sh provides defense-in-depth). (GH#19632 / t2176)
17
+ _resolve_modern_bash() {
18
+ local candidate
19
+ for candidate in /opt/homebrew/bin/bash /usr/local/bin/bash /home/linuxbrew/.linuxbrew/bin/bash; do
20
+ if [[ -x "$candidate" ]]; then
21
+ # Verify it's actually bash 4+
22
+ local ver
23
+ ver=$("$candidate" -c 'echo "${BASH_VERSINFO[0]}"' 2>/dev/null) || continue
24
+ if [[ "${ver:-0}" -ge 4 ]]; then
25
+ printf '%s' "$candidate"
26
+ return 0
27
+ fi
28
+ fi
29
+ done
30
+ # No modern bash found — fall back to /bin/bash. The re-exec guard in
31
+ # shared-constants.sh handles this case at runtime.
32
+ printf '%s' "/bin/bash"
33
+ return 0
34
+ }
35
+
12
36
  # Shell safety baseline
13
37
  set -Eeuo pipefail
14
38
  IFS=$'\n\t'
@@ -490,6 +514,11 @@ _generate_pulse_plist_content() {
490
514
  local _headless_xml_env
491
515
  _headless_xml_env=$(_build_pulse_headless_env_xml)
492
516
 
517
+ # Resolve modern bash for ProgramArguments — launchd bypasses shebangs
518
+ # when an explicit interpreter is specified. (GH#19632 / t2176)
519
+ local _xml_bash_bin
520
+ _xml_bash_bin=$(_xml_escape "$(_resolve_modern_bash)")
521
+
493
522
  cat <<PLIST
494
523
  <?xml version="1.0" encoding="UTF-8"?>
495
524
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -499,7 +528,7 @@ _generate_pulse_plist_content() {
499
528
  <string>${pulse_label}</string>
500
529
  <key>ProgramArguments</key>
501
530
  <array>
502
- <string>/bin/bash</string>
531
+ <string>${_xml_bash_bin}</string>
503
532
  <string>${_xml_wrapper_script}</string>
504
533
  </array>
505
534
  <key>StartInterval</key>
@@ -941,7 +970,7 @@ setup_stats_wrapper() {
941
970
  <string>${stats_label}</string>
942
971
  <key>ProgramArguments</key>
943
972
  <array>
944
- <string>/bin/bash</string>
973
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
945
974
  <string>${_xml_stats_script}</string>
946
975
  </array>
947
976
  <key>StartInterval</key>
@@ -1040,7 +1069,7 @@ setup_failure_miner() {
1040
1069
  <string>${miner_label}</string>
1041
1070
  <key>ProgramArguments</key>
1042
1071
  <array>
1043
- <string>/bin/bash</string>
1072
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
1044
1073
  <string>${_xml_miner_script}</string>
1045
1074
  <string>create-issues</string>
1046
1075
  <string>--since-hours</string>
@@ -1137,7 +1166,7 @@ setup_process_guard() {
1137
1166
  <string>${guard_label}</string>
1138
1167
  <key>ProgramArguments</key>
1139
1168
  <array>
1140
- <string>/bin/bash</string>
1169
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
1141
1170
  <string>${_xml_guard_script}</string>
1142
1171
  <string>kill-runaways</string>
1143
1172
  </array>
@@ -1232,7 +1261,7 @@ setup_memory_pressure_monitor() {
1232
1261
  <string>${monitor_label}</string>
1233
1262
  <key>ProgramArguments</key>
1234
1263
  <array>
1235
- <string>/bin/bash</string>
1264
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
1236
1265
  <string>${_xml_monitor_script}</string>
1237
1266
  </array>
1238
1267
  <key>StartInterval</key>
@@ -1320,7 +1349,7 @@ setup_screen_time_snapshot() {
1320
1349
  <string>${st_label}</string>
1321
1350
  <key>ProgramArguments</key>
1322
1351
  <array>
1323
- <string>/bin/bash</string>
1352
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
1324
1353
  <string>${_xml_st_script}</string>
1325
1354
  <string>snapshot</string>
1326
1355
  </array>
@@ -1425,7 +1454,7 @@ _install_cw_launchd() {
1425
1454
  <string>${cw_label}</string>
1426
1455
  <key>ProgramArguments</key>
1427
1456
  <array>
1428
- <string>/bin/bash</string>
1457
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
1429
1458
  <string>${_xml_cw_script}</string>
1430
1459
  <string>scan</string>
1431
1460
  </array>
@@ -1587,7 +1616,7 @@ _install_profile_readme_launchd() {
1587
1616
  <string>${pr_label}</string>
1588
1617
  <key>ProgramArguments</key>
1589
1618
  <array>
1590
- <string>/bin/bash</string>
1619
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
1591
1620
  <string>${_xml_pr_script}</string>
1592
1621
  <string>update</string>
1593
1622
  </array>
@@ -1793,7 +1822,7 @@ _install_token_refresh_launchd() {
1793
1822
  <string>${tr_label}</string>
1794
1823
  <key>ProgramArguments</key>
1795
1824
  <array>
1796
- <string>/bin/bash</string>
1825
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
1797
1826
  <string>-c</string>
1798
1827
  <string>&quot;${_xml_tr_script}&quot; refresh anthropic; &quot;${_xml_tr_script}&quot; refresh openai</string>
1799
1828
  </array>
package/setup.sh CHANGED
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
12
12
  # AI Assistant Server Access Framework Setup Script
13
13
  # Helps developers set up the framework for their infrastructure
14
14
  #
15
- # Version: 3.8.70
15
+ # Version: 3.8.72
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)
@@ -90,15 +90,17 @@ if [[ -d "$SETUP_MODULES_DIR" ]]; then
90
90
  # shellcheck disable=SC1091
91
91
  source "$SETUP_MODULES_DIR/_privacy_guard.sh"
92
92
  # shellcheck disable=SC1091
93
+ source "$SETUP_MODULES_DIR/_complexity_guard.sh"
94
+ # shellcheck disable=SC1091
93
95
  source "$SETUP_MODULES_DIR/_task_id_guard.sh"
94
96
  # shellcheck disable=SC1091
95
97
  source "$SETUP_MODULES_DIR/_canonical_guard.sh"
96
98
  fi
97
99
 
98
- print_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
99
- print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
100
- print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
101
- print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
100
+ print_info() { local _m="$1"; echo -e "${BLUE}[INFO]${NC} $_m"; return 0; }
101
+ print_success() { local _m="$1"; echo -e "${GREEN}[SUCCESS]${NC} $_m"; return 0; }
102
+ print_warning() { local _m="$1"; echo -e "${YELLOW}[WARNING]${NC} $_m"; return 0; }
103
+ print_error() { local _m="$1"; echo -e "${RED}[ERROR]${NC} $_m"; return 0; }
102
104
 
103
105
  # Source shared-constants for config support (is_feature_enabled / config_enabled)
104
106
  # Try repo-local first, then deployed location
@@ -245,9 +247,9 @@ _launchd_install_if_changed() {
245
247
 
246
248
  # Detect whether a scheduler is already installed via launchd, cron, or systemd.
247
249
  # Optionally migrates legacy launchd labels / cron entries to launchd on macOS.
248
- # Args: $1=scheduler_name, $2=launchd_label, $3=legacy_launchd_label,
249
- # $4=cron_marker, $5=migrate_script, $6=migrate_arg, $7=migrate_hint
250
- # $8=systemd_unit (optional — base name without .timer suffix, e.g. "aidevops-supervisor-pulse")
250
+ # Args: arg1=scheduler_name, arg2=launchd_label, arg3=legacy_launchd_label,
251
+ # arg4=cron_marker, arg5=migrate_script, arg6=migrate_arg, arg7=migrate_hint
252
+ # arg8=systemd_unit (optional — base name without .timer suffix, e.g. "aidevops-supervisor-pulse")
251
253
  _scheduler_detect_installed() {
252
254
  local scheduler_name="$1"
253
255
  local launchd_label="$2"
@@ -317,7 +319,7 @@ _should_setup_noninteractive_supervisor_pulse() {
317
319
  # Generic non-interactive scheduler detection (GH#17695 Finding B).
318
320
  # Returns 0 if the named scheduler is already installed on any backend,
319
321
  # meaning it should be regenerated during non-interactive setup.
320
- # Args: $1=name $2=launchd_label $3=cron_marker $4=systemd_unit
322
+ # Args: arg1=name arg2=launchd_label arg3=cron_marker arg4=systemd_unit
321
323
  _should_setup_noninteractive_scheduler() {
322
324
  local name="$1"
323
325
  local launchd_label="$2"
@@ -739,7 +741,8 @@ source "$(dirname "${BASH_SOURCE[0]}")/setup-modules/post-setup.sh"
739
741
 
740
742
  parse_args() {
741
743
  while [[ $# -gt 0 ]]; do
742
- case "$1" in
744
+ local _opt="$1"
745
+ case "$_opt" in
743
746
  --clean)
744
747
  CLEAN_MODE=true
745
748
  shift
@@ -774,7 +777,7 @@ parse_args() {
774
777
  exit 0
775
778
  ;;
776
779
  *)
777
- print_error "Unknown option: $1"
780
+ print_error "Unknown option: $_opt"
778
781
  echo "Use --help for usage information"
779
782
  exit 1
780
783
  ;;
@@ -912,6 +915,7 @@ _setup_run_non_interactive() {
912
915
  cleanup_deprecated_mcps
913
916
  cleanup_stale_bun_opencode
914
917
  cleanup_stale_health_issue_caches
918
+ cleanup_worktree_entries_in_repos_json
915
919
  _cleanup_legacy_model_config
916
920
  validate_opencode_config
917
921
  deploy_aidevops_agents
@@ -957,6 +961,10 @@ _setup_run_non_interactive() {
957
961
  # repo so TODO/todo/README/ISSUE_TEMPLATE pushes to public GitHub repos
958
962
  # are scanned for private slug leaks (t1968).
959
963
  setup_privacy_guard
964
+ # Install/refresh the complexity-regression pre-push hook in every
965
+ # initialized repo so pushes that introduce new function-complexity,
966
+ # nesting-depth, or file-size violations are caught before CI (t2198).
967
+ setup_complexity_guard
960
968
  # Install/refresh the canonical-on-main post-checkout hook in every
961
969
  # initialized repo so branch switches away from main in the canonical
962
970
  # directory are warned against (t1995). Complements pre-edit-check.sh's
@@ -1023,8 +1031,8 @@ _setup_run_interactive() {
1023
1031
  confirm_step "Backfill GitHub issue relationships (blocked-by, sub-issues)" && backfill_issue_relationships
1024
1032
  confirm_step "Cleanup deprecated MCP entries (hetzner, serper, etc.)" && cleanup_deprecated_mcps
1025
1033
  confirm_step "Cleanup stale bun opencode install" && cleanup_stale_bun_opencode
1026
- cleanup_stale_health_issue_caches
1027
- _cleanup_legacy_model_config
1034
+ # Silent one-shot migrations (idempotent, flag-guarded — no prompt needed).
1035
+ cleanup_stale_health_issue_caches; cleanup_worktree_entries_in_repos_json; _cleanup_legacy_model_config
1028
1036
  confirm_step "Validate and repair OpenCode config schema" && validate_opencode_config
1029
1037
  confirm_step "Extract OpenCode prompts" && extract_opencode_prompts
1030
1038
  confirm_step "Check OpenCode prompt drift" && check_opencode_prompt_drift