@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.
Files changed (139) hide show
  1. package/.mise/scripts/link-agentfiles.sh +9 -0
  2. package/dist/index.js +1492 -358
  3. package/dist/mcp-server.js +2391 -290
  4. package/package.json +5 -2
  5. package/templates/commonproject/.mise/scripts/create-plane-project.sh +26 -0
  6. package/templates/commonproject/copier.yml +2 -0
  7. package/templates/commonproject/template/.agents/hooks/README.md +151 -0
  8. package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +63 -0
  9. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-journal-write.sh +327 -0
  10. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-recall.sh +180 -0
  11. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-retain.sh +74 -0
  12. package/templates/commonproject/template/.agents/hooks/hindsight/hindsight-session-end.sh +57 -0
  13. package/templates/commonproject/template/.agents/hooks/hindsight/lib/hindsight-bank.sh +57 -0
  14. package/templates/commonproject/template/.agents/hooks/hindsight/lib/hindsight-journal.sh +48 -0
  15. package/templates/commonproject/template/.agents/hooks/hooks.master.json +97 -0
  16. package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +30 -0
  17. package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +51 -0
  18. package/templates/commonproject/template/.agents/hooks/reminder-for-skill-check.sh +20 -0
  19. package/templates/commonproject/template/.agents/hooks/sync.py +618 -0
  20. package/templates/commonproject/template/.agents/local.example.json +15 -0
  21. package/templates/commonproject/template/.mise/scripts/hindsight-setup.sh +62 -0
  22. package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +110 -0
  23. package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +45 -0
  24. package/templates/commonproject/template/mise.toml.jinja +42 -0
  25. package/templates/hermes-agent/.codegraph/daemon.pid +6 -0
  26. package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +10 -0
  27. package/templates/hermes-agent/README.md +16 -14
  28. package/templates/hermes-agent/copier.yml +7 -25
  29. package/templates/hermes-agent/docs/architecture.md +16 -18
  30. package/templates/hermes-agent/docs/fleet-control-plane/README.md +35 -0
  31. package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +253 -0
  32. package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +280 -0
  33. package/templates/hermes-agent/docs/fleet-control-plane/implementation-readiness-report-2026-06-27.md +118 -0
  34. package/templates/hermes-agent/docs/fleet-control-plane/prd.md +141 -0
  35. package/templates/hermes-agent/docs/operations.md +17 -19
  36. package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +17 -13
  37. package/templates/hermes-agent/docs/{scrum-master → sentinel}/README.md +47 -46
  38. package/templates/hermes-agent/docs/{scrum-master → sentinel}/architecture.md +39 -38
  39. package/templates/hermes-agent/docs/{scrum-master → sentinel}/development.md +63 -99
  40. package/templates/hermes-agent/docs/{scrum-master → sentinel}/providers.md +2 -2
  41. package/templates/hermes-agent/docs/sentinel.md +18 -0
  42. package/templates/hermes-agent/install-local.sh +22 -20
  43. package/templates/hermes-agent/runtime-scaffold/README.md +10 -9
  44. package/templates/hermes-agent/scripts/backfill-fleet-sot.sh +12 -21
  45. package/templates/hermes-agent/scripts/fleet-sync.sh +369 -0
  46. package/templates/hermes-agent/scripts/migrate-unify.sh +252 -0
  47. package/templates/hermes-agent/scripts/unify-pm.sh +148 -0
  48. package/templates/hermes-agent/template/.runtime-scaffold/README.md +10 -9
  49. package/templates/hermes-agent/template/.scripts/01-config.sh +2 -3
  50. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -2
  51. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +47 -19
  52. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +71 -27
  53. package/templates/hermes-agent/template/.scripts/30-telegram.sh +3 -3
  54. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +3 -3
  55. package/templates/hermes-agent/template/.scripts/70-systemd.sh +48 -43
  56. package/templates/hermes-agent/template/.scripts/80-registry.sh +2 -2
  57. package/templates/hermes-agent/template/.scripts/99-summary.sh +3 -4
  58. package/templates/hermes-agent/template/.scripts/_lib.sh +4 -19
  59. package/templates/hermes-agent/template/.scripts/checkpoint.sh +36 -0
  60. package/templates/hermes-agent/template/.scripts/config.example.toml +11 -5
  61. package/templates/hermes-agent/template/.scripts/{scrum-master/continuous-ticket-sentinel.sh → heartbeat.sh} +84 -31
  62. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +1 -1
  63. package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -3
  64. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/emit-event.py +5 -5
  65. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/issue-autonomous-review.sh +6 -6
  66. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/bin/issue-close-gate.sh +2 -2
  67. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/autonomous-delegated-review.md +7 -7
  68. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/bloodbank-events.md +3 -3
  69. package/templates/hermes-agent/template/.scripts/{scrum-master → sentinel}/docs/continuous-ticket-orchestration.md +6 -6
  70. package/templates/hermes-agent/template/.scripts/{scrum-master/continuous-ticket-sentinel.prompt.md.jinja → sentinel.prompt.md.jinja} +5 -4
  71. package/templates/hermes-agent/template/SOUL.md.jinja +3 -6
  72. package/templates/hermes-agent/template/hermes.jinja +6 -15
  73. package/templates/hermes-agent/template/role.yaml.jinja +9 -8
  74. package/templates/commonproject/.claude/commands/bmad/bmb/agents/agent-builder.md +0 -14
  75. package/templates/commonproject/.claude/commands/bmad/bmb/agents/module-builder.md +0 -14
  76. package/templates/commonproject/.claude/commands/bmad/bmb/agents/workflow-builder.md +0 -14
  77. package/templates/commonproject/.claude/commands/bmad/bmb/workflows/agent.md +0 -5
  78. package/templates/commonproject/.claude/commands/bmad/bmb/workflows/module.md +0 -5
  79. package/templates/commonproject/.claude/commands/bmad/bmb/workflows/workflow.md +0 -5
  80. package/templates/commonproject/.claude/commands/bmad/bmm/agents/analyst.md +0 -14
  81. package/templates/commonproject/.claude/commands/bmad/bmm/agents/architect.md +0 -14
  82. package/templates/commonproject/.claude/commands/bmad/bmm/agents/dev.md +0 -14
  83. package/templates/commonproject/.claude/commands/bmad/bmm/agents/pm.md +0 -14
  84. package/templates/commonproject/.claude/commands/bmad/bmm/agents/quick-flow-solo-dev.md +0 -14
  85. package/templates/commonproject/.claude/commands/bmad/bmm/agents/sm.md +0 -14
  86. package/templates/commonproject/.claude/commands/bmad/bmm/agents/tea.md +0 -14
  87. package/templates/commonproject/.claude/commands/bmad/bmm/agents/tech-writer.md +0 -14
  88. package/templates/commonproject/.claude/commands/bmad/bmm/agents/ux-designer.md +0 -14
  89. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/check-implementation-readiness.md +0 -5
  90. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/code-review.md +0 -13
  91. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/correct-course.md +0 -13
  92. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-architecture.md +0 -5
  93. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-epics-and-stories.md +0 -5
  94. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-dataflow.md +0 -13
  95. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-diagram.md +0 -13
  96. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-flowchart.md +0 -13
  97. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-excalidraw-wireframe.md +0 -13
  98. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-product-brief.md +0 -5
  99. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-story.md +0 -13
  100. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/create-ux-design.md +0 -5
  101. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/dev-story.md +0 -13
  102. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/document-project.md +0 -13
  103. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/generate-project-context.md +0 -5
  104. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/prd.md +0 -5
  105. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/quick-dev.md +0 -5
  106. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/quick-spec.md +0 -5
  107. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/research.md +0 -5
  108. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/retrospective.md +0 -13
  109. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/sprint-planning.md +0 -13
  110. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/sprint-status.md +0 -13
  111. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-atdd.md +0 -13
  112. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-automate.md +0 -13
  113. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-ci.md +0 -13
  114. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-framework.md +0 -13
  115. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-nfr.md +0 -13
  116. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-test-design.md +0 -13
  117. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-test-review.md +0 -13
  118. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/testarch-trace.md +0 -13
  119. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/workflow-init.md +0 -13
  120. package/templates/commonproject/.claude/commands/bmad/bmm/workflows/workflow-status.md +0 -13
  121. package/templates/commonproject/.claude/commands/bmad/cis/agents/brainstorming-coach.md +0 -14
  122. package/templates/commonproject/.claude/commands/bmad/cis/agents/creative-problem-solver.md +0 -14
  123. package/templates/commonproject/.claude/commands/bmad/cis/agents/design-thinking-coach.md +0 -14
  124. package/templates/commonproject/.claude/commands/bmad/cis/agents/innovation-strategist.md +0 -14
  125. package/templates/commonproject/.claude/commands/bmad/cis/agents/presentation-master.md +0 -14
  126. package/templates/commonproject/.claude/commands/bmad/cis/agents/storyteller.md +0 -14
  127. package/templates/commonproject/.claude/commands/bmad/cis/workflows/design-thinking.md +0 -13
  128. package/templates/commonproject/.claude/commands/bmad/cis/workflows/innovation-strategy.md +0 -13
  129. package/templates/commonproject/.claude/commands/bmad/cis/workflows/problem-solving.md +0 -13
  130. package/templates/commonproject/.claude/commands/bmad/cis/workflows/storytelling.md +0 -13
  131. package/templates/commonproject/.claude/commands/bmad/core/agents/bmad-master.md +0 -14
  132. package/templates/commonproject/.claude/commands/bmad/core/tasks/index-docs.md +0 -9
  133. package/templates/commonproject/.claude/commands/bmad/core/tasks/shard-doc.md +0 -9
  134. package/templates/commonproject/.claude/commands/bmad/core/workflows/brainstorming.md +0 -5
  135. package/templates/commonproject/.claude/commands/bmad/core/workflows/party-mode.md +0 -5
  136. package/templates/commonproject/.claude/commands/bmad/custom/workflows/ticket-lifecycle.md +0 -5
  137. package/templates/hermes-agent/docs/scrum-master-sentinel.md +0 -17
  138. package/templates/hermes-agent/template/.scripts/75-scrum-master.sh +0 -117
  139. 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