aidevops 3.11.17 → 3.13.0

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.11.17
1
+ 3.13.0
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.11.17
8
+ # Version: 3.13.0
9
9
 
10
10
  set -euo pipefail
11
11
 
@@ -480,6 +480,46 @@ _update_check_homebrew() {
480
480
  return 0
481
481
  }
482
482
 
483
+ # t2926 / GH#21102: Re-check setsid on every 'aidevops update' run.
484
+ # setsid (from util-linux) is required to detach pulse workers into their own
485
+ # process group — without it, every pulse restart sends SIGHUP to its PGID,
486
+ # killing in-flight workers. This check runs even when setup.sh is skipped
487
+ # (already up-to-date path), so Homebrew drift doesn't silently break workers.
488
+ _update_check_setsid() {
489
+ command -v setsid >/dev/null 2>&1 && return 0
490
+
491
+ # setsid is missing. On macOS with Homebrew, auto-install util-linux.
492
+ # Use a boolean flag to avoid repeating the OS literal string.
493
+ local _on_mac=false
494
+ [[ "$(uname -s)" == Darwin* ]] && _on_mac=true
495
+ if $_on_mac && command -v brew >/dev/null 2>&1; then
496
+ print_info "setsid not found — installing util-linux for worker PGID isolation (GH#21102)"
497
+ if brew install util-linux 2>&1 | tail -3; then
498
+ local brew_prefix=""
499
+ brew_prefix="$(brew --prefix 2>/dev/null || true)"
500
+ local keg_setsid="${brew_prefix}/opt/util-linux/bin/setsid"
501
+ local link_target="${brew_prefix}/bin/setsid"
502
+ if [[ -x "$keg_setsid" && ! -e "$link_target" ]]; then
503
+ ln -s "$keg_setsid" "$link_target" && \
504
+ print_success "Symlinked setsid: $keg_setsid → $link_target"
505
+ fi
506
+ if command -v setsid >/dev/null 2>&1; then
507
+ print_success "setsid installed at $(command -v setsid) (worker PGID isolation enabled)"
508
+ else
509
+ print_error "util-linux installed but setsid still not in PATH — check brew --prefix"
510
+ fi
511
+ else
512
+ print_error "brew install util-linux failed — workers will share pulse PGID until resolved"
513
+ fi
514
+ elif $_on_mac; then
515
+ print_error "setsid not found — worker isolation broken; install Homebrew then run: brew install util-linux"
516
+ else
517
+ print_error "setsid not found — worker isolation broken; install util-linux via your distro package manager"
518
+ fi
519
+
520
+ return 0
521
+ }
522
+
483
523
  # Verify supply chain signature after pulling framework updates.
484
524
  # Checks that the HEAD commit is signed by the trusted maintainer key.
485
525
  # Non-blocking: warns on failure, does not abort the update.
@@ -638,6 +678,8 @@ cmd_update() {
638
678
  _update_check_planning
639
679
  _update_check_tools
640
680
  _update_sweep_opencode_symlinks
681
+ # t2926: Re-check setsid on every update (runs even when setup.sh is skipped).
682
+ _update_check_setsid
641
683
 
642
684
  # t2898: When invoked interactively (terminal stdin AND not from the
643
685
  # auto-update daemon itself, which sets AIDEVOPS_AUTO_UPDATE=1 in its
@@ -1456,10 +1498,13 @@ _help_commands() {
1456
1498
  echo " approve <cmd> Cryptographic issue/PR approval (setup/issue/pr/verify/status)"
1457
1499
  echo " security [cmd] Full security assessment (posture + hygiene + supply chain)"
1458
1500
  echo " contributions External contributions inbox (bare: status | seed/scan/stop/restart/install/uninstall)"
1501
+ echo " inbox [cmd] Capture transit zone (bare: status | provision/add/find/digest/help)"
1502
+ echo " email [cmd] Email mailbox management (mailbox add/list/test/remove)"
1459
1503
  echo " ip-check <cmd> IP reputation checks (check/batch/report/providers)"
1460
1504
  echo " review-gate <cmd> Configure review_gate.rate_limit_behavior (list/set/unset)"
1461
1505
  echo " secret <cmd> Manage secrets (set/list/run/init/import/status)"
1462
1506
  echo " config <cmd> Feature toggles (list/get/set/reset/path/help)"
1507
+ echo " knowledge <cmd> Knowledge plane management (init/status/provision)"
1463
1508
  echo " stats <cmd> LLM usage analytics (summary/models/projects/costs/trend)"
1464
1509
  echo " tabby <cmd> Manage Tabby terminal profiles (sync/status/zshrc/help)"
1465
1510
  echo " parent-status <N> Show decomposition state of parent-task issue #N (alias: ps)"
@@ -1539,6 +1584,13 @@ _help_detailed_sections() {
1539
1584
  echo " aidevops config reset [key] # Reset toggle(s) to defaults"
1540
1585
  echo " aidevops config path # Show config file path"
1541
1586
  echo ""
1587
+ echo "Knowledge Plane:"
1588
+ echo " aidevops knowledge init repo # Provision _knowledge/ in current repo"
1589
+ echo " aidevops knowledge init personal # Provision at ~/.aidevops/.agent-workspace/knowledge/"
1590
+ echo " aidevops knowledge init off # Disable knowledge plane"
1591
+ echo " aidevops knowledge status # Show provisioning state"
1592
+ echo " aidevops knowledge provision [path] # Re-provision (idempotent)"
1593
+ echo ""
1542
1594
  echo "LLM Stats:"
1543
1595
  echo " aidevops stats # Show usage summary (last 30 days)"
1544
1596
  echo " aidevops stats summary # Overall usage summary"
@@ -1745,6 +1797,71 @@ _cmd_security() {
1745
1797
  return 0
1746
1798
  }
1747
1799
 
1800
+ # Route 'aidevops email [subcommand]' to email helpers
1801
+ _cmd_email() {
1802
+ local sub="${1:-help}"
1803
+ local _EPH="email-poll-helper.sh"
1804
+ shift || true
1805
+ case "$sub" in
1806
+ mailbox)
1807
+ local action="${1:-list}"
1808
+ shift || true
1809
+ local _EMR_HELPER="email-mailbox-register-helper.sh"
1810
+ case "$action" in
1811
+ add) _dispatch_helper "$_EMR_HELPER" "$_EMR_HELPER" add "$@" ;;
1812
+ list) _dispatch_helper "$_EPH" "$_EPH" list "$@" ;;
1813
+ test) _dispatch_helper "$_EPH" "$_EPH" test "$@" ;;
1814
+ remove) _dispatch_helper "$_EMR_HELPER" "$_EMR_HELPER" remove "$@" ;;
1815
+ *)
1816
+ echo "Usage: aidevops email mailbox <add|list|test|remove>"
1817
+ echo ""
1818
+ echo "Mailbox subcommands:"
1819
+ echo " add Interactive: prompt for provider, user, gopass path; test connection"
1820
+ echo " list Table of mailboxes with last-polled-at and last-error"
1821
+ echo " test <id> Dry-run fetch (1 message); does not commit state"
1822
+ echo " remove <id> Un-register a mailbox"
1823
+ ;;
1824
+ esac
1825
+ ;;
1826
+ poll)
1827
+ # Direct poll commands forwarded to email-poll-helper.sh
1828
+ local poll_action="${1:-tick}"
1829
+ shift || true
1830
+ _dispatch_helper "$_EPH" "$_EPH" "$poll_action" "$@" ;;
1831
+ thread)
1832
+ # Thread lookup: email thread <message-id> [knowledge-root]
1833
+ local _ETH="email-thread-helper.sh"
1834
+ _dispatch_helper "$_ETH" "$_ETH" thread "$@" ;;
1835
+ build)
1836
+ # Thread rebuild: email build [knowledge-root] [--force]
1837
+ local _ETH2="email-thread-helper.sh"
1838
+ _dispatch_helper "$_ETH2" "$_ETH2" build "$@" ;;
1839
+ filter)
1840
+ # Filter rules: email filter tick|add|test|list [knowledge-root]
1841
+ local _EFH="email-filter-helper.sh"
1842
+ [[ $# -eq 0 ]] && set -- list
1843
+ _dispatch_helper "$_EFH" "$_EFH" "$@" ;;
1844
+ *)
1845
+ echo "Usage: aidevops email <mailbox|poll|thread|build|filter> [subcommand]"
1846
+ echo ""
1847
+ echo "Email subcommands:"
1848
+ echo " mailbox add Register a new IMAP mailbox (interactive)"
1849
+ echo " mailbox list Show all mailboxes + polling status"
1850
+ echo " mailbox test <id> Dry-run connection test"
1851
+ echo " mailbox remove <id> Un-register a mailbox"
1852
+ echo " poll tick Poll all mailboxes now (same as routine r044)"
1853
+ echo " poll backfill <id> Backfill a mailbox from a given date"
1854
+ echo " thread <message-id> Look up thread by message-id"
1855
+ echo " build [--force] Rebuild thread index from email sources"
1856
+ echo " filter list List filter rules"
1857
+ echo " filter add Add a new filter rule (interactive)"
1858
+ echo " filter test <rule> Dry-run rule against last 50 sources"
1859
+ echo " filter tick Run filter pass (routine r045)"
1860
+ ;;
1861
+ esac
1862
+ return 0
1863
+ }
1864
+
1748
1865
  # Route 'aidevops client-format [subcommand]' to appropriate helpers
1749
1866
  _cmd_client_format() {
1750
1867
  case "${1:-status}" in
@@ -1835,10 +1952,22 @@ main() {
1835
1952
  [[ $# -eq 0 ]] && set -- status
1836
1953
  _dispatch_helper "contribution-watch-helper.sh" "contribution-watch-helper.sh" "$@"
1837
1954
  ;;
1955
+ inbox)
1956
+ # Bare `aidevops inbox` defaults to status (most common use).
1957
+ [[ $# -eq 0 ]] && set -- status
1958
+ _dispatch_helper "inbox-helper.sh" "inbox-helper.sh" "$@"
1959
+ ;;
1960
+ case | cases)
1961
+ # Bare `aidevops case` defaults to list (most common use).
1962
+ [[ $# -eq 0 ]] && set -- list
1963
+ _dispatch_helper "case-helper.sh" "case-helper.sh" "$@"
1964
+ ;;
1965
+ email) _cmd_email "$@" ;;
1838
1966
  stats | observability) _dispatch_helper "observability-helper.sh" "observability-helper.sh" "$@" ;;
1839
1967
  tabby) _dispatch_helper "tabby-helper.sh" "tabby-helper.sh" "$@" ;;
1840
1968
  init-routines) _dispatch_helper "init-routines-helper.sh" "init-routines-helper.sh" "$@" ;;
1841
1969
  parent-status | ps) _dispatch_helper "parent-status-helper.sh" "parent-status-helper.sh" "$@" ;;
1970
+ knowledge) _dispatch_helper "knowledge-helper.sh" "knowledge-helper.sh" "$@" ;;
1842
1971
  config | configure) _dispatch_config "$@" ;;
1843
1972
  uninstall | remove) cmd_uninstall ;;
1844
1973
  version | v | -v | --version) cmd_version ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.11.17",
3
+ "version": "3.13.0",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,11 @@ PULSE_STALE_THRESHOLD_SECONDS=1800
14
14
  # future cadence shift only touches one place.
15
15
  CRON_HOURLY="0 * * * *"
16
16
 
17
+ # Cron expression: every minute. Shared by process-guard, memory-pressure
18
+ # monitor, and pulse-watchdog schedulers (cron's minimum granularity).
19
+ # Kept DRY for the same reason as CRON_HOURLY.
20
+ CRON_EVERY_MINUTE="* * * * *"
21
+
17
22
  # Resolve the modern bash binary path for use in launchd ProgramArguments.
18
23
  # Launchd bypasses the shebang when ProgramArguments specifies an explicit
19
24
  # interpreter, so we must resolve the path at plist generation time.
@@ -668,7 +673,12 @@ ${_env_overrides_xml} </dict>
668
673
  <key>RunAtLoad</key>
669
674
  <true/>
670
675
  <key>KeepAlive</key>
671
- <false/>
676
+ <dict>
677
+ <key>SuccessfulExit</key>
678
+ <false/>
679
+ </dict>
680
+ <key>ThrottleInterval</key>
681
+ <integer>30</integer>
672
682
  </dict>
673
683
  </plist>
674
684
  PLIST
@@ -703,6 +713,17 @@ _install_pulse_launchd() {
703
713
  _interval_label="${_interval_sec}s"
704
714
  fi
705
715
 
716
+ # One-time legacy cleanup: unload and remove the old-label plist if present.
717
+ # Users on stale installs may have com.aidevops.supervisor-pulse (legacy) and
718
+ # com.aidevops.aidevops-supervisor-pulse (current) both loaded, causing 2x
719
+ # dispatch. Only targets the hardcoded legacy path; idempotent — no-op when
720
+ # the legacy file is absent.
721
+ local _legacy_plist="$HOME/Library/LaunchAgents/com.aidevops.supervisor-pulse.plist"
722
+ if [[ -f "$_legacy_plist" ]]; then
723
+ launchctl unload "$_legacy_plist" 2>/dev/null || true
724
+ rm -f "$_legacy_plist"
725
+ fi
726
+
706
727
  # _launchd_install_if_changed handles unload-before-replace only when content
707
728
  # has changed, and writes atomically via tmp+rename (see setup.sh).
708
729
  # shell-portability: ignore next — _install_pulse_launchd is macOS-only (launchd)
@@ -720,6 +741,161 @@ _install_pulse_launchd() {
720
741
  return 0
721
742
  }
722
743
 
744
+ # Generate the pulse-watchdog launchd plist XML content.
745
+ # Args: $1=label, $2=tick_script, $3=bash_bin
746
+ # Prints the complete plist XML to stdout.
747
+ #
748
+ # The watchdog is an independent launchd job that runs every 60s and revives
749
+ # pulse if it has been dead longer than (StartInterval + grace). Layered
750
+ # defense alongside the pulse plist's KeepAlive=<dict><SuccessfulExit=false>
751
+ # (auto-restart on crash) and StartInterval (scheduled cadence). Catches the
752
+ # "clean exit + lost launchd schedule" failure mode that no other layer covers.
753
+ # (t2939)
754
+ _generate_pulse_watchdog_plist_content() {
755
+ local watchdog_label="$1"
756
+ local tick_script="$2"
757
+ local bash_bin="$3"
758
+
759
+ local _xml_label _xml_tick _xml_bash _xml_home _xml_path
760
+ _xml_label=$(_xml_escape "$watchdog_label")
761
+ _xml_tick=$(_xml_escape "$tick_script")
762
+ _xml_bash=$(_xml_escape "$bash_bin")
763
+ _xml_home=$(_xml_escape "$HOME")
764
+ _xml_path=$(_xml_escape "$PATH")
765
+
766
+ cat <<PLIST
767
+ <?xml version="1.0" encoding="UTF-8"?>
768
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
769
+ <plist version="1.0">
770
+ <dict>
771
+ <key>Label</key>
772
+ <string>${_xml_label}</string>
773
+ <key>ProgramArguments</key>
774
+ <array>
775
+ <string>${_xml_bash}</string>
776
+ <string>${_xml_tick}</string>
777
+ </array>
778
+ <key>StartInterval</key>
779
+ <integer>60</integer>
780
+ <key>StandardOutPath</key>
781
+ <string>${_xml_home}/.aidevops/logs/pulse-watchdog-launchd.log</string>
782
+ <key>StandardErrorPath</key>
783
+ <string>${_xml_home}/.aidevops/logs/pulse-watchdog-launchd.log</string>
784
+ <key>EnvironmentVariables</key>
785
+ <dict>
786
+ <key>PATH</key>
787
+ <string>${_xml_path}</string>
788
+ <key>HOME</key>
789
+ <string>${_xml_home}</string>
790
+ </dict>
791
+ <key>RunAtLoad</key>
792
+ <true/>
793
+ <key>KeepAlive</key>
794
+ <false/>
795
+ <key>ThrottleInterval</key>
796
+ <integer>30</integer>
797
+ </dict>
798
+ </plist>
799
+ PLIST
800
+ return 0
801
+ }
802
+
803
+ # Install the pulse-watchdog via launchd (macOS).
804
+ # t2939: independent revival mechanism — see _generate_pulse_watchdog_plist_content
805
+ # header for the layering rationale.
806
+ _install_pulse_watchdog_launchd() {
807
+ local watchdog_label="sh.aidevops.pulse-watchdog"
808
+ local tick_script="$HOME/.aidevops/agents/scripts/pulse-watchdog-tick.sh"
809
+ local watchdog_plist="$HOME/Library/LaunchAgents/${watchdog_label}.plist"
810
+
811
+ # Refuse to install if the tick script is missing — the watchdog would
812
+ # fire-and-fail every 60s, polluting logs without doing useful work.
813
+ if [[ ! -x "$tick_script" ]]; then
814
+ print_warning "Pulse watchdog tick script missing or non-executable: $tick_script"
815
+ return 1
816
+ fi
817
+
818
+ local _xml_bash_bin
819
+ _xml_bash_bin=$(_resolve_modern_bash)
820
+
821
+ local watchdog_plist_content
822
+ watchdog_plist_content=$(_generate_pulse_watchdog_plist_content "$watchdog_label" "$tick_script" "$_xml_bash_bin")
823
+
824
+ if [[ -z "$watchdog_plist_content" ]]; then
825
+ print_warning "Pulse watchdog plist generation produced empty content — skipping"
826
+ return 1
827
+ fi
828
+
829
+ # shell-portability: ignore next — _install_pulse_watchdog_launchd is macOS-only
830
+ if _launchd_install_if_changed "$watchdog_label" "$watchdog_plist" "$watchdog_plist_content"; then
831
+ print_info "Pulse watchdog enabled (launchd, every 60s)"
832
+ else
833
+ print_warning "Failed to load pulse watchdog LaunchAgent"
834
+ fi
835
+ return 0
836
+ }
837
+
838
+ # Install the pulse-watchdog via systemd (Linux).
839
+ # t2939: parallels _install_pulse_watchdog_launchd for systems with systemd --user.
840
+ _install_pulse_watchdog_systemd() {
841
+ local tick_script="$HOME/.aidevops/agents/scripts/pulse-watchdog-tick.sh"
842
+ local watchdog_systemd="aidevops-pulse-watchdog"
843
+ local watchdog_log="$HOME/.aidevops/logs/pulse-watchdog-launchd.log"
844
+
845
+ if [[ ! -x "$tick_script" ]]; then
846
+ print_warning "Pulse watchdog tick script missing or non-executable: $tick_script"
847
+ return 1
848
+ fi
849
+
850
+ # Reuse the standard scheduler installer (cron-fallback aware).
851
+ # StartInterval=60 maps to every-minute cron schedule.
852
+ # shell-portability: ignore next — _install_scheduler_linux is Linux-only
853
+ _install_scheduler_linux \
854
+ "$watchdog_systemd" \
855
+ "aidevops: pulse-watchdog" \
856
+ "$CRON_EVERY_MINUTE" \
857
+ "\"${tick_script}\"" \
858
+ "60" \
859
+ "$watchdog_log" \
860
+ "" \
861
+ "Pulse watchdog enabled (every 60s)" \
862
+ "Failed to install pulse watchdog scheduler" \
863
+ "true" \
864
+ "false"
865
+ return 0
866
+ }
867
+
868
+ # Setup the pulse-watchdog scheduler (parallels setup_supervisor_pulse).
869
+ # t2939: layered defense — only installs when supervisor pulse is enabled,
870
+ # since a watchdog without a pulse to watch is a no-op every 60s.
871
+ #
872
+ # Args: $1 = pulse effective state ("true"/"false")
873
+ setup_pulse_watchdog() {
874
+ local _pulse_effective="$1"
875
+ local watchdog_label="sh.aidevops.pulse-watchdog"
876
+ local watchdog_systemd="aidevops-pulse-watchdog"
877
+
878
+ if [[ "$_pulse_effective" != "true" ]]; then
879
+ # Pulse disabled — uninstall the watchdog if present.
880
+ _uninstall_scheduler \
881
+ "$(uname -s)" \
882
+ "$watchdog_label" \
883
+ "$watchdog_systemd" \
884
+ "aidevops: pulse-watchdog" \
885
+ "Pulse watchdog disabled (pulse is off)"
886
+ return 0
887
+ fi
888
+
889
+ mkdir -p "$HOME/.aidevops/logs"
890
+
891
+ if [[ "$(uname -s)" == "Darwin" ]]; then
892
+ _install_pulse_watchdog_launchd
893
+ else
894
+ _install_pulse_watchdog_systemd
895
+ fi
896
+ return 0
897
+ }
898
+
723
899
  # Check if systemd user services are available on this Linux system.
724
900
  # Returns 0 if systemd --user is functional, 1 otherwise.
725
901
  _systemd_user_available() {
@@ -1341,7 +1517,7 @@ GUARD_PLIST
1341
1517
  _install_scheduler_linux \
1342
1518
  "$guard_systemd" \
1343
1519
  "aidevops: process-guard" \
1344
- "* * * * *" \
1520
+ "$CRON_EVERY_MINUTE" \
1345
1521
  "\"${guard_script}\" kill-runaways" \
1346
1522
  "30" \
1347
1523
  "$guard_log" \
@@ -1433,7 +1609,7 @@ MONITOR_PLIST
1433
1609
  _install_scheduler_linux \
1434
1610
  "$monitor_systemd" \
1435
1611
  "aidevops: memory-pressure-monitor" \
1436
- "* * * * *" \
1612
+ "$CRON_EVERY_MINUTE" \
1437
1613
  "\"${monitor_script}\"" \
1438
1614
  "60" \
1439
1615
  "$monitor_log" \
@@ -1799,6 +1975,118 @@ setup_complexity_scan() {
1799
1975
  return 0
1800
1976
  }
1801
1977
 
1978
+ # Install pulse-merge-routine launchd plist (macOS).
1979
+ # Args: $1=label $2=script $3=log_dir
1980
+ _install_pulse_merge_routine_launchd() {
1981
+ local pmr_label="$1"
1982
+ local pmr_script="$2"
1983
+ local _pmr_log_dir="$3"
1984
+ local pmr_plist="$HOME/Library/LaunchAgents/${pmr_label}.plist"
1985
+
1986
+ local _xml_pmr_script _xml_pmr_home _xml_pmr_log_dir
1987
+ _xml_pmr_script=$(_xml_escape "$pmr_script")
1988
+ _xml_pmr_home=$(_xml_escape "$HOME")
1989
+ _xml_pmr_log_dir=$(_xml_escape "$_pmr_log_dir")
1990
+
1991
+ local pmr_plist_content
1992
+ pmr_plist_content=$(
1993
+ cat <<PMR_PLIST
1994
+ <?xml version="1.0" encoding="UTF-8"?>
1995
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1996
+ <plist version="1.0">
1997
+ <dict>
1998
+ <key>Label</key>
1999
+ <string>${pmr_label}</string>
2000
+ <key>ProgramArguments</key>
2001
+ <array>
2002
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
2003
+ <string>${_xml_pmr_script}</string>
2004
+ <string>run</string>
2005
+ </array>
2006
+ <key>StartInterval</key>
2007
+ <integer>120</integer>
2008
+ <key>StandardOutPath</key>
2009
+ <string>${_xml_pmr_log_dir}/pulse-merge-routine.log</string>
2010
+ <key>StandardErrorPath</key>
2011
+ <string>${_xml_pmr_log_dir}/pulse-merge-routine.log</string>
2012
+ <key>EnvironmentVariables</key>
2013
+ <dict>
2014
+ <key>PATH</key>
2015
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
2016
+ <key>HOME</key>
2017
+ <string>${_xml_pmr_home}</string>
2018
+ </dict>
2019
+ <key>RunAtLoad</key>
2020
+ <true/>
2021
+ <key>KeepAlive</key>
2022
+ <false/>
2023
+ <key>ProcessType</key>
2024
+ <string>Background</string>
2025
+ <key>LowPriorityBackgroundIO</key>
2026
+ <true/>
2027
+ <key>Nice</key>
2028
+ <integer>10</integer>
2029
+ </dict>
2030
+ </plist>
2031
+ PMR_PLIST
2032
+ )
2033
+
2034
+ if _launchd_install_if_changed "$pmr_label" "$pmr_plist" "$pmr_plist_content"; then
2035
+ print_info "Pulse merge routine enabled (launchd, every 2 min)"
2036
+ else
2037
+ print_warning "Failed to load pulse merge routine LaunchAgent"
2038
+ fi
2039
+ return 0
2040
+ }
2041
+
2042
+ # Install pulse-merge-routine via systemd or cron (Linux).
2043
+ # Args: $1=script path, $2=log dir
2044
+ _install_pulse_merge_routine_linux() {
2045
+ local pmr_script="$1"
2046
+ local _pmr_log_dir="$2"
2047
+ local pmr_systemd="aidevops-pulse-merge-routine"
2048
+ _install_scheduler_linux \
2049
+ "$pmr_systemd" \
2050
+ "aidevops: pulse-merge-routine" \
2051
+ "*/2 * * * *" \
2052
+ "\"${pmr_script}\" run" \
2053
+ "120" \
2054
+ "${_pmr_log_dir}/pulse-merge-routine.log" \
2055
+ "" \
2056
+ "Pulse merge routine enabled (every 2 min)" \
2057
+ "Failed to install pulse merge routine scheduler" \
2058
+ "true" \
2059
+ "true"
2060
+ return 0
2061
+ }
2062
+
2063
+ # Setup pulse merge routine (t2862, GH#20919) — runs merge_ready_prs_all_repos()
2064
+ # as a fast 120s standalone routine, decoupled from the monolithic pulse cycle.
2065
+ # The pulse cycle's preflight stack (60-470s) meant the merge pass ran only ~7
2066
+ # times/24h despite ~40+ cycles. This routine ensures green PRs merge within ~3
2067
+ # min of CI completion. The in-cycle merge call in pulse-wrapper.sh is kept as
2068
+ # defense-in-depth but short-circuits when this routine ran within the last 60s.
2069
+ setup_pulse_merge_routine() {
2070
+ local pmr_script="$HOME/.aidevops/agents/scripts/pulse-merge-routine.sh"
2071
+ local pmr_label="sh.aidevops.pulse-merge-routine"
2072
+ if ! [[ -x "$pmr_script" ]]; then
2073
+ return 0
2074
+ fi
2075
+
2076
+ # Reuse contribution-watch's log-dir resolver (same logic, same config key).
2077
+ local _pmr_log_dir
2078
+ _pmr_log_dir=$(_resolve_cw_log_dir) || return 1
2079
+ mkdir -p "$_pmr_log_dir"
2080
+
2081
+ # Install/update scheduled runner
2082
+ if [[ "$(uname -s)" == "Darwin" ]]; then
2083
+ _install_pulse_merge_routine_launchd "$pmr_label" "$pmr_script" "$_pmr_log_dir"
2084
+ else
2085
+ _install_pulse_merge_routine_linux "$pmr_script" "$_pmr_log_dir"
2086
+ fi
2087
+ return 0
2088
+ }
2089
+
1802
2090
  # Setup draft responses — private repo + local draft storage for reviewing
1803
2091
  # AI-drafted replies to external contributions (t1555).
1804
2092
  # Respects config: aidevops config set orchestration.draft_responses false
@@ -2276,3 +2564,126 @@ setup_repo_aidevops_health() {
2276
2564
  fi
2277
2565
  return 0
2278
2566
  }
2567
+
2568
+ # ============================================================================
2569
+ # Peer productivity monitor (t2932)
2570
+ # ============================================================================
2571
+ #
2572
+ # Adaptive cross-runner dispatch coordination: observes peer GitHub activity
2573
+ # every 30 min and updates ~/.config/aidevops/dispatch-override.conf to
2574
+ # `ignore` peers whose pulse is broken (claims issues but never PRs) and
2575
+ # back to `honour` when they recover. Self-healing across the ecosystem —
2576
+ # each runner observes peers independently, no central coordinator needed.
2577
+ # Manual entries in dispatch-override.conf above the auto-managed marker
2578
+ # always take precedence.
2579
+
2580
+ # Install peer-productivity-monitor launchd plist (macOS).
2581
+ # Args: $1=label $2=script $3=log_dir
2582
+ _install_peer_productivity_monitor_launchd() {
2583
+ local ppm_label="$1"
2584
+ local ppm_script="$2"
2585
+ local _ppm_log_dir="$3"
2586
+ local ppm_plist="$HOME/Library/LaunchAgents/${ppm_label}.plist"
2587
+
2588
+ local _xml_ppm_script _xml_ppm_home _xml_ppm_log_dir
2589
+ _xml_ppm_script=$(_xml_escape "$ppm_script")
2590
+ _xml_ppm_home=$(_xml_escape "$HOME")
2591
+ _xml_ppm_log_dir=$(_xml_escape "$_ppm_log_dir")
2592
+
2593
+ local ppm_plist_content
2594
+ ppm_plist_content=$(
2595
+ cat <<PPM_PLIST
2596
+ <?xml version="1.0" encoding="UTF-8"?>
2597
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2598
+ <plist version="1.0">
2599
+ <dict>
2600
+ <key>Label</key>
2601
+ <string>${ppm_label}</string>
2602
+ <key>ProgramArguments</key>
2603
+ <array>
2604
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
2605
+ <string>${_xml_ppm_script}</string>
2606
+ <string>observe</string>
2607
+ </array>
2608
+ <key>StartInterval</key>
2609
+ <integer>1800</integer>
2610
+ <key>StandardOutPath</key>
2611
+ <string>${_xml_ppm_log_dir}/peer-productivity-launchd.log</string>
2612
+ <key>StandardErrorPath</key>
2613
+ <string>${_xml_ppm_log_dir}/peer-productivity-launchd.log</string>
2614
+ <key>EnvironmentVariables</key>
2615
+ <dict>
2616
+ <key>PATH</key>
2617
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
2618
+ <key>HOME</key>
2619
+ <string>${_xml_ppm_home}</string>
2620
+ </dict>
2621
+ <key>RunAtLoad</key>
2622
+ <true/>
2623
+ <key>KeepAlive</key>
2624
+ <false/>
2625
+ <key>ProcessType</key>
2626
+ <string>Background</string>
2627
+ <key>LowPriorityBackgroundIO</key>
2628
+ <true/>
2629
+ <key>Nice</key>
2630
+ <integer>10</integer>
2631
+ </dict>
2632
+ </plist>
2633
+ PPM_PLIST
2634
+ )
2635
+
2636
+ if _launchd_install_if_changed "$ppm_label" "$ppm_plist" "$ppm_plist_content"; then
2637
+ print_info "Peer productivity monitor enabled (launchd, every 30 min)"
2638
+ else
2639
+ print_warning "Failed to load peer-productivity-monitor LaunchAgent"
2640
+ fi
2641
+ return 0
2642
+ }
2643
+
2644
+ # Install peer-productivity-monitor via systemd or cron (Linux).
2645
+ # Args: $1=script path, $2=log dir
2646
+ _install_peer_productivity_monitor_linux() {
2647
+ local ppm_script="$1"
2648
+ local _ppm_log_dir="$2"
2649
+ local ppm_systemd="aidevops-peer-productivity-monitor"
2650
+ _install_scheduler_linux \
2651
+ "$ppm_systemd" \
2652
+ "aidevops: peer-productivity-monitor" \
2653
+ "*/30 * * * *" \
2654
+ "\"${ppm_script}\" observe" \
2655
+ "1800" \
2656
+ "${_ppm_log_dir}/peer-productivity-launchd.log" \
2657
+ "" \
2658
+ "Peer productivity monitor enabled (every 30 min)" \
2659
+ "Failed to install peer-productivity-monitor scheduler" \
2660
+ "true" \
2661
+ "true"
2662
+ return 0
2663
+ }
2664
+
2665
+ # Setup peer-productivity-monitor (t2932) — observes peer GitHub activity
2666
+ # every 30 min and updates ~/.config/aidevops/dispatch-override.conf so the
2667
+ # local pulse competes with broken peers and collaborates with healthy ones.
2668
+ # Manual entries in dispatch-override.conf above the auto-managed marker
2669
+ # always take precedence.
2670
+ setup_peer_productivity_monitor() {
2671
+ local ppm_script="$HOME/.aidevops/agents/scripts/peer-productivity-monitor.sh"
2672
+ local ppm_label="sh.aidevops.peer-productivity-monitor"
2673
+ if ! [[ -x "$ppm_script" ]]; then
2674
+ return 0
2675
+ fi
2676
+
2677
+ # Reuse contribution-watch's log-dir resolver (same logic, same config key).
2678
+ local _ppm_log_dir
2679
+ _ppm_log_dir=$(_resolve_cw_log_dir) || return 1
2680
+ mkdir -p "$_ppm_log_dir"
2681
+
2682
+ # Install/update scheduled runner
2683
+ if [[ "$(uname -s)" == "Darwin" ]]; then
2684
+ _install_peer_productivity_monitor_launchd "$ppm_label" "$ppm_script" "$_ppm_log_dir"
2685
+ else
2686
+ _install_peer_productivity_monitor_linux "$ppm_script" "$_ppm_log_dir"
2687
+ fi
2688
+ return 0
2689
+ }
@@ -390,12 +390,14 @@ setup_shell_linting_tools() {
390
390
 
391
391
  setup_setsid_advisory() {
392
392
  # setsid is required to detach pulse workers into their own process group
393
- # (t2757, GH#20561). Without it, workers inherit pulse's PGID and are
394
- # killed by any PG-scoped signal (launchd unload, restart chain).
393
+ # (t2757, GH#20561, GH#21102). Without it, workers inherit pulse's PGID and
394
+ # are killed by any PG-scoped signal (launchd unload, restart chain).
395
395
  #
396
396
  # Linux: setsid ships with util-linux (present on all mainstream distros).
397
- # macOS: available from macOS 12+ at /usr/bin/setsid. Older versions need
398
- # util-linux via Homebrew (brew install util-linux).
397
+ # macOS: available from macOS 12+ at /usr/bin/setsid. Older macOS or systems
398
+ # where /usr/bin/setsid is absent need util-linux via Homebrew.
399
+ # util-linux is keg-only on Homebrew — binary is not linked into PATH
400
+ # automatically, so we create a symlink after install.
399
401
  if command -v setsid >/dev/null 2>&1; then
400
402
  local setsid_path
401
403
  setsid_path="$(command -v setsid)"
@@ -403,23 +405,51 @@ setup_setsid_advisory() {
403
405
  return 0
404
406
  fi
405
407
 
406
- # setsid missing — emit an advisory; it's a quality-of-life improvement,
407
- # not a hard requirement (pulse falls back to nohup-only).
408
- print_warning "setsid not found pulse workers will share the pulse process group"
409
- echo " Impact: a pulse restart or launchd unload may kill in-flight workers"
410
- echo " (GH#20561 / t2757: worker survived 3/4 dispatches without setsid isolation)"
411
- echo ""
408
+ # setsid missing — on macOS with Homebrew, auto-install util-linux and
409
+ # symlink setsid into PATH (GH#21102 / t2926). On Linux and macOS without
410
+ # Homebrew, emit an actionable error with install instructions.
412
411
  if [[ "$(uname)" == "Darwin" ]]; then
413
412
  if command -v brew >/dev/null 2>&1; then
414
- echo " Install: brew install util-linux"
413
+ print_info "setsid not found — installing util-linux for worker PGID isolation (GH#21102)"
414
+ if brew install util-linux 2>&1 | tail -3; then
415
+ # util-linux is keg-only: binary lives under the keg, not in /opt/homebrew/bin.
416
+ # Symlink setsid into a standard PATH directory so 'command -v setsid' works.
417
+ local brew_prefix
418
+ brew_prefix="$(brew --prefix 2>/dev/null || echo "")"
419
+ local keg_setsid="${brew_prefix}/opt/util-linux/bin/setsid"
420
+ local link_target="${brew_prefix}/bin/setsid"
421
+ if [[ -x "$keg_setsid" && ! -e "$link_target" ]]; then
422
+ ln -s "$keg_setsid" "$link_target" && \
423
+ print_success "Symlinked setsid: $keg_setsid → $link_target"
424
+ fi
425
+ # Verify setsid is now reachable
426
+ if command -v setsid >/dev/null 2>&1; then
427
+ print_success "setsid installed at $(command -v setsid) (worker PGID isolation enabled)"
428
+ else
429
+ print_error "util-linux installed but setsid still not in PATH — check brew --prefix"
430
+ fi
431
+ else
432
+ print_error "brew install util-linux failed — workers will share pulse PGID until resolved"
433
+ echo " Manual fix: brew install util-linux"
434
+ fi
415
435
  else
416
- echo " Install Homebrew first, then: brew install util-linux"
436
+ print_error "setsid not found worker isolation broken; install util-linux"
437
+ echo " Impact: every pulse restart sends SIGHUP to workers in its PGID,"
438
+ echo " killing in-flight workers before they can finish (GH#21102)"
439
+ echo " Fix: install Homebrew, then run: brew install util-linux"
417
440
  echo " Or upgrade to macOS 12+ where /usr/bin/setsid ships by default"
418
441
  fi
419
442
  else
420
- echo " Install: sudo apt install util-linux # Debian/Ubuntu"
421
- echo " sudo dnf install util-linux # Fedora/RHEL"
422
- echo " sudo pacman -S util-linux # Arch"
443
+ # Linux: setsid should be present on all mainstream distros via util-linux.
444
+ # If it is missing, emit an error rather than a warning — workers will be
445
+ # killed on every pulse cycle restart without it.
446
+ print_error "setsid not found — worker isolation broken; install util-linux"
447
+ echo " Impact: every pulse restart sends SIGHUP to workers in its PGID,"
448
+ echo " killing in-flight workers before they can finish (GH#21102)"
449
+ echo " Fix: sudo apt install util-linux # Debian/Ubuntu"
450
+ echo " sudo dnf install util-linux # Fedora/RHEL"
451
+ echo " sudo pacman -S util-linux # Arch"
452
+ echo " sudo apk add util-linux # Alpine"
423
453
  fi
424
454
  echo ""
425
455
 
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.11.17
15
+ # Version: 3.13.0
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)
@@ -243,7 +243,16 @@ _launchd_install_if_changed() {
243
243
 
244
244
  # Atomic write: build at sibling tmp path, then rename into place.
245
245
  # If printf is killed mid-write, the destination is untouched.
246
- local tmp_plist="${plist_path}.tmp.$$"
246
+ # mktemp avoids predictable tmp names (defense-in-depth against symlink attacks).
247
+ local tmp_plist
248
+ tmp_plist=$(mktemp "${plist_path}.XXXXXX") || return 1
249
+ # Guard: refuse to write empty content — catching this before the write avoids
250
+ # creating a tmp file that the file-size check would also catch, but the
251
+ # content check is more direct and gives a clearer failure point.
252
+ if [[ -z "$new_content" ]]; then
253
+ rm -f "$tmp_plist"
254
+ return 1
255
+ fi
247
256
  if ! printf '%s\n' "$new_content" >"$tmp_plist"; then
248
257
  rm -f "$tmp_plist"
249
258
  return 1
@@ -254,7 +263,10 @@ _launchd_install_if_changed() {
254
263
  rm -f "$tmp_plist"
255
264
  return 1
256
265
  fi
257
- mv -f "$tmp_plist" "$plist_path"
266
+ if ! mv -f "$tmp_plist" "$plist_path"; then
267
+ rm -f "$tmp_plist"
268
+ return 1
269
+ fi
258
270
  launchctl load "$plist_path" 2>/dev/null || return 1
259
271
  return 0
260
272
  }
@@ -959,6 +971,62 @@ _deploy_hotfix_config() {
959
971
  return 0
960
972
  }
961
973
 
974
+ # t2919: Early pulse plist install. The pulse launchd agent is critical
975
+ # infrastructure — without it, every other pulse-driven feature (worker
976
+ # dispatch, issue routing, cross-repo coordination) is dead. Previously,
977
+ # setup_supervisor_pulse only ran inside _setup_post_setup_steps which
978
+ # executes AFTER ~25 other migration/setup steps. When `aidevops update`
979
+ # runs unattended and any earlier step times out (e.g. brew taps, MCP
980
+ # installs, slow repo scans), the pulse plist never gets installed/refreshed
981
+ # and the runner falls behind.
982
+ #
983
+ # Install immediately after deploy_aidevops_agents (so the scripts the plist
984
+ # references already exist on disk). The late install in _setup_post_setup_steps
985
+ # remains as the canonical regenerate-on-change path — _launchd_install_if_changed
986
+ # compares content and skips reload when identical, so the second call is a
987
+ # no-op when nothing changed. Failure here is non-fatal: the late path retries.
988
+ _setup_install_pulse_plist_early() {
989
+ local _early_os
990
+ _early_os="$(uname -s)"
991
+ if _should_setup_noninteractive_supervisor_pulse; then
992
+ setup_supervisor_pulse "$_early_os" || print_warning "Early pulse plist install failed (will retry late)"
993
+ fi
994
+ return 0
995
+ }
996
+
997
+ # Provision knowledge planes for all repos in repos.json where knowledge != "off".
998
+ # Idempotent: already-provisioned directories are not modified.
999
+ # Called from the non-interactive setup path (update) and after interactive init.
1000
+ setup_knowledge_planes() {
1001
+ local repos_file="$HOME/.config/aidevops/repos.json"
1002
+ local helper
1003
+ helper="${BASH_SOURCE[0]%/*}/.agents/scripts/knowledge-helper.sh"
1004
+ if [[ ! -f "$helper" ]]; then
1005
+ helper="$HOME/.aidevops/agents/scripts/knowledge-helper.sh"
1006
+ fi
1007
+ if [[ ! -f "$helper" ]]; then
1008
+ print_warning "knowledge-helper.sh not found — skipping knowledge plane provisioning"
1009
+ return 0
1010
+ fi
1011
+ if [[ ! -f "$repos_file" ]]; then
1012
+ return 0
1013
+ fi
1014
+ if ! command -v jq &>/dev/null; then
1015
+ print_warning "jq not installed — skipping knowledge plane provisioning"
1016
+ return 0
1017
+ fi
1018
+ local repo_path mode
1019
+ while IFS=$'\t' read -r repo_path mode; do
1020
+ [[ -z "$repo_path" || "$mode" == "off" ]] && continue
1021
+ if [[ ! -d "$repo_path" ]]; then
1022
+ print_warning "knowledge-plane: repo path not found: $repo_path"
1023
+ continue
1024
+ fi
1025
+ bash "$helper" provision "$repo_path" || print_warning "knowledge-plane: provision failed for $repo_path"
1026
+ done < <(jq -r '.initialized_repos[] | select(.knowledge != null and .knowledge != "off") | [.path, .knowledge] | @tsv' "$repos_file" 2>/dev/null || true)
1027
+ return 0
1028
+ }
1029
+
962
1030
  # Non-interactive path: deploy agents and run safe migrations only (no prompts).
963
1031
  _setup_run_non_interactive() {
964
1032
  print_info "Non-interactive mode: deploying agents and running safe migrations only"
@@ -966,6 +1034,9 @@ _setup_run_non_interactive() {
966
1034
  check_requirements
967
1035
  # Run quality tool detection in non-interactive mode too (warn-only path).
968
1036
  check_quality_tools
1037
+ # Check setsid availability; auto-install util-linux on macOS if missing
1038
+ # (GH#21102 / t2926: missing setsid kills workers on every pulse restart).
1039
+ setup_setsid_advisory
969
1040
  check_python_upgrade_available
970
1041
  set_permissions
971
1042
  migrate_old_backups
@@ -990,6 +1061,7 @@ _setup_run_non_interactive() {
990
1061
  setup_opencode_cli
991
1062
  validate_opencode_config
992
1063
  deploy_aidevops_agents
1064
+ _setup_install_pulse_plist_early
993
1065
  _deploy_hotfix_config
994
1066
  sync_agent_sources
995
1067
  install_aidevops_cli
@@ -1052,6 +1124,8 @@ _setup_run_non_interactive() {
1052
1124
  # copies doesn't burn CPU (t2885). Idempotent. macOS only — Linux
1053
1125
  # indexers tracked separately.
1054
1126
  setup_worktree_exclusions
1127
+ # Provision knowledge planes for repos where knowledge != "off" (idempotent).
1128
+ setup_knowledge_planes
1055
1129
  return 0
1056
1130
  }
1057
1131
 
@@ -1172,6 +1246,12 @@ _setup_noninteractive_schedulers() {
1172
1246
  if _should_setup_noninteractive_supervisor_pulse; then
1173
1247
  setup_supervisor_pulse "$os"
1174
1248
  fi
1249
+ # t2939: pulse-watchdog (independent revival mechanism). Always installed
1250
+ # alongside the pulse — it is a no-op when pulse is disabled. Skipping the
1251
+ # `_should_setup_noninteractive_*` guard intentionally: this is layered
1252
+ # defense, the cost of installing it is one plist file, and the user opts
1253
+ # in by enabling the pulse itself.
1254
+ setup_pulse_watchdog "${PULSE_ENABLED:-}"
1175
1255
  # Regenerate other schedulers if already installed (GH#17695 Finding B).
1176
1256
  # Stats wrapper is a pulse dependency — also install on first run when
1177
1257
  # the supervisor pulse is consented (t2418, GH#20016).
@@ -1197,6 +1277,15 @@ _setup_noninteractive_schedulers() {
1197
1277
  if _should_setup_noninteractive_scheduler "Complexity scan" "sh.aidevops.complexity-scan" "aidevops: complexity-scan" "aidevops-complexity-scan"; then
1198
1278
  setup_complexity_scan
1199
1279
  fi
1280
+ # t2862 (GH#20919): pulse merge routine — fast 120s standalone merge pass
1281
+ if _should_setup_noninteractive_scheduler "Pulse merge routine" "sh.aidevops.pulse-merge-routine" "aidevops: pulse-merge-routine" "aidevops-pulse-merge-routine"; then
1282
+ setup_pulse_merge_routine
1283
+ fi
1284
+ # t2932 (GH#21125): peer productivity monitor — adaptive cross-runner
1285
+ # dispatch coordination, runs every 30 min.
1286
+ if _should_setup_noninteractive_scheduler "Peer productivity monitor" "sh.aidevops.peer-productivity-monitor" "aidevops: peer-productivity-monitor" "aidevops-peer-productivity-monitor"; then
1287
+ setup_peer_productivity_monitor
1288
+ fi
1200
1289
  # Repo sync handles non-interactive mode internally (systemd detection fixed in GH#17861)
1201
1290
  setup_repo_sync
1202
1291
  # r914 repo-aidevops-health — daily drift keeper (t2366)
@@ -1248,6 +1337,8 @@ _setup_post_setup_steps() {
1248
1337
  # Post-setup: auto-update, schedulers, final instructions (GH#5793)
1249
1338
  setup_auto_update
1250
1339
  setup_supervisor_pulse "$os"
1340
+ # t2939: pulse-watchdog — independent revival mechanism, layered defense.
1341
+ setup_pulse_watchdog "${PULSE_ENABLED:-}"
1251
1342
  setup_stats_wrapper "${PULSE_ENABLED:-}"
1252
1343
  setup_failure_miner "${PULSE_ENABLED:-}"
1253
1344
  setup_repo_sync
@@ -1258,6 +1349,7 @@ _setup_post_setup_steps() {
1258
1349
  setup_screen_time_snapshot
1259
1350
  setup_contribution_watch
1260
1351
  setup_complexity_scan
1352
+ setup_pulse_merge_routine
1261
1353
  setup_draft_responses
1262
1354
  setup_profile_readme
1263
1355
  setup_oauth_token_refresh