aidevops 3.13.76 → 3.13.78

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.
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
4
+ # =============================================================================
5
+ # aidevops Status Library — status command helper functions
6
+ # =============================================================================
7
+ # Helper functions for `aidevops status`, extracted from aidevops.sh to keep
8
+ # the CLI orchestrator below the large-file gate while preserving behaviour.
9
+ #
10
+ # Usage: source "${INSTALL_DIR}/aidevops-status-lib.sh"
11
+ # Part of aidevops framework: https://aidevops.sh
12
+
13
+ # Apply strict mode only when executed directly (not when sourced)
14
+ [[ "${BASH_SOURCE[0]}" == "${0}" ]] && set -euo pipefail
15
+
16
+ # Include guard
17
+ [[ -n "${_AIDEVOPS_STATUS_LIB_LOADED:-}" ]] && return 0
18
+ _AIDEVOPS_STATUS_LIB_LOADED=1
19
+
20
+ if [[ -z "${SCRIPT_DIR:-}" ]]; then
21
+ _lib_path="${BASH_SOURCE[0]%/*}"
22
+ [[ "$_lib_path" == "${BASH_SOURCE[0]}" ]] && _lib_path="."
23
+ SCRIPT_DIR="$(cd "$_lib_path" && pwd)"
24
+ unset _lib_path
25
+ fi
26
+
27
+ _status_recommended_tools() {
28
+ print_header "Recommended Tools"
29
+ if [[ "$(uname)" == "Darwin" ]]; then
30
+ check_dir "/Applications/Tabby.app" && print_success "Tabby terminal" || print_warning "Tabby terminal - not installed"
31
+ if check_dir "/Applications/Zed.app"; then
32
+ print_success "Zed editor"
33
+ check_dir "$HOME/Library/Application Support/Zed/extensions/installed/opencode" && print_success " └─ OpenCode extension" || print_warning " └─ OpenCode extension - not installed"
34
+ else print_warning "Zed editor - not installed"; fi
35
+ else
36
+ check_cmd tabby && print_success "Tabby terminal" || print_warning "Tabby terminal - not installed"
37
+ if check_cmd zed; then
38
+ print_success "Zed editor"
39
+ check_dir "$HOME/.local/share/zed/extensions/installed/opencode" && print_success " └─ OpenCode extension" || print_warning " └─ OpenCode extension - not installed"
40
+ else print_warning "Zed editor - not installed"; fi
41
+ fi
42
+ echo ""
43
+ return 0
44
+ }
45
+
46
+ _status_ai_tools() {
47
+ print_header "AI Tools & MCPs"
48
+ check_cmd opencode && print_success "OpenCode CLI" || print_warning "OpenCode CLI - not installed"
49
+ if check_cmd auggie; then
50
+ check_file "$HOME/.augment/session.json" && print_success "Augment Context Engine (authenticated)" || print_warning "Augment Context Engine (not authenticated)"
51
+ else print_warning "Augment Context Engine - not installed"; fi
52
+ check_cmd bd && print_success "Beads CLI (task graph)" || print_warning "Beads CLI (bd) - not installed"
53
+ echo ""
54
+ return 0
55
+ }
56
+
57
+ _status_dev_envs() {
58
+ print_header "Development Environments"
59
+ check_dir "$INSTALL_DIR/python-env/dspy-env" && print_success "DSPy Python environment" || print_warning "DSPy Python environment - not created"
60
+ check_cmd dspyground && print_success "DSPyGround" || print_warning "DSPyGround - not installed"
61
+ echo ""
62
+ return 0
63
+ }
64
+
65
+ _status_ai_configs() {
66
+ print_header "AI Assistant Configurations"
67
+ local ai_configs=("$HOME/.config/opencode/opencode.json:OpenCode" "$HOME/.claude/commands:Claude Code CLI" "$HOME/CLAUDE.md:Claude Code memory")
68
+ for config in "${ai_configs[@]}"; do
69
+ local path="${config%%:*}" name="${config##*:}"
70
+ [[ -e "$path" ]] && print_success "$name" || print_warning "$name - not configured"
71
+ done
72
+ echo ""
73
+ return 0
74
+ }
75
+
76
+ # Status command
77
+ cmd_status() {
78
+ print_header "AI DevOps Framework Status"
79
+ echo "=========================="
80
+ echo ""
81
+ local current_version
82
+ current_version=$(get_version)
83
+ local remote_version
84
+ remote_version=$(get_remote_version)
85
+ print_header "Version"
86
+ echo " Installed: $current_version"
87
+ echo " Latest: $remote_version"
88
+ if [[ "$current_version" != "$remote_version" && "$remote_version" != "unknown" ]]; then
89
+ print_warning "Update available! Run: aidevops update"
90
+ elif [[ "$current_version" == "$remote_version" ]]; then print_success "Up to date"; fi
91
+ echo ""
92
+ print_header "Installation"
93
+ check_dir "$INSTALL_DIR" && print_success "Repository: $INSTALL_DIR" || print_error "Repository: Not found at $INSTALL_DIR"
94
+ if check_dir "$AGENTS_DIR"; then
95
+ local agent_count
96
+ agent_count=$(find "$AGENTS_DIR" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
97
+ print_success "Agents: $AGENTS_DIR ($agent_count files)"
98
+ else print_error "Agents: Not deployed"; fi
99
+ echo ""
100
+ print_header "Required Dependencies"
101
+ for cmd in git curl jq ssh; do check_cmd "$cmd" && print_success "$cmd" || print_error "$cmd - not installed"; done
102
+ echo ""
103
+ print_header "Optional Dependencies"
104
+ check_cmd sshpass && print_success "sshpass" || print_warning "sshpass - not installed (needed for password SSH)"
105
+ echo ""
106
+ _status_recommended_tools
107
+ print_header "Git CLI Tools"
108
+ check_cmd gh && print_success "GitHub CLI (gh)" || print_warning "GitHub CLI (gh) - not installed"
109
+ check_cmd glab && print_success "GitLab CLI (glab)" || print_warning "GitLab CLI (glab) - not installed"
110
+ check_cmd tea && print_success "Gitea CLI (tea)" || print_warning "Gitea CLI (tea) - not installed"
111
+ echo ""
112
+ _status_ai_tools
113
+ _status_dev_envs
114
+ _status_ai_configs
115
+ print_header "SSH Configuration"
116
+ check_file "$HOME/.ssh/id_ed25519" && print_success "Ed25519 SSH key" || print_warning "Ed25519 SSH key - not found"
117
+ echo ""
118
+ print_header "Commit Signing"
119
+ local signing_format signing_key signing_enabled
120
+ signing_format=$(git config --global gpg.format 2>/dev/null || echo "")
121
+ signing_key=$(git config --global user.signingkey 2>/dev/null || echo "")
122
+ signing_enabled=$(git config --global commit.gpgsign 2>/dev/null || echo "")
123
+ if [[ "$signing_format" == "ssh" && -n "$signing_key" && "$signing_enabled" == "true" ]]; then
124
+ print_success "SSH commit signing enabled"
125
+ if check_file "$HOME/.ssh/allowed_signers"; then
126
+ print_success "Allowed signers file configured"
127
+ else
128
+ print_warning "No allowed_signers file — run: aidevops signing setup"
129
+ fi
130
+ else
131
+ print_warning "Commit signing not configured — run: aidevops signing setup"
132
+ fi
133
+ echo ""
134
+ # t2424/GH#20030: Pulse operational counters (pre-dispatch aborts, etc.)
135
+ local stats_helper="$AGENTS_DIR/scripts/pulse-stats-helper.sh"
136
+ if [[ -x "$stats_helper" ]]; then
137
+ print_header "Pulse Stats"
138
+ "$stats_helper" status 2>/dev/null || print_info " (no stats recorded yet)"
139
+ echo ""
140
+ fi
141
+ }
@@ -0,0 +1,483 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
4
+ # =============================================================================
5
+ # aidevops Update Library — update command helper functions
6
+ # =============================================================================
7
+ # Helper functions for `aidevops update`, extracted from aidevops.sh to keep
8
+ # the CLI orchestrator below the large-file gate while preserving behaviour.
9
+ #
10
+ # Usage: source "${INSTALL_DIR}/aidevops-update-lib.sh"
11
+ # Part of aidevops framework: https://aidevops.sh
12
+
13
+ # Apply strict mode only when executed directly (not when sourced)
14
+ [[ "${BASH_SOURCE[0]}" == "${0}" ]] && set -euo pipefail
15
+
16
+ # Include guard
17
+ [[ -n "${_AIDEVOPS_UPDATE_LIB_LOADED:-}" ]] && return 0
18
+ _AIDEVOPS_UPDATE_LIB_LOADED=1
19
+
20
+ if [[ -z "${SCRIPT_DIR:-}" ]]; then
21
+ _lib_path="${BASH_SOURCE[0]%/*}"
22
+ [[ "$_lib_path" == "${BASH_SOURCE[0]}" ]] && _lib_path="."
23
+ SCRIPT_DIR="$(cd "$_lib_path" && pwd)"
24
+ unset _lib_path
25
+ fi
26
+
27
+ _AIDEVOPS_UPDATE_TRUE=true
28
+
29
+ _update_fresh_install() {
30
+ print_warning "Repository not found, performing fresh install..."
31
+ local tmp_setup
32
+ # t2997: drop .sh — XXXXXX must be at end for BSD mktemp.
33
+ tmp_setup=$(mktemp "${TMPDIR:-/tmp}/aidevops-setup-XXXXXX") || {
34
+ print_error "Failed to create temp file for setup script"
35
+ return 1
36
+ }
37
+ trap 'rm -f "${tmp_setup:-}"' RETURN
38
+ if curl -fsSL "https://raw.githubusercontent.com/marcusquinn/aidevops/main/setup.sh" -o "$tmp_setup" 2>/dev/null && [[ -s "$tmp_setup" ]]; then
39
+ chmod +x "$tmp_setup"
40
+ bash "$tmp_setup"
41
+ local setup_exit=$?
42
+ rm -f "$tmp_setup"
43
+ [[ $setup_exit -ne 0 ]] && return 1
44
+ else
45
+ rm -f "$tmp_setup"
46
+ print_error "Failed to download setup script"
47
+ print_info "Try: git clone https://github.com/marcusquinn/aidevops.git $INSTALL_DIR && bash $INSTALL_DIR/setup.sh"
48
+ return 1
49
+ fi
50
+ return 0
51
+ }
52
+
53
+ _update_sync_projects() {
54
+ local skip="$1" current_ver="$2"
55
+ echo ""
56
+ print_header "Syncing Initialized Projects"
57
+ if [[ "$skip" == "$_AIDEVOPS_UPDATE_TRUE" ]]; then
58
+ print_info "Project sync skipped (--skip-project-sync)"
59
+ return 0
60
+ fi
61
+ local repos_needing_upgrade=()
62
+ while IFS= read -r repo_path; do
63
+ [[ -z "$repo_path" ]] && continue
64
+ [[ -d "$repo_path" ]] && check_repo_needs_upgrade "$repo_path" && repos_needing_upgrade+=("$repo_path")
65
+ done < <(get_registered_repos)
66
+ if [[ ${#repos_needing_upgrade[@]} -eq 0 ]]; then
67
+ print_success "All registered projects are up to date"
68
+ return 0
69
+ fi
70
+ local synced=0 skipped=0 failed=0
71
+ for repo in "${repos_needing_upgrade[@]}"; do
72
+ [[ ! -f "$repo/.aidevops.json" ]] && {
73
+ skipped=$((skipped + 1))
74
+ continue
75
+ }
76
+ local did_sync=false
77
+ if command -v jq &>/dev/null; then
78
+ local temp_file="${repo}/.aidevops.json.tmp"
79
+ if jq --arg version "$current_ver" '.version = $version' "$repo/.aidevops.json" >"$temp_file" 2>/dev/null && [[ -s "$temp_file" ]]; then
80
+ mv "$temp_file" "$repo/.aidevops.json"
81
+ local features
82
+ features=$(jq -r '[.features | to_entries[] | select(.value == true) | .key] | join(",")' "$repo/.aidevops.json" 2>/dev/null || echo "")
83
+ register_repo "$repo" "$current_ver" "$features"
84
+ did_sync=true
85
+ else rm -f "$temp_file"; fi
86
+ fi
87
+ if [[ "$did_sync" != "$_AIDEVOPS_UPDATE_TRUE" ]]; then
88
+ sed -i '' "s/\"version\": *\"[^\"]*\"/\"version\": \"$current_ver\"/" "$repo/.aidevops.json" 2>/dev/null && did_sync=true
89
+ fi
90
+ [[ "$did_sync" == "$_AIDEVOPS_UPDATE_TRUE" ]] && synced=$((synced + 1)) || failed=$((failed + 1))
91
+ done
92
+ [[ $synced -gt 0 ]] && print_success "Synced $synced project(s) to v$current_ver"
93
+ [[ $skipped -gt 0 ]] && print_info "Skipped $skipped uninitialized project(s) (run 'aidevops init' in each to enable)"
94
+ [[ $failed -gt 0 ]] && print_warning "$failed project(s) failed to sync (jq missing or write error)"
95
+ return 0
96
+ }
97
+
98
+ _update_check_planning() {
99
+ echo ""
100
+ print_header "Checking Planning Templates"
101
+ local repos_needing_planning=()
102
+ while IFS= read -r repo_path; do
103
+ [[ -z "$repo_path" || ! -d "$repo_path" ]] && continue
104
+ if [[ -f "$repo_path/.aidevops.json" ]]; then
105
+ local has_planning
106
+ has_planning=$(grep -o '"planning": *true' "$repo_path/.aidevops.json" 2>/dev/null || true)
107
+ [[ -n "$has_planning" ]] && check_planning_needs_upgrade "$repo_path" && repos_needing_planning+=("$repo_path")
108
+ fi
109
+ done < <(get_registered_repos)
110
+ if [[ ${#repos_needing_planning[@]} -eq 0 ]]; then
111
+ print_success "All planning templates are up to date"
112
+ return 0
113
+ fi
114
+ echo ""
115
+ print_warning "${#repos_needing_planning[@]} project(s) have outdated planning templates:"
116
+ for repo in "${repos_needing_planning[@]}"; do
117
+ local repo_name
118
+ repo_name=$(basename "$repo")
119
+ local todo_ver
120
+ todo_ver=$(grep -A1 "TOON:meta" "$repo/TODO.md" 2>/dev/null | tail -1 | cut -d',' -f1)
121
+ echo " - $repo_name (v${todo_ver:-none})"
122
+ done
123
+ local template_ver
124
+ template_ver=$(grep -A1 "TOON:meta" "$AGENTS_DIR/templates/todo-template.md" 2>/dev/null | tail -1 | cut -d',' -f1)
125
+ echo ""
126
+ echo " Latest template: v${template_ver} (adds risk field, active session time estimates)"
127
+ echo ""
128
+ read -r -p "Upgrade planning templates in these projects? [y/N] " response
129
+ if [[ "$response" =~ ^[Yy]$ ]]; then
130
+ for repo in "${repos_needing_planning[@]}"; do
131
+ print_info "Upgrading $(basename "$repo")..."
132
+ (cd "$repo" && cmd_upgrade_planning --force) || print_warning "Failed to upgrade $(basename "$repo")"
133
+ done
134
+ else print_info "Run 'aidevops upgrade-planning' in each project to upgrade manually"; fi
135
+ return 0
136
+ }
137
+
138
+ _update_check_tools() {
139
+ echo ""
140
+ print_header "Checking Key Tools"
141
+ local tool_check_script="$AGENTS_DIR/scripts/tool-version-check.sh"
142
+ if [[ ! -f "$tool_check_script" ]]; then
143
+ print_info "Tool version check not available (run setup first)"
144
+ return 0
145
+ fi
146
+ local stale_count=0 stale_tools=""
147
+ local key_tool_cmds="opencode gh"
148
+ local key_tool_pkgs="opencode-ai brew:gh"
149
+ local idx=0
150
+ for cmd_name in $key_tool_cmds; do
151
+ local pkg_ref
152
+ pkg_ref=$(echo "$key_tool_pkgs" | cut -d' ' -f$((idx + 1)))
153
+ idx=$((idx + 1))
154
+ local installed="" latest=""
155
+ command -v "$cmd_name" &>/dev/null || continue
156
+ installed=$("$cmd_name" --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
157
+ [[ -z "$installed" ]] && continue
158
+ if [[ "$pkg_ref" == brew:* ]]; then
159
+ local brew_pkg="${pkg_ref#brew:}"
160
+ local brew_bin=""
161
+ brew_bin=$(command -v brew 2>/dev/null || true)
162
+ if [[ -n "$brew_bin" && -x "$brew_bin" ]]; then
163
+ latest=$(_timeout_cmd 30 "$brew_bin" info --json=v2 "$brew_pkg" | jq -r '.formulae[0].versions.stable // empty' || true)
164
+ elif [[ "$brew_pkg" == "gh" ]] && command -v gh &>/dev/null; then latest=$(get_public_release_tag "cli/cli"); fi
165
+ else latest=$(_timeout_cmd 30 npm view "$pkg_ref" version || true); fi
166
+ [[ -z "$latest" ]] && continue
167
+ [[ "$installed" != "$latest" ]] && {
168
+ stale_tools="${stale_tools:+$stale_tools, }$cmd_name ($installed -> $latest)"
169
+ ((++stale_count))
170
+ }
171
+ done
172
+ if [[ "$stale_count" -eq 0 ]]; then
173
+ print_success "Key tools are up to date"
174
+ else
175
+ print_warning "$stale_count tool(s) have updates: $stale_tools"
176
+ echo ""
177
+ read -r -p "Run full tool update check? [y/N] " response
178
+ [[ "$response" =~ ^[Yy]$ ]] && bash "$tool_check_script" --update || print_info "Run 'aidevops update-tools --update' to update later"
179
+ fi
180
+ return 0
181
+ }
182
+
183
+ # Check for stale Homebrew-installed copy after git update (GH#11470)
184
+ # Self-heal broken OpenCode runtime symlinks (t2172). A single dangling
185
+ # symlink in ~/.config/opencode/{command,agent,skills,tool}/ blocks new
186
+ # OpenCode sessions with "Failed to parse command ...". Running on every
187
+ # update is cheap (find+rm on 4 small dirs) and catches orphans left
188
+ # behind when users delete private agent source clones without going
189
+ # through `agent-sources-helper.sh remove`. Fail-open — must never
190
+ # break the update cron.
191
+ _update_sweep_opencode_symlinks() {
192
+ local sym_helper="${HOME}/.aidevops/agents/scripts/agent-sources-helper.sh"
193
+ [[ -x "$sym_helper" ]] || return 0
194
+ "$sym_helper" cleanup-broken-symlinks >/dev/null 2>&1 || true
195
+ return 0
196
+ }
197
+
198
+ _update_check_homebrew() {
199
+ command -v brew &>/dev/null || return 0
200
+ brew list aidevops &>/dev/null 2>&1 || return 0
201
+ local brew_version=""
202
+ brew_version=$(brew info aidevops --json=v2 2>/dev/null | jq -r '.formulae[0].installed[0].version // empty' 2>/dev/null || true)
203
+ [[ -z "$brew_version" ]] && return 0
204
+ local current_version
205
+ current_version=$(get_version)
206
+ [[ -z "$current_version" ]] && return 0
207
+ if [[ "$brew_version" != "$current_version" ]]; then
208
+ echo ""
209
+ print_warning "Homebrew-installed copy is outdated ($brew_version vs $current_version)"
210
+ print_info "The Homebrew wrapper should prefer your git copy, but if your PATH"
211
+ print_info "resolves the Homebrew libexec copy directly, you'll run the old version."
212
+ echo ""
213
+ read -r -p "Run 'brew upgrade aidevops' now? [y/N] " response
214
+ if [[ "$response" =~ ^[Yy]$ ]]; then
215
+ brew upgrade aidevops 2>&1 || print_warning "brew upgrade failed — run manually: brew upgrade aidevops"
216
+ else
217
+ print_info "Run 'brew upgrade aidevops' to sync the Homebrew copy"
218
+ fi
219
+ fi
220
+ return 0
221
+ }
222
+
223
+ # t2926 / GH#21102: Re-check setsid on every 'aidevops update' run.
224
+ # setsid (from util-linux) is required to detach pulse workers into their own
225
+ # process group — without it, every pulse restart sends SIGHUP to its PGID,
226
+ # killing in-flight workers. This check runs even when setup.sh is skipped
227
+ # (already up-to-date path), so Homebrew drift doesn't silently break workers.
228
+ _update_check_setsid() {
229
+ command -v setsid >/dev/null 2>&1 && return 0
230
+
231
+ # setsid is missing. On macOS with Homebrew, auto-install util-linux.
232
+ # Use a boolean flag to avoid repeating the OS literal string.
233
+ local _on_mac=false
234
+ [[ "$(uname -s)" == Darwin* ]] && _on_mac=true
235
+ if $_on_mac && command -v brew >/dev/null 2>&1; then
236
+ print_info "setsid not found — installing util-linux for worker PGID isolation (GH#21102)"
237
+ if brew install util-linux 2>&1 | tail -3; then
238
+ local brew_prefix=""
239
+ brew_prefix="$(brew --prefix 2>/dev/null || true)"
240
+ local keg_setsid="${brew_prefix}/opt/util-linux/bin/setsid"
241
+ local link_target="${brew_prefix}/bin/setsid"
242
+ if [[ -x "$keg_setsid" && ! -e "$link_target" ]]; then
243
+ ln -s "$keg_setsid" "$link_target" && \
244
+ print_success "Symlinked setsid: $keg_setsid → $link_target"
245
+ fi
246
+ if command -v setsid >/dev/null 2>&1; then
247
+ print_success "setsid installed at $(command -v setsid) (worker PGID isolation enabled)"
248
+ else
249
+ print_error "util-linux installed but setsid still not in PATH — check brew --prefix"
250
+ fi
251
+ else
252
+ print_error "brew install util-linux failed — workers will share pulse PGID until resolved"
253
+ fi
254
+ elif $_on_mac; then
255
+ print_error "setsid not found — worker isolation broken; install Homebrew then run: brew install util-linux"
256
+ else
257
+ print_error "setsid not found — worker isolation broken; install util-linux via your distro package manager"
258
+ fi
259
+
260
+ return 0
261
+ }
262
+
263
+ # GH#21735: Notify operator when framework workflow templates change.
264
+ # When .agents/templates/workflows/*.yml or *-reusable.yml workflows change
265
+ # in a framework update, downstream repos that use these as workflow_call
266
+ # callers may have drifted from the new template. Detection and remediation
267
+ # both already exist (`aidevops check-workflows`, `aidevops sync-workflows
268
+ # --apply`); the gap was the notification surface — operators only learned
269
+ # of drift when downstream CI failed (canonical incident: a managed
270
+ # downstream repo's issue-sync.yml failed silently after the upstream
271
+ # template added a new input).
272
+ #
273
+ # This check inspects the SHA-window diff for changes to workflow caller
274
+ # templates and reusable workflows, prints a warning, and emits a daily
275
+ # advisory so the next session greeting surfaces it if the operator
276
+ # misses the inline output.
277
+ #
278
+ # Args: $1=old_sha, $2=new_sha
279
+ # Returns: 0 (always — informational only, never breaks update)
280
+ _update_check_workflow_drift() {
281
+ local old_sha="$1"
282
+ local new_sha="$2"
283
+ [[ -z "$old_sha" || -z "$new_sha" || "$old_sha" == "$new_sha" ]] && return 0
284
+ # `.git` is a directory in a regular repo and a file in a worktree;
285
+ # `-e` covers both so the helper is testable from a worktree.
286
+ [[ ! -e "$INSTALL_DIR/.git" ]] && return 0
287
+
288
+ # Files that propagate to downstream caller workflows OR are themselves
289
+ # reusable workflow definitions referenced by downstream callers.
290
+ # Internal .github/workflows/*.yml hotfixes (e.g. self-test runs) are
291
+ # intentionally skipped to avoid false-positive nags.
292
+ local relevant_files
293
+ relevant_files=$(git -C "$INSTALL_DIR" diff --name-only "$old_sha" "$new_sha" -- \
294
+ '.agents/templates/workflows/' \
295
+ '.github/workflows/' \
296
+ 2>/dev/null \
297
+ | grep -E '(\.agents/templates/workflows/.*\.ya?ml$|\.github/workflows/.*-reusable\.ya?ml$)' \
298
+ || true)
299
+ [[ -z "$relevant_files" ]] && return 0
300
+
301
+ local file_count
302
+ file_count=$(printf '%s\n' "$relevant_files" | wc -l | tr -d ' ')
303
+ echo ""
304
+ print_warning "Workflow templates updated ($file_count file(s)) — downstream callers may have drifted."
305
+ print_info " Detect drift: aidevops check-workflows"
306
+ print_info " Apply fix: aidevops sync-workflows --apply [--repo OWNER/REPO]"
307
+
308
+ # Persist as advisory so the next session greeting surfaces it even if
309
+ # the operator misses the inline warning. Day-stamped ID makes repeated
310
+ # updates within the same day idempotent (one advisory per day);
311
+ # 'aidevops security dismiss <id>' silences a specific day's advisory.
312
+ _update_emit_workflow_drift_advisory "$relevant_files" || true
313
+ return 0
314
+ }
315
+
316
+ # Companion to _update_check_workflow_drift — separated for testability.
317
+ # Args: $1=relevant_files (newline-separated)
318
+ # Returns: 0 (always — fail-open; advisory write must never break update)
319
+ _update_emit_workflow_drift_advisory() {
320
+ local relevant_files="$1"
321
+ local advisories_dir="${HOME}/.aidevops/advisories"
322
+ local adv_id
323
+ adv_id="workflow-drift-$(date +%Y%m%d)"
324
+ local dismissed_file="$advisories_dir/dismissed.txt"
325
+
326
+ # Skip if today's advisory was already dismissed.
327
+ if [[ -f "$dismissed_file" ]] && grep -qxF "$adv_id" "$dismissed_file" 2>/dev/null; then
328
+ return 0
329
+ fi
330
+
331
+ mkdir -p "$advisories_dir" 2>/dev/null || return 0
332
+ local adv_file="$advisories_dir/${adv_id}.advisory"
333
+
334
+ {
335
+ printf 'Workflow templates changed — downstream caller workflows may have drifted.\n'
336
+ printf '\n'
337
+ printf 'Files changed in this update:\n'
338
+ printf '%s\n' "$relevant_files" | sed 's|^| |'
339
+ printf '\n'
340
+ printf 'Detect drift: aidevops check-workflows\n'
341
+ printf 'Apply fix: aidevops sync-workflows --apply [--repo OWNER/REPO]\n'
342
+ printf 'Background: reference/reusable-workflows.md\n'
343
+ } >"$adv_file" 2>/dev/null || return 0
344
+ return 0
345
+ }
346
+
347
+ # Verify supply chain signature after pulling framework updates.
348
+ # Checks that the HEAD commit is signed by the trusted maintainer key.
349
+ # Non-blocking: warns on failure, does not abort the update.
350
+ _update_verify_signature() {
351
+ local signing_helper="$AGENTS_DIR/scripts/signing-setup.sh"
352
+
353
+ # Cannot verify if the helper script is not yet deployed
354
+ if [[ ! -f "$signing_helper" ]]; then
355
+ return 0
356
+ fi
357
+
358
+ local result
359
+ result=$(bash "$signing_helper" verify-update "$INSTALL_DIR" 2>/dev/null || echo "UNKNOWN")
360
+
361
+ case "$result" in
362
+ VERIFIED)
363
+ print_success "Supply chain verified: HEAD commit is signed by trusted maintainer"
364
+ ;;
365
+ UNSIGNED)
366
+ print_warning "HEAD commit is not signed — cannot verify supply chain integrity"
367
+ print_info "This is expected for older releases. Signed commits start from v3.6.21+"
368
+ ;;
369
+ UNTRUSTED)
370
+ print_warning "HEAD commit is signed but by an untrusted key"
371
+ print_info "Run 'aidevops signing setup' to configure signature verification"
372
+ ;;
373
+ BAD_SIGNATURE)
374
+ print_error "HEAD commit has a BAD signature — update may be compromised"
375
+ print_info "Verify manually: cd $INSTALL_DIR && git log --show-signature -1"
376
+ ;;
377
+ UNVERIFIABLE)
378
+ # Signing not configured yet — silent, do not nag
379
+ ;;
380
+ esac
381
+ return 0
382
+ }
383
+
384
+ # One-shot, idempotent migration of supervisor.* → orchestration.* in settings.json (t2946).
385
+ # Safe: reads value from supervisor.* only when orchestration.* key is absent.
386
+ # Logs to ~/.aidevops/logs/settings-migration.log.
387
+ _migrate_settings_supervisor_to_orchestration() {
388
+ local _settings_file="${HOME}/.config/aidevops/settings.json"
389
+ local _log_file="${HOME}/.aidevops/logs/settings-migration.log"
390
+
391
+ if ! command -v jq >/dev/null 2>&1; then
392
+ return 0
393
+ fi
394
+ if [[ ! -f "$_settings_file" ]]; then
395
+ return 0
396
+ fi
397
+ if ! jq . "$_settings_file" >/dev/null 2>&1; then
398
+ return 0
399
+ fi
400
+
401
+ # Check if supervisor.pulse_interval_seconds exists and orchestration.pulse_interval_seconds is absent.
402
+ local _has_sv _has_orch
403
+ _has_sv=$(jq -r 'if .supervisor.pulse_interval_seconds != null then "yes" else "no" end' "$_settings_file" 2>/dev/null)
404
+ _has_orch=$(jq -r 'if .orchestration.pulse_interval_seconds != null then "yes" else "no" end' "$_settings_file" 2>/dev/null)
405
+
406
+ if [[ "$_has_sv" != "yes" ]]; then
407
+ return 0
408
+ fi
409
+
410
+ local _ts
411
+ _ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)
412
+ mkdir -p "$(dirname "$_log_file")" 2>/dev/null || true
413
+
414
+ local _tmp
415
+ _tmp=$(mktemp 2>/dev/null) || return 0
416
+
417
+ if [[ "$_has_orch" == "no" ]]; then
418
+ # Migrate: copy supervisor.pulse_interval_seconds to orchestration.pulse_interval_seconds,
419
+ # then remove supervisor.pulse_interval_seconds.
420
+ local _sv_val
421
+ _sv_val=$(jq -r '.supervisor.pulse_interval_seconds' "$_settings_file" 2>/dev/null)
422
+ if jq --argjson v "$_sv_val" \
423
+ '(.orchestration.pulse_interval_seconds) = $v | del(.supervisor.pulse_interval_seconds)' \
424
+ "$_settings_file" >"$_tmp" 2>/dev/null && [[ -s "$_tmp" ]]; then
425
+ mv "$_tmp" "$_settings_file"
426
+ printf '[%s] migrated supervisor.pulse_interval_seconds=%s → orchestration.pulse_interval_seconds\n' \
427
+ "$_ts" "$_sv_val" >>"$_log_file" 2>/dev/null || true
428
+ print_info "Settings migrated: supervisor.pulse_interval_seconds → orchestration.pulse_interval_seconds ($_sv_val)"
429
+ else
430
+ rm -f "$_tmp"
431
+ fi
432
+ else
433
+ # Both present: orchestration wins, remove the stale supervisor key.
434
+ local _orch_val
435
+ _orch_val=$(jq -r '.orchestration.pulse_interval_seconds' "$_settings_file" 2>/dev/null)
436
+ if jq 'del(.supervisor.pulse_interval_seconds)' \
437
+ "$_settings_file" >"$_tmp" 2>/dev/null && [[ -s "$_tmp" ]]; then
438
+ mv "$_tmp" "$_settings_file"
439
+ printf '[%s] removed stale supervisor.pulse_interval_seconds (orchestration.pulse_interval_seconds=%s wins)\n' \
440
+ "$_ts" "$_orch_val" >>"$_log_file" 2>/dev/null || true
441
+ print_info "Settings cleaned: removed stale supervisor.pulse_interval_seconds (orchestration value $_orch_val kept)"
442
+ else
443
+ rm -f "$_tmp"
444
+ fi
445
+ fi
446
+ return 0
447
+ }
448
+
449
+ _update_check_daemon_health() {
450
+ local helper="$HOME/.aidevops/agents/scripts/auto-update-helper.sh"
451
+ [[ -x "$helper" ]] || return 0
452
+ local advisory_dir="$HOME/.aidevops/advisories"
453
+ local advisory_file="$advisory_dir/daemon-disabled.advisory"
454
+
455
+ local hc_rc=0
456
+ "$helper" health-check --quiet >/dev/null 2>&1 || hc_rc=$?
457
+
458
+ if [[ "$hc_rc" -eq 0 ]]; then
459
+ # Healthy — clear any stale advisory.
460
+ [[ -f "$advisory_file" ]] && rm -f "$advisory_file"
461
+ return 0
462
+ fi
463
+
464
+ # Unhealthy — warn on stderr and write advisory.
465
+ mkdir -p "$advisory_dir" 2>/dev/null || return 0
466
+ local fix_cmd="aidevops auto-update enable"
467
+ [[ "$hc_rc" -eq 1 ]] && fix_cmd="aidevops auto-update check"
468
+ cat >"$advisory_file" <<EOF
469
+ auto-update daemon is not running normally on this runner. Without it, this
470
+ runner falls behind the fleet and may dispatch workers that fail because of
471
+ bugs already fixed upstream. See cross-runner-coordination.md §4.4.
472
+
473
+ Diagnose: aidevops auto-update health-check
474
+ Fix: ${fix_cmd}
475
+ EOF
476
+
477
+ if [[ "$hc_rc" -eq 1 ]]; then
478
+ print_warning "Auto-update daemon is stalled. Fix: ${fix_cmd}"
479
+ else
480
+ print_warning "Auto-update daemon is not running. Fix: ${fix_cmd}"
481
+ fi
482
+ return 0
483
+ }