@delorenj/pjangler 1.4.6 → 1.5.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 (83) hide show
  1. package/README.md +41 -596
  2. package/dist/assets/project-notebook-skill/SHA256SUMS +5 -5
  3. package/dist/assets/project-notebook-skill/export-manifest.json +5 -5
  4. package/dist/assets/project-notebook-skill/hooks/session-end.sh +20 -1
  5. package/dist/assets/project-notebook-skill/hooks/session-start.sh +20 -1
  6. package/dist/assets/project-notebook-skill/references/configuration.md +2 -0
  7. package/dist/assets/project-notebook-skill/scripts/project-hooks.py +33 -3
  8. package/dist/assets/project-notebook-skill/tests/test_project_hooks.py +37 -0
  9. package/dist/index.js +9385 -18379
  10. package/dist/mcp-server.js +9432 -17852
  11. package/dist/notebook/journal-service-main.js +5682 -0
  12. package/dist/project-registry-service.js +748 -0
  13. package/dist/prompt.js +2 -1
  14. package/package.json +15 -10
  15. package/scripts/install-project-registry.mjs +34 -0
  16. package/scripts/migrate-project-registry.mjs +117 -0
  17. package/src/project/registryClient.ts +62 -0
  18. package/templates/commonproject/copier.yml +16 -19
  19. package/templates/commonproject/template/.agents/hooks/README.md +10 -4
  20. package/templates/commonproject/template/.agents/hooks/hermes/hindsight-hook.sh +12 -2
  21. package/templates/commonproject/template/.agents/hooks/hooks.master.json +10 -10
  22. package/templates/commonproject/template/.agents/hooks/lib/hook-guard.sh +13 -0
  23. package/templates/commonproject/template/.agents/hooks/sync.py +66 -17
  24. package/templates/commonproject/template/.agents/skills.json +0 -1
  25. package/templates/commonproject/template/.github/workflows/code-review.yml.jinja +7 -1
  26. package/templates/commonproject/template/.project.json.jinja +1 -1
  27. package/templates/commonproject/template/mise.toml.jinja +11 -21
  28. package/contracts/fleet-contract.yaml +0 -584
  29. package/templates/commonproject/template/.mise/scripts/provision-packs.py +0 -587
  30. package/templates/commonproject/template/.mise/scripts/sync-skills.py +0 -2183
  31. package/templates/hermes-agent/copier.yml +0 -186
  32. package/templates/hermes-agent/template/.gitignore.jinja +0 -11
  33. package/templates/hermes-agent/template/.runtime-scaffold/.gitattributes +0 -10
  34. package/templates/hermes-agent/template/.runtime-scaffold/.gitignore.jinja +0 -44
  35. package/templates/hermes-agent/template/.runtime-scaffold/README.md +0 -56
  36. package/templates/hermes-agent/template/.runtime-scaffold/decisions/.gitkeep +0 -2
  37. package/templates/hermes-agent/template/.runtime-scaffold/logs/.gitkeep +0 -1
  38. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +0 -50
  39. package/templates/hermes-agent/template/.runtime-scaffold/memories/USER.md +0 -51
  40. package/templates/hermes-agent/template/.scripts/00-banner.sh +0 -21
  41. package/templates/hermes-agent/template/.scripts/01-config.sh +0 -60
  42. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -42
  43. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +0 -222
  44. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -199
  45. package/templates/hermes-agent/template/.scripts/30-telegram.sh +0 -363
  46. package/templates/hermes-agent/template/.scripts/31-slack.sh +0 -457
  47. package/templates/hermes-agent/template/.scripts/40-plane.sh +0 -60
  48. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +0 -373
  49. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +0 -18
  50. package/templates/hermes-agent/template/.scripts/70-systemd.sh +0 -354
  51. package/templates/hermes-agent/template/.scripts/80-registry.sh +0 -185
  52. package/templates/hermes-agent/template/.scripts/99-summary.sh +0 -87
  53. package/templates/hermes-agent/template/.scripts/_lib.sh +0 -1138
  54. package/templates/hermes-agent/template/.scripts/channel-transaction.py +0 -2340
  55. package/templates/hermes-agent/template/.scripts/checkpoint.sh +0 -82
  56. package/templates/hermes-agent/template/.scripts/config.example.toml +0 -70
  57. package/templates/hermes-agent/template/.scripts/credential-launch.sh +0 -85
  58. package/templates/hermes-agent/template/.scripts/heartbeat.sh +0 -283
  59. package/templates/hermes-agent/template/.scripts/lib/fleet-env.sh +0 -202
  60. package/templates/hermes-agent/template/.scripts/lib/parse-fleet-env.py +0 -761
  61. package/templates/hermes-agent/template/.scripts/lib/profile-config-lock.py +0 -182
  62. package/templates/hermes-agent/template/.scripts/lib/profile-config-seed.py +0 -108
  63. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +0 -199
  64. package/templates/hermes-agent/template/.scripts/lib/voice-config.py +0 -546
  65. package/templates/hermes-agent/template/.scripts/lifecycle.sh +0 -126
  66. package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +0 -137
  67. package/templates/hermes-agent/template/.scripts/providers/linear.sh +0 -329
  68. package/templates/hermes-agent/template/.scripts/providers/plane.sh +0 -671
  69. package/templates/hermes-agent/template/.scripts/providers/trello.sh +0 -241
  70. package/templates/hermes-agent/template/.scripts/reporter-watchdog.py +0 -242
  71. package/templates/hermes-agent/template/.scripts/secret-scan.py +0 -134
  72. package/templates/hermes-agent/template/.scripts/sentinel/bin/emit-event.py +0 -106
  73. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-autonomous-review.sh +0 -339
  74. package/templates/hermes-agent/template/.scripts/sentinel/bin/issue-close-gate.sh +0 -179
  75. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +0 -162
  76. package/templates/hermes-agent/template/.scripts/sentinel/docs/bloodbank-events.md +0 -42
  77. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +0 -77
  78. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +0 -116
  79. package/templates/hermes-agent/template/.scripts/store-onepassword-secret.py +0 -260
  80. package/templates/hermes-agent/template/SOUL.md.jinja +0 -197
  81. package/templates/hermes-agent/template/hermes.jinja +0 -109
  82. package/templates/hermes-agent/template/momo.jinja +0 -177
  83. package/templates/hermes-agent/template/role.yaml.jinja +0 -112
@@ -1,354 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Install systemd --user units: profile gateway and fused heartbeat timer
3
- # (board-reconciliation sentinel pass + gated runtime checkpoint, one tick).
4
- # shellcheck source=_lib.sh
5
- source "$(dirname "$0")/_lib.sh"
6
- load_role_env
7
-
8
- if [[ "$ROLE" == "reporter" ]]; then
9
- log "[70] reporter systemd — gateway/consumer intentionally not installed"
10
- log " use the reporter runtime's explicit install command after credential and policy preflight"
11
- yaml_upsert_block_value service_state gateway not-applicable
12
- yaml_upsert_block_value service_state heartbeat not-applicable
13
- mark_done 70-systemd
14
- exit 0
15
- fi
16
-
17
- # Resolve expected paths without creating directories or querying the user
18
- # manager. A legacy skip-created marker is complete only when all units exist.
19
- RUNTIME="$ROLE_DIR/runtime"
20
- FLEET_HOME="${HERMES_FLEET_HOME:-$HOME/.hermes}"
21
- PROFILE_HOME="$FLEET_HOME/profiles/${PROFILE_NAME:-$AGENT_ID}"
22
- REPO_ROOT="$(project_repo_path)" || REPO_ROOT="$ROLE_DIR"
23
-
24
- SYS_DIR="$HOME/.config/systemd/user"
25
- GW_UNIT="hermes-${AGENT_ID}-gateway.service"
26
- HB_SVC="hermes-${AGENT_ID}-heartbeat.service"
27
- HB_TIMER="hermes-${AGENT_ID}-heartbeat.timer"
28
-
29
- # Local-only provisioning must not inspect or mutate the host user manager.
30
- # Honor the explicit skip before creating unit directories or querying legacy
31
- # unit state; cleanup remains fail-closed whenever systemd management is active.
32
- if [[ "${SKIP_SYSTEMD:-0}" == "1" ]]; then
33
- if already_done 70-systemd \
34
- && [[ -f "$SYS_DIR/$GW_UNIT" && -f "$SYS_DIR/$HB_SVC" && -f "$SYS_DIR/$HB_TIMER" ]]; then
35
- log "[70] systemd — SKIPPED; existing complete unit set preserved"
36
- else
37
- clear_done 70-systemd
38
- log "[70] systemd — DEFERRED (completion marker cleared)"
39
- fi
40
- exit 0
41
- fi
42
-
43
- # Legacy manifests defaulted reconciliation off and had no way to distinguish
44
- # that default from an operator decision. The new explicit_opt_out sentinel is
45
- # authoritative: migrate unmarked roles to the operational PM default, while a
46
- # rendered/operator-recorded opt-out remains checkpoint-only on every rerun.
47
- if [[ "$(yaml_get reconcile.explicit_opt_out)" == "true" ]]; then
48
- yaml_upsert_block_value reconcile enabled false bool
49
- log " PM reconciliation explicit opt-out preserved"
50
- else
51
- yaml_upsert_block_value reconcile enabled true bool
52
- yaml_upsert_block_value reconcile explicit_opt_out false bool
53
- log " PM reconciliation enabled (operational default)"
54
- fi
55
-
56
- # Render every caller-controlled systemd scalar through the same data-only
57
- # serializer used by fleet backfill. This validation happens before mkdir,
58
- # systemctl, or unit writes, so CR/LF/NUL cannot create a second directive and
59
- # spaces/quotes/backslashes/specifier bytes remain exact.
60
- PYTHON_BIN="$(builtin type -P python3)" || die "python3 is unavailable for systemd unit serialization"
61
- systemd_value() {
62
- "$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-value "$1" \
63
- || die "systemd value validation failed"
64
- }
65
- systemd_scalar() {
66
- "$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-scalar "$1" \
67
- || die "systemd scalar validation failed"
68
- }
69
- systemd_environment() {
70
- "$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-environment "$1" "$2" \
71
- || die "systemd environment validation failed"
72
- }
73
- systemd_exec_value() {
74
- "$PYTHON_BIN" -I "$FLEET_ENV_PARSER" --systemd-exec-value "$1" \
75
- || die "systemd ExecStart value validation failed"
76
- }
77
- GW_DESCRIPTION="$(systemd_scalar "Hermes Gateway — $DISPLAY_NAME")"
78
- HB_DESCRIPTION="$(systemd_scalar "Hermes Heartbeat (reconcile + checkpoint) — $DISPLAY_NAME")"
79
- TIMER_DESCRIPTION="$(systemd_scalar "Heartbeat (reconcile + checkpoint) for $AGENT_ID")"
80
- ENV_HERMES_HOME="$(systemd_environment HERMES_HOME "$PROFILE_HOME")"
81
- ENV_HERMES_BIN="$(systemd_environment HERMES_BIN "$HERMES_BIN")"
82
- ENV_CODEX_HOME="$(systemd_environment CODEX_HOME "$CODEX_HOME")"
83
- ENV_TERMINAL_CWD="$(systemd_environment TERMINAL_CWD "$REPO_ROOT")"
84
- WORKING_DIRECTORY="$(systemd_scalar "$REPO_ROOT")"
85
- RUNTIME_ENV_FILE="$(systemd_scalar "-$RUNTIME/.env")"
86
- GW_LOG_OUTPUT="$(systemd_scalar "append:$RUNTIME/logs/gateway.systemd.log")"
87
- HB_LOG_OUTPUT="$(systemd_scalar "append:$RUNTIME/logs/heartbeat.log")"
88
- GW_EXEC_START="$(systemd_exec_value "$ROLE_DIR/.scripts/credential-launch.sh")"
89
- HB_EXEC_START="$GW_EXEC_START"
90
-
91
- # A model credential may be supplied through systemd's encrypted credential
92
- # store. Chat-channel values are never materialized here: Hermes resolves their
93
- # profile-scoped op:// references natively at process startup.
94
- CREDENTIAL_DIR="${HERMES_SYSTEMD_CREDENTIAL_DIR:-$HOME/.config/hermes-agent/credentials}"
95
- MODEL_CREDENTIAL="$CREDENTIAL_DIR/${AGENT_ID}-model-api-key.cred"
96
- GW_CREDENTIAL_LINES=""
97
- HB_CREDENTIAL_LINES=""
98
- if [[ -f "$MODEL_CREDENTIAL" ]]; then
99
- [[ -n "$(yaml_get model.key_env)" ]] \
100
- || die "encrypted model credential exists but model.key_env is blank in role.yaml"
101
- command -v systemd-creds >/dev/null 2>&1 \
102
- || die "encrypted model credential exists but systemd-creds is unavailable"
103
- GW_CREDENTIAL_LINES="${GW_CREDENTIAL_LINES}${GW_CREDENTIAL_LINES:+$'\n'}LoadCredentialEncrypted=$(systemd_value "model_api_key:$MODEL_CREDENTIAL")"
104
- HB_CREDENTIAL_LINES="LoadCredentialEncrypted=$(systemd_value "model_api_key:$MODEL_CREDENTIAL")"
105
- fi
106
-
107
- # A gateway is eligible only when at least one channel identity is verified AND
108
- # both the named-profile mapping and the referenced 1Password value resolve.
109
- # Role metadata alone is not sufficient: a stale done marker must never revive
110
- # a credential-less crash loop.
111
- gateway_ready=0
112
- gateway_validation_unavailable=0
113
- if [[ "$(yaml_get telegram.provisioning_status)" == "verified" ]] \
114
- && profile_onepassword_ref_exists "$PROFILE_HOME" TELEGRAM_BOT_TOKEN; then
115
- telegram_reference_rc=0
116
- profile_onepassword_ref_validate "$PROFILE_HOME" TELEGRAM_BOT_TOKEN \
117
- || telegram_reference_rc=$?
118
- if [[ $telegram_reference_rc -eq 0 ]]; then
119
- gateway_ready=1
120
- elif [[ $telegram_reference_rc -eq 75 ]]; then
121
- gateway_validation_unavailable=1
122
- fi
123
- fi
124
- if [[ "$(yaml_get slack.provisioning_status)" == "verified" ]] \
125
- && profile_onepassword_ref_exists "$PROFILE_HOME" SLACK_BOT_TOKEN \
126
- && profile_onepassword_ref_exists "$PROFILE_HOME" SLACK_APP_TOKEN; then
127
- slack_bot_reference_rc=0
128
- slack_app_reference_rc=0
129
- profile_onepassword_ref_validate "$PROFILE_HOME" SLACK_BOT_TOKEN \
130
- || slack_bot_reference_rc=$?
131
- profile_onepassword_ref_validate "$PROFILE_HOME" SLACK_APP_TOKEN \
132
- || slack_app_reference_rc=$?
133
- if [[ $slack_bot_reference_rc -eq 0 && $slack_app_reference_rc -eq 0 ]]; then
134
- gateway_ready=1
135
- elif [[ $slack_bot_reference_rc -eq 75 || $slack_app_reference_rc -eq 75 ]]; then
136
- gateway_validation_unavailable=1
137
- fi
138
- fi
139
- if [[ $gateway_ready -eq 0 && $gateway_validation_unavailable -eq 1 ]]; then
140
- die "channel 1Password validation is temporarily unavailable; preserving existing gateway state for retry"
141
- fi
142
-
143
- # Singleton-runtime contract: units set HERMES_HOME to the agent's NAMED PROFILE
144
- # dir, never the raw runtime path — Hermes derives profile identity and shared
145
- # fleet auth from the unresolved HERMES_HOME. $RUNTIME stays correct for
146
- # EnvironmentFile and logs: those are the repo-owned side the profile links to.
147
- mkdir -p "$SYS_DIR" "$RUNTIME/logs"
148
-
149
- # Upgrade remediation must run before honoring a legacy done marker. Older
150
- # templates installed one per-profile Bloodbank consumer; leaving even one
151
- # enabled races the fleet-shared durable gateway and can duplicate execution.
152
- LEGACY_CONSUMER_UNIT="hermes-${AGENT_ID}-consumer.service"
153
- LEGACY_CONSUMER_PATH="$SYS_DIR/$LEGACY_CONSUMER_UNIT"
154
- legacy_consumer_present=0
155
- [[ -e "$LEGACY_CONSUMER_PATH" || -L "$LEGACY_CONSUMER_PATH" ]] \
156
- && legacy_consumer_present=1
157
- if command -v systemctl >/dev/null 2>&1; then
158
- legacy_active_result="$(systemctl_user_unit_state is-active "$LEGACY_CONSUMER_UNIT")"
159
- legacy_enabled_result="$(systemctl_user_unit_state is-enabled "$LEGACY_CONSUMER_UNIT")"
160
- [[ "$legacy_active_result" != error\|* ]] \
161
- || die "cannot safely query legacy consumer activity; preserving unit: ${legacy_active_result#*|}"
162
- [[ "$legacy_enabled_result" != error\|* ]] \
163
- || die "cannot safely query legacy consumer enablement; preserving unit: ${legacy_enabled_result#*|}"
164
- legacy_active_state="${legacy_active_result#*|}"
165
- legacy_enabled_state="${legacy_enabled_result#*|}"
166
- [[ "$legacy_active_state" == "not-found" && "$legacy_enabled_state" == "not-found" ]] \
167
- || legacy_consumer_present=1
168
- elif [[ $legacy_consumer_present -eq 1 ]]; then
169
- die "systemctl is unavailable; cannot safely retire legacy consumer: $LEGACY_CONSUMER_UNIT"
170
- fi
171
- if [[ $legacy_consumer_present -eq 1 ]]; then
172
- systemctl --user disable --now "$LEGACY_CONSUMER_UNIT" >/dev/null 2>&1 \
173
- || die "legacy consumer disable failed; preserving unit: $LEGACY_CONSUMER_UNIT"
174
- legacy_active_result="$(systemctl_user_unit_state is-active "$LEGACY_CONSUMER_UNIT")"
175
- legacy_enabled_result="$(systemctl_user_unit_state is-enabled "$LEGACY_CONSUMER_UNIT")"
176
- [[ "$legacy_active_result" == "ok|inactive" ]] \
177
- || die "legacy consumer is not proven inactive; preserving unit: ${legacy_active_result#*|}"
178
- [[ "$legacy_enabled_result" == "ok|disabled" ]] \
179
- || die "legacy consumer is not proven disabled; preserving unit: ${legacy_enabled_result#*|}"
180
- rm -f -- "$LEGACY_CONSUMER_PATH"
181
- if systemd_user_available; then
182
- systemctl --user daemon-reload >/dev/null 2>&1 \
183
- || warn " systemd daemon-reload failed after legacy consumer retirement"
184
- fi
185
- log " retired legacy per-profile Bloodbank consumer: $LEGACY_CONSUMER_UNIT"
186
- fi
187
-
188
- if already_done 70-systemd; then
189
- if [[ -f "$SYS_DIR/$GW_UNIT" && -f "$SYS_DIR/$HB_SVC" && -f "$SYS_DIR/$HB_TIMER" ]]; then
190
- log "[70] systemd already installed — reconciling unit definitions"
191
- else
192
- clear_done 70-systemd
193
- log " stale/incomplete systemd marker cleared — reconciling required units"
194
- fi
195
- fi
196
-
197
- # The heartbeat runner (board-reconciliation sentinel pass + gated checkpoint)
198
- # and the checkpoint helper both render into the role dir; just ensure they are
199
- # executable. heartbeat.sh calls checkpoint.sh internally.
200
- HEARTBEAT_BIN="$ROLE_DIR/.scripts/heartbeat.sh"
201
- CREDENTIAL_LAUNCHER="$ROLE_DIR/.scripts/credential-launch.sh"
202
- chmod +x "$HEARTBEAT_BIN" "$CREDENTIAL_LAUNCHER" "$ROLE_DIR/.scripts/checkpoint.sh" 2>/dev/null || true
203
-
204
- [[ -d "$PROFILE_HOME" && ! -L "$PROFILE_HOME" ]] \
205
- || die "named profile is not a real directory; run: pj migrate hermes.runtime-singleton '$REPO_ROOT'"
206
-
207
- # Gateway unit
208
- #
209
- # StartLimit* belongs in [Unit], not [Service] — systemd only still parses it
210
- # under [Service] for backwards compatibility. Without it, Restart=on-failure
211
- # retries forever and a gateway that can never start (bad token, bad config)
212
- # sits in `activating` indefinitely instead of settling into `failed`, so
213
- # `systemctl --user --failed` never lists it and neither the sentinel nor a
214
- # human ever sees the crashloop. That is exactly how the fleet accumulated
215
- # 10,427 invisible restarts. 5 tries in 5 minutes, then stop and report failed.
216
- cat > "$SYS_DIR/$GW_UNIT" <<UNIT
217
- [Unit]
218
- Description=$GW_DESCRIPTION
219
- After=network-online.target
220
- Wants=network-online.target
221
- StartLimitIntervalSec=300
222
- StartLimitBurst=5
223
-
224
- [Service]
225
- Type=simple
226
- $ENV_HERMES_HOME
227
- $ENV_HERMES_BIN
228
- $ENV_CODEX_HOME
229
- $ENV_TERMINAL_CWD
230
- EnvironmentFile=$RUNTIME_ENV_FILE
231
- $GW_CREDENTIAL_LINES
232
- ExecStart=$GW_EXEC_START gateway
233
- Restart=on-failure
234
- RestartSec=10
235
- StandardOutput=$GW_LOG_OUTPUT
236
- StandardError=$GW_LOG_OUTPUT
237
-
238
- [Install]
239
- WantedBy=default.target
240
- UNIT
241
-
242
- # Fused heartbeat: board-reconciliation sentinel pass + gated runtime checkpoint.
243
- # Frequent ticks (1 min); heartbeat.sh's own cooldown/lock logic rate-limits the
244
- # full Hermes pass, and the checkpoint is gated to ~hourly inside the runner.
245
- # The per-agent EnvironmentFiles load ticket-provider creds for the sentinel pass.
246
- cat > "$SYS_DIR/$HB_SVC" <<UNIT
247
- [Unit]
248
- Description=$HB_DESCRIPTION
249
- After=network-online.target
250
- Wants=network-online.target
251
-
252
- [Service]
253
- Type=oneshot
254
- WorkingDirectory=$WORKING_DIRECTORY
255
- $ENV_HERMES_HOME
256
- $ENV_HERMES_BIN
257
- $ENV_CODEX_HOME
258
- $ENV_TERMINAL_CWD
259
- EnvironmentFile=-%h/.config/hermes-agent/env
260
- EnvironmentFile=-%h/.hermes/env
261
- EnvironmentFile=-%h/.hermes/hermes-agent.env
262
- EnvironmentFile=-%h/.hermes/${AGENT_ID}.env
263
- EnvironmentFile=$RUNTIME_ENV_FILE
264
- $HB_CREDENTIAL_LINES
265
- ExecStart=$HB_EXEC_START heartbeat
266
- TimeoutStartSec=45min
267
- StandardOutput=$HB_LOG_OUTPUT
268
- StandardError=$HB_LOG_OUTPUT
269
- UNIT
270
- cat > "$SYS_DIR/$HB_TIMER" <<UNIT
271
- [Unit]
272
- Description=$TIMER_DESCRIPTION
273
-
274
- [Timer]
275
- OnBootSec=1min
276
- OnUnitInactiveSec=1min
277
- Unit=$HB_SVC
278
- Persistent=true
279
-
280
- [Install]
281
- WantedBy=timers.target
282
- UNIT
283
-
284
- if systemd_user_available; then
285
- systemctl --user daemon-reload
286
- if systemctl --user enable --now "$HB_TIMER" >/dev/null 2>&1; then
287
- # The timer's first scheduled tick can be a minute away. Run the oneshot
288
- # once now so deployment proves the heartbeat command itself completed;
289
- # timer activity alone is not an operational postcondition.
290
- if ! systemctl --user start "$HB_SVC" >/dev/null 2>&1; then
291
- systemctl --user disable --now "$HB_TIMER" >/dev/null 2>&1 || true
292
- yaml_upsert_block_value service_state heartbeat error
293
- clear_done 70-systemd
294
- die "required heartbeat oneshot failed its deployment probe: $HB_SVC"
295
- fi
296
- if hb_health="$(systemd_wait_for_stable_health \
297
- systemd_timer_health_snapshot "$HB_TIMER" "$HB_SVC")"; then
298
- yaml_upsert_block_value service_state heartbeat active
299
- log " heartbeat enabled + active with healthy latest result: $HB_TIMER"
300
- else
301
- systemctl --user disable --now "$HB_TIMER" >/dev/null 2>&1 || true
302
- yaml_upsert_block_value service_state heartbeat error
303
- clear_done 70-systemd
304
- die "heartbeat did not stabilize healthy: $hb_health"
305
- fi
306
- else
307
- yaml_upsert_block_value service_state heartbeat error
308
- clear_done 70-systemd
309
- die "failed to enable/start required heartbeat timer: $HB_TIMER"
310
- fi
311
-
312
- if [[ $gateway_ready -eq 1 ]]; then
313
- if systemctl --user enable --now "$GW_UNIT" >/dev/null 2>&1; then
314
- if gw_health="$(systemd_wait_for_stable_health \
315
- systemd_service_health_snapshot "$GW_UNIT" running)"; then
316
- yaml_upsert_block_value service_state gateway active
317
- log " credentialed gateway enabled + active and stabilized: $GW_UNIT"
318
- else
319
- systemctl --user disable --now "$GW_UNIT" >/dev/null 2>&1 || true
320
- systemctl --user reset-failed "$GW_UNIT" >/dev/null 2>&1 || true
321
- yaml_upsert_block_value service_state gateway error
322
- clear_done 70-systemd
323
- die "credentialed gateway did not stabilize healthy: $gw_health"
324
- fi
325
- else
326
- yaml_upsert_block_value service_state gateway error
327
- clear_done 70-systemd
328
- die "failed to enable/start credentialed gateway: $GW_UNIT"
329
- fi
330
- else
331
- systemctl --user disable --now "$GW_UNIT" >/dev/null 2>&1 \
332
- || die "could not enforce deferred gateway disablement: $GW_UNIT"
333
- systemctl --user reset-failed "$GW_UNIT" >/dev/null 2>&1 || true
334
- gw_deferred_health="$(systemd_gateway_deferred_snapshot "$GW_UNIT")"
335
- if [[ "$gw_deferred_health" == "ok|deferred" ]]; then
336
- yaml_upsert_block_value service_state gateway deferred
337
- log " gateway deferred: disabled + inactive until a channel credential is verified"
338
- else
339
- yaml_upsert_block_value service_state gateway error
340
- clear_done 70-systemd
341
- die "gateway deferral was not proven disabled + inactive ($gw_deferred_health)"
342
- fi
343
- fi
344
- else
345
- warn " systemd --user not available; units installed at $SYS_DIR but not enabled"
346
- yaml_upsert_block_value service_state heartbeat installed
347
- if [[ $gateway_ready -eq 1 ]]; then
348
- yaml_upsert_block_value service_state gateway installed
349
- else
350
- yaml_upsert_block_value service_state gateway deferred
351
- fi
352
- fi
353
-
354
- mark_done 70-systemd
@@ -1,185 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Append this agent's entry to the global fleet registry.
3
-
4
- if [[ "${SKIP_HOST_STATE:-0}" == "1" ]]; then
5
- printf '%s\n' '[80] fleet registry — DEFERRED (SKIP_HOST_STATE=1)' >&2
6
- exit 0
7
- fi
8
-
9
- # shellcheck source=_lib.sh
10
- source "$(dirname "$0")/_lib.sh"
11
- load_role_env
12
-
13
- fleet_lock_acquire
14
- trap 'fleet_lock_release' EXIT
15
-
16
- [[ ! -L "$REGISTRY_FILE" ]] || die "refusing to update registry symlink: $REGISTRY_FILE"
17
- mkdir -p "$(dirname "$REGISTRY_FILE")"
18
- if [[ ! -f "$REGISTRY_FILE" ]]; then
19
- cat > "$REGISTRY_FILE" <<'YAML'
20
- # Hermes agent fleet registry.
21
- # One entry per provisioned agent. Managed by hermes-agent-template/.scripts/80-registry.sh.
22
- schema_version: 1
23
- agents: {}
24
- YAML
25
- fi
26
-
27
- PROJECT_PATH="$(project_repo_path)" || PROJECT_PATH=""
28
- PLANE_PROJECT_ID="$(cat "$ROLE_DIR/.scripts/.plane-project-id" 2>/dev/null || true)"
29
-
30
- log "[80] appending to fleet registry: $REGISTRY_FILE"
31
-
32
- python3 - "$REGISTRY_FILE" "$AGENT_ID" "$REPO" "$ROLE" "$DISPLAY_NAME" \
33
- "$PROJECT_PATH" "$ROLE_DIR" "$PROFILE_NAME" \
34
- "$(yaml_get telegram.provisioning_status)" "$BOT_HANDLE" "$(yaml_get telegram.bot_id)" \
35
- "$(yaml_get slack.provisioning_status)" "$(yaml_get slack.team_id)" \
36
- "$(yaml_get slack.team_name)" "$(yaml_get slack.bot_user_id)" \
37
- "$(yaml_get slack.bot_id)" "$(yaml_get slack.bot_username)" \
38
- "$(yaml_get bloodbank.enabled)" "$(yaml_get bloodbank.gateway_scope)" \
39
- "$(yaml_get bloodbank.target_agent_id)" \
40
- "$PLANE_WORKSPACE" "$PLANE_PROJECT_ID" "$(yaml_get plane.identifier)" \
41
- "$RUNTIME_REPO" "$HERMES_BIN" "$HERMES_AGENT_REPO" "$HERMES_RUNTIME_GIT_URL" \
42
- "$HERMES_RUNTIME_GIT_REF" "$HERMES_RUNTIME_GIT_SHA" "$FLEET_ENV" \
43
- "hermes-${AGENT_ID}-gateway.service" "hermes-${AGENT_ID}-heartbeat.timer" \
44
- "$(yaml_get service_state.gateway)" "$(yaml_get service_state.heartbeat)" <<'PYEOF'
45
- import datetime
46
- import copy
47
- import errno
48
- import os
49
- import pathlib
50
- import sys
51
- import tempfile
52
- try:
53
- import yaml # type: ignore
54
- except ImportError:
55
- sys.exit("PyYAML required; pip install pyyaml")
56
- (path, agent_id, repo, role, display, project, role_dir, profile,
57
- telegram_status, bot, telegram_bot_id,
58
- slack_status, slack_team_id, slack_team_name, slack_user_id, slack_bot_id,
59
- slack_username, bloodbank_enabled, bloodbank_scope, bloodbank_target, plane_ws, plane_id,
60
- plane_ident, runtime_repo, hermes_bin, hermes_repo, hermes_git_url,
61
- hermes_git_ref, hermes_git_sha, fleet_env, gw, heartbeat,
62
- gateway_state, heartbeat_state) = sys.argv[1:35]
63
- p = pathlib.Path(path)
64
- if p.is_symlink():
65
- raise SystemExit(f"refusing to update registry symlink: {p}")
66
- data = yaml.safe_load(p.read_text(encoding="utf-8")) or {"schema_version": 1, "agents": {}}
67
- if not isinstance(data, dict):
68
- raise SystemExit("fleet registry root must be a mapping")
69
- agents = data.setdefault("agents", {})
70
- if not isinstance(agents, dict):
71
- raise SystemExit("fleet registry agents must be a mapping")
72
- if bloodbank_enabled == "":
73
- bloodbank_enabled_value = False
74
- elif bloodbank_enabled == "true":
75
- bloodbank_enabled_value = True
76
- elif bloodbank_enabled == "false":
77
- bloodbank_enabled_value = False
78
- else:
79
- raise SystemExit("bloodbank.enabled must be the strict YAML boolean true or false")
80
- existing = agents.get(agent_id, {})
81
- if not isinstance(existing, dict):
82
- raise SystemExit(f"fleet registry entry for {agent_id} must be a mapping")
83
- provisioned_at = existing.get("provisioned_at")
84
- if not isinstance(provisioned_at, str) or not provisioned_at:
85
- provisioned_at = datetime.datetime.now(datetime.timezone.utc).isoformat().replace("+00:00", "Z")
86
- managed = {
87
- "repo": repo, "role": role, "display_name": display,
88
- "project_path": project, "role_dir": role_dir,
89
- "profile_name": profile,
90
- "telegram": {
91
- "provisioning_status": telegram_status,
92
- "bot_username": bot,
93
- "bot_id": telegram_bot_id,
94
- },
95
- "slack": {
96
- "provisioning_status": slack_status,
97
- "team_id": slack_team_id,
98
- "team_name": slack_team_name,
99
- "bot_user_id": slack_user_id,
100
- "bot_id": slack_bot_id,
101
- "bot_username": slack_username,
102
- },
103
- "bloodbank": {
104
- "enabled": bloodbank_enabled_value,
105
- "gateway_scope": bloodbank_scope,
106
- "target_agent_id": bloodbank_target,
107
- },
108
- "plane": {"workspace": plane_ws, "project_id": plane_id, "identifier": plane_ident},
109
- "runtime_repo": runtime_repo,
110
- "hermes": {
111
- "bin": hermes_bin,
112
- "repo": hermes_repo,
113
- "git_url": hermes_git_url,
114
- "git_ref": hermes_git_ref,
115
- "git_sha": hermes_git_sha,
116
- "fleet_env": fleet_env,
117
- },
118
- "systemd": {
119
- "gateway_unit": gw,
120
- "heartbeat_timer": heartbeat,
121
- "gateway_state": gateway_state,
122
- "heartbeat_state": heartbeat_state,
123
- },
124
- "provisioned_at": provisioned_at,
125
- }
126
-
127
- def merge_managed(current, update):
128
- result = copy.deepcopy(current)
129
- for key, value in update.items():
130
- if isinstance(value, dict) and isinstance(result.get(key), dict):
131
- result[key] = merge_managed(result[key], value)
132
- else:
133
- result[key] = copy.deepcopy(value)
134
- return result
135
-
136
- entry = merge_managed(existing, managed)
137
- # This is retired managed schema, not extension metadata. Keeping it would
138
- # falsely advertise a second per-agent Bloodbank execution path.
139
- systemd = entry.get("systemd")
140
- if isinstance(systemd, dict):
141
- systemd.pop("consumer_unit", None)
142
- agents[agent_id] = entry
143
- rendered = yaml.safe_dump(data, sort_keys=False)
144
- p.parent.mkdir(parents=True, exist_ok=True)
145
-
146
- def fsync_parent(target):
147
- unsupported = {errno.EINVAL, getattr(errno, "ENOTSUP", errno.EINVAL), errno.ENOSYS}
148
- flags = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)
149
- try:
150
- directory_fd = os.open(target.parent, flags)
151
- except OSError as exc:
152
- if exc.errno in unsupported:
153
- return
154
- raise
155
- try:
156
- try:
157
- os.fsync(directory_fd)
158
- except OSError as exc:
159
- if exc.errno not in unsupported:
160
- raise
161
- finally:
162
- os.close(directory_fd)
163
-
164
- fd, temporary = tempfile.mkstemp(prefix=f".{p.name}.registry-", dir=p.parent)
165
- try:
166
- os.fchmod(fd, 0o600)
167
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
168
- handle.write(rendered)
169
- handle.flush()
170
- os.fsync(handle.fileno())
171
- os.replace(temporary, p)
172
- os.chmod(p, 0o600)
173
- fsync_parent(p)
174
- except BaseException:
175
- try:
176
- os.unlink(temporary)
177
- except FileNotFoundError:
178
- pass
179
- raise
180
- PYEOF
181
-
182
- fleet_lock_release
183
- trap - EXIT
184
-
185
- mark_done 80-registry
@@ -1,87 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Print a reconciliation summary derived from durable role/service state.
3
- # shellcheck source=_lib.sh
4
- source "$(dirname "$0")/_lib.sh"
5
- load_role_env
6
-
7
- GW_UNIT="hermes-${AGENT_ID}-gateway.service"
8
- HB_TIMER="hermes-${AGENT_ID}-heartbeat.timer"
9
- GW_STATE="$(yaml_get service_state.gateway)"
10
- HB_STATE="$(yaml_get service_state.heartbeat)"
11
- TELEGRAM_STATE="$(yaml_get telegram.provisioning_status)"
12
- SLACK_STATE="$(yaml_get slack.provisioning_status)"
13
- RECONCILE_STATE="$(yaml_get reconcile.enabled)"
14
- RECONCILE_OPT_OUT="$(yaml_get reconcile.explicit_opt_out)"
15
- BOARD_ID="$(yaml_get ticket_provider.board_id)"
16
-
17
- # Reconcile durable claims with live unit truth at presentation time. A stale
18
- # role.yaml must never turn an exited launcher or failed heartbeat into an
19
- # operational summary.
20
- if [[ "$HB_STATE" == "active" || "$GW_STATE" =~ ^(active|deferred)$ ]]; then
21
- if systemd_user_available; then
22
- if [[ "$HB_STATE" == "active" ]]; then
23
- hb_live="$(systemd_timer_health_snapshot "$HB_TIMER" "hermes-${AGENT_ID}-heartbeat.service")"
24
- [[ "$hb_live" == ok\|* ]] || HB_STATE="error (stale active claim)"
25
- fi
26
- if [[ "$GW_STATE" == "active" ]]; then
27
- gw_live="$(systemd_service_health_snapshot "$GW_UNIT" running)"
28
- [[ "$gw_live" == ok\|* ]] || GW_STATE="error (stale active claim)"
29
- elif [[ "$GW_STATE" == "deferred" ]]; then
30
- gw_live="$(systemd_gateway_deferred_snapshot "$GW_UNIT")"
31
- [[ "$gw_live" == "ok|deferred" ]] || GW_STATE="error (stale deferred claim)"
32
- fi
33
- else
34
- [[ "$HB_STATE" != "active" ]] || HB_STATE="unverified (user manager unavailable)"
35
- [[ "$GW_STATE" != "active" && "$GW_STATE" != "deferred" ]] \
36
- || GW_STATE="unverified (user manager unavailable)"
37
- fi
38
- fi
39
-
40
- case "$HB_STATE:$GW_STATE" in
41
- active:active) MODE="OPERATIONAL" ;;
42
- active:deferred) MODE="OPERATIONAL_WITH_GATEWAY_DEFERRED" ;;
43
- installed:installed|installed:deferred) MODE="INSTALLED_NOT_ACTIVE" ;;
44
- *) MODE="INCOMPLETE" ;;
45
- esac
46
-
47
- cat >&2 <<EOF
48
-
49
- ╭─ Hermes deployment: $AGENT_ID ───────────────────────────────────────╮
50
- │
51
- │ Mode: $MODE
52
- │ Gateway: $GW_STATE ($GW_UNIT)
53
- │ Heartbeat: $HB_STATE ($HB_TIMER)
54
- │ Reconciliation: $RECONCILE_STATE (explicit opt-out: ${RECONCILE_OPT_OUT:-false})
55
- │ Telegram: $TELEGRAM_STATE
56
- │ Slack: $SLACK_STATE
57
- │ Board: ${BOARD_ID:-deferred}
58
- │ Runtime: $ROLE_DIR/runtime (pure-local, ignored)
59
- │ Hermes bin: $HERMES_BIN
60
- │ Fleet env: $FLEET_ENV
61
- │
62
- EOF
63
-
64
- if [[ "$TELEGRAM_STATE" == "verified" ]]; then
65
- printf '│ Talk: @%s (Telegram DM)\n' "$BOT_HANDLE" >&2
66
- fi
67
- if [[ "$SLACK_STATE" == "verified" ]]; then
68
- printf '│ Slack bot: %s\n' "$(yaml_get slack.bot_username)" >&2
69
- fi
70
- printf '│ Shell: %s/hermes chat "status"\n' "$ROLE_DIR" >&2
71
-
72
- if [[ "$GW_STATE" == "deferred" ]]; then
73
- cat >&2 <<EOF
74
- │
75
- │ Gateway activation is deferred. Store and verify a dedicated Telegram or
76
- │ Slack credential through the provisioner; do not start $GW_UNIT manually.
77
- EOF
78
- elif [[ "$GW_STATE" == "active" ]]; then
79
- printf '│ Gateway log: journalctl --user -fu %s\n' "$GW_UNIT" >&2
80
- fi
81
-
82
- cat >&2 <<'EOF'
83
- │
84
- ╰────────────────────────────────────────────────────────────────────────╯
85
-
86
- EOF
87
- mark_done 99-summary