aidevops 3.5.892 → 3.8.2

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,251 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
4
+ # Runtime agent deployment: convert and copy agents to each runtime's native directory.
5
+ # Strips aidevops-only frontmatter (mode, subagents); keeps standard fields
6
+ # (name, description, tools, model, permissionMode, hooks, mcpServers, etc.).
7
+ # Split from agent-deploy.sh (t1940)
8
+
9
+ # Shell safety baseline
10
+ set -Eeuo pipefail
11
+ IFS=$'\n\t'
12
+ # shellcheck disable=SC2154 # rc is assigned by $? in the trap string
13
+ trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
14
+ shopt -s inherit_errexit 2>/dev/null || true
15
+
16
+ # _convert_agent_frontmatter: strips aidevops-only fields from agent markdown.
17
+ # Reads from stdin, writes converted content to stdout.
18
+ # Tracks whether we're inside an indented block (subagents list) to correctly
19
+ # skip its child lines without stripping other indented YAML fields.
20
+ _convert_agent_frontmatter() {
21
+ local in_frontmatter=false
22
+ local frontmatter_started=false
23
+ local in_skip_block=false
24
+ local line_num=0
25
+
26
+ while IFS= read -r line || [[ -n "$line" ]]; do
27
+ line_num=$((line_num + 1))
28
+ if [[ $line_num -eq 1 && "$line" == "---" ]]; then
29
+ in_frontmatter=true
30
+ frontmatter_started=true
31
+ echo "$line"
32
+ continue
33
+ fi
34
+ if [[ "$frontmatter_started" == "true" && "$in_frontmatter" == "true" && "$line" == "---" ]]; then
35
+ in_frontmatter=false
36
+ echo "$line"
37
+ continue
38
+ fi
39
+ if [[ "$in_frontmatter" == "true" ]]; then
40
+ # Detect top-level keys (no leading whitespace)
41
+ case "$line" in
42
+ mode:*)
43
+ in_skip_block=false
44
+ continue
45
+ ;;
46
+ subagents:*)
47
+ in_skip_block=true
48
+ continue
49
+ ;;
50
+ esac
51
+ # If inside a skipped block, consume indented continuation lines
52
+ if [[ "$in_skip_block" == "true" ]]; then
53
+ case "$line" in
54
+ [[:space:]]*)
55
+ # Indented line under a skipped key — skip it
56
+ continue
57
+ ;;
58
+ *)
59
+ # Non-indented line — we've left the skip block
60
+ in_skip_block=false
61
+ echo "$line"
62
+ ;;
63
+ esac
64
+ else
65
+ echo "$line"
66
+ fi
67
+ else
68
+ echo "$line"
69
+ fi
70
+ done
71
+ return 0
72
+ }
73
+
74
+ # _is_agent_definition: check if a markdown file has agent frontmatter (name: field).
75
+ # Returns 0 if the file is an agent definition, 1 otherwise.
76
+ _is_agent_definition() {
77
+ local file="$1"
78
+ # Check first 30 lines for name: in YAML frontmatter (fast path)
79
+ head -30 "$file" 2>/dev/null | grep -q '^name:' 2>/dev/null
80
+ return $?
81
+ }
82
+
83
+ # _agent_source_dirs: list directories under agents_source that contain subagents.
84
+ # Excludes framework infrastructure directories that are not agent definitions.
85
+ _agent_source_dirs() {
86
+ local agents_source="$1"
87
+ local dir
88
+ for dir in "$agents_source"/*/; do
89
+ [[ -d "$dir" ]] || continue
90
+ local dirname
91
+ dirname=$(basename "$dir")
92
+ # Skip framework infrastructure directories
93
+ case "$dirname" in
94
+ scripts | reference | prompts | templates | configs | hooks | \
95
+ plugins | bundles | loop-state | advisories | aidevops | \
96
+ custom | draft | tests | rules)
97
+ continue
98
+ ;;
99
+ *)
100
+ echo "$dir"
101
+ ;;
102
+ esac
103
+ done
104
+ return 0
105
+ }
106
+
107
+ # _collect_agent_files: print "abspath|relpath" lines for all deployable agent files
108
+ # under agents_source. Excludes AGENTS.md, SKILL.md stubs, and non-agent markdown.
109
+ _collect_agent_files() {
110
+ local agents_source="$1"
111
+ local f bn
112
+
113
+ # Top-level agents
114
+ for f in "$agents_source"/*.md; do
115
+ [[ -f "$f" ]] || continue
116
+ bn=$(basename "$f")
117
+ [[ "$bn" == "AGENTS.md" ]] && continue
118
+ if _is_agent_definition "$f"; then
119
+ printf '%s|%s\n' "$f" "$bn"
120
+ fi
121
+ done
122
+
123
+ # Subagent directories (recursive)
124
+ local subdir
125
+ while IFS= read -r subdir; do
126
+ while IFS= read -r f; do
127
+ [[ -f "$f" ]] || continue
128
+ bn=$(basename "$f")
129
+ # Skip SKILL.md stubs — they're directory indexes, not real agents
130
+ [[ "$bn" == "SKILL.md" ]] && continue
131
+ if _is_agent_definition "$f"; then
132
+ local relpath="${f#"$agents_source"/}"
133
+ printf '%s|%s\n' "$f" "$relpath"
134
+ fi
135
+ done < <(find "$subdir" -name '*.md' -type f 2>/dev/null)
136
+ done < <(_agent_source_dirs "$agents_source")
137
+ return 0
138
+ }
139
+
140
+ # _deploy_agents_to_single_runtime: convert and copy all agent files to one runtime.
141
+ # Arguments: runtime_id agent_dir agent_list_file
142
+ # agent_list_file contains "abspath|relpath" lines produced by _collect_agent_files.
143
+ # Prints the count of successfully deployed agents to stdout.
144
+ _deploy_agents_to_single_runtime() {
145
+ local runtime_id="$1"
146
+ local agent_dir="$2"
147
+ local agent_list_file="$3"
148
+
149
+ # Only deploy if the runtime is actually installed
150
+ local binary config_path config_dir
151
+ binary=$(rt_binary "$runtime_id")
152
+ config_path=$(rt_config_path "$runtime_id")
153
+ config_dir="$(dirname "$config_path" 2>/dev/null)"
154
+
155
+ if ! type -P "$binary" >/dev/null 2>&1 && [[ ! -d "$config_dir" ]]; then
156
+ echo "0"
157
+ return 0
158
+ fi
159
+
160
+ mkdir -p "$agent_dir"
161
+ local agent_count=0
162
+ local src rel target target_parent
163
+
164
+ while IFS='|' read -r src rel; do
165
+ [[ -n "$src" && -n "$rel" ]] || continue
166
+ target="$agent_dir/$rel"
167
+ target_parent=$(dirname "$target")
168
+ [[ -d "$target_parent" ]] || mkdir -p "$target_parent"
169
+ if _convert_agent_frontmatter <"$src" >"$target"; then
170
+ agent_count=$((agent_count + 1))
171
+ fi
172
+ done <"$agent_list_file"
173
+
174
+ echo "$agent_count"
175
+ return 0
176
+ }
177
+
178
+ # deploy_agents_to_runtimes: main entry point called by setup.sh.
179
+ # Iterates all installed runtimes with agent directory support, converts and
180
+ # deploys aidevops agents to each runtime's native agent directory.
181
+ # Only files with name: frontmatter are deployed. SKILL.md stubs are excluded.
182
+ deploy_agents_to_runtimes() {
183
+ # Source runtime registry if not already loaded
184
+ local registry_script="${INSTALL_DIR:-.}/.agents/scripts/runtime-registry.sh"
185
+ if [[ -z "${_RUNTIME_REGISTRY_LOADED:-}" ]]; then
186
+ if [[ -f "$registry_script" ]]; then
187
+ # shellcheck source=/dev/null
188
+ source "$registry_script"
189
+ else
190
+ print_warning "Runtime registry not found — skipping agent deployment to runtimes"
191
+ return 0
192
+ fi
193
+ fi
194
+
195
+ local agents_source="${HOME}/.aidevops/agents"
196
+ if [[ ! -d "$agents_source" ]]; then
197
+ print_warning "No deployed agents found at $agents_source — skipping"
198
+ return 0
199
+ fi
200
+
201
+ # Build the agent file list once (shared across all runtimes) into a temp file.
202
+ # Each line: "abspath|relpath"
203
+ local agent_list_file
204
+ agent_list_file=$(mktemp)
205
+ trap 'rm -f "${agent_list_file:-}"' RETURN
206
+ _collect_agent_files "$agents_source" >"$agent_list_file"
207
+
208
+ local total_agents
209
+ total_agents=$(wc -l <"$agent_list_file" | tr -d ' ')
210
+ if [[ "$total_agents" -eq 0 ]]; then
211
+ print_warning "No agent definitions found in $agents_source"
212
+ return 0
213
+ fi
214
+
215
+ local deployed_count=0
216
+ local runtime_count=0
217
+
218
+ local runtime_id agent_dir agent_count display_name feature_flag
219
+ while IFS= read -r runtime_id; do
220
+ agent_dir=$(rt_agent_dir "$runtime_id")
221
+ [[ -z "$agent_dir" ]] && continue
222
+
223
+ # Feature flag gate — allow users to disable agent installation per
224
+ # runtime via AIDEVOPS_FEATURE_AGENTS_<SUFFIX>=no (see runtime-registry).
225
+ if declare -F rt_feature_agents >/dev/null 2>&1; then
226
+ feature_flag=$(rt_feature_agents "$runtime_id" 2>/dev/null || echo "yes")
227
+ if [[ "$feature_flag" != "yes" ]]; then
228
+ display_name=$(rt_display_name "$runtime_id")
229
+ print_info "Agent installation disabled for $display_name (feature flag)"
230
+ continue
231
+ fi
232
+ fi
233
+
234
+ agent_count=$(_deploy_agents_to_single_runtime "$runtime_id" "$agent_dir" "$agent_list_file")
235
+ # A count of 0 means runtime not installed (skipped) — don't increment runtime_count
236
+ if [[ "$agent_count" -gt 0 ]]; then
237
+ display_name=$(rt_display_name "$runtime_id")
238
+ print_info "Deployed $agent_count agents to $display_name ($agent_dir)"
239
+ deployed_count=$((deployed_count + agent_count))
240
+ runtime_count=$((runtime_count + 1))
241
+ fi
242
+ done < <(rt_list_with_agents)
243
+
244
+ if [[ $runtime_count -eq 0 ]]; then
245
+ print_info "No runtimes with agent directory support detected — skipping"
246
+ else
247
+ print_success "Deployed $deployed_count agent(s) across $runtime_count runtime(s)"
248
+ fi
249
+
250
+ return 0
251
+ }
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
2
4
  # Configuration functions: setup_configs, set_permissions, ssh, aidevops-cli, opencode-config, claude-config, validate, extract-prompts, drift-check
3
5
  # Part of aidevops setup.sh modularization (t316.3)
4
6
 
@@ -393,3 +395,84 @@ PYEOF
393
395
  echo " Done -- $mcp_count new MCP servers added to Cursor config"
394
396
  return 0
395
397
  }
398
+
399
+ # Deploy slash commands to every installed runtime that supports them.
400
+ #
401
+ # Background: update_opencode_config and update_claude_config already invoke
402
+ # the unified generator (.agents/scripts/generate-runtime-config.sh) for
403
+ # their runtimes. The other per-client update_*_config functions (Codex,
404
+ # Cursor, Droid, etc.) were written before the unified generator existed
405
+ # and only handle MCP registration. This function closes that gap by
406
+ # invoking the generator for every other installed client.
407
+ #
408
+ # Gated on rt_feature_commands so users can disable command installation
409
+ # per-runtime via AIDEVOPS_FEATURE_COMMANDS_<SUFFIX>=no. Clients with no
410
+ # _RT_COMMAND_DIR (windsurf, amp, kilo, aider) are skipped automatically.
411
+ #
412
+ # Fixes GH#18106 / t15474.
413
+ deploy_commands_to_all_runtimes() {
414
+ local registry_script="${INSTALL_DIR:-.}/.agents/scripts/runtime-registry.sh"
415
+ local generator_script="${INSTALL_DIR:-.}/.agents/scripts/generate-runtime-config.sh"
416
+
417
+ if [[ ! -f "$registry_script" ]]; then
418
+ print_info "Runtime registry not found — skipping unified command deployment"
419
+ return 0
420
+ fi
421
+ if [[ ! -x "$generator_script" ]]; then
422
+ print_info "Runtime config generator not executable — skipping unified command deployment"
423
+ return 0
424
+ fi
425
+
426
+ # Source registry if not already loaded
427
+ if [[ -z "${_RUNTIME_REGISTRY_LOADED:-}" ]]; then
428
+ # shellcheck source=/dev/null
429
+ source "$registry_script"
430
+ fi
431
+
432
+ local runtime_id cmd_dir feature_flag display_name
433
+ local deployed_count=0 skipped_count=0
434
+
435
+ while IFS= read -r runtime_id; do
436
+ # OpenCode and Claude Code are already handled by their dedicated
437
+ # update_*_config functions above — skip to avoid double-deploy
438
+ # and keep the log output clean.
439
+ case "$runtime_id" in
440
+ opencode | claude-code) continue ;;
441
+ esac
442
+
443
+ # Skip runtimes with no command directory in the registry (repo-only
444
+ # clients like Windsurf/Amp, and clients without native slash command
445
+ # support like Kilo/Aider).
446
+ cmd_dir=$(rt_command_dir "$runtime_id" 2>/dev/null || echo "")
447
+ [[ -z "$cmd_dir" ]] && continue
448
+
449
+ # Honour the rt_feature_commands flag.
450
+ feature_flag=$(rt_feature_commands "$runtime_id" 2>/dev/null || echo "yes")
451
+ if [[ "$feature_flag" != "yes" ]]; then
452
+ display_name=$(rt_display_name "$runtime_id" 2>/dev/null || echo "$runtime_id")
453
+ print_info "Commands installation disabled for $display_name (feature flag)"
454
+ skipped_count=$((skipped_count + 1))
455
+ continue
456
+ fi
457
+
458
+ # Invoke the unified generator — it prints its own success/failure.
459
+ # Redirect stdin from /dev/null so the generator cannot accidentally
460
+ # read from the `while read` loop's process-substitution pipe and
461
+ # steal the remaining runtime IDs — a classic bash pitfall.
462
+ if "$generator_script" commands --runtime "$runtime_id" </dev/null; then
463
+ deployed_count=$((deployed_count + 1))
464
+ else
465
+ display_name=$(rt_display_name "$runtime_id" 2>/dev/null || echo "$runtime_id")
466
+ print_warning "Failed to deploy commands for $display_name"
467
+ fi
468
+ done < <(rt_detect_installed 2>/dev/null)
469
+
470
+ if [[ $deployed_count -gt 0 ]]; then
471
+ print_success "Deployed slash commands to $deployed_count additional runtime(s)"
472
+ elif [[ $skipped_count -gt 0 ]]; then
473
+ print_info "All remaining runtimes had commands installation disabled via feature flags"
474
+ else
475
+ print_info "No additional runtimes needed command deployment"
476
+ fi
477
+ return 0
478
+ }
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
2
4
  # Core setup functions: requirements, permissions, location
3
5
  # Part of aidevops setup.sh modularization (t316.3)
4
6
 
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
2
4
  # MCP setup functions: install_mcp_packages, resolve_mcp_binary, localwp, augment, seo, analytics, quickfile, browser-tools, opencode-plugins
3
5
  # Part of aidevops setup.sh modularization (t316.3)
4
6
 
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
2
4
  # Migration functions: migrate_* and cleanup_* functions
3
5
  # Part of aidevops setup.sh modularization (t316.3)
4
6
 
@@ -275,8 +277,9 @@ cleanup_stale_bun_opencode() {
275
277
  if [[ "$npm_opencode" -eq 0 ]]; then
276
278
  # npm version not installed — install it first, then clean up bun
277
279
  if command -v npm >/dev/null 2>&1; then
280
+ local pin_ver="${OPENCODE_PINNED_VERSION:-latest}"
278
281
  print_info "Installing opencode via npm (replacing bun install)..."
279
- npm_global_install "opencode-ai" >/dev/null 2>&1 || true
282
+ npm_global_install "opencode-ai@${pin_ver}" >/dev/null 2>&1 || true
280
283
  else
281
284
  # Can't install npm version — leave bun version in place
282
285
  return 0
@@ -299,6 +302,83 @@ cleanup_stale_bun_opencode() {
299
302
  return 0
300
303
  }
301
304
 
305
+ # t1929: Remove stale contributor/legacy health issue cache files and close
306
+ # the corresponding GitHub issues. One-time migration — the root cause
307
+ # (API failure in _get_runner_role defaulting to "contributor") is fixed
308
+ # by the 4-layer role resolution in stats-functions.sh.
309
+ #
310
+ # Gated by a flag file so it runs exactly once per install.
311
+ cleanup_stale_health_issue_caches() {
312
+ local flag_file="${HOME}/.aidevops/logs/.migrated-health-issue-caches-t1929"
313
+ [[ -f "$flag_file" ]] && return 0
314
+
315
+ local cache_dir="${HOME}/.aidevops/logs"
316
+ [[ -d "$cache_dir" ]] || return 0
317
+
318
+ local cleaned=0
319
+
320
+ # 1. Remove contributor cache files (the duplicates).
321
+ # The correct files are health-issue-{user}-supervisor-{slug}.
322
+ local contributor_cache
323
+ for contributor_cache in "${cache_dir}"/health-issue-*-contributor-*; do
324
+ [[ -f "$contributor_cache" ]] || continue
325
+ local stale_num
326
+ stale_num=$(cat "$contributor_cache" 2>/dev/null || echo "")
327
+ # Extract slug from filename: health-issue-{user}-contributor-{slug}
328
+ # Best-effort close via gh if available and authenticated
329
+ if [[ -n "$stale_num" ]] && command -v gh &>/dev/null && gh auth status &>/dev/null 2>&1; then
330
+ local fname
331
+ fname=$(basename "$contributor_cache")
332
+ local slug_safe="${fname##*-contributor-}"
333
+ local supervisor_cache="${cache_dir}/health-issue-${fname%-contributor-*}-supervisor-${slug_safe}"
334
+ # Only close if there IS a supervisor counterpart (confirms it's a duplicate)
335
+ if [[ -f "$supervisor_cache" ]]; then
336
+ # Resolve actual slug from repos.json — the slug-safe format
337
+ # (hyphens replacing /) is lossy for owners/repos containing hyphens.
338
+ local repos_json="${HOME}/.config/aidevops/repos.json"
339
+ local repo_slug=""
340
+ if [[ -f "$repos_json" ]]; then
341
+ repo_slug=$(jq -r --arg ss "$slug_safe" \
342
+ '.initialized_repos[] | select((.slug // "") | gsub("/"; "-") == $ss) | .slug' \
343
+ "$repos_json" 2>/dev/null | head -1)
344
+ fi
345
+ if [[ -n "$repo_slug" ]]; then
346
+ # Remove "persistent" label first — a GitHub Actions workflow
347
+ # auto-reopens issues with this label (health issues get it on creation).
348
+ gh issue edit "$stale_num" --repo "$repo_slug" \
349
+ --remove-label persistent 2>/dev/null || true
350
+ gh issue close "$stale_num" --repo "$repo_slug" \
351
+ --comment "Closing duplicate contributor health issue (t1929 migration)." 2>/dev/null || true
352
+ fi
353
+ fi
354
+ fi
355
+ rm -f "$contributor_cache"
356
+ cleaned=$((cleaned + 1))
357
+ done
358
+
359
+ # 2. Remove legacy cache files (no role prefix, pre-role-naming).
360
+ # Pattern: health-issue-{user}-{slug} where {slug} has no "supervisor" or "contributor".
361
+ local legacy_cache
362
+ for legacy_cache in "${cache_dir}"/health-issue-*; do
363
+ [[ -f "$legacy_cache" ]] || continue
364
+ local fname
365
+ fname=$(basename "$legacy_cache")
366
+ # Skip files that already have a role prefix (they're the correct format)
367
+ [[ "$fname" == *-supervisor-* || "$fname" == *-contributor-* ]] && continue
368
+ rm -f "$legacy_cache"
369
+ cleaned=$((cleaned + 1))
370
+ done
371
+
372
+ # Write flag file
373
+ mkdir -p "$(dirname "$flag_file")"
374
+ date -u +"%Y-%m-%dT%H:%M:%SZ" >"$flag_file"
375
+
376
+ if [[ "$cleaned" -gt 0 ]]; then
377
+ print_info "Cleaned up $cleaned stale health issue cache file(s) (t1929)"
378
+ fi
379
+ return 0
380
+ }
381
+
302
382
  # Migrate legacy .agent symlink/directory to .agents in a single repo.
303
383
  # Args: $1 = repo_path
304
384
  # Prints: info messages for each migration action
@@ -1234,3 +1314,189 @@ migrate_orphaned_supervisor() {
1234
1314
 
1235
1315
  return 0
1236
1316
  }
1317
+
1318
+ # Backfill GitHub issue relationships from TODO.md metadata (t1889)
1319
+ # One-time migration: reads blocked-by:/blocks: and subtask hierarchy from
1320
+ # TODO.md in each pulse-enabled repo, and sets the corresponding GitHub
1321
+ # issue relationships (blocked-by, sub-issues) via the GraphQL API.
1322
+ #
1323
+ # Uses marker file to ensure it runs only once per install.
1324
+ # Safe to re-run — the GraphQL mutations are idempotent (duplicates are skipped).
1325
+ backfill_issue_relationships() {
1326
+ local marker_file="$HOME/.aidevops/.migrations/t1889-relationships-backfill"
1327
+ local marker_dir
1328
+ marker_dir=$(dirname "$marker_file")
1329
+
1330
+ # Skip if already done
1331
+ if [[ -f "$marker_file" ]]; then
1332
+ return 0
1333
+ fi
1334
+
1335
+ # Require gh CLI and authentication
1336
+ if ! command -v gh &>/dev/null; then
1337
+ print_warning "gh CLI not installed — skipping issue relationships backfill"
1338
+ return 0
1339
+ fi
1340
+ if ! gh auth status &>/dev/null 2>&1; then
1341
+ print_warning "gh CLI not authenticated — skipping issue relationships backfill"
1342
+ return 0
1343
+ fi
1344
+
1345
+ # Require jq for repos.json parsing
1346
+ if ! command -v jq &>/dev/null; then
1347
+ print_warning "jq not installed — skipping issue relationships backfill"
1348
+ return 0
1349
+ fi
1350
+
1351
+ local repos_file="$HOME/.config/aidevops/repos.json"
1352
+ if [[ ! -f "$repos_file" ]]; then
1353
+ print_info "No repos.json — skipping issue relationships backfill"
1354
+ mkdir -p "$marker_dir"
1355
+ touch "$marker_file"
1356
+ return 0
1357
+ fi
1358
+
1359
+ local sync_script="$HOME/.aidevops/agents/scripts/issue-sync-helper.sh"
1360
+ if [[ ! -x "$sync_script" ]]; then
1361
+ print_warning "issue-sync-helper.sh not found — skipping relationships backfill"
1362
+ return 0
1363
+ fi
1364
+
1365
+ print_info "Backfilling GitHub issue relationships (blocked-by, sub-issues) from TODO.md..."
1366
+
1367
+ local total_repos=0 total_rels=0 failed_repos=0
1368
+ local repo_path repo_slug local_only
1369
+
1370
+ while IFS=$'\t' read -r repo_path repo_slug local_only; do
1371
+ [[ -z "$repo_path" ]] && continue
1372
+ local expanded_path="${repo_path/#\~/$HOME}"
1373
+
1374
+ # Skip local-only repos (no GitHub remote)
1375
+ [[ "$local_only" == "true" ]] && continue
1376
+
1377
+ # Skip repos without TODO.md
1378
+ [[ ! -f "$expanded_path/TODO.md" ]] && continue
1379
+
1380
+ # Skip repos with no ref:GH# entries
1381
+ if ! grep -qE 'ref:GH#[0-9]+' "$expanded_path/TODO.md" 2>/dev/null; then
1382
+ continue
1383
+ fi
1384
+
1385
+ # Skip repos with no blocked-by:/blocks: or subtask entries
1386
+ local has_deps=false
1387
+ grep -qE 'blocked-by:|blocks:' "$expanded_path/TODO.md" 2>/dev/null && has_deps=true
1388
+ grep -qE '^\s+- \[.\] t[0-9]+\.[0-9]+.*ref:GH#' "$expanded_path/TODO.md" 2>/dev/null && has_deps=true
1389
+ [[ "$has_deps" == "false" ]] && continue
1390
+
1391
+ total_repos=$((total_repos + 1))
1392
+ local repo_arg=""
1393
+ [[ -n "$repo_slug" ]] && repo_arg="--repo $repo_slug"
1394
+
1395
+ print_info " $(basename "$expanded_path"): syncing relationships..."
1396
+ # shellcheck disable=SC2086
1397
+ if (cd "$expanded_path" && bash "$sync_script" relationships $repo_arg --verbose 2>&1 | tail -3); then
1398
+ true
1399
+ else
1400
+ print_warning " $(basename "$expanded_path"): relationships sync had errors"
1401
+ failed_repos=$((failed_repos + 1))
1402
+ fi
1403
+ done < <(jq -r '.initialized_repos[] | select(.pulse == true) | [.path, .slug, (.local_only // false | tostring)] | @tsv' "$repos_file" 2>/dev/null)
1404
+
1405
+ # Create marker directory and file
1406
+ mkdir -p "$marker_dir"
1407
+ date -u +%Y-%m-%dT%H:%M:%SZ >"$marker_file"
1408
+
1409
+ if [[ $total_repos -eq 0 ]]; then
1410
+ print_info "No repos with relationship data to backfill"
1411
+ elif [[ $failed_repos -eq 0 ]]; then
1412
+ print_success "Issue relationships backfilled for $total_repos repo(s)"
1413
+ else
1414
+ print_warning "Backfilled $total_repos repo(s), $failed_repos had errors"
1415
+ fi
1416
+
1417
+ return 0
1418
+ }
1419
+
1420
+ # Migrate aidevops cron entries to systemd user timers (GH#17695 Finding D).
1421
+ # On Linux systems with systemd, scans cron for aidevops markers and removes
1422
+ # entries that have a corresponding systemd timer already installed. This
1423
+ # prevents dual-execution for existing installations that were set up before
1424
+ # the systemd preference was added.
1425
+ # Safe to run on macOS (no-op) and on Linux without systemd (no-op).
1426
+ # Idempotent: uses a marker file to run only once.
1427
+ migrate_cron_to_systemd() {
1428
+ # Only run on Linux with systemd available
1429
+ if [[ "$(uname -s)" == "Darwin" ]]; then
1430
+ return 0
1431
+ fi
1432
+ if ! command -v systemctl >/dev/null 2>&1 || ! systemctl --user status >/dev/null 2>&1; then
1433
+ return 0
1434
+ fi
1435
+
1436
+ # Versioned migration marker — bump version when new entries are added so
1437
+ # existing systems re-run the migration (GH#17861: added auto-update + repo-sync).
1438
+ local marker_dir="$HOME/.aidevops/cache/migrations"
1439
+ local marker_file="$marker_dir/cron-to-systemd-v2-done"
1440
+ if [[ -f "$marker_file" ]]; then
1441
+ return 0
1442
+ fi
1443
+
1444
+ # Parallel arrays: cron markers and their corresponding systemd timer names.
1445
+ # Bash 3.2 compatible (no associative arrays).
1446
+ local cron_markers="aidevops: stats-wrapper
1447
+ aidevops: gh-failure-miner
1448
+ aidevops: process-guard
1449
+ aidevops: memory-pressure-monitor
1450
+ aidevops: screen-time-snapshot
1451
+ aidevops: contribution-watch
1452
+ aidevops: profile-readme-update
1453
+ aidevops: token-refresh
1454
+ aidevops-auto-update
1455
+ aidevops-repo-sync"
1456
+
1457
+ local systemd_timers="aidevops-stats-wrapper
1458
+ aidevops-gh-failure-miner
1459
+ aidevops-process-guard
1460
+ aidevops-memory-pressure-monitor
1461
+ aidevops-screen-time-snapshot
1462
+ aidevops-contribution-watch
1463
+ aidevops-profile-readme-update
1464
+ aidevops-token-refresh
1465
+ aidevops-auto-update
1466
+ aidevops-repo-sync"
1467
+
1468
+ local current_cron
1469
+ current_cron=$(crontab -l 2>/dev/null) || current_cron=""
1470
+ if [[ -z "$current_cron" ]]; then
1471
+ mkdir -p "$marker_dir"
1472
+ date -u +%Y-%m-%dT%H:%M:%SZ >"$marker_file"
1473
+ return 0
1474
+ fi
1475
+
1476
+ local migrated=0
1477
+ local new_cron="$current_cron"
1478
+ local i=0
1479
+
1480
+ while IFS= read -r marker; do
1481
+ local timer_name
1482
+ timer_name=$(echo "$systemd_timers" | sed -n "$((i + 1))p")
1483
+ i=$((i + 1))
1484
+ # Only remove cron entry if the corresponding systemd timer is active
1485
+ if echo "$new_cron" | grep -qF "$marker" &&
1486
+ systemctl --user is-enabled "${timer_name}.timer" >/dev/null 2>&1; then
1487
+ new_cron=$(echo "$new_cron" | grep -vF "$marker")
1488
+ migrated=$((migrated + 1))
1489
+ print_info "Migrated $marker from cron to systemd (${timer_name}.timer)"
1490
+ fi
1491
+ done <<<"$cron_markers"
1492
+
1493
+ if [[ $migrated -gt 0 ]]; then
1494
+ echo "$new_cron" | crontab -
1495
+ print_success "Cron-to-systemd migration: $migrated scheduler(s) migrated"
1496
+ fi
1497
+
1498
+ # Write versioned marker regardless of whether anything was migrated
1499
+ mkdir -p "$marker_dir"
1500
+ date -u +%Y-%m-%dT%H:%M:%SZ >"$marker_file"
1501
+ return 0
1502
+ }
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
2
4
  # Plugin functions: deploy_plugins, sanitize_plugin_namespace, generate_agent_skills, create_skill_symlinks, check_skill_updates, scan_imported_skills, multi-tenant
3
5
  # Part of aidevops setup.sh modularization (t316.3)
4
6
 
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: MIT
3
+ # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
2
4
  # Post-setup functions: auto-update enablement, final instructions, onboarding prompt.
3
5
  # Part of aidevops setup.sh modularization (GH#5793)
4
6
 
@@ -26,7 +28,8 @@ setup_auto_update() {
26
28
  "aidevops-auto-update" \
27
29
  "$auto_update_script" \
28
30
  "enable" \
29
- "aidevops auto-update enable"; then
31
+ "aidevops auto-update enable" \
32
+ "aidevops-auto-update"; then
30
33
  _auto_update_installed=true
31
34
  fi
32
35
  if [[ "$_auto_update_installed" == "false" ]]; then
@@ -162,6 +165,10 @@ setup_tabby() {
162
165
 
163
166
  print_info "Tabby terminal detected"
164
167
 
168
+ # Ensure default local profile uses /bin/zsh (macOS).
169
+ # After macOS updates, Tabby can fall back to bash when this is unset.
170
+ bash "$tabby_helper" fix-shell || true
171
+
165
172
  # Install zshrc hook (idempotent)
166
173
  if ! bash "$tabby_helper" zshrc; then
167
174
  print_warning "Failed to install Tabby zshrc hook — run manually: aidevops tabby zshrc"