@delorenj/pjangler 1.1.5 → 1.2.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.
- package/.mise/scripts/link-agentfiles.sh +9 -0
- package/dist/index.js +1492 -358
- package/dist/mcp-server.js +2391 -290
- package/package.json +5 -2
- package/templates/commonproject/.mise/scripts/create-plane-project.sh +26 -0
- package/templates/commonproject/copier.yml +2 -0
- package/templates/commonproject/template/.agents/hooks/README.md +151 -0
- package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +63 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-journal-write.sh +327 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-recall.sh +180 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-retain.sh +74 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-session-end.sh +57 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/lib/hindsight-bank.sh +57 -0
- package/templates/commonproject/template/.agents/hooks/hindsight/lib/hindsight-journal.sh +48 -0
- package/templates/commonproject/template/.agents/hooks/hooks.master.json +97 -0
- package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +30 -0
- package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +51 -0
- package/templates/commonproject/template/.agents/hooks/reminder-for-skill-check.sh +20 -0
- package/templates/commonproject/template/.agents/hooks/sync.py +618 -0
- package/templates/commonproject/template/.agents/local.example.json +15 -0
- package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +62 -0
- package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +110 -0
- package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +45 -0
- package/templates/commonproject/template/mise.toml.jinja +42 -0
- package/templates/hermes-agent/.codegraph/daemon.pid +6 -0
- package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +10 -0
- package/templates/hermes-agent/README.md +16 -14
- package/templates/hermes-agent/copier.yml +7 -25
- package/templates/hermes-agent/docs/architecture.md +16 -18
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +35 -0
- package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +253 -0
- package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +280 -0
- package/templates/hermes-agent/docs/fleet-control-plane/implementation-readiness-report-2026-06-27.md +118 -0
- package/templates/hermes-agent/docs/fleet-control-plane/prd.md +141 -0
- package/templates/hermes-agent/docs/operations.md +17 -19
- package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +17 -13
- package/templates/hermes-agent/docs/{scrum-master → sentinel}/README.md +47 -46
- package/templates/hermes-agent/docs/{scrum-master → sentinel}/architecture.md +39 -38
- package/templates/hermes-agent/docs/{scrum-master → sentinel}/development.md +63 -99
- package/templates/hermes-agent/docs/{scrum-master → sentinel}/providers.md +2 -2
- package/templates/hermes-agent/docs/sentinel.md +18 -0
- package/templates/hermes-agent/install-local.sh +22 -20
- package/templates/hermes-agent/runtime-scaffold/README.md +10 -9
- package/templates/hermes-agent/scripts/backfill-fleet-sot.sh +12 -21
- package/templates/hermes-agent/scripts/fleet-sync.sh +369 -0
- package/templates/hermes-agent/scripts/migrate-unify.sh +252 -0
- package/templates/hermes-agent/scripts/unify-pm.sh +148 -0
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +10 -9
- package/templates/hermes-agent/template/.scripts/01-config.sh +2 -3
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -2
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +47 -19
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +71 -27
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +3 -3
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +3 -3
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +48 -43
- package/templates/hermes-agent/template/.scripts/80-registry.sh +2 -2
- package/templates/hermes-agent/template/.scripts/99-summary.sh +3 -4
- package/templates/hermes-agent/template/.scripts/_lib.sh +4 -19
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +36 -0
- package/templates/hermes-agent/template/.scripts/config.example.toml +11 -5
- package/templates/hermes-agent/template/.scripts/{scrum-master/continuous-ticket-sentinel.sh → heartbeat.sh} +84 -31
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +1 -1
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -3
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/emit-event.py +5 -5
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/issue-autonomous-review.sh +6 -6
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/issue-close-gate.sh +2 -2
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/autonomous-delegated-review.md +7 -7
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/bloodbank-events.md +3 -3
- package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/continuous-ticket-orchestration.md +6 -6
- package/templates/hermes-agent/template/.scripts/{scrum-master/continuous-ticket-sentinel.prompt.md.jinja → sentinel.prompt.md.jinja} +5 -4
- package/templates/hermes-agent/template/SOUL.md.jinja +3 -6
- package/templates/hermes-agent/template/hermes.jinja +6 -15
- package/templates/hermes-agent/template/role.yaml.jinja +9 -8
- package/templates/commonproject/.claude/commands/bmad/bmb/agents/agent-builder.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmb/agents/module-builder.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmb/agents/workflow-builder.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmb/workflows/agent.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmb/workflows/module.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmb/workflows/workflow.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/analyst.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/architect.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/dev.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/pm.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/quick-flow-solo-dev.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/sm.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/tea.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/tech-writer.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/agents/ux-designer.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/check-implementation-readiness.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/code-review.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/correct-course.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-architecture.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-epics-and-stories.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-dataflow.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-diagram.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-flowchart.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-wireframe.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-product-brief.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-story.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-ux-design.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/dev-story.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/document-project.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/generate-project-context.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/prd.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/quick-dev.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/quick-spec.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/research.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/retrospective.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/sprint-planning.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/sprint-status.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-atdd.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-automate.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-ci.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-framework.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-nfr.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-test-design.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-test-review.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-trace.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/workflow-init.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/bmm/workflows/workflow-status.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/cis/agents/brainstorming-coach.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/agents/creative-problem-solver.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/agents/design-thinking-coach.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/agents/innovation-strategist.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/agents/presentation-master.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/agents/storyteller.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/cis/workflows/design-thinking.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/cis/workflows/innovation-strategy.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/cis/workflows/problem-solving.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/cis/workflows/storytelling.md +0 -13
- package/templates/commonproject/.claude/commands/bmad/core/agents/bmad-master.md +0 -14
- package/templates/commonproject/.claude/commands/bmad/core/tasks/index-docs.md +0 -9
- package/templates/commonproject/.claude/commands/bmad/core/tasks/shard-doc.md +0 -9
- package/templates/commonproject/.claude/commands/bmad/core/workflows/brainstorming.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/core/workflows/party-mode.md +0 -5
- package/templates/commonproject/.claude/commands/bmad/custom/workflows/ticket-lifecycle.md +0 -5
- package/templates/hermes-agent/docs/scrum-master-sentinel.md +0 -17
- package/templates/hermes-agent/template/.scripts/75-scrum-master.sh +0 -117
- package/templates/hermes-agent/template/.scripts/90-chain-scrum-master.sh +0 -45
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# fleet-sync — reconcile every deployed Hermes agent with the current
|
|
5
|
+
# template and fleet contract. The registry (~/.hermes/agents-registry.yaml)
|
|
6
|
+
# is the iteration source; the vendored template is the content source.
|
|
7
|
+
#
|
|
8
|
+
# Per agent:
|
|
9
|
+
# wrapper <role_dir>/hermes regenerated from template/hermes.jinja
|
|
10
|
+
# contract runtime/profile.yaml opts into config.inherit_from: default
|
|
11
|
+
# registry ~/.hermes/profiles/<profile_name> -> <role_dir>/runtime
|
|
12
|
+
# role <role_dir>/role.yaml profile: <profile_name>
|
|
13
|
+
# services systemd gateway/consumer units restarted when something changed
|
|
14
|
+
#
|
|
15
|
+
# Default is a DRY-RUN drift report (exit 1 when drift exists, 0 when clean).
|
|
16
|
+
# --apply writes the fixes and restarts the changed agents' services.
|
|
17
|
+
# Anything that would require destroying or merging existing data (a real
|
|
18
|
+
# directory where a symlink belongs, a profile.yaml with foreign content)
|
|
19
|
+
# is reported as MANUAL and never touched.
|
|
20
|
+
#
|
|
21
|
+
# Usage: fleet-sync.sh [--apply] [--no-restart] [--agent <id>]...
|
|
22
|
+
|
|
23
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
24
|
+
WRAPPER_TEMPLATE="$SCRIPT_DIR/../template/hermes.jinja"
|
|
25
|
+
|
|
26
|
+
HERMES_TEMPLATE_CONFIG="${HERMES_TEMPLATE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/hermes-agent-template/config.toml}"
|
|
27
|
+
cfg() { # cfg <dotted.key> <default>
|
|
28
|
+
local key="$1" def="$2"
|
|
29
|
+
{ [[ -f "$HERMES_TEMPLATE_CONFIG" ]] && command -v python3 >/dev/null 2>&1; } || { printf '%s' "$def"; return 0; }
|
|
30
|
+
python3 - "$HERMES_TEMPLATE_CONFIG" "$key" "$def" <<'PYEOF' || printf '%s' "$def"
|
|
31
|
+
import sys, os
|
|
32
|
+
try:
|
|
33
|
+
import tomllib
|
|
34
|
+
except ModuleNotFoundError:
|
|
35
|
+
print(sys.argv[3], end=""); sys.exit(0)
|
|
36
|
+
try:
|
|
37
|
+
cur = tomllib.load(open(sys.argv[1], "rb"))
|
|
38
|
+
except Exception:
|
|
39
|
+
print(sys.argv[3], end=""); sys.exit(0)
|
|
40
|
+
for p in sys.argv[2].split("."):
|
|
41
|
+
if isinstance(cur, dict) and p in cur:
|
|
42
|
+
cur = cur[p]
|
|
43
|
+
else:
|
|
44
|
+
print(sys.argv[3], end=""); sys.exit(0)
|
|
45
|
+
print(os.path.expanduser(str(cur)), end="")
|
|
46
|
+
PYEOF
|
|
47
|
+
return 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
FLEET_ENV="${HERMES_FLEET_ENV:-$(cfg fleet.fleet_env "$HOME/.hermes/fleet.env")}"
|
|
51
|
+
if [[ -f "$FLEET_ENV" ]]; then
|
|
52
|
+
# shellcheck disable=SC1090
|
|
53
|
+
source "$FLEET_ENV"
|
|
54
|
+
fi
|
|
55
|
+
REGISTRY_FILE="${HERMES_FLEET_REGISTRY_FILE:-$(cfg fleet.registry_file "$HOME/.hermes/agents-registry.yaml")}"
|
|
56
|
+
PROFILES_DIR="${HERMES_FLEET_HOME:-$HOME/.hermes}/profiles"
|
|
57
|
+
VOXXY_PLUGIN_DIR="${VOXXY_PLUGIN_DIR:-$(cfg fleet.voxxy_plugin_dir "$HOME/code/voxxy/plugins/tts/voxxy")}"
|
|
58
|
+
VOX_URL_VALUE="${VOX_URL:-$(cfg fleet.vox_url 'https://vox.delo.sh')}"
|
|
59
|
+
|
|
60
|
+
APPLY=0
|
|
61
|
+
RESTART=1
|
|
62
|
+
declare -a ONLY_AGENTS=()
|
|
63
|
+
while [[ $# -gt 0 ]]; do
|
|
64
|
+
case "$1" in
|
|
65
|
+
--apply) APPLY=1 ;;
|
|
66
|
+
--no-restart) RESTART=0 ;;
|
|
67
|
+
--agent) shift; ONLY_AGENTS+=("$1") ;;
|
|
68
|
+
-h|--help) sed -n '3,21p' "$0"; exit 0 ;;
|
|
69
|
+
*) echo "fleet-sync: unknown flag $1" >&2; exit 2 ;;
|
|
70
|
+
esac
|
|
71
|
+
shift
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
[[ -f "$REGISTRY_FILE" ]] || { echo "fleet-sync: registry not found: $REGISTRY_FILE" >&2; exit 2; }
|
|
75
|
+
[[ -f "$WRAPPER_TEMPLATE" ]] || { echo "fleet-sync: wrapper template not found: $WRAPPER_TEMPLATE" >&2; exit 2; }
|
|
76
|
+
|
|
77
|
+
# Registry -> TSV: agent_id, role_dir, profile_name, gateway_unit, consumer_unit
|
|
78
|
+
read_registry() {
|
|
79
|
+
python3 - "$REGISTRY_FILE" <<'PYEOF'
|
|
80
|
+
import sys
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
import yaml
|
|
84
|
+
with open(sys.argv[1], encoding="utf-8") as f:
|
|
85
|
+
data = yaml.safe_load(f) or {}
|
|
86
|
+
except ModuleNotFoundError:
|
|
87
|
+
sys.stderr.write("fleet-sync: python3-yaml is required to read the registry\n")
|
|
88
|
+
sys.exit(2)
|
|
89
|
+
|
|
90
|
+
for agent_id, a in sorted((data.get("agents") or {}).items()):
|
|
91
|
+
if not isinstance(a, dict):
|
|
92
|
+
continue
|
|
93
|
+
systemd = a.get("systemd") or {}
|
|
94
|
+
print("\t".join([
|
|
95
|
+
agent_id,
|
|
96
|
+
str(a.get("role_dir") or ""),
|
|
97
|
+
str(a.get("profile_name") or agent_id),
|
|
98
|
+
str(systemd.get("gateway_unit") or ""),
|
|
99
|
+
str(systemd.get("consumer_unit") or ""),
|
|
100
|
+
]))
|
|
101
|
+
PYEOF
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
render_wrapper() { # render_wrapper <agent_id>
|
|
105
|
+
sed "s/{{ agent_id }}/$1/g" "$WRAPPER_TEMPLATE"
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
DRIFT=0
|
|
109
|
+
FIXED=0
|
|
110
|
+
declare -a RESTART_UNITS=()
|
|
111
|
+
|
|
112
|
+
# Registry agents sharing a role_dir would overwrite each other's wrapper
|
|
113
|
+
# and role.yaml on --apply; refuse to write into contested dirs.
|
|
114
|
+
CONTESTED_DIRS="$(read_registry | cut -f2 | sort | uniq -d)"
|
|
115
|
+
|
|
116
|
+
note() { # note <agent> <status> <message>
|
|
117
|
+
printf '%-34s %-7s %s\n' "$1" "$2" "$3"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
ensure_env_key() { # ensure_env_key <path> <key> <value>
|
|
121
|
+
python3 - "$1" "$2" "$3" <<'PYEOF'
|
|
122
|
+
import pathlib, sys
|
|
123
|
+
|
|
124
|
+
path = pathlib.Path(sys.argv[1])
|
|
125
|
+
key = sys.argv[2]
|
|
126
|
+
value = sys.argv[3]
|
|
127
|
+
lines = path.read_text().splitlines() if path.exists() else []
|
|
128
|
+
needle = f"{key}="
|
|
129
|
+
for idx, line in enumerate(lines):
|
|
130
|
+
if line.startswith(needle):
|
|
131
|
+
lines[idx] = f'{key}="{value}"'
|
|
132
|
+
break
|
|
133
|
+
else:
|
|
134
|
+
lines.append(f'{key}="{value}"')
|
|
135
|
+
path.write_text("\n".join(lines) + "\n")
|
|
136
|
+
PYEOF
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
wanted_agent() {
|
|
140
|
+
[[ ${#ONLY_AGENTS[@]} -eq 0 ]] && return 0
|
|
141
|
+
local a
|
|
142
|
+
for a in "${ONLY_AGENTS[@]}"; do [[ "$a" == "$1" ]] && return 0; done
|
|
143
|
+
return 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
while IFS=$'\t' read -r agent_id role_dir profile_name gateway_unit consumer_unit; do
|
|
147
|
+
wanted_agent "$agent_id" || continue
|
|
148
|
+
if [[ -z "$role_dir" || ! -d "$role_dir" ]]; then
|
|
149
|
+
note "$agent_id" DRIFT "role_dir missing: ${role_dir:-<unset>} (MANUAL: deprovision or fix registry)"
|
|
150
|
+
DRIFT=$((DRIFT + 1))
|
|
151
|
+
continue
|
|
152
|
+
fi
|
|
153
|
+
if [[ -n "$CONTESTED_DIRS" ]] && grep -qx "$role_dir" <<<"$CONTESTED_DIRS"; then
|
|
154
|
+
note "$agent_id" DRIFT "role_dir claimed by multiple registry agents: $role_dir (MANUAL: fix registry)"
|
|
155
|
+
DRIFT=$((DRIFT + 1))
|
|
156
|
+
continue
|
|
157
|
+
fi
|
|
158
|
+
runtime="$role_dir/runtime"
|
|
159
|
+
changed=0
|
|
160
|
+
|
|
161
|
+
# 1. Launcher wrapper regenerated from the current template.
|
|
162
|
+
expected_wrapper="$(render_wrapper "$agent_id")"
|
|
163
|
+
if [[ ! -f "$role_dir/hermes" ]] || ! diff -q <(printf '%s\n' "$expected_wrapper") "$role_dir/hermes" >/dev/null 2>&1; then
|
|
164
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
165
|
+
printf '%s\n' "$expected_wrapper" > "$role_dir/hermes"
|
|
166
|
+
chmod +x "$role_dir/hermes"
|
|
167
|
+
note "$agent_id" FIXED "wrapper regenerated from template"
|
|
168
|
+
changed=1; FIXED=$((FIXED + 1))
|
|
169
|
+
else
|
|
170
|
+
note "$agent_id" DRIFT "wrapper differs from template"
|
|
171
|
+
DRIFT=$((DRIFT + 1))
|
|
172
|
+
fi
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# 2. Inherited-config contract in runtime/profile.yaml.
|
|
176
|
+
profile_yaml="$runtime/profile.yaml"
|
|
177
|
+
if [[ ! -d "$runtime" ]]; then
|
|
178
|
+
note "$agent_id" DRIFT "runtime dir missing: $runtime (MANUAL: re-provision)"
|
|
179
|
+
DRIFT=$((DRIFT + 1))
|
|
180
|
+
elif [[ ! -f "$profile_yaml" ]]; then
|
|
181
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
182
|
+
printf 'config:\n inherit_from: default\n save_mode: delta\n' > "$profile_yaml"
|
|
183
|
+
note "$agent_id" FIXED "profile.yaml written (inherit_from: default)"
|
|
184
|
+
changed=1; FIXED=$((FIXED + 1))
|
|
185
|
+
else
|
|
186
|
+
note "$agent_id" DRIFT "profile.yaml missing (no inherited config)"
|
|
187
|
+
DRIFT=$((DRIFT + 1))
|
|
188
|
+
fi
|
|
189
|
+
elif ! grep -q 'inherit_from: default' "$profile_yaml"; then
|
|
190
|
+
note "$agent_id" DRIFT "profile.yaml lacks inherit_from: default (MANUAL: merge contract)"
|
|
191
|
+
DRIFT=$((DRIFT + 1))
|
|
192
|
+
fi
|
|
193
|
+
|
|
194
|
+
# 3. Profile symlink ~/.hermes/profiles/<profile_name> -> runtime.
|
|
195
|
+
# LOAD-BEARING: hermes resolves agents launched as `hermes --profile <name>`
|
|
196
|
+
# through this path (and recreates it as a fresh standalone dir if missing,
|
|
197
|
+
# disconnecting the agent from its runtime) — so it MUST point at the runtime.
|
|
198
|
+
link="$PROFILES_DIR/$profile_name"
|
|
199
|
+
if [[ -L "$link" ]]; then
|
|
200
|
+
if [[ "$(readlink -f "$link")" != "$(readlink -f "$runtime")" ]]; then
|
|
201
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
202
|
+
ln -sfn "$runtime" "$link"
|
|
203
|
+
note "$agent_id" FIXED "profile symlink repointed -> $runtime"
|
|
204
|
+
changed=1; FIXED=$((FIXED + 1))
|
|
205
|
+
else
|
|
206
|
+
note "$agent_id" DRIFT "profile symlink points at $(readlink "$link")"
|
|
207
|
+
DRIFT=$((DRIFT + 1))
|
|
208
|
+
fi
|
|
209
|
+
fi
|
|
210
|
+
elif [[ -e "$link" ]]; then
|
|
211
|
+
note "$agent_id" DRIFT "$link is a real directory, expected symlink (MANUAL: merge state)"
|
|
212
|
+
DRIFT=$((DRIFT + 1))
|
|
213
|
+
else
|
|
214
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
215
|
+
mkdir -p "$PROFILES_DIR"
|
|
216
|
+
ln -sfn "$runtime" "$link"
|
|
217
|
+
note "$agent_id" FIXED "profile symlink created -> $runtime"
|
|
218
|
+
changed=1; FIXED=$((FIXED + 1))
|
|
219
|
+
else
|
|
220
|
+
note "$agent_id" DRIFT "profile symlink missing"
|
|
221
|
+
DRIFT=$((DRIFT + 1))
|
|
222
|
+
fi
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
# 4. role.yaml binds the registry profile name.
|
|
226
|
+
role_yaml="$role_dir/role.yaml"
|
|
227
|
+
if [[ -f "$role_yaml" ]] && ! grep -q "^profile: $profile_name$" "$role_yaml"; then
|
|
228
|
+
if [[ $APPLY -eq 1 ]] && grep -q '^profile: ' "$role_yaml"; then
|
|
229
|
+
sed -i "s/^profile: .*/profile: $profile_name/" "$role_yaml"
|
|
230
|
+
note "$agent_id" FIXED "role.yaml profile -> $profile_name"
|
|
231
|
+
changed=1; FIXED=$((FIXED + 1))
|
|
232
|
+
else
|
|
233
|
+
note "$agent_id" DRIFT "role.yaml profile != $profile_name"
|
|
234
|
+
DRIFT=$((DRIFT + 1))
|
|
235
|
+
fi
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
# 5. PM fleet voice contract: runtime plugin symlink + Voxxy config + VOX_URL.
|
|
239
|
+
role_name="$(basename "$role_dir")"
|
|
240
|
+
if [[ "$role_name" == "pm" && -d "$runtime" ]]; then
|
|
241
|
+
plugin_link="$runtime/plugins/tts/voxxy"
|
|
242
|
+
if [[ -d "$VOXXY_PLUGIN_DIR" ]]; then
|
|
243
|
+
if [[ -L "$plugin_link" ]]; then
|
|
244
|
+
if [[ "$(readlink -f "$plugin_link")" != "$(readlink -f "$VOXXY_PLUGIN_DIR")" ]]; then
|
|
245
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
246
|
+
mkdir -p "$runtime/plugins/tts"
|
|
247
|
+
ln -sfn "$VOXXY_PLUGIN_DIR" "$plugin_link"
|
|
248
|
+
note "$agent_id" FIXED "runtime Voxxy plugin relinked"
|
|
249
|
+
changed=1; FIXED=$((FIXED + 1))
|
|
250
|
+
else
|
|
251
|
+
note "$agent_id" DRIFT "runtime Voxxy plugin points at $(readlink "$plugin_link")"
|
|
252
|
+
DRIFT=$((DRIFT + 1))
|
|
253
|
+
fi
|
|
254
|
+
fi
|
|
255
|
+
elif [[ -e "$plugin_link" ]]; then
|
|
256
|
+
note "$agent_id" DRIFT "$plugin_link exists and is not a symlink (MANUAL: merge runtime plugin state)"
|
|
257
|
+
DRIFT=$((DRIFT + 1))
|
|
258
|
+
else
|
|
259
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
260
|
+
mkdir -p "$runtime/plugins/tts"
|
|
261
|
+
ln -sfn "$VOXXY_PLUGIN_DIR" "$plugin_link"
|
|
262
|
+
note "$agent_id" FIXED "runtime Voxxy plugin linked"
|
|
263
|
+
changed=1; FIXED=$((FIXED + 1))
|
|
264
|
+
else
|
|
265
|
+
note "$agent_id" DRIFT "runtime Voxxy plugin missing"
|
|
266
|
+
DRIFT=$((DRIFT + 1))
|
|
267
|
+
fi
|
|
268
|
+
fi
|
|
269
|
+
else
|
|
270
|
+
note "$agent_id" DRIFT "configured Voxxy plugin dir missing: $VOXXY_PLUGIN_DIR (MANUAL: install voxxy repo)"
|
|
271
|
+
DRIFT=$((DRIFT + 1))
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
runtime_env="$runtime/.env"
|
|
275
|
+
if [[ -n "$VOX_URL_VALUE" ]]; then
|
|
276
|
+
vox_env_status="$(python3 - "$runtime_env" "$VOX_URL_VALUE" <<'PYEOF'
|
|
277
|
+
import pathlib, sys
|
|
278
|
+
path = pathlib.Path(sys.argv[1])
|
|
279
|
+
expected = sys.argv[2]
|
|
280
|
+
if not path.exists():
|
|
281
|
+
print('missing-file')
|
|
282
|
+
raise SystemExit(0)
|
|
283
|
+
for line in path.read_text().splitlines():
|
|
284
|
+
if line.startswith('VOX_URL='):
|
|
285
|
+
current = line.split('=', 1)[1].strip().strip('"').strip("'")
|
|
286
|
+
print('ok' if current == expected else 'mismatch')
|
|
287
|
+
raise SystemExit(0)
|
|
288
|
+
print('missing-key')
|
|
289
|
+
PYEOF
|
|
290
|
+
)"
|
|
291
|
+
case "$vox_env_status" in
|
|
292
|
+
ok) ;;
|
|
293
|
+
*)
|
|
294
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
295
|
+
ensure_env_key "$runtime_env" VOX_URL "$VOX_URL_VALUE"
|
|
296
|
+
chmod 600 "$runtime_env" 2>/dev/null || true
|
|
297
|
+
note "$agent_id" FIXED "runtime .env VOX_URL -> $VOX_URL_VALUE"
|
|
298
|
+
changed=1; FIXED=$((FIXED + 1))
|
|
299
|
+
else
|
|
300
|
+
note "$agent_id" DRIFT "runtime .env missing/incorrect VOX_URL"
|
|
301
|
+
DRIFT=$((DRIFT + 1))
|
|
302
|
+
fi
|
|
303
|
+
;;
|
|
304
|
+
esac
|
|
305
|
+
fi
|
|
306
|
+
|
|
307
|
+
config_status="$(python3 - "$runtime/config.yaml" <<'PYEOF'
|
|
308
|
+
import pathlib, sys
|
|
309
|
+
|
|
310
|
+
path = pathlib.Path(sys.argv[1])
|
|
311
|
+
if not path.exists():
|
|
312
|
+
print('missing')
|
|
313
|
+
raise SystemExit(0)
|
|
314
|
+
text = path.read_text()
|
|
315
|
+
provider = 'voxxy' if 'provider: voxxy' in text else 'other'
|
|
316
|
+
plugin = 'yes' if 'tts/voxxy' in text else 'no'
|
|
317
|
+
voice = 'rick' if 'voice: rick' in text else 'other'
|
|
318
|
+
print(f'{provider}|{plugin}|{voice}')
|
|
319
|
+
PYEOF
|
|
320
|
+
)"
|
|
321
|
+
if [[ "$config_status" != "voxxy|yes|rick" ]]; then
|
|
322
|
+
if [[ "$APPLY" -eq 1 ]]; then
|
|
323
|
+
HERMES_HOME="$runtime" "${HERMES_FLEET_BIN:-$(cfg fleet.hermes_bin "$HOME/.hermes/hermes-agent/.venv/bin/hermes")}" config set plugins.enabled.0 tts/voxxy >/dev/null 2>&1 || true
|
|
324
|
+
HERMES_HOME="$runtime" "${HERMES_FLEET_BIN:-$(cfg fleet.hermes_bin "$HOME/.hermes/hermes-agent/.venv/bin/hermes")}" config set tts.provider voxxy >/dev/null 2>&1 || true
|
|
325
|
+
HERMES_HOME="$runtime" "${HERMES_FLEET_BIN:-$(cfg fleet.hermes_bin "$HOME/.hermes/hermes-agent/.venv/bin/hermes")}" config set tts.voice rick >/dev/null 2>&1 || true
|
|
326
|
+
note "$agent_id" FIXED "runtime TTS config enforced -> voxxy/rick"
|
|
327
|
+
changed=1; FIXED=$((FIXED + 1))
|
|
328
|
+
else
|
|
329
|
+
note "$agent_id" DRIFT "runtime Voxxy config missing from config.yaml"
|
|
330
|
+
DRIFT=$((DRIFT + 1))
|
|
331
|
+
fi
|
|
332
|
+
fi
|
|
333
|
+
fi
|
|
334
|
+
|
|
335
|
+
if [[ $changed -eq 1 && $RESTART -eq 1 ]]; then
|
|
336
|
+
[[ -n "$gateway_unit" ]] && RESTART_UNITS+=("$gateway_unit")
|
|
337
|
+
[[ -n "$consumer_unit" ]] && RESTART_UNITS+=("$consumer_unit")
|
|
338
|
+
fi
|
|
339
|
+
done < <(read_registry)
|
|
340
|
+
|
|
341
|
+
# 5. Stale registry symlinks: entries pointing into fleet runtimes under a
|
|
342
|
+
# name the registry does not know. Reported only — removal is a human call.
|
|
343
|
+
known_names="$(read_registry | cut -f3 | sort -u)"
|
|
344
|
+
if [[ -d "$PROFILES_DIR" ]]; then
|
|
345
|
+
for link in "$PROFILES_DIR"/*; do
|
|
346
|
+
[[ -L "$link" ]] || continue
|
|
347
|
+
name="$(basename "$link")"
|
|
348
|
+
grep -qx "$name" <<<"$known_names" && continue
|
|
349
|
+
target="$(readlink "$link")"
|
|
350
|
+
if [[ "$target" == */agents/hermes/* ]]; then
|
|
351
|
+
note "$name" STALE "registry symlink not in agents-registry -> $target (MANUAL: remove or register)"
|
|
352
|
+
DRIFT=$((DRIFT + 1))
|
|
353
|
+
fi
|
|
354
|
+
done
|
|
355
|
+
fi
|
|
356
|
+
|
|
357
|
+
if [[ ${#RESTART_UNITS[@]} -gt 0 ]]; then
|
|
358
|
+
echo
|
|
359
|
+
echo "Restarting: ${RESTART_UNITS[*]}"
|
|
360
|
+
systemctl --user try-restart "${RESTART_UNITS[@]}" || true
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
echo
|
|
364
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
365
|
+
echo "fleet-sync: $FIXED fixed, $DRIFT unresolved (manual)"
|
|
366
|
+
else
|
|
367
|
+
echo "fleet-sync: $DRIFT drift item(s). Re-run with --apply to fix."
|
|
368
|
+
fi
|
|
369
|
+
[[ $DRIFT -eq 0 ]] || exit 1
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# migrate-unify — one-time fleet migration to the unified single-PM model.
|
|
5
|
+
#
|
|
6
|
+
# Decommissions the legacy `scrum-master` agents, collapsing each repo from a
|
|
7
|
+
# PM + scrum-master pair down to a single PM. The PM keeps running untouched;
|
|
8
|
+
# its checkpoint→heartbeat refresh is a separate uniform pass (and with
|
|
9
|
+
# reconcile disabled by default the PM heartbeat is checkpoint-only, so removing
|
|
10
|
+
# the scrum-master is behavior-complete for the collapse).
|
|
11
|
+
#
|
|
12
|
+
# Drives ENTIRELY off the registry's role=scrum-master entries
|
|
13
|
+
# (role_dir / project_path / runtime_repo) — NEVER name-derived paths, because
|
|
14
|
+
# keepy-money lives at ~/code/tiller, zshyzsh at ~/.config/zshyzsh, and
|
|
15
|
+
# delocontainers at ~/docker.
|
|
16
|
+
#
|
|
17
|
+
# Safety model:
|
|
18
|
+
# * DRY-RUN by default — prints exactly what it WOULD do, touches nothing.
|
|
19
|
+
# * --apply performs the REVERSIBLE local teardown:
|
|
20
|
+
# - stop + disable + remove the scrum-master's systemd --user units
|
|
21
|
+
# - `git submodule deinit` + `git rm` the scrum-master role dir + clean
|
|
22
|
+
# .gitmodules / .git/modules (STAGED only — never commits or pushes)
|
|
23
|
+
# - remove the ~/.hermes/profiles/<sm-id> symlink
|
|
24
|
+
# - drop the scrum-master entry from the fleet registry (.bak first)
|
|
25
|
+
# * The IRREVERSIBLE `gh repo delete` of each scrum-master runtime repo is held
|
|
26
|
+
# behind BOTH --decommission-sm-remotes AND --yes-i-mean-it, and runs only
|
|
27
|
+
# after all local teardown has succeeded.
|
|
28
|
+
#
|
|
29
|
+
# It never commits or pushes any project repo — staged submodule removals are
|
|
30
|
+
# left for you to review and commit (some live in sensitive repos: ~/docker,
|
|
31
|
+
# ~/.config/zshyzsh).
|
|
32
|
+
#
|
|
33
|
+
# Usage:
|
|
34
|
+
# migrate-unify.sh # dry-run report (all scrum-masters)
|
|
35
|
+
# migrate-unify.sh --apply # reversible local teardown
|
|
36
|
+
# migrate-unify.sh --apply --decommission-sm-remotes --yes-i-mean-it
|
|
37
|
+
# # + delete the runtime repos on GitHub
|
|
38
|
+
# migrate-unify.sh --agent pjangler-scrum-master [--apply ...] # one only
|
|
39
|
+
|
|
40
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
41
|
+
|
|
42
|
+
HERMES_TEMPLATE_CONFIG="${HERMES_TEMPLATE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/hermes-agent-template/config.toml}"
|
|
43
|
+
cfg() { # cfg <dotted.key> <default>
|
|
44
|
+
local key="$1" def="$2"
|
|
45
|
+
{ [[ -f "$HERMES_TEMPLATE_CONFIG" ]] && command -v python3 >/dev/null 2>&1; } || { printf '%s' "$def"; return 0; }
|
|
46
|
+
python3 - "$HERMES_TEMPLATE_CONFIG" "$key" "$def" <<'PYEOF' || printf '%s' "$def"
|
|
47
|
+
import sys, os
|
|
48
|
+
try:
|
|
49
|
+
import tomllib
|
|
50
|
+
except ModuleNotFoundError:
|
|
51
|
+
print(sys.argv[3], end=""); sys.exit(0)
|
|
52
|
+
try:
|
|
53
|
+
cur = tomllib.load(open(sys.argv[1], "rb"))
|
|
54
|
+
except Exception:
|
|
55
|
+
print(sys.argv[3], end=""); sys.exit(0)
|
|
56
|
+
for p in sys.argv[2].split("."):
|
|
57
|
+
if isinstance(cur, dict) and p in cur:
|
|
58
|
+
cur = cur[p]
|
|
59
|
+
else:
|
|
60
|
+
print(sys.argv[3], end=""); sys.exit(0)
|
|
61
|
+
print(os.path.expanduser(str(cur)), end="")
|
|
62
|
+
PYEOF
|
|
63
|
+
return 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
FLEET_ENV="${HERMES_FLEET_ENV:-$(cfg fleet.fleet_env "$HOME/.hermes/fleet.env")}"
|
|
67
|
+
[[ -f "$FLEET_ENV" ]] && { # shellcheck disable=SC1090
|
|
68
|
+
source "$FLEET_ENV"; }
|
|
69
|
+
REGISTRY_FILE="${HERMES_FLEET_REGISTRY_FILE:-$(cfg fleet.registry_file "$HOME/.hermes/agents-registry.yaml")}"
|
|
70
|
+
PROFILES_DIR="${HERMES_FLEET_HOME:-$HOME/.hermes}/profiles"
|
|
71
|
+
SYS_DIR="$HOME/.config/systemd/user"
|
|
72
|
+
|
|
73
|
+
APPLY=0
|
|
74
|
+
DELETE_REMOTES=0
|
|
75
|
+
CONFIRM=0
|
|
76
|
+
declare -a ONLY_AGENTS=()
|
|
77
|
+
while [[ $# -gt 0 ]]; do
|
|
78
|
+
case "$1" in
|
|
79
|
+
--apply) APPLY=1 ;;
|
|
80
|
+
--decommission-sm-remotes) DELETE_REMOTES=1 ;;
|
|
81
|
+
--yes-i-mean-it) CONFIRM=1 ;;
|
|
82
|
+
--all) : ;; # accepted for symmetry; default is already all scrum-masters
|
|
83
|
+
--agent) shift; ONLY_AGENTS+=("$1") ;;
|
|
84
|
+
-h|--help) sed -n '3,55p' "$0"; exit 0 ;;
|
|
85
|
+
*) echo "migrate-unify: unknown flag $1" >&2; exit 2 ;;
|
|
86
|
+
esac
|
|
87
|
+
shift
|
|
88
|
+
done
|
|
89
|
+
|
|
90
|
+
[[ -f "$REGISTRY_FILE" ]] || { echo "migrate-unify: registry not found: $REGISTRY_FILE" >&2; exit 2; }
|
|
91
|
+
command -v python3 >/dev/null || { echo "migrate-unify: python3 required" >&2; exit 2; }
|
|
92
|
+
|
|
93
|
+
MODE="DRY-RUN"; [[ $APPLY -eq 1 ]] && MODE="APPLY"
|
|
94
|
+
echo "migrate-unify — mode: $MODE registry: $REGISTRY_FILE"
|
|
95
|
+
echo "================================================================"
|
|
96
|
+
|
|
97
|
+
# Emit scrum-master rows (pjangler first), TSV: agent_id, repo, role_dir, project_path, runtime_repo, profile_name
|
|
98
|
+
read_sm() {
|
|
99
|
+
python3 - "$REGISTRY_FILE" <<'PYEOF'
|
|
100
|
+
import sys, yaml
|
|
101
|
+
data = yaml.safe_load(open(sys.argv[1], encoding="utf-8")) or {}
|
|
102
|
+
rows = []
|
|
103
|
+
for aid, a in (data.get("agents") or {}).items():
|
|
104
|
+
if not isinstance(a, dict) or a.get("role") != "scrum-master":
|
|
105
|
+
continue
|
|
106
|
+
rows.append((aid, str(a.get("repo") or ""), str(a.get("role_dir") or ""),
|
|
107
|
+
str(a.get("project_path") or ""), str(a.get("runtime_repo") or ""),
|
|
108
|
+
str(a.get("profile_name") or aid)))
|
|
109
|
+
# pjangler first (the reference repo / canary), then alphabetical.
|
|
110
|
+
rows.sort(key=lambda r: (r[1] != "pjangler", r[0]))
|
|
111
|
+
for r in rows:
|
|
112
|
+
print("\t".join(r))
|
|
113
|
+
PYEOF
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
wanted() {
|
|
117
|
+
[[ ${#ONLY_AGENTS[@]} -eq 0 ]] && return 0
|
|
118
|
+
local a; for a in "${ONLY_AGENTS[@]}"; do [[ "$a" == "$1" ]] && return 0; done; return 1
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
run() { # run <human-description> -- <cmd...> (prints always; executes only on --apply)
|
|
122
|
+
local desc="$1"; shift
|
|
123
|
+
[[ "${1:-}" == "--" ]] && shift
|
|
124
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
125
|
+
printf ' [do] %s\n' "$desc"
|
|
126
|
+
local rc=0
|
|
127
|
+
"$@" >/dev/null 2>&1 || rc=$?
|
|
128
|
+
if [[ $rc -ne 0 ]]; then printf ' (non-fatal: returned %s)\n' "$rc"; fi
|
|
129
|
+
else
|
|
130
|
+
printf ' [dry] %s\n' "$desc"
|
|
131
|
+
fi
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
declare -a REMOVE_IDS=() # registry ids to drop (batched)
|
|
135
|
+
declare -a DELETE_REPOS=() # runtime repos to delete (gated, last)
|
|
136
|
+
declare -a STAGED_REPOS=() # project repos left with staged submodule removals
|
|
137
|
+
PROCESSED=0
|
|
138
|
+
|
|
139
|
+
while IFS=$'\t' read -r sm_id repo role_dir project_path runtime_repo profile_name; do
|
|
140
|
+
wanted "$sm_id" || continue
|
|
141
|
+
PROCESSED=$((PROCESSED + 1))
|
|
142
|
+
echo
|
|
143
|
+
echo ">>> $sm_id (repo=$repo)"
|
|
144
|
+
echo " role_dir : $role_dir"
|
|
145
|
+
echo " project_path : $project_path"
|
|
146
|
+
echo " runtime_repo : $runtime_repo"
|
|
147
|
+
|
|
148
|
+
# 1. systemd --user units: stop + disable + remove every hermes-<sm_id>-* unit.
|
|
149
|
+
shopt -s nullglob
|
|
150
|
+
units=("$SYS_DIR/hermes-${sm_id}-"*)
|
|
151
|
+
shopt -u nullglob
|
|
152
|
+
if [[ ${#units[@]} -eq 0 ]]; then
|
|
153
|
+
echo " units : (none found on disk)"
|
|
154
|
+
else
|
|
155
|
+
for uf in "${units[@]}"; do
|
|
156
|
+
u="$(basename "$uf")"
|
|
157
|
+
run "stop+disable $u" -- systemctl --user disable --now "$u"
|
|
158
|
+
run "rm unit file $u" -- rm -f "$uf"
|
|
159
|
+
done
|
|
160
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
161
|
+
systemctl --user daemon-reload 2>/dev/null || true
|
|
162
|
+
systemctl --user reset-failed 2>/dev/null || true
|
|
163
|
+
fi
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# 2. scrum-master runtime submodule + role dir (STAGED removal, never committed).
|
|
167
|
+
if [[ -n "$project_path" && ( -d "$project_path/.git" || -f "$project_path/.git" ) ]]; then
|
|
168
|
+
sm_rel="agents/hermes/scrum-master"
|
|
169
|
+
if [[ -e "$project_path/$sm_rel" ]]; then
|
|
170
|
+
run "git submodule deinit -f $sm_rel/runtime" -- git -C "$project_path" submodule deinit -f "$sm_rel/runtime"
|
|
171
|
+
run "git rm -rf $sm_rel (staged)" -- git -C "$project_path" rm -rf "$sm_rel"
|
|
172
|
+
run "rm .git/modules/$sm_rel residue" -- rm -rf "$project_path/.git/modules/$sm_rel"
|
|
173
|
+
run "scrub .gitmodules section" -- git -C "$project_path" config -f "$project_path/.gitmodules" --remove-section "submodule.$sm_rel/runtime"
|
|
174
|
+
if [[ $APPLY -eq 1 ]]; then STAGED_REPOS+=("$project_path"); fi
|
|
175
|
+
else
|
|
176
|
+
echo " role dir : already absent at $project_path/$sm_rel"
|
|
177
|
+
fi
|
|
178
|
+
else
|
|
179
|
+
echo " submodule : project_path is not a git repo, skipping submodule teardown: $project_path"
|
|
180
|
+
fi
|
|
181
|
+
|
|
182
|
+
# 3. profile symlink ~/.hermes/profiles/<sm_id>.
|
|
183
|
+
link="$PROFILES_DIR/$profile_name"
|
|
184
|
+
if [[ -L "$link" ]]; then
|
|
185
|
+
run "rm profile symlink $link" -- rm -f "$link"
|
|
186
|
+
elif [[ -e "$link" ]]; then
|
|
187
|
+
echo " profile : $link is a real dir, NOT a symlink — left untouched (MANUAL)"
|
|
188
|
+
else
|
|
189
|
+
echo " profile : no symlink at $link"
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# 4. registry entry (batched; rewritten once at the end with a .bak).
|
|
193
|
+
REMOVE_IDS+=("$sm_id")
|
|
194
|
+
echo " registry : will drop agents.$sm_id"
|
|
195
|
+
|
|
196
|
+
# 5. runtime repo delete (gated, deferred to the very end).
|
|
197
|
+
[[ -n "$runtime_repo" ]] && DELETE_REPOS+=("$runtime_repo")
|
|
198
|
+
done < <(read_sm)
|
|
199
|
+
|
|
200
|
+
if [[ $PROCESSED -eq 0 ]]; then
|
|
201
|
+
echo; echo "migrate-unify: no scrum-master agents matched. Nothing to do."
|
|
202
|
+
exit 0
|
|
203
|
+
fi
|
|
204
|
+
|
|
205
|
+
# --- Registry rewrite (batched) ---
|
|
206
|
+
echo
|
|
207
|
+
if [[ ${#REMOVE_IDS[@]} -gt 0 ]]; then
|
|
208
|
+
if [[ $APPLY -eq 1 ]]; then
|
|
209
|
+
python3 - "$REGISTRY_FILE" "${REMOVE_IDS[@]}" <<'PYEOF'
|
|
210
|
+
import sys, shutil, datetime, yaml
|
|
211
|
+
path = sys.argv[1]; ids = sys.argv[2:]
|
|
212
|
+
shutil.copy(path, path + ".bak-" + datetime.datetime.now().strftime("%Y%m%d%H%M%S"))
|
|
213
|
+
data = yaml.safe_load(open(path, encoding="utf-8")) or {}
|
|
214
|
+
agents = data.get("agents") or {}
|
|
215
|
+
removed = [i for i in ids if agents.pop(i, None) is not None]
|
|
216
|
+
data["agents"] = agents
|
|
217
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
218
|
+
f.write(yaml.safe_dump(data, sort_keys=False))
|
|
219
|
+
print(" registry: removed " + ", ".join(removed) + " (backup written)")
|
|
220
|
+
PYEOF
|
|
221
|
+
else
|
|
222
|
+
echo " [dry] would drop registry entries: ${REMOVE_IDS[*]} (after .bak)"
|
|
223
|
+
fi
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
# --- Irreversible remote deletes (double-gated, last) ---
|
|
227
|
+
echo
|
|
228
|
+
echo "Runtime repos to delete on GitHub:"
|
|
229
|
+
printf ' %s\n' "${DELETE_REPOS[@]}"
|
|
230
|
+
if [[ ${#DELETE_REPOS[@]} -gt 0 ]]; then
|
|
231
|
+
if [[ $APPLY -eq 1 && $DELETE_REMOTES -eq 1 && $CONFIRM -eq 1 ]]; then
|
|
232
|
+
if command -v gh >/dev/null 2>&1; then
|
|
233
|
+
for r in "${DELETE_REPOS[@]}"; do
|
|
234
|
+
printf ' [do] gh repo delete %s --yes\n' "$r"
|
|
235
|
+
gh repo delete "$r" --yes 2>&1 | tail -1 || echo " (delete failed or already gone: $r)"
|
|
236
|
+
done
|
|
237
|
+
else
|
|
238
|
+
echo " gh not on PATH — cannot delete remotes."
|
|
239
|
+
fi
|
|
240
|
+
else
|
|
241
|
+
echo " [HELD] add --apply --decommission-sm-remotes --yes-i-mean-it to delete these (IRREVERSIBLE)."
|
|
242
|
+
fi
|
|
243
|
+
fi
|
|
244
|
+
|
|
245
|
+
echo
|
|
246
|
+
echo "================================================================"
|
|
247
|
+
echo "migrate-unify: processed $PROCESSED scrum-master agent(s) in $MODE mode."
|
|
248
|
+
if [[ $APPLY -eq 1 && ${#STAGED_REPOS[@]} -gt 0 ]]; then
|
|
249
|
+
echo
|
|
250
|
+
echo "Review + commit the staged scrum-master removals in these repos (NOT auto-committed):"
|
|
251
|
+
printf '%s\n' "${STAGED_REPOS[@]}" | sort -u | sed 's/^/ git -C /; s/$/ status/'
|
|
252
|
+
fi
|