aidevops 3.11.16 → 3.12.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.16
1
+ 3.12.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.16
8
+ # Version: 3.12.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,50 @@ _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
+ _dispatch_helper "$_EPH" "$_EPH" "$action" "$@" ;;
1829
+ *)
1830
+ echo "Usage: aidevops email <mailbox|poll> [subcommand]"
1831
+ echo ""
1832
+ echo "Email subcommands:"
1833
+ echo " mailbox add Register a new IMAP mailbox (interactive)"
1834
+ echo " mailbox list Show all mailboxes + polling status"
1835
+ echo " mailbox test <id> Dry-run connection test"
1836
+ echo " mailbox remove <id> Un-register a mailbox"
1837
+ echo " poll tick Poll all mailboxes now (same as routine r044)"
1838
+ echo " poll backfill <id> Backfill a mailbox from a given date"
1839
+ ;;
1840
+ esac
1841
+ return 0
1842
+ }
1843
+
1748
1844
  # Route 'aidevops client-format [subcommand]' to appropriate helpers
1749
1845
  _cmd_client_format() {
1750
1846
  case "${1:-status}" in
@@ -1835,10 +1931,22 @@ main() {
1835
1931
  [[ $# -eq 0 ]] && set -- status
1836
1932
  _dispatch_helper "contribution-watch-helper.sh" "contribution-watch-helper.sh" "$@"
1837
1933
  ;;
1934
+ inbox)
1935
+ # Bare `aidevops inbox` defaults to status (most common use).
1936
+ [[ $# -eq 0 ]] && set -- status
1937
+ _dispatch_helper "inbox-helper.sh" "inbox-helper.sh" "$@"
1938
+ ;;
1939
+ case | cases)
1940
+ # Bare `aidevops case` defaults to list (most common use).
1941
+ [[ $# -eq 0 ]] && set -- list
1942
+ _dispatch_helper "case-helper.sh" "case-helper.sh" "$@"
1943
+ ;;
1944
+ email) _cmd_email "$@" ;;
1838
1945
  stats | observability) _dispatch_helper "observability-helper.sh" "observability-helper.sh" "$@" ;;
1839
1946
  tabby) _dispatch_helper "tabby-helper.sh" "tabby-helper.sh" "$@" ;;
1840
1947
  init-routines) _dispatch_helper "init-routines-helper.sh" "init-routines-helper.sh" "$@" ;;
1841
1948
  parent-status | ps) _dispatch_helper "parent-status-helper.sh" "parent-status-helper.sh" "$@" ;;
1949
+ knowledge) _dispatch_helper "knowledge-helper.sh" "knowledge-helper.sh" "$@" ;;
1842
1950
  config | configure) _dispatch_config "$@" ;;
1843
1951
  uninstall | remove) cmd_uninstall ;;
1844
1952
  version | v | -v | --version) cmd_version ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.11.16",
3
+ "version": "3.12.0",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -703,6 +703,17 @@ _install_pulse_launchd() {
703
703
  _interval_label="${_interval_sec}s"
704
704
  fi
705
705
 
706
+ # One-time legacy cleanup: unload and remove the old-label plist if present.
707
+ # Users on stale installs may have com.aidevops.supervisor-pulse (legacy) and
708
+ # com.aidevops.aidevops-supervisor-pulse (current) both loaded, causing 2x
709
+ # dispatch. Only targets the hardcoded legacy path; idempotent — no-op when
710
+ # the legacy file is absent.
711
+ local _legacy_plist="$HOME/Library/LaunchAgents/com.aidevops.supervisor-pulse.plist"
712
+ if [[ -f "$_legacy_plist" ]]; then
713
+ launchctl unload "$_legacy_plist" 2>/dev/null || true
714
+ rm -f "$_legacy_plist"
715
+ fi
716
+
706
717
  # _launchd_install_if_changed handles unload-before-replace only when content
707
718
  # has changed, and writes atomically via tmp+rename (see setup.sh).
708
719
  # shell-portability: ignore next — _install_pulse_launchd is macOS-only (launchd)
@@ -1684,6 +1695,233 @@ setup_contribution_watch() {
1684
1695
  return 0
1685
1696
  }
1686
1697
 
1698
+ # Install complexity scan via launchd (macOS).
1699
+ # Args: $1=label, $2=script path, $3=log dir
1700
+ # (t2903) Extracted from pulse dispatch preflight — independent schedule so
1701
+ # the 200-470s scan never starves dispatch or downstream scanners.
1702
+ _install_complexity_scan_launchd() {
1703
+ local cs_label="$1"
1704
+ local cs_script="$2"
1705
+ local _cs_log_dir="$3"
1706
+ local cs_plist="$HOME/Library/LaunchAgents/${cs_label}.plist"
1707
+
1708
+ local _xml_cs_script _xml_cs_home _xml_cs_log_dir
1709
+ _xml_cs_script=$(_xml_escape "$cs_script")
1710
+ _xml_cs_home=$(_xml_escape "$HOME")
1711
+ _xml_cs_log_dir=$(_xml_escape "$_cs_log_dir")
1712
+
1713
+ local cs_plist_content
1714
+ cs_plist_content=$(
1715
+ cat <<CS_PLIST
1716
+ <?xml version="1.0" encoding="UTF-8"?>
1717
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1718
+ <plist version="1.0">
1719
+ <dict>
1720
+ <key>Label</key>
1721
+ <string>${cs_label}</string>
1722
+ <key>ProgramArguments</key>
1723
+ <array>
1724
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
1725
+ <string>${_xml_cs_script}</string>
1726
+ <string>run</string>
1727
+ </array>
1728
+ <key>StartInterval</key>
1729
+ <integer>3600</integer>
1730
+ <key>StandardOutPath</key>
1731
+ <string>${_xml_cs_log_dir}/complexity-scan-runner.log</string>
1732
+ <key>StandardErrorPath</key>
1733
+ <string>${_xml_cs_log_dir}/complexity-scan-runner.log</string>
1734
+ <key>EnvironmentVariables</key>
1735
+ <dict>
1736
+ <key>PATH</key>
1737
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
1738
+ <key>HOME</key>
1739
+ <string>${_xml_cs_home}</string>
1740
+ </dict>
1741
+ <key>RunAtLoad</key>
1742
+ <true/>
1743
+ <key>KeepAlive</key>
1744
+ <false/>
1745
+ <key>ProcessType</key>
1746
+ <string>Background</string>
1747
+ <key>LowPriorityBackgroundIO</key>
1748
+ <true/>
1749
+ <key>Nice</key>
1750
+ <integer>10</integer>
1751
+ </dict>
1752
+ </plist>
1753
+ CS_PLIST
1754
+ )
1755
+
1756
+ if _launchd_install_if_changed "$cs_label" "$cs_plist" "$cs_plist_content"; then
1757
+ print_info "Complexity scan enabled (launchd, hourly run)"
1758
+ else
1759
+ print_warning "Failed to load complexity scan LaunchAgent"
1760
+ fi
1761
+ return 0
1762
+ }
1763
+
1764
+ # Install complexity scan via systemd or cron (Linux).
1765
+ # Args: $1=script path, $2=log dir
1766
+ _install_complexity_scan_linux() {
1767
+ local cs_script="$1"
1768
+ local _cs_log_dir="$2"
1769
+ local cs_systemd="aidevops-complexity-scan"
1770
+ _install_scheduler_linux \
1771
+ "$cs_systemd" \
1772
+ "aidevops: complexity-scan" \
1773
+ "$CRON_HOURLY" \
1774
+ "\"${cs_script}\" run" \
1775
+ "3600" \
1776
+ "${_cs_log_dir}/complexity-scan-runner.log" \
1777
+ "" \
1778
+ "Complexity scan enabled (hourly run)" \
1779
+ "Failed to install complexity scan scheduler" \
1780
+ "true" \
1781
+ "true"
1782
+ return 0
1783
+ }
1784
+
1785
+ # Setup complexity scan (t2903) — extracts the weekly complexity scan from
1786
+ # pulse dispatch preflight into its own launchd/cron schedule. The scan was
1787
+ # observed consuming 200-470s per pulse cycle (26%+ of the 1800s pulse stale
1788
+ # ceiling), starving downstream scanners. Promoting it to its own schedule
1789
+ # decouples it from dispatch entirely. The runner reuses run_weekly_complexity_scan
1790
+ # from pulse-simplification.sh, which has internal 15-min cadence gating
1791
+ # (COMPLEXITY_SCAN_INTERVAL=900) so hourly launchd ticks are always safe.
1792
+ setup_complexity_scan() {
1793
+ local cs_script="$HOME/.aidevops/agents/scripts/complexity-scan-runner.sh"
1794
+ local cs_label="sh.aidevops.complexity-scan"
1795
+ if ! [[ -x "$cs_script" ]]; then
1796
+ return 0
1797
+ fi
1798
+
1799
+ # Reuse contribution-watch's log-dir resolver (same logic, same config key).
1800
+ local _cs_log_dir
1801
+ _cs_log_dir=$(_resolve_cw_log_dir) || return 1
1802
+ mkdir -p "$_cs_log_dir"
1803
+
1804
+ # Install/update scheduled runner
1805
+ if [[ "$(uname -s)" == "Darwin" ]]; then
1806
+ _install_complexity_scan_launchd "$cs_label" "$cs_script" "$_cs_log_dir"
1807
+ else
1808
+ _install_complexity_scan_linux "$cs_script" "$_cs_log_dir"
1809
+ fi
1810
+ return 0
1811
+ }
1812
+
1813
+ # Install pulse-merge-routine launchd plist (macOS).
1814
+ # Args: $1=label $2=script $3=log_dir
1815
+ _install_pulse_merge_routine_launchd() {
1816
+ local pmr_label="$1"
1817
+ local pmr_script="$2"
1818
+ local _pmr_log_dir="$3"
1819
+ local pmr_plist="$HOME/Library/LaunchAgents/${pmr_label}.plist"
1820
+
1821
+ local _xml_pmr_script _xml_pmr_home _xml_pmr_log_dir
1822
+ _xml_pmr_script=$(_xml_escape "$pmr_script")
1823
+ _xml_pmr_home=$(_xml_escape "$HOME")
1824
+ _xml_pmr_log_dir=$(_xml_escape "$_pmr_log_dir")
1825
+
1826
+ local pmr_plist_content
1827
+ pmr_plist_content=$(
1828
+ cat <<PMR_PLIST
1829
+ <?xml version="1.0" encoding="UTF-8"?>
1830
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1831
+ <plist version="1.0">
1832
+ <dict>
1833
+ <key>Label</key>
1834
+ <string>${pmr_label}</string>
1835
+ <key>ProgramArguments</key>
1836
+ <array>
1837
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
1838
+ <string>${_xml_pmr_script}</string>
1839
+ <string>run</string>
1840
+ </array>
1841
+ <key>StartInterval</key>
1842
+ <integer>120</integer>
1843
+ <key>StandardOutPath</key>
1844
+ <string>${_xml_pmr_log_dir}/pulse-merge-routine.log</string>
1845
+ <key>StandardErrorPath</key>
1846
+ <string>${_xml_pmr_log_dir}/pulse-merge-routine.log</string>
1847
+ <key>EnvironmentVariables</key>
1848
+ <dict>
1849
+ <key>PATH</key>
1850
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
1851
+ <key>HOME</key>
1852
+ <string>${_xml_pmr_home}</string>
1853
+ </dict>
1854
+ <key>RunAtLoad</key>
1855
+ <true/>
1856
+ <key>KeepAlive</key>
1857
+ <false/>
1858
+ <key>ProcessType</key>
1859
+ <string>Background</string>
1860
+ <key>LowPriorityBackgroundIO</key>
1861
+ <true/>
1862
+ <key>Nice</key>
1863
+ <integer>10</integer>
1864
+ </dict>
1865
+ </plist>
1866
+ PMR_PLIST
1867
+ )
1868
+
1869
+ if _launchd_install_if_changed "$pmr_label" "$pmr_plist" "$pmr_plist_content"; then
1870
+ print_info "Pulse merge routine enabled (launchd, every 2 min)"
1871
+ else
1872
+ print_warning "Failed to load pulse merge routine LaunchAgent"
1873
+ fi
1874
+ return 0
1875
+ }
1876
+
1877
+ # Install pulse-merge-routine via systemd or cron (Linux).
1878
+ # Args: $1=script path, $2=log dir
1879
+ _install_pulse_merge_routine_linux() {
1880
+ local pmr_script="$1"
1881
+ local _pmr_log_dir="$2"
1882
+ local pmr_systemd="aidevops-pulse-merge-routine"
1883
+ _install_scheduler_linux \
1884
+ "$pmr_systemd" \
1885
+ "aidevops: pulse-merge-routine" \
1886
+ "*/2 * * * *" \
1887
+ "\"${pmr_script}\" run" \
1888
+ "120" \
1889
+ "${_pmr_log_dir}/pulse-merge-routine.log" \
1890
+ "" \
1891
+ "Pulse merge routine enabled (every 2 min)" \
1892
+ "Failed to install pulse merge routine scheduler" \
1893
+ "true" \
1894
+ "true"
1895
+ return 0
1896
+ }
1897
+
1898
+ # Setup pulse merge routine (t2862, GH#20919) — runs merge_ready_prs_all_repos()
1899
+ # as a fast 120s standalone routine, decoupled from the monolithic pulse cycle.
1900
+ # The pulse cycle's preflight stack (60-470s) meant the merge pass ran only ~7
1901
+ # times/24h despite ~40+ cycles. This routine ensures green PRs merge within ~3
1902
+ # min of CI completion. The in-cycle merge call in pulse-wrapper.sh is kept as
1903
+ # defense-in-depth but short-circuits when this routine ran within the last 60s.
1904
+ setup_pulse_merge_routine() {
1905
+ local pmr_script="$HOME/.aidevops/agents/scripts/pulse-merge-routine.sh"
1906
+ local pmr_label="sh.aidevops.pulse-merge-routine"
1907
+ if ! [[ -x "$pmr_script" ]]; then
1908
+ return 0
1909
+ fi
1910
+
1911
+ # Reuse contribution-watch's log-dir resolver (same logic, same config key).
1912
+ local _pmr_log_dir
1913
+ _pmr_log_dir=$(_resolve_cw_log_dir) || return 1
1914
+ mkdir -p "$_pmr_log_dir"
1915
+
1916
+ # Install/update scheduled runner
1917
+ if [[ "$(uname -s)" == "Darwin" ]]; then
1918
+ _install_pulse_merge_routine_launchd "$pmr_label" "$pmr_script" "$_pmr_log_dir"
1919
+ else
1920
+ _install_pulse_merge_routine_linux "$pmr_script" "$_pmr_log_dir"
1921
+ fi
1922
+ return 0
1923
+ }
1924
+
1687
1925
  # Setup draft responses — private repo + local draft storage for reviewing
1688
1926
  # AI-drafted replies to external contributions (t1555).
1689
1927
  # Respects config: aidevops config set orchestration.draft_responses false
@@ -2161,3 +2399,126 @@ setup_repo_aidevops_health() {
2161
2399
  fi
2162
2400
  return 0
2163
2401
  }
2402
+
2403
+ # ============================================================================
2404
+ # Peer productivity monitor (t2932)
2405
+ # ============================================================================
2406
+ #
2407
+ # Adaptive cross-runner dispatch coordination: observes peer GitHub activity
2408
+ # every 30 min and updates ~/.config/aidevops/dispatch-override.conf to
2409
+ # `ignore` peers whose pulse is broken (claims issues but never PRs) and
2410
+ # back to `honour` when they recover. Self-healing across the ecosystem —
2411
+ # each runner observes peers independently, no central coordinator needed.
2412
+ # Manual entries in dispatch-override.conf above the auto-managed marker
2413
+ # always take precedence.
2414
+
2415
+ # Install peer-productivity-monitor launchd plist (macOS).
2416
+ # Args: $1=label $2=script $3=log_dir
2417
+ _install_peer_productivity_monitor_launchd() {
2418
+ local ppm_label="$1"
2419
+ local ppm_script="$2"
2420
+ local _ppm_log_dir="$3"
2421
+ local ppm_plist="$HOME/Library/LaunchAgents/${ppm_label}.plist"
2422
+
2423
+ local _xml_ppm_script _xml_ppm_home _xml_ppm_log_dir
2424
+ _xml_ppm_script=$(_xml_escape "$ppm_script")
2425
+ _xml_ppm_home=$(_xml_escape "$HOME")
2426
+ _xml_ppm_log_dir=$(_xml_escape "$_ppm_log_dir")
2427
+
2428
+ local ppm_plist_content
2429
+ ppm_plist_content=$(
2430
+ cat <<PPM_PLIST
2431
+ <?xml version="1.0" encoding="UTF-8"?>
2432
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
2433
+ <plist version="1.0">
2434
+ <dict>
2435
+ <key>Label</key>
2436
+ <string>${ppm_label}</string>
2437
+ <key>ProgramArguments</key>
2438
+ <array>
2439
+ <string>$(_xml_escape "$(_resolve_modern_bash)")</string>
2440
+ <string>${_xml_ppm_script}</string>
2441
+ <string>observe</string>
2442
+ </array>
2443
+ <key>StartInterval</key>
2444
+ <integer>1800</integer>
2445
+ <key>StandardOutPath</key>
2446
+ <string>${_xml_ppm_log_dir}/peer-productivity-launchd.log</string>
2447
+ <key>StandardErrorPath</key>
2448
+ <string>${_xml_ppm_log_dir}/peer-productivity-launchd.log</string>
2449
+ <key>EnvironmentVariables</key>
2450
+ <dict>
2451
+ <key>PATH</key>
2452
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
2453
+ <key>HOME</key>
2454
+ <string>${_xml_ppm_home}</string>
2455
+ </dict>
2456
+ <key>RunAtLoad</key>
2457
+ <true/>
2458
+ <key>KeepAlive</key>
2459
+ <false/>
2460
+ <key>ProcessType</key>
2461
+ <string>Background</string>
2462
+ <key>LowPriorityBackgroundIO</key>
2463
+ <true/>
2464
+ <key>Nice</key>
2465
+ <integer>10</integer>
2466
+ </dict>
2467
+ </plist>
2468
+ PPM_PLIST
2469
+ )
2470
+
2471
+ if _launchd_install_if_changed "$ppm_label" "$ppm_plist" "$ppm_plist_content"; then
2472
+ print_info "Peer productivity monitor enabled (launchd, every 30 min)"
2473
+ else
2474
+ print_warning "Failed to load peer-productivity-monitor LaunchAgent"
2475
+ fi
2476
+ return 0
2477
+ }
2478
+
2479
+ # Install peer-productivity-monitor via systemd or cron (Linux).
2480
+ # Args: $1=script path, $2=log dir
2481
+ _install_peer_productivity_monitor_linux() {
2482
+ local ppm_script="$1"
2483
+ local _ppm_log_dir="$2"
2484
+ local ppm_systemd="aidevops-peer-productivity-monitor"
2485
+ _install_scheduler_linux \
2486
+ "$ppm_systemd" \
2487
+ "aidevops: peer-productivity-monitor" \
2488
+ "*/30 * * * *" \
2489
+ "\"${ppm_script}\" observe" \
2490
+ "1800" \
2491
+ "${_ppm_log_dir}/peer-productivity-launchd.log" \
2492
+ "" \
2493
+ "Peer productivity monitor enabled (every 30 min)" \
2494
+ "Failed to install peer-productivity-monitor scheduler" \
2495
+ "true" \
2496
+ "true"
2497
+ return 0
2498
+ }
2499
+
2500
+ # Setup peer-productivity-monitor (t2932) — observes peer GitHub activity
2501
+ # every 30 min and updates ~/.config/aidevops/dispatch-override.conf so the
2502
+ # local pulse competes with broken peers and collaborates with healthy ones.
2503
+ # Manual entries in dispatch-override.conf above the auto-managed marker
2504
+ # always take precedence.
2505
+ setup_peer_productivity_monitor() {
2506
+ local ppm_script="$HOME/.aidevops/agents/scripts/peer-productivity-monitor.sh"
2507
+ local ppm_label="sh.aidevops.peer-productivity-monitor"
2508
+ if ! [[ -x "$ppm_script" ]]; then
2509
+ return 0
2510
+ fi
2511
+
2512
+ # Reuse contribution-watch's log-dir resolver (same logic, same config key).
2513
+ local _ppm_log_dir
2514
+ _ppm_log_dir=$(_resolve_cw_log_dir) || return 1
2515
+ mkdir -p "$_ppm_log_dir"
2516
+
2517
+ # Install/update scheduled runner
2518
+ if [[ "$(uname -s)" == "Darwin" ]]; then
2519
+ _install_peer_productivity_monitor_launchd "$ppm_label" "$ppm_script" "$_ppm_log_dir"
2520
+ else
2521
+ _install_peer_productivity_monitor_linux "$ppm_script" "$_ppm_log_dir"
2522
+ fi
2523
+ return 0
2524
+ }
@@ -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.16
15
+ # Version: 3.12.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
 
@@ -1159,6 +1233,72 @@ _setup_run_interactive() {
1159
1233
  }
1160
1234
 
1161
1235
  # Post-setup steps: schedulers, final instructions, optional tool update check.
1236
+ # Non-interactive scheduler installation. Extracted from
1237
+ # `_setup_post_setup_steps` (t2903) to keep the parent under the
1238
+ # function-complexity gate threshold. Each `_should_setup_noninteractive_*`
1239
+ # guard returns 0 when the corresponding scheduler is already installed
1240
+ # (regenerate on update) or first-time install is consented via config.
1241
+ _setup_noninteractive_schedulers() {
1242
+ local os="$1"
1243
+
1244
+ # Auto-update handles non-interactive internally (systemd detection fixed in GH#17861)
1245
+ setup_auto_update
1246
+ if _should_setup_noninteractive_supervisor_pulse; then
1247
+ setup_supervisor_pulse "$os"
1248
+ fi
1249
+ # Regenerate other schedulers if already installed (GH#17695 Finding B).
1250
+ # Stats wrapper is a pulse dependency — also install on first run when
1251
+ # the supervisor pulse is consented (t2418, GH#20016).
1252
+ if _should_setup_noninteractive_stats_wrapper; then
1253
+ setup_stats_wrapper "${PULSE_ENABLED:-}"
1254
+ fi
1255
+ if _should_setup_noninteractive_scheduler "Failure miner" "sh.aidevops.routine-gh-failure-miner" "aidevops: gh-failure-miner" "aidevops-gh-failure-miner"; then
1256
+ setup_failure_miner "${PULSE_ENABLED:-}"
1257
+ fi
1258
+ if _should_setup_noninteractive_scheduler "Process guard" "sh.aidevops.process-guard" "aidevops: process-guard" "aidevops-process-guard"; then
1259
+ setup_process_guard
1260
+ fi
1261
+ if _should_setup_noninteractive_scheduler "Memory pressure" "sh.aidevops.memory-pressure-monitor" "aidevops: memory-pressure-monitor" "aidevops-memory-pressure-monitor"; then
1262
+ setup_memory_pressure_monitor
1263
+ fi
1264
+ if _should_setup_noninteractive_scheduler "Screen time" "sh.aidevops.screen-time-snapshot" "aidevops: screen-time-snapshot" "aidevops-screen-time-snapshot"; then
1265
+ setup_screen_time_snapshot
1266
+ fi
1267
+ if _should_setup_noninteractive_scheduler "Contribution watch" "sh.aidevops.contribution-watch" "aidevops: contribution-watch" "aidevops-contribution-watch"; then
1268
+ setup_contribution_watch
1269
+ fi
1270
+ # t2903 (#21049): complexity scan — extracted from pulse dispatch preflight
1271
+ if _should_setup_noninteractive_scheduler "Complexity scan" "sh.aidevops.complexity-scan" "aidevops: complexity-scan" "aidevops-complexity-scan"; then
1272
+ setup_complexity_scan
1273
+ fi
1274
+ # t2862 (GH#20919): pulse merge routine — fast 120s standalone merge pass
1275
+ if _should_setup_noninteractive_scheduler "Pulse merge routine" "sh.aidevops.pulse-merge-routine" "aidevops: pulse-merge-routine" "aidevops-pulse-merge-routine"; then
1276
+ setup_pulse_merge_routine
1277
+ fi
1278
+ # t2932 (GH#21125): peer productivity monitor — adaptive cross-runner
1279
+ # dispatch coordination, runs every 30 min.
1280
+ if _should_setup_noninteractive_scheduler "Peer productivity monitor" "sh.aidevops.peer-productivity-monitor" "aidevops: peer-productivity-monitor" "aidevops-peer-productivity-monitor"; then
1281
+ setup_peer_productivity_monitor
1282
+ fi
1283
+ # Repo sync handles non-interactive mode internally (systemd detection fixed in GH#17861)
1284
+ setup_repo_sync
1285
+ # r914 repo-aidevops-health — daily drift keeper (t2366)
1286
+ setup_repo_aidevops_health
1287
+ if _should_setup_noninteractive_scheduler "Profile README" "sh.aidevops.profile-readme-update" "aidevops: profile-readme-update" "aidevops-profile-readme-update"; then
1288
+ setup_profile_readme
1289
+ fi
1290
+ if _should_setup_noninteractive_scheduler "OAuth token refresh" "sh.aidevops.token-refresh" "aidevops: token-refresh" "aidevops-token-refresh"; then
1291
+ setup_oauth_token_refresh
1292
+ fi
1293
+ # opencode DB maintenance (r913, t2183). Helper self-noops on missing
1294
+ # DB — safe to install unconditionally in non-interactive mode too.
1295
+ setup_opencode_db_maintenance
1296
+ # Migrate cron entries to systemd after schedulers are installed (GH#17695 Finding D)
1297
+ migrate_cron_to_systemd
1298
+ setup_tabby
1299
+ return 0
1300
+ }
1301
+
1162
1302
  _setup_post_setup_steps() {
1163
1303
  local os="$1"
1164
1304
 
@@ -1184,48 +1324,7 @@ _setup_post_setup_steps() {
1184
1324
  # Exceptions: regenerate existing schedulers (GH#17381, GH#17695 Finding B)
1185
1325
  # and allow first-time install when config consent is explicitly true (GH#17403).
1186
1326
  if [[ "$NON_INTERACTIVE" == "true" ]]; then
1187
- # Auto-update handles non-interactive internally (systemd detection fixed in GH#17861)
1188
- setup_auto_update
1189
- if _should_setup_noninteractive_supervisor_pulse; then
1190
- setup_supervisor_pulse "$os"
1191
- fi
1192
- # Regenerate other schedulers if already installed (GH#17695 Finding B).
1193
- # Stats wrapper is a pulse dependency — also install on first run when
1194
- # the supervisor pulse is consented (t2418, GH#20016).
1195
- if _should_setup_noninteractive_stats_wrapper; then
1196
- setup_stats_wrapper "${PULSE_ENABLED:-}"
1197
- fi
1198
- if _should_setup_noninteractive_scheduler "Failure miner" "sh.aidevops.routine-gh-failure-miner" "aidevops: gh-failure-miner" "aidevops-gh-failure-miner"; then
1199
- setup_failure_miner "${PULSE_ENABLED:-}"
1200
- fi
1201
- if _should_setup_noninteractive_scheduler "Process guard" "sh.aidevops.process-guard" "aidevops: process-guard" "aidevops-process-guard"; then
1202
- setup_process_guard
1203
- fi
1204
- if _should_setup_noninteractive_scheduler "Memory pressure" "sh.aidevops.memory-pressure-monitor" "aidevops: memory-pressure-monitor" "aidevops-memory-pressure-monitor"; then
1205
- setup_memory_pressure_monitor
1206
- fi
1207
- if _should_setup_noninteractive_scheduler "Screen time" "sh.aidevops.screen-time-snapshot" "aidevops: screen-time-snapshot" "aidevops-screen-time-snapshot"; then
1208
- setup_screen_time_snapshot
1209
- fi
1210
- if _should_setup_noninteractive_scheduler "Contribution watch" "sh.aidevops.contribution-watch" "aidevops: contribution-watch" "aidevops-contribution-watch"; then
1211
- setup_contribution_watch
1212
- fi
1213
- # Repo sync handles non-interactive mode internally (systemd detection fixed in GH#17861)
1214
- setup_repo_sync
1215
- # r914 repo-aidevops-health — daily drift keeper (t2366)
1216
- setup_repo_aidevops_health
1217
- if _should_setup_noninteractive_scheduler "Profile README" "sh.aidevops.profile-readme-update" "aidevops: profile-readme-update" "aidevops-profile-readme-update"; then
1218
- setup_profile_readme
1219
- fi
1220
- if _should_setup_noninteractive_scheduler "OAuth token refresh" "sh.aidevops.token-refresh" "aidevops: token-refresh" "aidevops-token-refresh"; then
1221
- setup_oauth_token_refresh
1222
- fi
1223
- # opencode DB maintenance (r913, t2183). Helper self-noops on missing
1224
- # DB — safe to install unconditionally in non-interactive mode too.
1225
- setup_opencode_db_maintenance
1226
- # Migrate cron entries to systemd after schedulers are installed (GH#17695 Finding D)
1227
- migrate_cron_to_systemd
1228
- setup_tabby
1327
+ _setup_noninteractive_schedulers "$os"
1229
1328
  return 0
1230
1329
  fi
1231
1330
 
@@ -1241,6 +1340,8 @@ _setup_post_setup_steps() {
1241
1340
  setup_memory_pressure_monitor
1242
1341
  setup_screen_time_snapshot
1243
1342
  setup_contribution_watch
1343
+ setup_complexity_scan
1344
+ setup_pulse_merge_routine
1244
1345
  setup_draft_responses
1245
1346
  setup_profile_readme
1246
1347
  setup_oauth_token_refresh