@delorenj/pjangler 1.2.19 → 1.2.25

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 (72) hide show
  1. package/dist/index.js +2353 -518
  2. package/dist/mcp-server.js +2331 -494
  3. package/package.json +14 -3
  4. package/templates/commonproject/copier.yml +6 -4
  5. package/templates/commonproject/template/.agents/hooks/README.md +13 -26
  6. package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
  7. package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
  8. package/templates/commonproject/template/.agents/local.example.json +2 -8
  9. package/templates/commonproject/template/.agents/skills.json +6 -0
  10. package/templates/commonproject/template/.mise/scripts/provision-bmad-skills.py +480 -0
  11. package/templates/commonproject/template/.mise/scripts/sync-skills.py +449 -0
  12. package/templates/commonproject/template/mise.toml.jinja +16 -16
  13. package/templates/commonproject/tests/test_provision_bmad_skills.py +203 -0
  14. package/templates/commonproject/tests/test_sync_skills_topology.py +120 -0
  15. package/templates/hermes-agent/README.md +70 -29
  16. package/templates/hermes-agent/copier.yml +9 -6
  17. package/templates/hermes-agent/docs/architecture.md +75 -51
  18. package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +11 -6
  19. package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +2 -2
  20. package/templates/hermes-agent/docs/fleet-control-plane/prd.md +2 -3
  21. package/templates/hermes-agent/docs/operations.md +179 -81
  22. package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +31 -171
  23. package/templates/hermes-agent/docs/sentinel/README.md +10 -10
  24. package/templates/hermes-agent/docs/sentinel/architecture.md +6 -8
  25. package/templates/hermes-agent/docs/sentinel/development.md +12 -15
  26. package/templates/hermes-agent/docs/sentinel.md +1 -1
  27. package/templates/hermes-agent/install-local.sh +41 -4
  28. package/templates/hermes-agent/runtime-scaffold/README.md +4 -2
  29. package/templates/hermes-agent/scripts/degit-runtime.sh +192 -0
  30. package/templates/hermes-agent/scripts/fleet-prune-debris.sh +128 -0
  31. package/templates/hermes-agent/scripts/fleet-sync.sh +164 -7
  32. package/templates/hermes-agent/scripts/hermes-runtime-templatize.py +321 -0
  33. package/templates/hermes-agent/scripts/migrate-profile-scoped-chat-secrets.py +136 -0
  34. package/templates/hermes-agent/scripts/momo-unify-agent.py +224 -0
  35. package/templates/hermes-agent/scripts/repair-runtime-checkpoint.sh +7 -119
  36. package/templates/hermes-agent/template/.gitignore.jinja +5 -2
  37. package/templates/hermes-agent/template/.runtime-scaffold/README.md +4 -2
  38. package/templates/hermes-agent/template/.scripts/00-banner.sh +1 -1
  39. package/templates/hermes-agent/template/.scripts/01-config.sh +3 -0
  40. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +12 -1
  41. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +55 -129
  42. package/templates/hermes-agent/template/.scripts/30-telegram.sh +276 -22
  43. package/templates/hermes-agent/template/.scripts/31-slack.sh +362 -0
  44. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +8 -66
  45. package/templates/hermes-agent/template/.scripts/70-systemd.sh +44 -30
  46. package/templates/hermes-agent/template/.scripts/80-registry.sh +93 -13
  47. package/templates/hermes-agent/template/.scripts/99-summary.sh +1 -4
  48. package/templates/hermes-agent/template/.scripts/_lib.sh +66 -1
  49. package/templates/hermes-agent/template/.scripts/checkpoint.sh +3 -2
  50. package/templates/hermes-agent/template/.scripts/config.example.toml +10 -10
  51. package/templates/hermes-agent/template/.scripts/heartbeat.sh +12 -1
  52. package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
  53. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +21 -0
  54. package/templates/hermes-agent/template/SOUL.md.jinja +36 -15
  55. package/templates/hermes-agent/template/hermes.jinja +6 -3
  56. package/templates/hermes-agent/template/role.yaml.jinja +24 -13
  57. package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +429 -130
  58. package/templates/hermes-agent/tests/test_fleet_upgrade_contract.py +176 -0
  59. package/templates/hermes-agent/tests/test_launcher_chat_secret_isolation.py +85 -0
  60. package/templates/hermes-agent/tests/test_local_runtime_provisioning.py +130 -0
  61. package/templates/hermes-agent/tests/test_operator_docs_local_runtime_contract.py +132 -0
  62. package/templates/hermes-agent/tests/test_profile_env_detach_contract.py +14 -0
  63. package/templates/hermes-agent/tests/test_profile_scoped_chat_secrets.py +89 -0
  64. package/templates/hermes-agent/tests/test_repository_privacy_contract.py +32 -0
  65. package/templates/hermes-agent/tests/test_slack_provisioning.py +344 -0
  66. package/templates/hermes-agent/tests/test_telegram_provisioning.py +475 -0
  67. package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
  68. package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
  69. package/templates/hermes-agent/.codegraph/daemon.pid +0 -6
  70. package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +0 -10
  71. package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +0 -181
  72. package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +0 -181
@@ -1,171 +1,31 @@
1
- # Runbook: repair a failing runtime-checkpoint service
2
-
3
- Status: Active operations runbook
4
- Applies to: every Hermes agent whose `runtime/` is a git submodule checkpointed
5
- by its `hermes-<agent>-heartbeat.service` systemd unit (the heartbeat runner
6
- calls `.scripts/checkpoint.sh` to commit+push the runtime).
7
-
8
- This runbook fixes the failure where the runtime checkpoint dies with
9
- **exit 128** and the agent's runtime "brain" stops being backed up. The
10
- checkpoint now runs inside the fused heartbeat tick (board-reconciliation
11
- sentinel pass + gated checkpoint), so the failing unit is the agent's
12
- `heartbeat.service`. It is written so any agent or operator can copy it to
13
- another repo and apply it safely. First captured 2026-06-01 fixing the
14
- Drumjangler PM checkpoint; the same fault was present on other agents in the
15
- fleet.
16
-
17
- ## Symptom
18
-
19
- ```bash
20
- systemctl --user status hermes-<agent>-heartbeat.service
21
- # Active: failed (Result: exit-code) ... status=128/n/a
22
- # the heartbeat invokes .../agents/hermes/<role>/.scripts/checkpoint.sh
23
- ```
24
-
25
- `checkpoint.sh` is just `cd runtime && git add -A && commit && push`. Run it by
26
- hand and you see the real error:
27
-
28
- ```text
29
- fatal: in unpopulated submodule 'agents/hermes/<role>/runtime'
30
- ```
31
-
32
- ## Root cause
33
-
34
- `agents/hermes/<role>/runtime` is registered as a **git submodule** in the
35
- project repo (`.gitmodules` has its URL; the parent index holds a `160000`
36
- gitlink), **but the submodule is not populated** — there is no `runtime/.git`,
37
- and `.git/modules/<path>` is gone. So any git command run *inside* `runtime/`
38
- resolves **up to the parent repo**, which treats `runtime/` as an unpopulated
39
- submodule path and makes `git add -A` fatal (exit 128).
40
-
41
- Confirm it:
42
-
43
- ```bash
44
- RT=agents/hermes/<role>/runtime
45
- git -C "$RT" rev-parse --show-toplevel # prints the PARENT repo, not RT -> red flag
46
- ls "$RT/.git" # missing -> unpopulated
47
- git ls-files -s "$RT" | head -1 # mode 160000 <sha> ... -> gitlink
48
- git config -f .gitmodules --get-regexp "$RT" # submodule.<path>.url git@...
49
- ls .git/modules/"$RT" 2>/dev/null || echo "objects gone -> must re-fetch from remote"
50
- ```
51
-
52
- Consequence: the agent's durable brain (`SOUL.md`, `memories/`, `profiles/`,
53
- `skills/`, config) lives on disk (the running agent still reads it) but is **not
54
- committed or pushed anywhere** — no backup until this is repaired.
55
-
56
- ## The fix (safe, in-place, preserves all on-disk state)
57
-
58
- The key safety property: `git reset --mixed` moves `HEAD` + the index only and
59
- **never rewrites working-tree files**, so the live brain (and a multi-hundred-MB
60
- `state.db`) is untouched. Never use `git submodule update`/`git checkout <tree>`
61
- here — those would overwrite live, uncommitted edits with the older committed
62
- versions.
63
-
64
- ```bash
65
- RT=agents/hermes/<role>/runtime
66
- URL=$(git config -f .gitmodules --get submodule."$RT".url)
67
-
68
- # 0. remote must be reachable (read-only)
69
- GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=15' git ls-remote "$URL" >/dev/null
70
-
71
- cd "$RT"
72
- # 1. re-init git in place on branch 'main' (creates .git/ only; no file changes)
73
- git init -q
74
- git symbolic-ref HEAD refs/heads/main
75
- git remote add origin "$URL"
76
- git fetch -q origin
77
- # 2. re-base history onto the remote tip WITHOUT touching the working tree
78
- git reset --mixed -q origin/main
79
- git branch --set-upstream-to=origin/main main 2>/dev/null || true
80
- # 3. the repo uses git-lfs for *.db/images (.gitattributes); register filters
81
- git lfs install --local
82
- # 4. restore tracked config that may be missing on disk (avoids spurious deletes)
83
- git checkout origin/main -- .gitattributes README.md 2>/dev/null || true
84
- ```
85
-
86
- ### Make it sustainable: back up the brain, not volatile state
87
-
88
- Even though `.gitattributes` routes `*.db` through LFS, committing a live,
89
- constantly-growing `state.db` (often hundreds of MB) every hour is not
90
- sustainable, and `lsp/` (node_modules), `checkpoints/` (a nested object store),
91
- and `sessions/` (ephemeral dumps) must never be committed. The checkpoint repo's
92
- job is the **durable brain**. Ensure `runtime/.gitignore` contains:
93
-
94
- ```gitignore
95
- # Runtime working state — NOT part of the durable brain backup.
96
- *.db
97
- *.db-wal
98
- *.db-shm
99
- *.sqlite
100
- *.sqlite3
101
- *.lock
102
- .update_check
103
- lsp/
104
- checkpoints/
105
- sessions/
106
- cron/
107
- *_cache.json
108
- ```
109
-
110
- Then untrack anything volatile that is currently tracked (keeps the files on
111
- disk):
112
-
113
- ```bash
114
- git rm -r --cached --ignore-unmatch state.db state.db-wal state.db-shm cron
115
- ```
116
-
117
- ### Verify BEFORE committing (mandatory safety gate)
118
-
119
- ```bash
120
- git add -A --dry-run | awk '{print $2}' | while read -r f; do
121
- [ -f "$f" ] && s=$(stat -c%s "$f") && [ "$s" -gt 1048576 ] && echo "BIG: $((s/1048576))MB $f"; done
122
- # ^ must print nothing. Also confirm no state.db / lsp/ / sessions/ / checkpoints/,
123
- # and no secrets (.env, auth.json, *.key, *token*) in the staged set.
124
- ```
125
-
126
- ### Commit, push, and bring the service green
127
-
128
- ```bash
129
- bash agents/hermes/<role>/.scripts/checkpoint.sh # exits 0, pushes brain
130
- systemctl --user reset-failed hermes-<agent>-heartbeat.service
131
- systemctl --user start hermes-<agent>-heartbeat.service
132
- systemctl --user show hermes-<agent>-heartbeat.service -p Result -p ExecMainStatus
133
- # Result=success ExecMainStatus=0
134
- ```
135
-
136
- ## One-shot
137
-
138
- `scripts/repair-runtime-checkpoint.sh` automates all of the above. It is
139
- **dry-run by default** (diagnoses and prints the plan) and only mutates with
140
- `--apply`. Run it from a project repo root:
141
-
142
- ```bash
143
- # diagnose
144
- hermes-agent-template/scripts/repair-runtime-checkpoint.sh agents/hermes/pm/runtime
145
- # apply the in-place re-attach + .gitignore policy + untrack (no commit)
146
- hermes-agent-template/scripts/repair-runtime-checkpoint.sh --apply agents/hermes/pm/runtime
147
- # then validate the service
148
- systemctl --user reset-failed <svc> && systemctl --user start <svc>
149
- ```
150
-
151
- ## Fleet sweep — this is rarely just one repo
152
-
153
- ```bash
154
- systemctl --user list-units --all '*-heartbeat.service' | grep -i failed
155
- # for each failing agent, locate its runtime and run the repair script.
156
- ```
157
-
158
- ## Lessons learned (why this happened / what to remember)
159
-
160
- - An **unpopulated submodule** silently redirects in-directory git commands to
161
- the parent repo; `git add -A` then fatals with exit 128. The fix is to restore
162
- the submodule's own `.git` (objects from the remote), not to touch the parent.
163
- - `git reset --mixed <ref>` is the safe way to re-attach history to a populated
164
- working directory: it preserves every on-disk file.
165
- - The checkpoint must **exclude volatile working state** (`state.db*`, `lsp/`,
166
- `checkpoints/`, `sessions/`, caches, locks). Back up identity + memory + skills.
167
- - Core dumps from the agent will **not** appear in `coredumpctl`/`/var/crash` on
168
- Ubuntu: apport discards crashes from unpackaged venv-Python binaries. Use
169
- `PYTHONFAULTHANDLER=1` or a private `core_pattern` to capture a backtrace.
170
- - Always run the fleet sweep — provisioning copies this layout to every agent,
171
- so a structural fault tends to be fleet-wide.
1
+ # Historical runbook: retired checkpoint architecture
2
+
3
+ Status: Historical evidence only — **do not execute**
4
+
5
+ This page formerly described recovery for an older per-agent persistence
6
+ design. That design is not the current Hermes template contract, and its
7
+ commands have intentionally been removed so an operator cannot mistake them
8
+ for a supported repair procedure.
9
+
10
+ Current agents use an ignored, pure-local
11
+ `agents/hermes/<role>/runtime/` directory. Provisioning refuses stale project
12
+ gitlinks or mappings at that exact path and preserves all existing runtime
13
+ bytes. The heartbeat performs board reconciliation; it is not the runtime
14
+ backup mechanism.
15
+
16
+ For current recovery and retirement procedures, use
17
+ [Operations](../operations.md#back-up-and-restore-an-agent). In particular:
18
+
19
+ - configure and verify an encrypted filesystem backup for the exact runtime
20
+ path;
21
+ - treat Hindsight as recovery for only the memories/events previously written
22
+ to its bank;
23
+ - treat the secret manager as recovery for only credentials deliberately
24
+ stored there;
25
+ - retire services and profile links without removing runtime data; and
26
+ - preserve retired runtime data because this release intentionally ships no
27
+ automated purge; any future purge requires a separately reviewed path-safe
28
+ tool and a verified off-host backup.
29
+
30
+ The earlier procedure remains recoverable from repository history for incident
31
+ forensics. Repository history is evidence, not an active operator interface.
@@ -20,9 +20,9 @@ and emits a decision event. A real finding is `held` and the ticket goes back to
20
20
  active.
21
21
 
22
22
  There is no longer a separate `scrum-master` role; the sentinel folded into the
23
- PM and now runs as part of the PM's **heartbeat** — a fused systemd-timer tick
24
- that does the board-reconciliation pass and then a gated runtime checkpoint
25
- (`.scripts/heartbeat.sh`). The engine itself lives under the PM at
23
+ PM and now runs as the PM's **heartbeat** systemd-timer tick
24
+ (`.scripts/heartbeat.sh`). Under the pure-local runtime contract this timer
25
+ performs board reconciliation only. The engine itself lives under the PM at
26
26
  `.scripts/sentinel/`.
27
27
 
28
28
  It talks to the ticket board through a pluggable adapter, so the same engine
@@ -50,8 +50,8 @@ The following is true as of June 1, 2026.
50
50
 
51
51
  ## Quick local install
52
52
 
53
- For a local, single-machine install (no GitHub runtime repo, no Telegram, no
54
- NATS), use the one-command bootstrap. From inside the target project:
53
+ For a local, single-machine install (no Telegram and no NATS), use the
54
+ one-command bootstrap. From inside the target project:
55
55
 
56
56
  ```bash
57
57
  export PLANE_API_KEY=<key> # or LINEAR_API_KEY / TRELLO_KEY + TRELLO_TOKEN
@@ -65,9 +65,9 @@ macOS). See [Development guide: local install](development.md#local-install-one-
65
65
 
66
66
  <!-- prettier-ignore -->
67
67
  > [!IMPORTANT]
68
- > Full (non-local) provisioning is outward-facing. It can create a GitHub
69
- > runtime repo, a Telegram bot, and a Plane project, and the Telegram step is
70
- > interactive. Use `install-local.sh` or the `SKIP_*` flags described in
68
+ > Full provisioning is outward-facing. It can configure a Telegram bot and a
69
+ > Plane project, and the Telegram step is interactive. Use `install-local.sh`
70
+ > or the `SKIP_*` flags described in
71
71
  > [Development guide:
72
72
  > provisioning](development.md#provisioning-the-pm-manual) for local or lean
73
73
  > installs.
@@ -83,9 +83,9 @@ All paths are relative to the repository root.
83
83
  | `template/.scripts/lib/ticket-provider.sh` | The adapter dispatcher (`tp`). The engine's only seam to a ticket system. |
84
84
  | `template/.scripts/providers/{linear,plane,trello}.sh` | The provider adapters. |
85
85
  | `template/.scripts/42-ticket-provider.sh` | Provisioning step that resolves or creates the board. |
86
- | `template/.scripts/70-systemd.sh` | Provisioning step that installs the gateway, consumer, and the fused `heartbeat` timer (board-reconciliation sentinel pass + gated runtime checkpoint). |
86
+ | `template/.scripts/70-systemd.sh` | Provisioning step that installs the profile gateway and board-reconciliation `heartbeat` timer. |
87
87
  | `install-local.sh` | One-command local install (no cloud, macOS + Linux). |
88
- | `template/.scripts/heartbeat.sh` | The heartbeat runner: the sentinel full-pass dispatch (with its own cooldown/lock) plus the gated runtime checkpoint, fused into one tick. |
88
+ | `template/.scripts/heartbeat.sh` | The heartbeat runner: sentinel full-pass dispatch with its own cooldown and lock. |
89
89
  | `template/.scripts/sentinel.prompt.md.jinja` | The prompt the runner feeds to Hermes for a full reconciliation pass (rendered to `.scripts/sentinel.prompt.md`). |
90
90
  | `template/.scripts/sentinel/bin/` | Enforcement tools: `issue-autonomous-review.sh`, `issue-close-gate.sh`, `emit-event.py`. |
91
91
  | `template/.scripts/sentinel/docs/` | Runtime protocol docs shipped to each deployment. |
@@ -22,12 +22,11 @@ provisioning](development.md#provisioning-the-pm-manual).
22
22
  ## The heartbeat loop
23
23
 
24
24
  A scheduler (`systemd` timer on Linux, `launchd` agent on macOS) fires the
25
- runner, `template/.scripts/heartbeat.sh`, about once a minute. Each tick fuses
26
- two jobs: a board-reconciliation **sentinel pass** and a **gated runtime
27
- checkpoint**. The sentinel pass is a cheap heartbeat that decides whether a
28
- full, LLM-backed reconciliation pass is worth running; the checkpoint
29
- (commit+push of the runtime submodule) is gated to at most once an hour. This
30
- keeps cost low while staying responsive.
25
+ runner, `template/.scripts/heartbeat.sh`, about once a minute. For a pure-local
26
+ runtime, each tick performs a board-reconciliation **sentinel pass**. The cheap
27
+ heartbeat decides whether a full, LLM-backed reconciliation pass is worth
28
+ running. This keeps cost low while staying responsive; runtime backup remains
29
+ an operator-managed filesystem concern outside the sentinel.
31
30
 
32
31
  The sentinel reads the work-state file,
33
32
  `runtime/continuous-ticket-sentinel-state.json`, and chooses one of these
@@ -193,8 +192,7 @@ A full pass moves through these components in order:
193
192
  which render an `accepted` or `held` verdict and emit a decision event; the
194
193
  loop acts on the verdict (treat as done and unblock dependents, or send the
195
194
  ticket back to active) without waiting on the operator.
196
- 6. The runner writes the outcome to the state file for the next tick, then
197
- opportunistically checkpoints the runtime (gated to ~hourly) before exiting.
195
+ 6. The runner writes the outcome to the state file for the next tick and exits.
198
196
 
199
197
  ## Read next
200
198
 
@@ -127,8 +127,8 @@ The script:
127
127
  5. binds the PM to the existing board (it never creates one — it scrubs the
128
128
  provider key from Copier's environment so `42-ticket-provider.sh` skips board
129
129
  creation),
130
- 6. installs the PM's heartbeat timer (board-reconciliation sentinel pass + gated
131
- runtime checkpoint) as a `launchd` agent on macOS or a `systemd` timer on
130
+ 6. installs the PM's board-reconciliation heartbeat timer as a `launchd` agent
131
+ on macOS or a `systemd` timer on
132
132
  Linux,
133
133
  7. smoke-tests the board connection through the adapter.
134
134
 
@@ -160,7 +160,8 @@ order. Several of them reach outside the repository.
160
160
  <!-- prettier-ignore -->
161
161
  > [!CAUTION]
162
162
  > Full provisioning is outward-facing and partly interactive.
163
- > `20-runtime-repo.sh` creates a private GitHub repo with `gh repo create`,
163
+ > `20-runtime-repo.sh` populates the ignored local runtime and links its profile;
164
+ > it creates no GitHub storage,
164
165
  > `30-telegram.sh` prompts for a BotFather token and blocks waiting for input,
165
166
  > and `42-ticket-provider.sh` can create a Plane project. Don't run full
166
167
  > provisioning unattended.
@@ -171,10 +172,10 @@ script checks its flag and skips cleanly.
171
172
  | Flag | Skips |
172
173
  | --- | --- |
173
174
  | `SKIP_TELEGRAM` | The interactive BotFather token step. |
174
- | `SKIP_RUNTIME_REPO` | Creating the GitHub runtime repo. |
175
+ | `SKIP_RUNTIME_REPO` | Legacy flag name: skip local runtime population/profile linking. No remote storage is created. |
175
176
  | `SKIP_PLANE` | Creating a Plane project. |
176
- | `SKIP_BLOODBANK` | Installing the BloodBank consumer. |
177
- | `SKIP_SYSTEMD` | Installing `systemd` units (gateway, consumer, heartbeat timer). |
177
+ | `SKIP_BLOODBANK` | Compatibility no-op; Bloodbank ingress is fleet-shared. |
178
+ | `SKIP_SYSTEMD` | Installing `systemd` units (profile gateway and heartbeat timer). |
178
179
 
179
180
  For example, a local install that creates no cloud resources:
180
181
 
@@ -191,9 +192,8 @@ on Linux through a `systemd` `EnvironmentFile` (for example
191
192
  `~/.hermes/<agent_id>.env`); on macOS the `launchd` agent sources that same
192
193
  per-agent env file, so write the key there.
193
194
 
194
- `SKIP_SYSTEMD` gates the gateway, consumer, and the fused `heartbeat` timer in
195
- `70-systemd.sh`. The heartbeat timer is what drives the sentinel pass and the
196
- gated checkpoint, so a local install that wants the sentinel running must leave
195
+ `SKIP_SYSTEMD` gates the profile gateway and `heartbeat` timer in
196
+ `70-systemd.sh`. A local install that wants the sentinel running must leave
197
197
  `SKIP_SYSTEMD` unset (or install the timer afterward).
198
198
 
199
199
  ## Propagating changes
@@ -219,12 +219,9 @@ The sentinel began as a bespoke loop on the `pm` role, was briefly extracted int
219
219
  a standalone `scrum-master` role with its own `continuous-ticket-sentinel` timer,
220
220
  and has now been folded back into the unified PM. Today there is exactly one
221
221
  role (`pm`), one engine (under `.scripts/sentinel/`), and one timer
222
- (`hermes-<agent>-heartbeat.timer`) that fuses the board-reconciliation sentinel
223
- pass with a gated runtime checkpoint. The old per-agent
224
- `hermes-<agent>-continuous-ticket-sentinel.timer` and the separate
225
- `hermes-<agent>-checkpoint.timer` are gone; both are replaced by the single
226
- `heartbeat` timer. The earlier standalone-role design is recoverable from Git
227
- history if you ever need to compare.
222
+ (`hermes-<agent>-heartbeat.timer`) for board reconciliation. The earlier
223
+ standalone-role design is recoverable from Git history if you ever need to
224
+ compare.
228
225
 
229
226
  ## Known gotchas
230
227
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  The PM runs a provider-agnostic ticket sentinel out-of-band on its heartbeat
4
4
  timer: a board-reconciliation pass with an autonomous adversarial review (act,
5
- do not wait), fused with a gated runtime checkpoint into a single timer tick.
5
+ do not wait). Pure-local runtime persistence is outside the heartbeat contract.
6
6
  The full documentation lives in a dedicated handoff guide.
7
7
 
8
8
  This page is kept as a stable pointer so existing links keep working.
@@ -21,6 +21,12 @@
21
21
  # Provider creds: LINEAR_API_KEY | PLANE_API_KEY+PLANE_BASE | TRELLO_KEY+TRELLO_TOKEN
22
22
  set -eu
23
23
 
24
+ # Fleet runtime publication. These values intentionally identify the reviewed
25
+ # fork commit; clean installs must never fall back to NousResearch/main.
26
+ HERMES_RUNTIME_GIT_URL="https://github.com/delorenj/hermes-agent.git"
27
+ HERMES_RUNTIME_GIT_REF="feature/PJAN-19-routing-publication"
28
+ HERMES_RUNTIME_GIT_SHA="113e1b182b6d72a7dd02a191f134a41668ceaf0e"
29
+
24
30
  say() { printf '\033[36m%s\033[0m\n' "$*"; }
25
31
  warn() { printf '\033[33m%s\033[0m\n' "$*" >&2; }
26
32
  die() { printf '\033[31m%s\033[0m\n' "$*" >&2; exit 1; }
@@ -53,7 +59,38 @@ if command -v hermes >/dev/null 2>&1; then
53
59
  say "1. hermes: found ($(command -v hermes))"
54
60
  else
55
61
  say "1. hermes: not found — installing"
56
- run "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash"
62
+ HERMES_INSTALL_DIR="${HERMES_INSTALL_DIR:-$HOME/.hermes/hermes-agent}"
63
+ if [ "${HAT_DRY_RUN:-0}" = "1" ]; then
64
+ say " [dry-run] clone $HERMES_RUNTIME_GIT_URL@$HERMES_RUNTIME_GIT_REF"
65
+ say " [dry-run] verify and install commit $HERMES_RUNTIME_GIT_SHA"
66
+ else
67
+ command -v git >/dev/null 2>&1 || die "git is required to install the pinned Hermes runtime"
68
+ if [ -d "$HERMES_INSTALL_DIR/.git" ]; then
69
+ _origin="$(git -C "$HERMES_INSTALL_DIR" remote get-url origin 2>/dev/null || true)"
70
+ [ "$_origin" = "$HERMES_RUNTIME_GIT_URL" ] \
71
+ || die "existing Hermes checkout origin is not the reviewed fleet fork: $HERMES_INSTALL_DIR"
72
+ git -C "$HERMES_INSTALL_DIR" fetch origin "$HERMES_RUNTIME_GIT_REF"
73
+ elif [ -e "$HERMES_INSTALL_DIR" ]; then
74
+ die "Hermes install path exists but is not the reviewed fork checkout: $HERMES_INSTALL_DIR"
75
+ else
76
+ mkdir -p "$(dirname "$HERMES_INSTALL_DIR")"
77
+ git clone --branch "$HERMES_RUNTIME_GIT_REF" --single-branch \
78
+ "$HERMES_RUNTIME_GIT_URL" "$HERMES_INSTALL_DIR"
79
+ fi
80
+ git -C "$HERMES_INSTALL_DIR" cat-file -e "$HERMES_RUNTIME_GIT_SHA^{commit}" \
81
+ || die "pinned Hermes commit is unavailable from the reviewed fork"
82
+ git -C "$HERMES_INSTALL_DIR" merge-base --is-ancestor \
83
+ "$HERMES_RUNTIME_GIT_SHA" "origin/$HERMES_RUNTIME_GIT_REF" \
84
+ || die "pinned Hermes commit is not on the reviewed publication ref"
85
+ bash "$HERMES_INSTALL_DIR/scripts/install.sh" \
86
+ --dir "$HERMES_INSTALL_DIR" \
87
+ --branch "$HERMES_RUNTIME_GIT_REF" \
88
+ --commit "$HERMES_RUNTIME_GIT_SHA" \
89
+ --skip-setup
90
+ _installed_sha="$(git -C "$HERMES_INSTALL_DIR" rev-parse HEAD)"
91
+ [ "$_installed_sha" = "$HERMES_RUNTIME_GIT_SHA" ] \
92
+ || die "Hermes installer did not retain pinned commit $HERMES_RUNTIME_GIT_SHA"
93
+ fi
57
94
  command -v hermes >/dev/null 2>&1 || die "hermes install did not put 'hermes' on PATH. Open a new shell and re-run."
58
95
  fi
59
96
  HERMES_BIN="$(command -v hermes)"
@@ -82,6 +119,9 @@ else
82
119
  [fleet]
83
120
  hermes_bin = "$HERMES_BIN"
84
121
  hermes_repo = "$HOME/.hermes/hermes-agent"
122
+ hermes_git_url = "$HERMES_RUNTIME_GIT_URL"
123
+ hermes_git_ref = "$HERMES_RUNTIME_GIT_REF"
124
+ hermes_git_sha = "$HERMES_RUNTIME_GIT_SHA"
85
125
  fleet_env = "~/.hermes/fleet.env"
86
126
  registry_file = "~/.hermes/agents-registry.yaml"
87
127
 
@@ -92,9 +132,6 @@ runtime_repo_owner = ""
92
132
  base = "${PLANE_BASE:-}"
93
133
  workspace = ""
94
134
 
95
- [bloodbank]
96
- nats_host = "127.0.0.1"
97
- nats_port = 4222
98
135
  TOML
99
136
  fi
100
137
  fi
@@ -13,13 +13,15 @@ and recoverable on failure.
13
13
  | `memories/MEMORY.md` | yes | The condensed mental-model summary loaded each session |
14
14
  | `memories/USER.md` | yes | The operator's persona (Jarad DeLorenzo, ...) |
15
15
  | `sessions/sessions.db` | yes (LFS) | SQLite store of every conversation |
16
- | `bloodbank-consumer.py` | yes | NATS subscriber using canonical subjects plus envelope routing data |
17
16
  | `decisions/` | yes | Agent-emitted decisions, one file per important call |
18
17
  | `.env` | **no** | API keys + Telegram bot token (per-machine secret) |
19
18
  | `auth.json` | **no** | Deprecated local OAuth store; fleet auth defaults to `HERMES_OAUTH_FILE=~/.hermes/auth.json` |
20
19
  | `audio_cache/`, `image_cache/` | **no** | Regenerable caches |
21
20
  | `sandboxes/` | **no** | Per-session ephemeral execution dirs |
22
- | `bloodbank-inbox/` | **no** | Inbox queue for incoming bloodbank events |
21
+
22
+ Bloodbank ingress is fleet-shared. The fleet gateway discovers this profile
23
+ through `~/.hermes/agents-registry.yaml` and routes commands by
24
+ `data.target_agent_id`; this runtime has no consumer process or inbox bridge.
23
25
 
24
26
  ## Checkpoint cadence
25
27
 
@@ -0,0 +1,192 @@
1
+ #!/usr/bin/env bash
2
+ # degit-runtime — make a repo's Hermes agent runtime(s) PURE LOCAL state.
3
+ #
4
+ # Run from (or point --root at) the ROOT of a project repo. Discovers every
5
+ # agents/hermes/*/runtime under it and, in one shot, removes it from git and
6
+ # gitignores it so it never re-enters version control.
7
+ #
8
+ # Policy = pure-local (fleet decision D2, 2026-07-21): the runtime is no longer
9
+ # a git-tracked submodule NOR its own checkpoint repo. Per-agent memory
10
+ # durability moves to the per-repo Hindsight bank. The
11
+ # github.com/delorenj/agent-hm-*-pm remotes are left intact (recoverable) but
12
+ # are no longer pushed to. This is the INVERSE of repair-runtime-checkpoint.sh,
13
+ # which keeps the checkpoint repo alive.
14
+ #
15
+ # Per runtime:
16
+ # Layer A (project repo, if one exists):
17
+ # - gitlink submodule -> git rm --cached <rel>
18
+ # + drop [submodule "<rel>"] from .gitmodules
19
+ # + rm -rf .git/modules/<rel>
20
+ # - plain tracked files -> git rm -r --cached <rel>
21
+ # - ensure .gitignore ignores agents/hermes/*/runtime/
22
+ # Layer B (the runtime itself):
23
+ # - if it has unpushed commits, push once (best-effort) unless --force
24
+ # - rm -rf <runtime>/.git (retire the checkpoint repo)
25
+ # - write <runtime>/.gitignore = "*\n!.gitignore" (pure-local marker)
26
+ #
27
+ # REPORTED but not touched here (fleet-level; handled by fleet-prune-debris.sh):
28
+ # ~/.hermes/profiles/<profile> symlink, agents-registry.yaml runtime_repo,
29
+ # and the now-dead -checkpoint.service/.timer units.
30
+ #
31
+ # Dry-run by default (prints the plan). --apply mutates. Idempotent: every step
32
+ # checks state first, so a second run is a clean no-op.
33
+ #
34
+ # Usage: degit-runtime.sh [--apply] [--force] [--root DIR]
35
+ set -euo pipefail
36
+
37
+ APPLY=0; FORCE=0; ROOT=""
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --apply) APPLY=1 ;;
41
+ --force) FORCE=1 ;;
42
+ --root) shift; ROOT="${1:-}" ;;
43
+ -h|--help) sed -n '2,37p' "$0"; exit 0 ;;
44
+ *) printf 'degit-runtime: unknown arg: %s\n' "$1" >&2; exit 2 ;;
45
+ esac
46
+ shift
47
+ done
48
+
49
+ die(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }
50
+ note(){ printf ' %s\n' "$*"; }
51
+ # run CMD... — execute on --apply, otherwise print the plan line.
52
+ run(){ if [ "$APPLY" -eq 1 ]; then "$@"; else printf ' [would] %s\n' "$*"; fi; }
53
+
54
+ ROOT="${ROOT:-$PWD}"
55
+ [ -d "$ROOT" ] || die "no such directory: $ROOT"
56
+ ROOT="$(cd "$ROOT" && pwd)"
57
+ [ -d "$ROOT/agents/hermes" ] || die "no agents/hermes under $ROOT — run from a project repo root (or pass --root)"
58
+
59
+ MODE="dry-run"; [ "$APPLY" -eq 1 ] && MODE="APPLY"
60
+ printf '== degit-runtime (%s) :: %s ==\n' "$MODE" "$ROOT"
61
+
62
+ # Project repo root (the superproject). Empty when the project has no git at all
63
+ # (e.g. DeLoDocs, pjangler) — Layer A is then skipped.
64
+ PROJ="$(git -C "$ROOT" rev-parse --show-toplevel 2>/dev/null || true)"
65
+ if [ -n "$PROJ" ]; then note "project repo: $PROJ"; else note "project repo: (none — Layer A skipped)"; fi
66
+
67
+ # Discover runtimes: agents/hermes/<role>/runtime (depth 2 under agents/hermes).
68
+ mapfile -t RUNTIMES < <(find "$ROOT/agents/hermes" -mindepth 2 -maxdepth 2 -type d -name runtime 2>/dev/null | sort)
69
+ [ "${#RUNTIMES[@]}" -gt 0 ] || die "no agents/hermes/*/runtime found under $ROOT"
70
+ note "runtimes: ${#RUNTIMES[@]}"
71
+
72
+ CHANGED=0
73
+
74
+ ensure_project_ignore(){ # $1 = relpath of runtime from project root
75
+ local rel="$1" gi="$PROJ/.gitignore"
76
+ # Prefer a broad, role-agnostic rule; fall back to the exact path.
77
+ if git -C "$PROJ" check-ignore -q "$rel" 2>/dev/null; then
78
+ note ".gitignore already ignores $rel"; return 0
79
+ fi
80
+ local rule="agents/hermes/*/runtime/"
81
+ if [ -f "$gi" ] && grep -qxF "$rule" "$gi" 2>/dev/null; then
82
+ note ".gitignore already has rule: $rule"; return 0
83
+ fi
84
+ if [ "$APPLY" -eq 1 ]; then
85
+ printf '\n# Hermes agent runtime — pure-local state, never tracked (degit-runtime).\n%s\n' "$rule" >> "$gi"
86
+ note "appended ignore rule to .gitignore: $rule"
87
+ else
88
+ printf ' [would] append %s to %s\n' "$rule" "$gi"
89
+ fi
90
+ CHANGED=1
91
+ }
92
+
93
+ layer_a(){ # $1 = absolute runtime dir
94
+ local rt="$1" rel
95
+ [ -n "$PROJ" ] || return 0
96
+ rel="${rt#"$PROJ"/}"
97
+ # Is it a gitlink submodule (mode 160000) in the index?
98
+ if git -C "$PROJ" ls-files --stage -- "$rel" 2>/dev/null | grep -q '^160000 '; then
99
+ note "Layer A: '$rel' is a gitlink submodule -> de-submodule"
100
+ # -f: the gitlink may be staged with a pointer differing from HEAD (e.g. a
101
+ # freshly-added, not-yet-committed submodule). --cached is index-only and
102
+ # never touches the working tree, so forcing is safe.
103
+ run git -C "$PROJ" rm --cached -f -q -- "$rel"
104
+ if [ -f "$PROJ/.gitmodules" ] && git -C "$PROJ" config -f "$PROJ/.gitmodules" --get "submodule.$rel.url" >/dev/null 2>&1; then
105
+ run git -C "$PROJ" config -f "$PROJ/.gitmodules" --remove-section "submodule.$rel"
106
+ # Drop .gitmodules entirely once no [submodule] sections remain.
107
+ # (In dry-run the section is still present, so this stays conservative.)
108
+ if [ ! -f "$PROJ/.gitmodules" ] || ! grep -q '^\[submodule' "$PROJ/.gitmodules" 2>/dev/null; then
109
+ run rm -f "$PROJ/.gitmodules"
110
+ run git -C "$PROJ" rm --cached -q --ignore-unmatch -- .gitmodules
111
+ fi
112
+ fi
113
+ # The real gitdir (runtime layer already removed the runtime/.git pointer).
114
+ [ -e "$PROJ/.git/modules/$rel" ] && run rm -rf "$PROJ/.git/modules/$rel"
115
+ CHANGED=1
116
+ elif git -C "$PROJ" ls-files --error-unmatch -- "$rel" >/dev/null 2>&1; then
117
+ note "Layer A: '$rel' is tracked as plain content -> unstage"
118
+ run git -C "$PROJ" rm -r --cached -q -- "$rel"
119
+ CHANGED=1
120
+ else
121
+ note "Layer A: '$rel' is not tracked in the project repo (good)"
122
+ fi
123
+ ensure_project_ignore "$rel"
124
+ }
125
+
126
+ layer_b(){ # $1 = absolute runtime dir
127
+ local rt="$1"
128
+ # hard guard before any rm -rf
129
+ case "$rt" in */runtime) : ;; *) die "refusing: '$rt' does not end in /runtime" ;; esac
130
+ [ -d "$rt" ] || die "refusing: runtime dir vanished: $rt"
131
+
132
+ if [ -e "$rt/.git" ]; then
133
+ # Guard unpushed local commits: push once (best-effort) unless --force.
134
+ local ahead=0
135
+ if git -C "$rt" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1; then
136
+ ahead="$(git -C "$rt" rev-list --count '@{u}..HEAD' 2>/dev/null || echo 0)"
137
+ else
138
+ ahead="unknown(no-upstream)"
139
+ fi
140
+ note "Layer B: runtime has .git (unpushed ahead: $ahead)"
141
+ if [ "$ahead" != "0" ]; then
142
+ if [ "$FORCE" -eq 1 ]; then
143
+ note " --force: discarding local .git without pushing ($ahead ahead)"
144
+ else
145
+ note " attempting one final push before retiring .git (--force to skip)"
146
+ if [ "$APPLY" -eq 1 ]; then
147
+ if GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=20' \
148
+ timeout 45 git -C "$rt" push 2>/dev/null; then
149
+ note " final push OK"
150
+ else
151
+ note " MANUAL: final push FAILED and commits are unpushed — skipping .git removal for this runtime (re-run with --force to discard, or push by hand)"
152
+ return 0
153
+ fi
154
+ else
155
+ printf ' [would] git -C %s push (final backup)\n' "$rt"
156
+ fi
157
+ fi
158
+ fi
159
+ run rm -rf "$rt/.git"
160
+ CHANGED=1
161
+ else
162
+ note "Layer B: runtime already has no .git (good)"
163
+ fi
164
+
165
+ # Pure-local marker: ignore everything except the marker itself.
166
+ local gi="$rt/.gitignore" want=$'*\n!.gitignore'
167
+ if [ -f "$gi" ] && [ "$(cat "$gi" 2>/dev/null)" = "$want" ]; then
168
+ note "Layer B: runtime/.gitignore already pure-local"
169
+ else
170
+ if [ "$APPLY" -eq 1 ]; then printf '%s\n' "$want" > "$gi"; note "wrote pure-local runtime/.gitignore"
171
+ else printf ' [would] write pure-local %s\n' "$gi"; fi
172
+ CHANGED=1
173
+ fi
174
+ }
175
+
176
+ for rt in "${RUNTIMES[@]}"; do
177
+ printf -- '-- %s\n' "$rt"
178
+ # Runtime layer FIRST: any final push needs the gitdir, which the project
179
+ # layer's `rm -rf .git/modules/<rel>` would otherwise remove.
180
+ layer_b "$rt"
181
+ layer_a "$rt"
182
+ done
183
+
184
+ echo
185
+ if [ "$CHANGED" -eq 0 ]; then
186
+ printf 'RESULT: already pure-local — nothing to do.\n'
187
+ elif [ "$APPLY" -eq 1 ]; then
188
+ printf 'RESULT: applied. Commit the project-repo changes (.gitignore / .gitmodules / unstaged runtime) when ready.\n'
189
+ printf ' Fleet-level cleanup (dead -checkpoint units, profile symlink) -> fleet-prune-debris.sh\n'
190
+ else
191
+ printf 'RESULT: dry-run only. Re-run with --apply to mutate.\n'
192
+ fi