aidevops 3.13.10 → 3.13.13

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.10
1
+ 3.13.13
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.10
8
+ # Version: 3.13.13
9
9
 
10
10
  set -euo pipefail
11
11
 
@@ -521,6 +521,90 @@ _update_check_setsid() {
521
521
  return 0
522
522
  }
523
523
 
524
+ # GH#21735: Notify operator when framework workflow templates change.
525
+ # When .agents/templates/workflows/*.yml or *-reusable.yml workflows change
526
+ # in a framework update, downstream repos that use these as workflow_call
527
+ # callers may have drifted from the new template. Detection and remediation
528
+ # both already exist (`aidevops check-workflows`, `aidevops sync-workflows
529
+ # --apply`); the gap was the notification surface — operators only learned
530
+ # of drift when downstream CI failed (canonical incident: a managed
531
+ # downstream repo's issue-sync.yml failed silently after the upstream
532
+ # template added a new input).
533
+ #
534
+ # This check inspects the SHA-window diff for changes to workflow caller
535
+ # templates and reusable workflows, prints a warning, and emits a daily
536
+ # advisory so the next session greeting surfaces it if the operator
537
+ # misses the inline output.
538
+ #
539
+ # Args: $1=old_sha, $2=new_sha
540
+ # Returns: 0 (always — informational only, never breaks update)
541
+ _update_check_workflow_drift() {
542
+ local old_sha="$1"
543
+ local new_sha="$2"
544
+ [[ -z "$old_sha" || -z "$new_sha" || "$old_sha" == "$new_sha" ]] && return 0
545
+ # `.git` is a directory in a regular repo and a file in a worktree;
546
+ # `-e` covers both so the helper is testable from a worktree.
547
+ [[ ! -e "$INSTALL_DIR/.git" ]] && return 0
548
+
549
+ # Files that propagate to downstream caller workflows OR are themselves
550
+ # reusable workflow definitions referenced by downstream callers.
551
+ # Internal .github/workflows/*.yml hotfixes (e.g. self-test runs) are
552
+ # intentionally skipped to avoid false-positive nags.
553
+ local relevant_files
554
+ relevant_files=$(git -C "$INSTALL_DIR" diff --name-only "$old_sha" "$new_sha" -- \
555
+ '.agents/templates/workflows/' \
556
+ '.github/workflows/' \
557
+ 2>/dev/null \
558
+ | grep -E '(\.agents/templates/workflows/.*\.ya?ml$|\.github/workflows/.*-reusable\.ya?ml$)' \
559
+ || true)
560
+ [[ -z "$relevant_files" ]] && return 0
561
+
562
+ local file_count
563
+ file_count=$(printf '%s\n' "$relevant_files" | wc -l | tr -d ' ')
564
+ echo ""
565
+ print_warning "Workflow templates updated ($file_count file(s)) — downstream callers may have drifted."
566
+ print_info " Detect drift: aidevops check-workflows"
567
+ print_info " Apply fix: aidevops sync-workflows --apply [--repo OWNER/REPO]"
568
+
569
+ # Persist as advisory so the next session greeting surfaces it even if
570
+ # the operator misses the inline warning. Day-stamped ID makes repeated
571
+ # updates within the same day idempotent (one advisory per day);
572
+ # 'aidevops security dismiss <id>' silences a specific day's advisory.
573
+ _update_emit_workflow_drift_advisory "$relevant_files" || true
574
+ return 0
575
+ }
576
+
577
+ # Companion to _update_check_workflow_drift — separated for testability.
578
+ # Args: $1=relevant_files (newline-separated)
579
+ # Returns: 0 (always — fail-open; advisory write must never break update)
580
+ _update_emit_workflow_drift_advisory() {
581
+ local relevant_files="$1"
582
+ local advisories_dir="${HOME}/.aidevops/advisories"
583
+ local adv_id
584
+ adv_id="workflow-drift-$(date +%Y%m%d)"
585
+ local dismissed_file="$advisories_dir/dismissed.txt"
586
+
587
+ # Skip if today's advisory was already dismissed.
588
+ if [[ -f "$dismissed_file" ]] && grep -qxF "$adv_id" "$dismissed_file" 2>/dev/null; then
589
+ return 0
590
+ fi
591
+
592
+ mkdir -p "$advisories_dir" 2>/dev/null || return 0
593
+ local adv_file="$advisories_dir/${adv_id}.advisory"
594
+
595
+ {
596
+ printf 'Workflow templates changed — downstream caller workflows may have drifted.\n'
597
+ printf '\n'
598
+ printf 'Files changed in this update:\n'
599
+ printf '%s\n' "$relevant_files" | sed 's|^| |'
600
+ printf '\n'
601
+ printf 'Detect drift: aidevops check-workflows\n'
602
+ printf 'Apply fix: aidevops sync-workflows --apply [--repo OWNER/REPO]\n'
603
+ printf 'Background: reference/reusable-workflows.md\n'
604
+ } >"$adv_file" 2>/dev/null || return 0
605
+ return 0
606
+ }
607
+
524
608
  # Verify supply chain signature after pulling framework updates.
525
609
  # Checks that the HEAD commit is signed by the trusted maintainer key.
526
610
  # Non-blocking: warns on failure, does not abort the update.
@@ -687,6 +771,11 @@ cmd_update() {
687
771
  print_info "Re-running setup to deploy latest scripts..."
688
772
  bash "$INSTALL_DIR/setup.sh" --non-interactive
689
773
  fi
774
+ # GH#21735: workflow templates can change between
775
+ # releases without triggering has_code_drift (templates
776
+ # live outside the deploy-affecting paths). Check the
777
+ # template subset separately and surface drift.
778
+ _update_check_workflow_drift "$deployed_sha" "$local_hash"
690
779
  fi
691
780
  fi
692
781
  fi
@@ -717,6 +806,9 @@ cmd_update() {
717
806
  git log --oneline "$old_hash..$new_hash" | grep -E '^[a-f0-9]+ (feat|fix|refactor|perf|docs):' | head -20
718
807
  [[ "$total_commits" -gt 20 ]] && echo " ... and more (run 'git log --oneline' in $INSTALL_DIR for full list)"
719
808
  fi
809
+ # GH#21735: surface workflow template drift so the
810
+ # operator can resync downstream callers before CI bites.
811
+ _update_check_workflow_drift "$old_hash" "$new_hash"
720
812
  fi
721
813
  echo ""
722
814
  # Verify supply chain integrity before applying changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.13.10",
3
+ "version": "3.13.13",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -47,27 +47,22 @@ fi
47
47
  # --- Functions ---
48
48
 
49
49
  # Resolve and validate the log directory from config for contribution watch.
50
- # Reads paths.log_dir from jsonc config, validates characters, expands tilde.
50
+ # Delegates resolution to _resolve_log_dir (shared-constants.sh), then applies
51
+ # install-time character validation (safe for shell paths and cron lines).
51
52
  # Prints the resolved absolute path. Returns 1 on invalid characters.
52
53
  _resolve_cw_log_dir() {
53
54
  local _cw_log_dir
54
- # shellcheck disable=SC2088 # Tilde is intentionally literal here; expanded below via ${/#\~/$HOME}
55
- if type _jsonc_get &>/dev/null; then
56
- _cw_log_dir=$(_jsonc_get "paths.log_dir" "~/.aidevops/logs")
57
- else
58
- _cw_log_dir="~/.aidevops/logs"
59
- fi
55
+ _cw_log_dir=$(_resolve_log_dir)
60
56
  # Whitelist: only allow characters safe in shell paths and cron lines.
61
- # Reject anything outside [A-Za-z0-9_./ ~-] (tilde allowed before expansion).
57
+ # Reject anything outside [A-Za-z0-9_./ -] (tilde already expanded by _resolve_log_dir).
62
58
  # Store regex in variable — bash [[ =~ ]] requires unquoted RHS for regex,
63
59
  # and a variable avoids quoting issues with special chars in the pattern.
64
- local _cw_log_dir_re='^[A-Za-z0-9_./ ~-]+$'
60
+ local _cw_log_dir_re='^[A-Za-z0-9_./ -]+$'
65
61
  if ! [[ "$_cw_log_dir" =~ $_cw_log_dir_re ]]; then
66
62
  # Redirect to stderr so $() captures only the path result
67
- print_error "Invalid characters in paths.log_dir (only [A-Za-z0-9_./ ~-] allowed): $_cw_log_dir" >&2
63
+ print_error "Invalid characters in paths.log_dir (only [A-Za-z0-9_./ -] allowed): $_cw_log_dir" >&2
68
64
  return 1
69
65
  fi
70
- _cw_log_dir="${_cw_log_dir/#\~/$HOME}"
71
66
  printf '%s' "$_cw_log_dir"
72
67
  return 0
73
68
  }
@@ -310,18 +305,35 @@ setup_complexity_scan() {
310
305
  return 0
311
306
  }
312
307
 
313
- # Install pulse-merge-routine launchd plist (macOS).
314
- # Args: $1=label $2=script $3=log_dir
308
+ # Install the dedicated merge-pass launchd plist (macOS).
309
+ # Supersedes the t2862 sh.aidevops.pulse-merge-routine approach (t21247, GH#21247):
310
+ # - Label: com.aidevops.aidevops-supervisor-merge
311
+ # - Script: pulse-wrapper.sh --merge-only (full bootstrap + merge only)
312
+ # - Interval: 60s (down from 120s) — targets <=90s PR-green-to-merged latency
313
+ # - Log: pulse-merge.log (isolated from main pulse log)
314
+ #
315
+ # Args: $1=label $2=wrapper_script $3=log_dir
315
316
  _install_pulse_merge_routine_launchd() {
316
317
  local pmr_label="$1"
317
318
  local pmr_script="$2"
318
319
  local _pmr_log_dir="$3"
319
320
  local pmr_plist="$HOME/Library/LaunchAgents/${pmr_label}.plist"
320
321
 
321
- local _xml_pmr_script _xml_pmr_home _xml_pmr_log_dir
322
+ # One-time migration: unload and remove the legacy sh.aidevops.pulse-merge-routine
323
+ # plist (installed by t2862) to avoid running two merge passes simultaneously.
324
+ local _legacy_pmr_label="sh.aidevops.pulse-merge-routine"
325
+ local _legacy_pmr_plist="$HOME/Library/LaunchAgents/${_legacy_pmr_label}.plist"
326
+ if [[ -f "$_legacy_pmr_plist" ]]; then
327
+ launchctl unload "$_legacy_pmr_plist" 2>/dev/null || true
328
+ rm -f "$_legacy_pmr_plist"
329
+ print_info "Removed legacy pulse-merge-routine plist (superseded by ${pmr_label})"
330
+ fi
331
+
332
+ local _xml_pmr_script _xml_pmr_home _xml_pmr_log_dir _xml_bash_bin
322
333
  _xml_pmr_script=$(_xml_escape "$pmr_script")
323
334
  _xml_pmr_home=$(_xml_escape "$HOME")
324
335
  _xml_pmr_log_dir=$(_xml_escape "$_pmr_log_dir")
336
+ _xml_bash_bin=$(_xml_escape "$(_resolve_modern_bash)")
325
337
 
326
338
  local pmr_plist_content
327
339
  pmr_plist_content=$(
@@ -334,16 +346,16 @@ _install_pulse_merge_routine_launchd() {
334
346
  <string>${pmr_label}</string>
335
347
  <key>ProgramArguments</key>
336
348
  <array>
337
- <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
349
+ <string>${_xml_bash_bin}</string>
338
350
  <string>${_xml_pmr_script}</string>
339
- <string>run</string>
351
+ <string>--merge-only</string>
340
352
  </array>
341
353
  <key>StartInterval</key>
342
- <integer>120</integer>
354
+ <integer>60</integer>
343
355
  <key>StandardOutPath</key>
344
- <string>${_xml_pmr_log_dir}/pulse-merge-routine.log</string>
356
+ <string>${_xml_pmr_log_dir}/pulse-merge.log</string>
345
357
  <key>StandardErrorPath</key>
346
- <string>${_xml_pmr_log_dir}/pulse-merge-routine.log</string>
358
+ <string>${_xml_pmr_log_dir}/pulse-merge.log</string>
347
359
  <key>EnvironmentVariables</key>
348
360
  <dict>
349
361
  <key>PATH</key>
@@ -352,8 +364,6 @@ _install_pulse_merge_routine_launchd() {
352
364
  <string>${_xml_pmr_home}</string>
353
365
  </dict>
354
366
  <key>RunAtLoad</key>
355
- <true/>
356
- <key>KeepAlive</key>
357
367
  <false/>
358
368
  <key>ProcessType</key>
359
369
  <string>Background</string>
@@ -361,50 +371,77 @@ _install_pulse_merge_routine_launchd() {
361
371
  <true/>
362
372
  <key>Nice</key>
363
373
  <integer>10</integer>
374
+ <key>SoftResourceLimits</key>
375
+ <dict>
376
+ <key>NumberOfFiles</key>
377
+ <integer>4096</integer>
378
+ </dict>
364
379
  </dict>
365
380
  </plist>
366
381
  PMR_PLIST
367
382
  )
368
383
 
369
384
  if _launchd_install_if_changed "$pmr_label" "$pmr_plist" "$pmr_plist_content"; then
370
- print_info "Pulse merge routine enabled (launchd, every 2 min)"
385
+ print_info "Pulse merge pass enabled (launchd, every 60s via --merge-only)"
371
386
  else
372
- print_warning "Failed to load pulse merge routine LaunchAgent"
387
+ print_warning "Failed to load pulse merge pass LaunchAgent"
373
388
  fi
374
389
  return 0
375
390
  }
376
391
 
377
- # Install pulse-merge-routine via systemd or cron (Linux).
378
- # Args: $1=script path, $2=log dir
392
+ # Install the dedicated merge-pass scheduler via systemd or cron (Linux).
393
+ # Supersedes the t2862 aidevops-pulse-merge-routine approach (t21247, GH#21247):
394
+ # - Command: pulse-wrapper.sh --merge-only
395
+ # - Interval: every 60s (cron * * * * *, systemd OnUnitActiveSec=60)
396
+ # - Log: pulse-merge.log
397
+ # Args: $1=wrapper_script $2=log_dir
379
398
  _install_pulse_merge_routine_linux() {
380
399
  local pmr_script="$1"
381
400
  local _pmr_log_dir="$2"
382
- local pmr_systemd="aidevops-pulse-merge-routine"
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
+
383
414
  _install_scheduler_linux \
384
415
  "$pmr_systemd" \
385
- "aidevops: pulse-merge-routine" \
386
- "*/2 * * * *" \
387
- "\"${pmr_script}\" run" \
388
- "120" \
389
- "${_pmr_log_dir}/pulse-merge-routine.log" \
416
+ "aidevops: pulse-merge (--merge-only)" \
417
+ "* * * * *" \
418
+ "\"${pmr_script}\" --merge-only" \
419
+ "60" \
420
+ "${_pmr_log_dir}/pulse-merge.log" \
390
421
  "" \
391
- "Pulse merge routine enabled (every 2 min)" \
392
- "Failed to install pulse merge routine scheduler" \
422
+ "Pulse merge pass enabled (every 60s via --merge-only)" \
423
+ "Failed to install pulse merge pass scheduler" \
393
424
  "true" \
394
425
  "true"
395
426
  return 0
396
427
  }
397
428
 
398
- # Setup pulse merge routine (t2862, GH#20919) — runs merge_ready_prs_all_repos()
399
- # as a fast 120s standalone routine, decoupled from the monolithic pulse cycle.
400
- # The pulse cycle's preflight stack (60-470s) meant the merge pass ran only ~7
401
- # times/24h despite ~40+ cycles. This routine ensures green PRs merge within ~3
402
- # min of CI completion. The in-cycle merge call in pulse-wrapper.sh is kept as
403
- # defense-in-depth but short-circuits when this routine ran within the last 60s.
429
+ # Setup the dedicated merge-pass scheduler (t21247, GH#21247).
430
+ #
431
+ # Supersedes t2862/GH#20919 (pulse-merge-routine.sh, 120s interval). The new
432
+ # approach runs pulse-wrapper.sh --merge-only every 60s so green PRs merge
433
+ # within <=90s of CI completion regardless of dispatch-cycle length (~23 min
434
+ # average). The full pulse bootstrap ensures all PULSE_* config and repo state
435
+ # are available; the separate lockdir (pulse-merge-instance.lock) prevents
436
+ # overlap with the main dispatch cycle lock.
437
+ #
438
+ # Requires: pulse-wrapper.sh must be installed and executable.
439
+ # The in-cycle merge pass in pulse-wrapper.sh is kept as defense-in-depth —
440
+ # it short-circuits when a merge pass ran within the last 60s.
404
441
  setup_pulse_merge_routine() {
405
- local pmr_script="$HOME/.aidevops/agents/scripts/pulse-merge-routine.sh"
406
- local pmr_label="sh.aidevops.pulse-merge-routine"
407
- if ! [[ -x "$pmr_script" ]]; then
442
+ local pmr_wrapper="$HOME/.aidevops/agents/scripts/pulse-wrapper.sh"
443
+ local pmr_label="com.aidevops.aidevops-supervisor-merge"
444
+ if ! [[ -x "$pmr_wrapper" ]]; then
408
445
  return 0
409
446
  fi
410
447
 
@@ -415,9 +452,9 @@ setup_pulse_merge_routine() {
415
452
 
416
453
  # Install/update scheduled runner
417
454
  if [[ "$(uname -s)" == "Darwin" ]]; then
418
- _install_pulse_merge_routine_launchd "$pmr_label" "$pmr_script" "$_pmr_log_dir"
455
+ _install_pulse_merge_routine_launchd "$pmr_label" "$pmr_wrapper" "$_pmr_log_dir"
419
456
  else
420
- _install_pulse_merge_routine_linux "$pmr_script" "$_pmr_log_dir"
457
+ _install_pulse_merge_routine_linux "$pmr_wrapper" "$_pmr_log_dir"
421
458
  fi
422
459
  return 0
423
460
  }
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.10
15
+ # Version: 3.13.13
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)