aidevops 3.13.41 → 3.13.43

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.13.41
1
+ 3.13.43
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.13.41
8
+ # Version: 3.13.43
9
9
 
10
10
  set -euo pipefail
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.13.41",
3
+ "version": "3.13.43",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -391,11 +391,34 @@ _atomic_stage_and_deploy_agents() {
391
391
  fi
392
392
  done
393
393
 
394
- # Atomic swap: mv is atomic on the same filesystem (POSIX rename())
394
+ # Atomic swap: mv is atomic on the same filesystem (POSIX rename()).
395
+ # IMPORTANT: explicit error checks are REQUIRED here because this function
396
+ # is called via `|| return 1` which disables set -e inside the function
397
+ # body (bash set -e semantics: disabled in any function called as part of
398
+ # a compound list such as `fn || ...`). Without these checks, a failed mv
399
+ # falls through silently, the backup is deleted, and the function returns
400
+ # 0 with $target_dir absent — the root cause of GH#22014 where worktree
401
+ # setup left ~/.aidevops/agents missing while reporting [SETUP_COMPLETE].
395
402
  if [[ -d "$target_dir" ]]; then
396
- mv "$target_dir" "$old_dir"
403
+ if ! mv "$target_dir" "$old_dir"; then
404
+ print_error "Failed to move live agents to backup ($old_dir) — agents directory preserved"
405
+ rm -rf "$staging_dir"
406
+ return 1
407
+ fi
408
+ fi
409
+ if ! mv "$staging_dir" "$target_dir"; then
410
+ print_error "Failed to move staging to live agents directory — attempting rollback"
411
+ # Restore the previous agents dir from backup so the system stays functional.
412
+ if [[ -d "$old_dir" ]]; then
413
+ if mv "$old_dir" "$target_dir"; then
414
+ print_info "Rollback successful — previous agents directory restored"
415
+ else
416
+ print_error "Rollback failed — agents directory is missing! Previous state preserved in $old_dir"
417
+ fi
418
+ fi
419
+ rm -rf "$staging_dir"
420
+ return 1
397
421
  fi
398
- mv "$staging_dir" "$target_dir"
399
422
  rm -rf "$old_dir"
400
423
  return 0
401
424
  }
@@ -503,6 +526,9 @@ _deploy_agents_post_copy() {
503
526
  # (those edits are overwritten by every deploy). Emits a warning listing drifted
504
527
  # files and the canonical source path to edit instead.
505
528
  # Non-fatal: always returns 0 so deployment proceeds.
529
+ #
530
+ # Performance: uses a single rsync --checksum --dry-run call instead of one
531
+ # diff -q subprocess per script (was 783 calls → now 1 call; t3221).
506
532
  _warn_deployed_script_drift() {
507
533
  local source_dir="$1"
508
534
  local target_dir="$2"
@@ -512,20 +538,35 @@ _warn_deployed_script_drift() {
512
538
  if [[ ! -d "$source_scripts" || ! -d "$target_scripts" ]]; then
513
539
  return 0
514
540
  fi
515
- if ! command -v diff &>/dev/null; then
516
- return 0
517
- fi
518
541
 
519
542
  local -a drifted=()
520
- local f bn
521
- for f in "$target_scripts"/*.sh; do
522
- [[ -f "$f" ]] || continue
523
- bn=$(basename "$f")
524
- local src="$source_scripts/$bn"
525
- if [[ -f "$src" ]] && ! diff -q "$src" "$f" &>/dev/null; then
526
- drifted+=("$bn")
527
- fi
528
- done
543
+ if command -v rsync &>/dev/null; then
544
+ # Single bulk comparison: rsync --checksum --dry-run reports changed files
545
+ # without transferring anything. --out-format='%f' prints only the relative
546
+ # path of each changed file. Filter to top-level *.sh only (no subdirs).
547
+ local changed_file
548
+ while IFS= read -r changed_file; do
549
+ [[ -n "$changed_file" ]] || continue
550
+ # Skip subdirectory scripts (only warn about top-level scripts/)
551
+ [[ "$changed_file" == */* ]] && continue
552
+ [[ "$changed_file" == *.sh ]] || continue
553
+ drifted+=("$changed_file")
554
+ done < <(rsync --checksum --dry-run \
555
+ --out-format='%f' \
556
+ --include='*.sh' --exclude='*/' --exclude='*' \
557
+ "$source_scripts/" "$target_scripts/" 2>/dev/null || true)
558
+ elif command -v diff &>/dev/null; then
559
+ # Fallback: one diff -q per script (slow, only reached when rsync absent)
560
+ local f bn
561
+ for f in "$target_scripts"/*.sh; do
562
+ [[ -f "$f" ]] || continue
563
+ bn=$(basename "$f")
564
+ local src="$source_scripts/$bn"
565
+ if [[ -f "$src" ]] && ! diff -q "$src" "$f" &>/dev/null; then
566
+ drifted+=("$bn")
567
+ fi
568
+ done
569
+ fi
529
570
 
530
571
  if [[ ${#drifted[@]} -gt 0 ]]; then
531
572
  print_warning "Deployed scripts differ from canonical source (local edits will be overwritten; backup will be created):"
@@ -577,9 +618,18 @@ deploy_aidevops_agents() {
577
618
  _warn_deployed_script_drift "$source_dir" "$target_dir"
578
619
  fi
579
620
 
580
- # Create backup if target exists (with rotation)
621
+ # Create backup if target exists (with rotation).
622
+ # Skip when the deployed SHA matches the current HEAD — nothing changed on
623
+ # disk, so there is nothing worth backing up (t3221: steady-state perf).
581
624
  if [[ -d "$target_dir" ]]; then
582
- create_backup_with_rotation "$target_dir" "agents"
625
+ local _cur_sha _dep_sha
626
+ _cur_sha=$(git -C "$repo_dir" rev-parse HEAD 2>/dev/null || echo "")
627
+ _dep_sha=$(cat "${HOME}/.aidevops/.deployed-sha" 2>/dev/null || echo "")
628
+ if [[ -n "$_cur_sha" && -n "$_dep_sha" && "$_cur_sha" == "$_dep_sha" ]]; then
629
+ print_info "No changes since last deploy (${_cur_sha:0:8}) — skipping backup"
630
+ else
631
+ create_backup_with_rotation "$target_dir" "agents"
632
+ fi
583
633
  fi
584
634
 
585
635
  mkdir -p "$target_dir"
@@ -591,20 +641,24 @@ deploy_aidevops_agents() {
591
641
  _atomic_stage_and_deploy_agents "$source_dir" "$target_dir" || return 1
592
642
  fi
593
643
 
644
+ # Postcondition: verify the swap actually produced a functional agents dir.
645
+ # _atomic_stage_and_deploy_agents returns 0 on success, but a belt-and-
646
+ # suspenders check here catches any future regression where the function
647
+ # might return early without correctly populating $target_dir (GH#22014).
648
+ if [[ ! -d "$target_dir/scripts" ]]; then
649
+ print_error "Deploy verification failed: $target_dir/scripts missing after swap"
650
+ print_error "The agents directory was not correctly deployed — setup cannot continue"
651
+ return 1
652
+ fi
653
+
594
654
  print_success "Deployed agents to $target_dir"
595
655
  _deploy_agents_post_copy "$target_dir" "$repo_dir" "$source_dir" "$plugins_file"
596
656
 
597
- # Restart pulse if running bash processes load source files at startup
598
- # and don't re-read them when files change on disk. Without a restart,
599
- # fixes to pulse-*.sh, dispatch-dedup-*.sh, headless-runtime-*.sh, and
600
- # other sourced scripts don't take effect until the next manual restart.
601
- # This was the root cause of a multi-hour outage where deployed fixes
602
- # were correct but the running pulse kept using old code in memory.
603
- _restart_pulse_if_running
604
-
605
- # Write deployed-SHA stamp so aidevops-update-check.sh can detect
606
- # script drift between this deploy and future canonical-repo commits.
607
- # Written AFTER pulse restart so the stamp reflects a fully-applied deploy.
657
+ # Write deployed-SHA stamp BEFORE the pulse restart so the stamp is
658
+ # available immediately for subsequent setup steps and the next run's
659
+ # backup-skip check (t3221). Previously written after the blocking
660
+ # restart wait; moving it here has no correctness impact the deploy
661
+ # is already fully on disk at this point.
608
662
  # t2156: enables auto-redeploy when local commits land between releases.
609
663
  local deployed_sha
610
664
  deployed_sha=$(git -C "$repo_dir" rev-parse HEAD 2>/dev/null || echo "")
@@ -614,6 +668,20 @@ deploy_aidevops_agents() {
614
668
  printf '%s\n' "$deployed_sha" >"${aidevops_dir}/.deployed-sha"
615
669
  fi
616
670
 
671
+ # Restart pulse in the background — bash processes load source files at
672
+ # startup and don't re-read them when files change on disk. Without a
673
+ # restart, fixes to pulse-*.sh, dispatch-dedup-*.sh, and other sourced
674
+ # scripts don't take effect until the next manual restart.
675
+ #
676
+ # t3221: running this asynchronously saves the 10-15s blocking wait
677
+ # (pkill + up to 10s die-wait + sleep 5 launchd grace period). The
678
+ # deploy is already complete on disk; the pulse picks up the new scripts
679
+ # once it restarts regardless of when that happens relative to setup.sh
680
+ # finishing. disown prevents SIGHUP propagation if setup.sh is sourced
681
+ # interactively; in script mode the orphan survives the exit anyway.
682
+ _restart_pulse_if_running &
683
+ disown
684
+
617
685
  return 0
618
686
  }
619
687
 
@@ -616,7 +616,16 @@ _migrate_ai_config_agent_refs() {
616
616
  # 3. .gitignore entries in user projects
617
617
  # 4. References in user's AI assistant configs
618
618
  # 5. References in ~/.aidevops/ config files
619
+ #
620
+ # Guarded by a sentinel file: on a converged system the function does
621
+ # a repos.json scan and a find(1) scan of ~/Git, both of which cost
622
+ # several seconds per run (t3221).
619
623
  migrate_agent_to_agents_folder() {
624
+ local _sentinel="${HOME}/.aidevops/.migrations/agent-to-agents-done"
625
+ if [[ -f "$_sentinel" ]]; then
626
+ return 0
627
+ fi
628
+
620
629
  print_info "Checking for .agent -> .agents migration..."
621
630
 
622
631
  local migrated=0
@@ -649,6 +658,9 @@ migrate_agent_to_agents_folder() {
649
658
  print_info "No .agent -> .agents migration needed"
650
659
  fi
651
660
 
661
+ # Write sentinel so subsequent setup runs skip the repos+find scans (t3221)
662
+ mkdir -p "$(dirname "$_sentinel")"
663
+ date -u +%Y-%m-%dT%H:%M:%SZ >"$_sentinel"
652
664
  return 0
653
665
  }
654
666
 
@@ -782,6 +794,12 @@ _migrate_mcp_npx_to_binary() {
782
794
 
783
795
  # Remove deprecated MCP entries from opencode.json
784
796
  # These MCPs have been replaced by curl-based subagents (zero context cost)
797
+ #
798
+ # The one-time cleanup (remove deprecated entries + migrate npx→binary) is
799
+ # guarded by a versioned sentinel (t3221). Bump the sentinel version when new
800
+ # deprecated MCPs are added to _remove_deprecated_mcp_entries.
801
+ # The recurring update_mcp_paths_in_opencode call is NOT guarded — it resolves
802
+ # stale binary paths on every run (paths can change after package upgrades).
785
803
  cleanup_deprecated_mcps() {
786
804
  local opencode_config
787
805
  opencode_config=$(find_opencode_config) || return 0
@@ -794,27 +812,36 @@ cleanup_deprecated_mcps() {
794
812
  return 0
795
813
  fi
796
814
 
797
- local cleaned=0
798
- local tmp_config
799
- tmp_config=$(mktemp)
800
- trap 'rm -f "${tmp_config:-}"' RETURN
815
+ # One-time cleanup: remove deprecated MCPs and migrate npx→binary paths.
816
+ # Sentinel version must be bumped whenever new deprecated MCPs are added.
817
+ local _sentinel="${HOME}/.aidevops/.migrations/cleanup-deprecated-mcps-v1"
818
+ if [[ ! -f "$_sentinel" ]]; then
819
+ local cleaned=0
820
+ local tmp_config
821
+ tmp_config=$(mktemp)
822
+ trap 'rm -f "${tmp_config:-}"' RETURN
801
823
 
802
- cp "$opencode_config" "$tmp_config"
824
+ cp "$opencode_config" "$tmp_config"
803
825
 
804
- # Remove deprecated MCP and tool entries
805
- _remove_deprecated_mcp_entries "$tmp_config"
806
- cleaned=$((cleaned + _cleanup_count))
826
+ # Remove deprecated MCP and tool entries
827
+ _remove_deprecated_mcp_entries "$tmp_config"
828
+ cleaned=$((cleaned + _cleanup_count))
807
829
 
808
- # Migrate npx/pipx commands to full binary paths (faster startup, PATH-independent)
809
- _migrate_mcp_npx_to_binary "$tmp_config"
810
- cleaned=$((cleaned + _cleanup_count))
830
+ # Migrate npx/pipx commands to full binary paths (faster startup, PATH-independent)
831
+ _migrate_mcp_npx_to_binary "$tmp_config"
832
+ cleaned=$((cleaned + _cleanup_count))
811
833
 
812
- if [[ $cleaned -gt 0 ]]; then
813
- create_backup_with_rotation "$opencode_config" "opencode"
814
- mv "$tmp_config" "$opencode_config"
815
- print_info "Updated $cleaned MCP entry/entries in opencode.json (using full binary paths)"
816
- else
817
- rm -f "$tmp_config"
834
+ if [[ $cleaned -gt 0 ]]; then
835
+ create_backup_with_rotation "$opencode_config" "opencode"
836
+ mv "$tmp_config" "$opencode_config"
837
+ print_info "Updated $cleaned MCP entry/entries in opencode.json (using full binary paths)"
838
+ else
839
+ rm -f "$tmp_config"
840
+ fi
841
+
842
+ # Write sentinel
843
+ mkdir -p "$(dirname "$_sentinel")"
844
+ date -u +%Y-%m-%dT%H:%M:%SZ >"$_sentinel"
818
845
  fi
819
846
 
820
847
  # Always resolve bare binary names to full paths (fixes PATH-dependent startup)
@@ -1127,7 +1154,15 @@ migrate_old_backups() {
1127
1154
  # Migrate loop state from .claude/ to .agents/loop-state/ in user projects
1128
1155
  # Also migrates from legacy .agents/loop-state/ to .agents/loop-state/
1129
1156
  # The migration is non-destructive: moves files, doesn't delete originals until confirmed
1157
+ #
1158
+ # Guarded by a sentinel file: on a converged system the function does a
1159
+ # find(1) scan of ~/Git which costs several seconds per run (t3221).
1130
1160
  migrate_loop_state_directories() {
1161
+ local _sentinel="${HOME}/.aidevops/.migrations/loop-state-dirs-migrated"
1162
+ if [[ -f "$_sentinel" ]]; then
1163
+ return 0
1164
+ fi
1165
+
1131
1166
  print_info "Checking for legacy loop state directories..."
1132
1167
 
1133
1168
  local migrated=0
@@ -1218,6 +1253,9 @@ migrate_loop_state_directories() {
1218
1253
  print_info "No legacy loop state directories found"
1219
1254
  fi
1220
1255
 
1256
+ # Write sentinel so subsequent setup runs skip the find scan (t3221)
1257
+ mkdir -p "$(dirname "$_sentinel")"
1258
+ date -u +%Y-%m-%dT%H:%M:%SZ >"$_sentinel"
1221
1259
  return 0
1222
1260
  }
1223
1261
 
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.13.41
15
+ # Version: 3.13.43
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)
@@ -152,6 +152,13 @@ _cron_escape() {
152
152
  _time_step() {
153
153
  local _ts_stage="$1"
154
154
  shift
155
+ # GH#22012: In non-interactive mode, emit the stage name before running so
156
+ # an operator watching setup.sh output can identify which step is blocking.
157
+ # The TSV log entry below is written AFTER the step returns — it provides no
158
+ # signal during a hang. This print is the only in-flight indicator.
159
+ if [[ "${NON_INTERACTIVE:-false}" == "true" ]]; then
160
+ printf '[SETUP] stage: %s\n' "$_ts_stage"
161
+ fi
155
162
  local _ts_start _ts_end _ts_duration _ts_exit
156
163
  _ts_start=$(date +%s.%N 2>/dev/null || date +%s)
157
164
  _ts_exit=0
@@ -1355,70 +1362,74 @@ _setup_run_interactive() {
1355
1362
  _setup_noninteractive_schedulers() {
1356
1363
  local os="$1"
1357
1364
 
1365
+ # GH#22012: Wrap every scheduler step with _time_step so the stage-timing
1366
+ # log ($HOME/.aidevops/logs/setup-stage-timings.log) covers post-deploy
1367
+ # scheduler setup — the same visibility _setup_run_non_interactive has.
1368
+
1358
1369
  # Auto-update handles non-interactive internally (systemd detection fixed in GH#17861)
1359
- setup_auto_update
1370
+ _time_step "setup_auto_update" setup_auto_update
1360
1371
  if _should_setup_noninteractive_supervisor_pulse; then
1361
- setup_supervisor_pulse "$os"
1372
+ _time_step "setup_supervisor_pulse" setup_supervisor_pulse "$os"
1362
1373
  fi
1363
1374
  # t2939: pulse-watchdog (independent revival mechanism). Always installed
1364
1375
  # alongside the pulse — it is a no-op when pulse is disabled. Skipping the
1365
1376
  # `_should_setup_noninteractive_*` guard intentionally: this is layered
1366
1377
  # defense, the cost of installing it is one plist file, and the user opts
1367
1378
  # in by enabling the pulse itself.
1368
- setup_pulse_watchdog "${PULSE_ENABLED:-}"
1379
+ _time_step "setup_pulse_watchdog" setup_pulse_watchdog "${PULSE_ENABLED:-}"
1369
1380
  # Regenerate other schedulers if already installed (GH#17695 Finding B).
1370
1381
  # Stats wrapper is a pulse dependency — also install on first run when
1371
1382
  # the supervisor pulse is consented (t2418, GH#20016).
1372
1383
  if _should_setup_noninteractive_stats_wrapper; then
1373
- setup_stats_wrapper "${PULSE_ENABLED:-}"
1384
+ _time_step "setup_stats_wrapper" setup_stats_wrapper "${PULSE_ENABLED:-}"
1374
1385
  fi
1375
1386
  if _should_setup_noninteractive_scheduler "Failure miner" "sh.aidevops.routine-gh-failure-miner" "aidevops: gh-failure-miner" "aidevops-gh-failure-miner"; then
1376
- setup_failure_miner "${PULSE_ENABLED:-}"
1387
+ _time_step "setup_failure_miner" setup_failure_miner "${PULSE_ENABLED:-}"
1377
1388
  fi
1378
1389
  if _should_setup_noninteractive_scheduler "Process guard" "sh.aidevops.process-guard" "aidevops: process-guard" "aidevops-process-guard"; then
1379
- setup_process_guard
1390
+ _time_step "setup_process_guard" setup_process_guard
1380
1391
  fi
1381
1392
  if _should_setup_noninteractive_scheduler "Memory pressure" "sh.aidevops.memory-pressure-monitor" "aidevops: memory-pressure-monitor" "aidevops-memory-pressure-monitor"; then
1382
- setup_memory_pressure_monitor
1393
+ _time_step "setup_memory_pressure_monitor" setup_memory_pressure_monitor
1383
1394
  fi
1384
1395
  if _should_setup_noninteractive_scheduler "Screen time" "sh.aidevops.screen-time-snapshot" "aidevops: screen-time-snapshot" "aidevops-screen-time-snapshot"; then
1385
- setup_screen_time_snapshot
1396
+ _time_step "setup_screen_time_snapshot" setup_screen_time_snapshot
1386
1397
  fi
1387
1398
  if _should_setup_noninteractive_scheduler "Contribution watch" "sh.aidevops.contribution-watch" "aidevops: contribution-watch" "aidevops-contribution-watch"; then
1388
- setup_contribution_watch
1399
+ _time_step "setup_contribution_watch" setup_contribution_watch
1389
1400
  fi
1390
1401
  # t2903 (#21049): complexity scan — extracted from pulse dispatch preflight
1391
1402
  if _should_setup_noninteractive_scheduler "Complexity scan" "sh.aidevops.complexity-scan" "aidevops: complexity-scan" "aidevops-complexity-scan"; then
1392
- setup_complexity_scan
1403
+ _time_step "setup_complexity_scan" setup_complexity_scan
1393
1404
  fi
1394
1405
  # t2862 (GH#20919): pulse merge routine — fast 120s standalone merge pass.
1395
1406
  # t3036 (GH#21616): use the pulse-dependency escape hatch instead of the
1396
1407
  # generic chicken-and-egg gate so the routine installs on existing systems
1397
1408
  # whenever the supervisor pulse is consented.
1398
1409
  if _should_setup_noninteractive_pulse_merge_routine; then
1399
- setup_pulse_merge_routine
1410
+ _time_step "setup_pulse_merge_routine" setup_pulse_merge_routine
1400
1411
  fi
1401
1412
  # t2932 (GH#21125): peer productivity monitor — adaptive cross-runner
1402
1413
  # dispatch coordination, runs every 30 min.
1403
1414
  if _should_setup_noninteractive_scheduler "Peer productivity monitor" "sh.aidevops.peer-productivity-monitor" "aidevops: peer-productivity-monitor" "aidevops-peer-productivity-monitor"; then
1404
- setup_peer_productivity_monitor
1415
+ _time_step "setup_peer_productivity_monitor" setup_peer_productivity_monitor
1405
1416
  fi
1406
1417
  # Repo sync handles non-interactive mode internally (systemd detection fixed in GH#17861)
1407
- setup_repo_sync
1418
+ _time_step "setup_repo_sync" setup_repo_sync
1408
1419
  # r914 repo-aidevops-health — daily drift keeper (t2366)
1409
- setup_repo_aidevops_health
1420
+ _time_step "setup_repo_aidevops_health" setup_repo_aidevops_health
1410
1421
  if _should_setup_noninteractive_scheduler "Profile README" "sh.aidevops.profile-readme-update" "aidevops: profile-readme-update" "aidevops-profile-readme-update"; then
1411
- setup_profile_readme
1422
+ _time_step "setup_profile_readme" setup_profile_readme
1412
1423
  fi
1413
1424
  if _should_setup_noninteractive_scheduler "OAuth token refresh" "sh.aidevops.token-refresh" "aidevops: token-refresh" "aidevops-token-refresh"; then
1414
- setup_oauth_token_refresh
1425
+ _time_step "setup_oauth_token_refresh" setup_oauth_token_refresh
1415
1426
  fi
1416
1427
  # opencode DB maintenance (r913, t2183). Helper self-noops on missing
1417
1428
  # DB — safe to install unconditionally in non-interactive mode too.
1418
- setup_opencode_db_maintenance
1429
+ _time_step "setup_opencode_db_maintenance" setup_opencode_db_maintenance
1419
1430
  # Migrate cron entries to systemd after schedulers are installed (GH#17695 Finding D)
1420
- migrate_cron_to_systemd
1421
- setup_tabby
1431
+ _time_step "migrate_cron_to_systemd" migrate_cron_to_systemd
1432
+ _time_step "setup_tabby" setup_tabby
1422
1433
  return 0
1423
1434
  }
1424
1435
 
@@ -1551,9 +1562,17 @@ main() {
1551
1562
  # No-op if pulse is not running, or if AIDEVOPS_SKIP_PULSE_RESTART=1.
1552
1563
  # Uses the deployed helper (not the repo-local one) so the restart runs
1553
1564
  # against the agents directory setup.sh just populated.
1565
+ # GH#22012: bounded 120 s timeout prevents setup.sh hanging here when the
1566
+ # pulse helper takes unusually long to stop a stalled instance. Falls back
1567
+ # to an unbounded call on platforms without timeout(1) (old macOS w/o
1568
+ # coreutils, embedded shells).
1554
1569
  local _pulse_helper="${HOME}/.aidevops/agents/scripts/pulse-lifecycle-helper.sh"
1555
1570
  if [[ -x "$_pulse_helper" ]]; then
1556
- "$_pulse_helper" restart-if-running || print_warning "Pulse restart failed (non-fatal)"
1571
+ if command -v timeout >/dev/null 2>&1; then
1572
+ timeout 120 "$_pulse_helper" restart-if-running || print_warning "Pulse restart failed (non-fatal)"
1573
+ else
1574
+ "$_pulse_helper" restart-if-running || print_warning "Pulse restart failed (non-fatal)"
1575
+ fi
1557
1576
  fi
1558
1577
 
1559
1578
  # GH#18492 / t2026: completion sentinel. Must be the last output of a