@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,148 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # unify-pm — Phase C (safe subset): move every deployed PM toward the unified
5
+ # single-PM model WITHOUT disrupting running services.
6
+ #
7
+ # Per PM (driven off registry role=pm, so specialized non-pm profiles are
8
+ # naturally skipped):
9
+ # 1. refresh .scripts/checkpoint.sh — applies the secret-scan gate to the live
10
+ # hourly checkpoint (the existing checkpoint.service ExecStarts this file).
11
+ # 2. refresh the <role_dir>/hermes launcher to the runtime-only template
12
+ # (drops the stale profile-fallback; used for interactive `hermes chat`,
13
+ # never by the systemd units).
14
+ # 3. ensure ~/.hermes/profiles/<profile_name> symlinks to ./runtime. This is
15
+ # LOAD-BEARING — hermes resolves `--profile <name>` through it (and recreates
16
+ # it as a fresh standalone dir if missing, disconnecting the agent from its
17
+ # runtime), so it is repaired/created, NEVER dropped.
18
+ #
19
+ # Does NOT touch gateway/consumer/checkpoint units, the registry, or runtimes.
20
+ # The checkpoint→heartbeat unit rename + sentinel install is a separate pass.
21
+ #
22
+ # DRY-RUN by default; --apply performs; --agent <id>... scopes; pjangler-pm first.
23
+ #
24
+ # Usage: unify-pm.sh [--apply] [--agent <pm-id>]...
25
+
26
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
27
+ TEMPLATE_DIR="$SCRIPT_DIR/../template"
28
+ CHECKPOINT_SRC="$TEMPLATE_DIR/.scripts/checkpoint.sh"
29
+ WRAPPER_TEMPLATE="$TEMPLATE_DIR/hermes.jinja"
30
+
31
+ HERMES_TEMPLATE_CONFIG="${HERMES_TEMPLATE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/hermes-agent-template/config.toml}"
32
+ cfg() {
33
+ local key="$1" def="$2"
34
+ { [[ -f "$HERMES_TEMPLATE_CONFIG" ]] && command -v python3 >/dev/null 2>&1; } || { printf '%s' "$def"; return 0; }
35
+ python3 - "$HERMES_TEMPLATE_CONFIG" "$key" "$def" <<'PYEOF' || printf '%s' "$def"
36
+ import sys, os
37
+ try:
38
+ import tomllib
39
+ except ModuleNotFoundError:
40
+ print(sys.argv[3], end=""); sys.exit(0)
41
+ try:
42
+ cur = tomllib.load(open(sys.argv[1], "rb"))
43
+ except Exception:
44
+ print(sys.argv[3], end=""); sys.exit(0)
45
+ for p in sys.argv[2].split("."):
46
+ if isinstance(cur, dict) and p in cur:
47
+ cur = cur[p]
48
+ else:
49
+ print(sys.argv[3], end=""); sys.exit(0)
50
+ print(os.path.expanduser(str(cur)), end="")
51
+ PYEOF
52
+ return 0
53
+ }
54
+
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
+ SYS_DIR="$HOME/.config/systemd/user"
58
+
59
+ APPLY=0
60
+ declare -a ONLY=()
61
+ while [[ $# -gt 0 ]]; do
62
+ case "$1" in
63
+ --apply) APPLY=1 ;;
64
+ --agent) shift; ONLY+=("$1") ;;
65
+ -h|--help) sed -n '3,30p' "$0"; exit 0 ;;
66
+ *) echo "unify-pm: unknown flag $1" >&2; exit 2 ;;
67
+ esac
68
+ shift
69
+ done
70
+
71
+ [[ -f "$REGISTRY_FILE" ]] || { echo "unify-pm: registry not found: $REGISTRY_FILE" >&2; exit 2; }
72
+ [[ -f "$CHECKPOINT_SRC" ]] || { echo "unify-pm: template checkpoint.sh not found: $CHECKPOINT_SRC" >&2; exit 2; }
73
+ [[ -f "$WRAPPER_TEMPLATE" ]] || { echo "unify-pm: wrapper template not found: $WRAPPER_TEMPLATE" >&2; exit 2; }
74
+
75
+ MODE="DRY-RUN"; [[ $APPLY -eq 1 ]] && MODE="APPLY"
76
+ echo "unify-pm — mode: $MODE registry: $REGISTRY_FILE"
77
+ echo "================================================================"
78
+
79
+ # PM rows (pjangler first), TSV: agent_id, role_dir, profile_name
80
+ read_pm() {
81
+ python3 - "$REGISTRY_FILE" <<'PYEOF'
82
+ import sys, yaml
83
+ d = yaml.safe_load(open(sys.argv[1], encoding="utf-8")) or {}
84
+ rows = [(aid, str(a.get("role_dir") or ""), str(a.get("profile_name") or aid))
85
+ for aid, a in (d.get("agents") or {}).items()
86
+ if isinstance(a, dict) and a.get("role") == "pm"]
87
+ rows.sort(key=lambda r: (r[0] != "pjangler-pm", r[0]))
88
+ for r in rows:
89
+ print("\t".join(r))
90
+ PYEOF
91
+ }
92
+
93
+ wanted() { [[ ${#ONLY[@]} -eq 0 ]] && return 0; local a; for a in "${ONLY[@]}"; do [[ "$a" == "$1" ]] && return 0; done; return 1; }
94
+
95
+ CK=0; WR=0; SL=0; SKIP=0
96
+ while IFS=$'\t' read -r aid role_dir profile_name; do
97
+ wanted "$aid" || continue
98
+ echo
99
+ echo ">>> $aid"
100
+ if [[ -z "$role_dir" || ! -d "$role_dir" ]]; then
101
+ echo " role_dir missing: ${role_dir:-<unset>} — skipping"; SKIP=$((SKIP+1)); continue
102
+ fi
103
+ runtime="$role_dir/runtime"
104
+
105
+ # 1. checkpoint.sh secret-gate refresh.
106
+ dst="$role_dir/.scripts/checkpoint.sh"
107
+ if [[ ! -f "$dst" ]] || ! diff -q "$CHECKPOINT_SRC" "$dst" >/dev/null 2>&1; then
108
+ if [[ $APPLY -eq 1 ]]; then
109
+ mkdir -p "$role_dir/.scripts"; cp "$CHECKPOINT_SRC" "$dst"; chmod +x "$dst"
110
+ echo " [do] checkpoint.sh refreshed (secret-gate)"; CK=$((CK+1))
111
+ else
112
+ echo " [dry] checkpoint.sh would be refreshed (secret-gate)"; CK=$((CK+1))
113
+ fi
114
+ else
115
+ echo " checkpoint.sh already current"
116
+ fi
117
+
118
+ # 2. launcher → runtime-only template.
119
+ expected_wrapper="$(sed "s/{{ agent_id }}/$aid/g" "$WRAPPER_TEMPLATE")"
120
+ if [[ ! -f "$role_dir/hermes" ]] || ! diff -q <(printf '%s\n' "$expected_wrapper") "$role_dir/hermes" >/dev/null 2>&1; then
121
+ if [[ $APPLY -eq 1 ]]; then
122
+ printf '%s\n' "$expected_wrapper" > "$role_dir/hermes"; chmod +x "$role_dir/hermes"
123
+ echo " [do] launcher regenerated (runtime-only)"; WR=$((WR+1))
124
+ else
125
+ echo " [dry] launcher would be regenerated (runtime-only)"; WR=$((WR+1))
126
+ fi
127
+ else
128
+ echo " launcher already current"
129
+ fi
130
+
131
+ # 3. ensure the profile symlink points at the runtime (load-bearing; never drop).
132
+ link="$PROFILES_DIR/$profile_name"
133
+ if [[ -L "$link" && "$(readlink -f "$link")" == "$(readlink -f "$runtime")" ]]; then
134
+ echo " profile symlink ok -> runtime"
135
+ elif [[ -e "$link" && ! -L "$link" ]]; then
136
+ echo " profile entry is a REAL dir (MANUAL merge): $link — left untouched"; SKIP=$((SKIP+1))
137
+ else
138
+ if [[ $APPLY -eq 1 ]]; then
139
+ mkdir -p "$PROFILES_DIR"; ln -sfn "$runtime" "$link"; echo " [do] profile symlink set -> runtime"; SL=$((SL+1))
140
+ else
141
+ echo " [dry] profile symlink would be set -> runtime"; SL=$((SL+1))
142
+ fi
143
+ fi
144
+ done < <(read_pm)
145
+
146
+ echo
147
+ echo "================================================================"
148
+ echo "unify-pm: checkpoint=$CK launcher=$WR symlink-repair=$SL skipped=$SKIP ($MODE)"
@@ -1,16 +1,14 @@
1
1
  # {{agent_id}} — runtime
2
2
 
3
- This is the durable runtime for the **{{display_name}}** agent. The named
4
- Hermes profile `{{profile_name}}` points here from `~/.hermes/profiles/{{profile_name}}`,
5
- so Hermes sees an ordinary profile while this repo stays git-tracked and
6
- recoverable across machines.
3
+ This is the HERMES_HOME for the **{{display_name}}** agent. It is git-tracked
4
+ so memory, SOUL evolution, and conversation history are durable across machines
5
+ and recoverable on failure.
7
6
 
8
7
  ## What's in here
9
8
 
10
9
  | Path | Tracked? | Purpose |
11
10
  | --- | --- | --- |
12
- | `profile.yaml` | yes | Opts this profile into inheriting config from default |
13
- | `config.yaml` | yes | Local config overrides only; shared defaults come from the fleet default |
11
+ | `config.yaml` | yes | Inference + skill config (cloned from global at provision time) |
14
12
  | `SOUL.md` | yes | The agent's personality, evolves over time |
15
13
  | `memories/MEMORY.md` | yes | The condensed mental-model summary loaded each session |
16
14
  | `memories/USER.md` | yes | The operator's persona (Jarad DeLorenzo, ...) |
@@ -25,11 +23,14 @@ recoverable across machines.
25
23
 
26
24
  ## Checkpoint cadence
27
25
 
28
- - Hourly: systemd `--user` timer `hermes-{{agent_id}}-checkpoint.timer`
26
+ - Heartbeat: systemd `--user` timer `hermes-{{agent_id}}-heartbeat.timer`. Each
27
+ tick reconciles the ticket board (the PM's sentinel pass) and then, gated to
28
+ at most once an hour, checkpoints this runtime.
29
29
  - On session end: hermes Stop hook (TODO: hook script in `~/.hermes/hooks/`)
30
30
 
31
- The checkpoint script lives in the parent's `.scripts/checkpoint.sh`. It
32
- `git add -A`, commits only if dirty, and pushes to `origin`.
31
+ The heartbeat runner lives in the parent's `.scripts/heartbeat.sh`; it calls
32
+ `.scripts/checkpoint.sh` internally, which `git add -A`, commits only if dirty,
33
+ and pushes to `origin`.
33
34
 
34
35
  ## Restoring on a new machine
35
36
 
@@ -21,9 +21,8 @@ else
21
21
  cat > "$CONFIG" <<'TOML'
22
22
  # hermes-agent-template config — edit for your environment.
23
23
  [fleet]
24
- home = "~/.hermes"
25
- hermes_bin = "/home/delorenj/code/hermes-agent/.venv/bin/hermes"
26
- hermes_repo = "/home/delorenj/code/hermes-agent"
24
+ hermes_bin = "~/.hermes/hermes-agent/.venv/bin/hermes"
25
+ hermes_repo = "~/.hermes/hermes-agent"
27
26
  fleet_env = "~/.hermes/fleet.env"
28
27
  registry_file = "~/.hermes/agents-registry.yaml"
29
28
  canonical_skills_dir = "/home/delorenj/.agents/skills"
@@ -15,7 +15,6 @@ if [[ ! -f "$FLEET_ENV" ]]; then
15
15
  # All generated wrappers and provisioning scripts read this file.
16
16
  HERMES_FLEET_BIN=${HERMES_BIN}
17
17
  HERMES_FLEET_REPO=${HERMES_AGENT_REPO}
18
- HERMES_FLEET_HOME=${FLEET_HOME}
19
18
  HERMES_FLEET_REGISTRY_FILE=${REGISTRY_FILE}
20
19
  HERMES_FLEET_OAUTH_FILE=${HERMES_OAUTH_FILE}
21
20
  HERMES_FLEET_CODEX_HOME=${CODEX_HOME}
@@ -46,7 +45,6 @@ path.write_text("\n".join(lines) + "\n")
46
45
  PYEOF
47
46
  }
48
47
 
49
- upsert_fleet_env HERMES_FLEET_HOME "$FLEET_HOME"
50
48
  upsert_fleet_env HERMES_FLEET_OAUTH_FILE "$HERMES_OAUTH_FILE"
51
49
  upsert_fleet_env HERMES_FLEET_CODEX_HOME "$CODEX_HOME"
52
50
  chmod 600 "$FLEET_ENV"
@@ -1,6 +1,10 @@
1
1
  #!/usr/bin/env bash
2
- # Create the per-agent Hermes profile. Config inherits explicitly from the
3
- # default fleet profile; secrets and runtime state stay profile-local.
2
+ # Stage the per-agent runtime config/secrets via a TRANSIENT Hermes profile.
3
+ # HERMES_HOME is the ./runtime submodule; this profile dir is a staging area
4
+ # (cred-stripped .env + SOUL) that 20-runtime-repo.sh folds into the runtime and
5
+ # then replaces with a symlink ~/.hermes/profiles/<name> -> ./runtime. That
6
+ # symlink is load-bearing: hermes resolves `--profile <name>` invocations
7
+ # through it (recreating it wrongly if it goes missing).
4
8
  # shellcheck source=_lib.sh
5
9
  source "$(dirname "$0")/_lib.sh"
6
10
  load_role_env
@@ -8,7 +12,7 @@ load_role_env
8
12
  already_done 10-hermes-profile && { log "[10] profile already created — skipping"; exit 0; }
9
13
 
10
14
  log "[10] creating hermes profile: $PROFILE_NAME"
11
- PROFILE_HOME="$(profile_home)"
15
+ PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
12
16
 
13
17
  # Set repo path to directory where .git is
14
18
  REPO_PATH="$(cd "$ROLE_DIR" && git rev-parse --show-toplevel 2>/dev/null || echo "$ROLE_DIR")"
@@ -16,21 +20,18 @@ REPO_PATH="$(cd "$ROLE_DIR" && git rev-parse --show-toplevel 2>/dev/null || echo
16
20
  if [[ -d "$PROFILE_HOME" ]]; then
17
21
  log " profile dir already exists; reusing"
18
22
  else
19
- env HERMES_HOME="$FLEET_HOME" "$HERMES_BIN" profile create \
20
- "$PROFILE_NAME" --inherit-config --no-alias
23
+ # --clone (NOT --clone-all): copies config.yaml, .env, SOUL.md only.
24
+ # --clone-all has a recursion bug — it copies the entire ~/.hermes tree,
25
+ # including profiles/ itself, producing nested profiles/<name>/profiles/<name>/...
26
+ # We do explicit skill + plugin + hooks copies below to avoid that.
27
+ "$HERMES_BIN" profile create "$PROFILE_NAME" --clone --no-alias
21
28
  fi
22
29
 
23
- # Existing profiles from older template runs may not have the inheritance
24
- # metadata yet. Make the operation idempotent and preserve unrelated
25
- # profile.yaml metadata such as descriptions.
26
- env HERMES_HOME="$FLEET_HOME" "$HERMES_BIN" profile config-inherit \
27
- "$PROFILE_NAME" --from default
28
-
29
- # Copy local profile assets from the default profile without copying config or
30
- # credentials. Only config.yaml inherits; these directories remain local state.
31
- log " mirroring local skills, plugins, hooks from default profile"
30
+ # Manually copy the inheritable bits that --clone doesn't get.
31
+ # These are content-only dirs; safe to mirror without recursion risk.
32
+ log " mirroring skills, plugins, hooks from default profile"
32
33
  for sub in skills plugins hooks cron skins; do
33
- src="$FLEET_HOME/$sub"
34
+ src="$HOME/.hermes/$sub"
34
35
  dst="$PROFILE_HOME/$sub"
35
36
  if [[ -d "$src" && "$src" != "$PROFILE_HOME"* ]]; then
36
37
  mkdir -p "$dst"
@@ -45,10 +46,15 @@ rm -f "$PROFILE_HOME/gateway.pid" "$PROFILE_HOME/gateway_state.json" \
45
46
  # Belt-and-suspenders: if a profiles/ dir somehow exists, remove it
46
47
  [[ -d "$PROFILE_HOME/profiles" ]] && rm -rf "$PROFILE_HOME/profiles"
47
48
 
48
- # Strip messaging-platform credentials from any existing .env left by an older
49
- # clone-based profile. New inherited profiles do not copy default .env at all.
50
- # A sub-agent must establish its OWN identity via the Wire steps
51
- # (30-telegram etc), never borrow the operator's default profile token.
49
+ # Strip inherited messaging-platform credentials from the cloned .env.
50
+ # `profile create --clone` copies the DEFAULT profile's .env verbatim — and the
51
+ # default profile is an operator's own agent (e.g. Condaleeza on Slack). Without
52
+ # this, every sub-agent inherits the parent's bot token and would (a) hijack the
53
+ # parent's Slack/Telegram socket if it ever connects, and (b) crash-loop: the
54
+ # gateway treats an inherited-but-unusable platform as "configured", fails to
55
+ # connect it, and exits non-fatal only when ZERO platforms are configured. A
56
+ # sub-agent must establish its OWN identity via the Wire steps (30-telegram etc),
57
+ # never borrow the parent's.
52
58
  PROFILE_ENV="$PROFILE_HOME/.env"
53
59
  if [[ -f "$PROFILE_ENV" ]]; then
54
60
  log " stripping inherited platform credentials from profile .env"
@@ -73,6 +79,28 @@ PYEOF
73
79
  chmod 600 "$PROFILE_ENV"
74
80
  fi
75
81
 
82
+ if [[ "$ROLE" == "pm" ]]; then
83
+ VOX_URL_VALUE="${VOX_URL:-$(config_get fleet.vox_url 'https://vox.delo.sh')}"
84
+ if [[ -n "$VOX_URL_VALUE" ]]; then
85
+ log " ensuring VOX_URL is present for PM voice mode"
86
+ python3 - "$PROFILE_ENV" "$VOX_URL_VALUE" <<'PYEOF'
87
+ import pathlib, sys
88
+
89
+ path = pathlib.Path(sys.argv[1])
90
+ vox_url = sys.argv[2]
91
+ lines = path.read_text().splitlines() if path.exists() else []
92
+ for idx, line in enumerate(lines):
93
+ if line.startswith("VOX_URL="):
94
+ lines[idx] = f'VOX_URL="{vox_url}"'
95
+ break
96
+ else:
97
+ lines.append(f'VOX_URL="{vox_url}"')
98
+ path.write_text("\n".join(lines) + "\n")
99
+ PYEOF
100
+ chmod 600 "$PROFILE_ENV"
101
+ fi
102
+ fi
103
+
76
104
  # Apply role-specific config overrides.
77
105
  log " setting terminal.cwd = $REPO_PATH"
78
106
  env HERMES_HOME="$PROFILE_HOME" "$HERMES_BIN" config set terminal.cwd "$REPO_PATH"
@@ -7,7 +7,7 @@ load_role_env
7
7
  already_done 20-runtime-repo && { log "[20] runtime repo already set up — skipping"; exit 0; }
8
8
  [[ "${SKIP_RUNTIME_REPO:-0}" == "1" ]] && { log "[20] runtime repo — SKIPPED (SKIP_RUNTIME_REPO=1)"; mark_done 20-runtime-repo; exit 0; }
9
9
 
10
- PROFILE_HOME="$(profile_home)"
10
+ PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
11
11
  RUNTIME_LOCAL="$ROLE_DIR/runtime"
12
12
  GH_OWNER="${RUNTIME_REPO%%/*}"
13
13
  GH_NAME="${RUNTIME_REPO##*/}"
@@ -41,13 +41,13 @@ log " populating scaffold in $TMP"
41
41
  cp -a "$RUNTIME_SCAFFOLD_DIR/." "$TMP/"
42
42
 
43
43
  # Render scaffold templates with role-specific values
44
- python3 - "$TMP" "$AGENT_ID" "$REPO" "$ROLE" "$DISPLAY_NAME" "$PROFILE_NAME" <<'PYEOF'
44
+ python3 - "$TMP" "$AGENT_ID" "$REPO" "$ROLE" "$DISPLAY_NAME" <<'PYEOF'
45
45
  import sys, pathlib, re
46
- root, agent_id, repo, role, display, profile_name = sys.argv[1:7]
46
+ root, agent_id, repo, role, display = sys.argv[1:6]
47
47
  root = pathlib.Path(root)
48
48
  mapping = {
49
49
  "{{agent_id}}": agent_id, "{{repo}}": repo, "{{role}}": role,
50
- "{{display_name}}": display, "{{profile_name}}": profile_name,
50
+ "{{display_name}}": display,
51
51
  }
52
52
  for p in root.rglob("*"):
53
53
  if p.is_file() and p.suffix in (".md", ".yaml", ".yml", ".sh", ".py", ".gitignore", ".gitattributes"):
@@ -59,12 +59,16 @@ for p in root.rglob("*"):
59
59
  pass
60
60
  PYEOF
61
61
 
62
- # 3. Copy the profile's delta config + metadata and the project's SOUL.md as
63
- # the canonical starting personality. Shared defaults stay in the fleet default
64
- # profile and are not copied into the runtime repo.
65
- for f in config.yaml profile.yaml; do
66
- [[ -f "$PROFILE_HOME/$f" ]] && cp "$PROFILE_HOME/$f" "$TMP/$f"
67
- done
62
+ # 3. Seed the runtime config from the canonical PM config — the fleet's single
63
+ # config source of truth (defaults to the global ~/.hermes/config.yaml). This
64
+ # is a provision-time snapshot; Hermes loads $HERMES_HOME/config.yaml directly
65
+ # (there is no live profile inheritance). Override via config.toml
66
+ # [fleet].canonical_pm_config to share one curated PM config across all repos.
67
+ CANONICAL_PM_CONFIG="$(config_get fleet.canonical_pm_config "$HOME/.hermes/config.yaml")"
68
+ if [[ -f "$CANONICAL_PM_CONFIG" ]]; then
69
+ cp "$CANONICAL_PM_CONFIG" "$TMP/config.yaml"
70
+ fi
71
+ # Copy the project's SOUL.md as the canonical starting personality.
68
72
  cp "$ROLE_DIR/SOUL.md" "$TMP/SOUL.md"
69
73
 
70
74
  # 4. Git init + LFS + initial commit + push (skip if remote already has content)
@@ -118,26 +122,66 @@ else
118
122
  )
119
123
  fi
120
124
 
121
- # 6. Symlink the runtime back into ~/.hermes/profiles/<name>/ so hermes finds it.
122
- # Actually we WANT HERMES_HOME = the runtime dir, not the cloned profile.
123
- # Move the profile contents into runtime, then symlink the profile dir to runtime.
124
- PROFILE_HOME="$(profile_home)"
125
+ # 6. Fold the staging profile state into the runtime, then symlink the profile
126
+ # name AT the runtime. HERMES_HOME is the runtime submodule, but the profile
127
+ # symlink is LOAD-BEARING: hermes resolves agents launched as
128
+ # `hermes --profile <name>` through ~/.hermes/profiles/<name> (and recreates it
129
+ # as a fresh standalone dir if it's missing, disconnecting the agent from its
130
+ # runtime), so that name MUST resolve to the runtime.
131
+ PROFILE_HOME="$HOME/.hermes/profiles/$PROFILE_NAME"
125
132
  if [[ -d "$PROFILE_HOME" && ! -L "$PROFILE_HOME" ]]; then
126
- log " migrating profile state into the runtime submodule"
127
- # Preserve per-runtime config metadata, secrets, SOUL, and local profile
128
- # assets from profile creation. OAuth provider credentials are fleet-shared
129
- # via HERMES_OAUTH_FILE, so do not clone auth.json/auth.lock into each runtime.
130
- for f in .env config.yaml profile.yaml SOUL.md; do
131
- [[ -f "$PROFILE_HOME/$f" && ! -e "$RUNTIME_LOCAL/$f" ]] \
132
- && cp "$PROFILE_HOME/$f" "$RUNTIME_LOCAL/$f"
133
- done
134
- for sub in skills plugins hooks cron skins; do
135
- [[ -d "$PROFILE_HOME/$sub" && ! -e "$RUNTIME_LOCAL/$sub" ]] \
136
- && cp -a "$PROFILE_HOME/$sub" "$RUNTIME_LOCAL/$sub"
133
+ log " migrating staging profile state into the runtime submodule"
134
+ # OAuth provider credentials are fleet-shared via HERMES_OAUTH_FILE, so do not
135
+ # clone auth.json/auth.lock into each runtime.
136
+ for f in .env config.yaml; do
137
+ [[ -f "$PROFILE_HOME/$f" && ! -e "$RUNTIME_LOCAL/$f" ]] && cp "$PROFILE_HOME/$f" "$RUNTIME_LOCAL/$f"
137
138
  done
138
139
  rm -rf "$PROFILE_HOME"
139
- ln -sfn "$RUNTIME_LOCAL" "$PROFILE_HOME"
140
- log " $PROFILE_HOME -> $RUNTIME_LOCAL"
140
+ fi
141
+ ln -sfn "$RUNTIME_LOCAL" "$PROFILE_HOME"
142
+ log " profile symlink $PROFILE_HOME -> $RUNTIME_LOCAL"
143
+
144
+ # Apply the one genuine per-repo config delta directly to the runtime config.
145
+ env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set terminal.cwd "$PROJECT_PATH" >/dev/null 2>&1 || true
146
+
147
+ if [[ "$ROLE" == "pm" ]]; then
148
+ VOXXY_PLUGIN_DIR="${VOXXY_PLUGIN_DIR:-$(config_get fleet.voxxy_plugin_dir "$HOME/code/voxxy/plugins/tts/voxxy")}"
149
+ if [[ -d "$VOXXY_PLUGIN_DIR" ]]; then
150
+ mkdir -p "$RUNTIME_LOCAL/plugins/tts"
151
+ ln -sfn "$VOXXY_PLUGIN_DIR" "$RUNTIME_LOCAL/plugins/tts/voxxy"
152
+ log " linked Voxxy plugin into runtime"
153
+ else
154
+ warn " Voxxy plugin dir missing: $VOXXY_PLUGIN_DIR"
155
+ fi
156
+
157
+ if [[ -x "$HERMES_BIN" ]]; then
158
+ env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set plugins.enabled.0 tts/voxxy >/dev/null 2>&1 || true
159
+ env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set tts.provider voxxy >/dev/null 2>&1 || true
160
+ env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set tts.voice rick >/dev/null 2>&1 || true
161
+ log " set PM runtime TTS provider -> voxxy"
162
+ else
163
+ warn " Hermes bin missing; skipped PM Voxxy config"
164
+ fi
165
+ fi
166
+
167
+ if [[ "$ROLE" == "pm" ]]; then
168
+ VOXXY_PLUGIN_DIR="${VOXXY_PLUGIN_DIR:-$(config_get fleet.voxxy_plugin_dir "$HOME/code/voxxy/plugins/tts/voxxy")}"
169
+ if [[ -d "$VOXXY_PLUGIN_DIR" ]]; then
170
+ mkdir -p "$RUNTIME_LOCAL/plugins/tts"
171
+ ln -sfn "$VOXXY_PLUGIN_DIR" "$RUNTIME_LOCAL/plugins/tts/voxxy"
172
+ log " linked Voxxy plugin into runtime"
173
+ else
174
+ warn " Voxxy plugin dir missing: $VOXXY_PLUGIN_DIR"
175
+ fi
176
+
177
+ if [[ -x "$HERMES_BIN" ]]; then
178
+ env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set plugins.enabled.0 tts/voxxy >/dev/null 2>&1 || true
179
+ env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set tts.provider voxxy >/dev/null 2>&1 || true
180
+ env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set tts.voice rick >/dev/null 2>&1 || true
181
+ log " set PM runtime TTS provider -> voxxy"
182
+ else
183
+ warn " Hermes bin missing; skipped PM Voxxy config"
184
+ fi
141
185
  fi
142
186
 
143
187
  rm -rf "$TMP"
@@ -12,7 +12,7 @@ if [[ "${SKIP_TELEGRAM:-0}" == "1" ]]; then
12
12
  exit 0
13
13
  fi
14
14
 
15
- RUNTIME="$(profile_or_runtime_home)"
15
+ RUNTIME="$ROLE_DIR/runtime"
16
16
 
17
17
  cat >&2 <<EOF
18
18
 
@@ -59,7 +59,7 @@ if [[ -z "${TELEGRAM_ALLOWED_USERS:-}" ]]; then
59
59
  read -r -p "Your Telegram user id (allow-list for this bot): " TELEGRAM_ALLOWED_USERS
60
60
  fi
61
61
 
62
- # Write into the profile-local .env (symlinked to runtime on full installs).
62
+ # Write into runtime/.env (this is HERMES_HOME for the agent)
63
63
  ENVF="$RUNTIME/.env"
64
64
  python3 - "$ENVF" "$TELEGRAM_BOT_TOKEN" "$TELEGRAM_ALLOWED_USERS" <<'PYEOF'
65
65
  import sys, pathlib, re
@@ -76,7 +76,7 @@ chmod 600 "$ENVF"
76
76
  # Update role.yaml with the real bot username (in case it differs from the slug)
77
77
  yaml_set telegram.bot_username "$bot_username"
78
78
 
79
- # Enable telegram toolset for the named profile.
79
+ # Enable telegram toolset for the profile
80
80
  env HERMES_HOME="$RUNTIME" "$HERMES_BIN" tools enable telegram hermes-telegram 2>/dev/null \
81
81
  || warn " couldn't auto-enable telegram toolset; run: $ROLE_DIR/hermes tools"
82
82
 
@@ -3,9 +3,9 @@
3
3
  #
4
4
  # Source of truth is the repo-root .project.json `ticket_provider` block
5
5
  # (written by the CommonProject base template). The model is:
6
- # ONE board per repo — the PM owns it, the Scrum Master sentinel watches it.
7
- # So we never mint a per-agent, role-suffixed board ("Foo PM" / "Foo Scrum
8
- # Master"). Instead:
6
+ # ONE board per repo — the PM owns it, and its heartbeat reconciliation pass
7
+ # watches it. So we never mint a per-agent, role-suffixed board ("Foo PM" /
8
+ # "Foo Sentinel"). Instead:
9
9
  # 1. If .project.json already names a board -> BIND to it (no creation).
10
10
  # 2. Otherwise (hermes run on a repo with no CommonProject board yet)
11
11
  # -> create ONE repo-named board and write it back into .project.json so
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
- # Install systemd --user units: gateway, consumer, and hourly checkpoint timer.
2
+ # Install systemd --user units: gateway, consumer, and a fused heartbeat timer
3
+ # (board-reconciliation sentinel pass + gated runtime checkpoint, one tick).
3
4
  # shellcheck source=_lib.sh
4
5
  source "$(dirname "$0")/_lib.sh"
5
6
  load_role_env
@@ -8,23 +9,15 @@ already_done 70-systemd && { log "[70] systemd already installed — skipping";
8
9
  [[ "${SKIP_SYSTEMD:-0}" == "1" ]] && { log "[70] systemd — SKIPPED"; mark_done 70-systemd; exit 0; }
9
10
 
10
11
  RUNTIME="$ROLE_DIR/runtime"
11
- PROFILE_HOME="$(profile_or_runtime_home)"
12
+ REPO_ROOT="$(project_repo_path)" || REPO_ROOT="$ROLE_DIR"
12
13
  SYS_DIR="$HOME/.config/systemd/user"
13
- mkdir -p "$SYS_DIR"
14
+ mkdir -p "$SYS_DIR" "$RUNTIME/logs"
14
15
 
15
- # Resolve absolute path to the runtime checkpoint helper
16
- CHECKPOINT_BIN="$ROLE_DIR/.scripts/checkpoint.sh"
17
- cp "$(dirname "$0")/checkpoint.sh" "$CHECKPOINT_BIN" 2>/dev/null || cat > "$CHECKPOINT_BIN" <<'CKPT'
18
- #!/usr/bin/env bash
19
- set -euo pipefail
20
- RUNTIME_DIR="$(cd "$(dirname "$0")/../runtime" && pwd)"
21
- cd "$RUNTIME_DIR"
22
- git add -A
23
- if git diff --cached --quiet; then exit 0; fi
24
- git -c commit.gpgsign=false commit -m "checkpoint $(date -Iseconds)" >/dev/null
25
- git push origin HEAD 2>&1 | tail -1 || true
26
- CKPT
27
- chmod +x "$CHECKPOINT_BIN"
16
+ # The heartbeat runner (board-reconciliation sentinel pass + gated checkpoint)
17
+ # and the checkpoint helper both render into the role dir; just ensure they are
18
+ # executable. heartbeat.sh calls checkpoint.sh internally.
19
+ HEARTBEAT_BIN="$ROLE_DIR/.scripts/heartbeat.sh"
20
+ chmod +x "$HEARTBEAT_BIN" "$ROLE_DIR/.scripts/checkpoint.sh" 2>/dev/null || true
28
21
 
29
22
  # Gateway unit
30
23
  GW_UNIT="hermes-${AGENT_ID}-gateway.service"
@@ -36,15 +29,15 @@ Wants=network-online.target
36
29
 
37
30
  [Service]
38
31
  Type=simple
39
- Environment=HERMES_HOME=$PROFILE_HOME
32
+ Environment=HERMES_HOME=$RUNTIME
40
33
  Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
41
34
  Environment=CODEX_HOME=$CODEX_HOME
42
- EnvironmentFile=-$PROFILE_HOME/.env
35
+ EnvironmentFile=-$RUNTIME/.env
43
36
  ExecStart=$HERMES_BIN gateway run --replace
44
37
  Restart=on-failure
45
38
  RestartSec=10
46
- StandardOutput=append:$PROFILE_HOME/logs/gateway.systemd.log
47
- StandardError=append:$PROFILE_HOME/logs/gateway.systemd.log
39
+ StandardOutput=append:$RUNTIME/logs/gateway.systemd.log
40
+ StandardError=append:$RUNTIME/logs/gateway.systemd.log
48
41
 
49
42
  [Install]
50
43
  WantedBy=default.target
@@ -60,57 +53,69 @@ Wants=network-online.target
60
53
 
61
54
  [Service]
62
55
  Type=simple
63
- WorkingDirectory=$PROFILE_HOME
64
- Environment=HERMES_HOME=$PROFILE_HOME
56
+ WorkingDirectory=$RUNTIME
57
+ Environment=HERMES_HOME=$RUNTIME
65
58
  Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
66
59
  Environment=CODEX_HOME=$CODEX_HOME
67
- EnvironmentFile=-$PROFILE_HOME/.env
68
- ExecStart=$HERMES_AGENT_REPO/.venv/bin/python $PROFILE_HOME/bloodbank-consumer.py
60
+ EnvironmentFile=-$RUNTIME/.env
61
+ ExecStart=$HERMES_AGENT_REPO/.venv/bin/python $RUNTIME/bloodbank-consumer.py
69
62
  Restart=on-failure
70
63
  RestartSec=5
71
- StandardOutput=append:$PROFILE_HOME/logs/consumer.log
72
- StandardError=append:$PROFILE_HOME/logs/consumer.log
64
+ StandardOutput=append:$RUNTIME/logs/consumer.log
65
+ StandardError=append:$RUNTIME/logs/consumer.log
73
66
 
74
67
  [Install]
75
68
  WantedBy=default.target
76
69
  UNIT
77
70
 
78
- # Hourly checkpoint timer
79
- CKPT_SVC="hermes-${AGENT_ID}-checkpoint.service"
80
- CKPT_TIMER="hermes-${AGENT_ID}-checkpoint.timer"
81
- cat > "$SYS_DIR/$CKPT_SVC" <<UNIT
71
+ # Fused heartbeat: board-reconciliation sentinel pass + gated runtime checkpoint.
72
+ # Frequent ticks (1 min); heartbeat.sh's own cooldown/lock logic rate-limits the
73
+ # full Hermes pass, and the checkpoint is gated to ~hourly inside the runner.
74
+ # The per-agent EnvironmentFiles load ticket-provider creds for the sentinel pass.
75
+ HB_SVC="hermes-${AGENT_ID}-heartbeat.service"
76
+ HB_TIMER="hermes-${AGENT_ID}-heartbeat.timer"
77
+ cat > "$SYS_DIR/$HB_SVC" <<UNIT
82
78
  [Unit]
83
- Description=Hermes Runtime Checkpoint — $DISPLAY_NAME
79
+ Description=Hermes Heartbeat (reconcile + checkpoint) — $DISPLAY_NAME
80
+ After=network-online.target
81
+ Wants=network-online.target
84
82
 
85
83
  [Service]
86
84
  Type=oneshot
87
- ExecStart=$CHECKPOINT_BIN
88
- StandardOutput=append:$RUNTIME/logs/checkpoint.log
89
- StandardError=append:$RUNTIME/logs/checkpoint.log
85
+ WorkingDirectory=$REPO_ROOT
86
+ Environment=HERMES_HOME=$RUNTIME
87
+ Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
88
+ Environment=CODEX_HOME=$CODEX_HOME
89
+ EnvironmentFile=-%h/.config/hermes-agent/env
90
+ EnvironmentFile=-%h/.hermes/env
91
+ EnvironmentFile=-%h/.hermes/hermes-agent.env
92
+ EnvironmentFile=-%h/.hermes/${AGENT_ID}.env
93
+ EnvironmentFile=-$RUNTIME/.env
94
+ ExecStart=$HEARTBEAT_BIN
95
+ TimeoutStartSec=45min
96
+ StandardOutput=append:$RUNTIME/logs/heartbeat.log
97
+ StandardError=append:$RUNTIME/logs/heartbeat.log
90
98
  UNIT
91
- cat > "$SYS_DIR/$CKPT_TIMER" <<UNIT
99
+ cat > "$SYS_DIR/$HB_TIMER" <<UNIT
92
100
  [Unit]
93
- Description=Hourly checkpoint for $AGENT_ID
101
+ Description=Heartbeat (reconcile + checkpoint) for $AGENT_ID
94
102
 
95
103
  [Timer]
96
- OnBootSec=15min
97
- OnUnitActiveSec=1h
98
- Unit=$CKPT_SVC
104
+ OnBootSec=1min
105
+ OnUnitInactiveSec=1min
106
+ Unit=$HB_SVC
99
107
  Persistent=true
100
108
 
101
109
  [Install]
102
110
  WantedBy=timers.target
103
111
  UNIT
104
112
 
105
- mkdir -p "$RUNTIME/logs"
106
- mkdir -p "$PROFILE_HOME/logs"
107
-
108
113
  if systemd_user_available; then
109
114
  systemctl --user daemon-reload
110
115
  # `enable --now` both enables (persist across login) AND starts the unit now, so a
111
116
  # freshly provisioned agent comes up live instead of dormant. Units with missing
112
117
  # creds (e.g. a gateway with no Telegram token yet) fail softly via Restart=on-failure.
113
- for u in "$GW_UNIT" "$CSM_UNIT" "$CKPT_TIMER"; do
118
+ for u in "$GW_UNIT" "$CSM_UNIT" "$HB_TIMER"; do
114
119
  systemctl --user enable --now "$u" >/dev/null 2>&1 && log " enabled + started: $u" || warn " failed to enable/start: $u"
115
120
  done
116
121
  else