aidevops 3.5.896 → 3.8.3
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/README.md +96 -16
- package/VERSION +1 -1
- package/aidevops.sh +293 -34
- package/package.json +1 -1
- package/setup-modules/agent-deploy.sh +297 -669
- package/setup-modules/agent-runtime.sh +251 -0
- package/setup-modules/config.sh +83 -0
- package/setup-modules/core.sh +2 -0
- package/setup-modules/mcp-setup.sh +2 -0
- package/setup-modules/migrations.sh +267 -1
- package/setup-modules/plugins.sh +2 -0
- package/setup-modules/post-setup.sh +8 -1
- package/setup-modules/schedulers.sh +762 -336
- package/setup-modules/shell-env.sh +54 -16
- package/setup-modules/tool-beads.sh +324 -0
- package/setup-modules/tool-install.sh +6 -1
- package/setup.sh +207 -2
- package/templates/deploy-templates.sh +2 -0
- package/templates/home/.agents/README.md +3 -0
- package/templates/home/AGENTS.md +3 -0
- package/templates/home/git/.agent/README.md +3 -0
- package/templates/home/git/AGENTS.md +3 -0
- package/templates/opencode-config-agents.md +3 -0
- package/templates/standard-functions.sh +2 -0
- package/templates/wordpress-performance-workflow.md +3 -0
|
@@ -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
|
+
}
|
package/setup-modules/config.sh
CHANGED
|
@@ -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
|
+
}
|
package/setup-modules/core.sh
CHANGED
|
@@ -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
|
+
}
|
package/setup-modules/plugins.sh
CHANGED
|
@@ -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"
|
|
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"
|