@delorenj/pjangler 1.2.21 → 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 (63) hide show
  1. package/dist/index.js +1852 -368
  2. package/dist/mcp-server.js +1782 -296
  3. package/package.json +14 -3
  4. package/templates/commonproject/copier.yml +3 -0
  5. package/templates/commonproject/template/.mise/scripts/provision-bmad-skills.py +480 -0
  6. package/templates/commonproject/template/.mise/scripts/sync-skills.py +449 -0
  7. package/templates/commonproject/template/mise.toml.jinja +9 -2
  8. package/templates/commonproject/tests/test_provision_bmad_skills.py +203 -0
  9. package/templates/commonproject/tests/test_sync_skills_topology.py +120 -0
  10. package/templates/hermes-agent/README.md +70 -29
  11. package/templates/hermes-agent/copier.yml +9 -6
  12. package/templates/hermes-agent/docs/architecture.md +75 -51
  13. package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +11 -6
  14. package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +2 -2
  15. package/templates/hermes-agent/docs/fleet-control-plane/prd.md +2 -3
  16. package/templates/hermes-agent/docs/operations.md +179 -81
  17. package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +31 -171
  18. package/templates/hermes-agent/docs/sentinel/README.md +10 -10
  19. package/templates/hermes-agent/docs/sentinel/architecture.md +6 -8
  20. package/templates/hermes-agent/docs/sentinel/development.md +12 -15
  21. package/templates/hermes-agent/docs/sentinel.md +1 -1
  22. package/templates/hermes-agent/install-local.sh +41 -4
  23. package/templates/hermes-agent/runtime-scaffold/README.md +4 -2
  24. package/templates/hermes-agent/scripts/degit-runtime.sh +192 -0
  25. package/templates/hermes-agent/scripts/fleet-prune-debris.sh +128 -0
  26. package/templates/hermes-agent/scripts/fleet-sync.sh +164 -7
  27. package/templates/hermes-agent/scripts/hermes-runtime-templatize.py +321 -0
  28. package/templates/hermes-agent/scripts/migrate-profile-scoped-chat-secrets.py +136 -0
  29. package/templates/hermes-agent/scripts/momo-unify-agent.py +224 -0
  30. package/templates/hermes-agent/scripts/repair-runtime-checkpoint.sh +7 -119
  31. package/templates/hermes-agent/template/.gitignore.jinja +5 -2
  32. package/templates/hermes-agent/template/.runtime-scaffold/README.md +4 -2
  33. package/templates/hermes-agent/template/.scripts/00-banner.sh +1 -1
  34. package/templates/hermes-agent/template/.scripts/01-config.sh +3 -0
  35. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +12 -1
  36. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +55 -129
  37. package/templates/hermes-agent/template/.scripts/30-telegram.sh +276 -22
  38. package/templates/hermes-agent/template/.scripts/31-slack.sh +362 -0
  39. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +8 -66
  40. package/templates/hermes-agent/template/.scripts/70-systemd.sh +44 -30
  41. package/templates/hermes-agent/template/.scripts/80-registry.sh +93 -13
  42. package/templates/hermes-agent/template/.scripts/99-summary.sh +1 -4
  43. package/templates/hermes-agent/template/.scripts/_lib.sh +66 -1
  44. package/templates/hermes-agent/template/.scripts/checkpoint.sh +3 -2
  45. package/templates/hermes-agent/template/.scripts/config.example.toml +10 -10
  46. package/templates/hermes-agent/template/.scripts/heartbeat.sh +1 -1
  47. package/templates/hermes-agent/template/SOUL.md.jinja +2 -2
  48. package/templates/hermes-agent/template/hermes.jinja +6 -3
  49. package/templates/hermes-agent/template/role.yaml.jinja +24 -13
  50. package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +429 -130
  51. package/templates/hermes-agent/tests/test_fleet_upgrade_contract.py +176 -0
  52. package/templates/hermes-agent/tests/test_launcher_chat_secret_isolation.py +85 -0
  53. package/templates/hermes-agent/tests/test_local_runtime_provisioning.py +130 -0
  54. package/templates/hermes-agent/tests/test_operator_docs_local_runtime_contract.py +132 -0
  55. package/templates/hermes-agent/tests/test_profile_env_detach_contract.py +14 -0
  56. package/templates/hermes-agent/tests/test_profile_scoped_chat_secrets.py +89 -0
  57. package/templates/hermes-agent/tests/test_repository_privacy_contract.py +32 -0
  58. package/templates/hermes-agent/tests/test_slack_provisioning.py +344 -0
  59. package/templates/hermes-agent/tests/test_telegram_provisioning.py +475 -0
  60. package/templates/hermes-agent/.codegraph/daemon.pid +0 -6
  61. package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +0 -10
  62. package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +0 -181
  63. package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +0 -181
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env python3
2
+ """momo-unify-agent — apply the Momo/Hermes unification to a DEPLOYED agent.
3
+
4
+ Per role dir (agents/hermes/<role>) it:
5
+ 1. renders SOUL.md from momo/spec/momo-agent.spec.yaml × the agent's role.yaml
6
+ identity (charter, tone, prime directives + WIP lease, memory hygiene, doctrine),
7
+ 2. honcho-neutralizes runtime/config.yaml (memory.provider='', disabled_toolsets
8
+ += memory) so memory is the shared Hindsight bank only,
9
+ 3. installs the shared WIP=1 lease: copies .scripts/momo-wip-lock.py and wires
10
+ .scripts/heartbeat.sh (if present) to acquire/release it around the reconcile pass.
11
+
12
+ Idempotent; dry-run by default (--apply mutates). This is the existing-fleet
13
+ counterpart to the copier template (future agents get the same via provisioning).
14
+ Needs PyYAML — run with ~/.hermes/hermes-agent/.venv/bin/python.
15
+
16
+ Usage: momo-unify-agent.py --role-dir <agents/hermes/pm> [--apply] [--spec P]
17
+ """
18
+ from __future__ import annotations
19
+ import argparse, sys
20
+ from pathlib import Path
21
+ try:
22
+ import yaml
23
+ except ImportError:
24
+ sys.exit("needs PyYAML — run with ~/.hermes/hermes-agent/.venv/bin/python")
25
+
26
+ SPEC_DEFAULT = Path("~/code/33GOD/momo/spec/momo-agent.spec.yaml").expanduser()
27
+ LOCK_SRC = Path("~/code/33GOD/momo/skill/scripts/momo-wip-lock.py").expanduser()
28
+
29
+ SOUL_TMPL = """# {display_name}
30
+
31
+ You are **{display_name}** — the Momo PM/EM orchestrator for the `{repo}`
32
+ repository, running as its autonomous **Hermes carrier**. You are the autonomous
33
+ twin of the human-drivable Momo; you share ONE board and ONE Hindsight bank with
34
+ it, so stay attributable and never split-brain the state.
35
+
36
+ <!-- Rendered from momo/spec/momo-agent.spec.yaml (role: {role}) × this repo's identity.
37
+ Regenerate via momo-unify-agent.py / the Hermes adapter, not by hand. -->
38
+
39
+ ## Identity
40
+
41
+ | | |
42
+ | --- | --- |
43
+ | Agent ID | `{agent_id}` |
44
+ | Repo | `{repo}` |
45
+ | Role | `{role}` |
46
+ | Telegram | {telegram} |
47
+ | Purpose | {purpose} |
48
+
49
+ ## Scope
50
+
51
+ You operate only within the working directory of `{repo}`. Your HERMES_HOME is the
52
+ local runtime at `./runtime/`; Hermes loads its `config.yaml` directly.
53
+
54
+ ## Tone
55
+
56
+ {tone}
57
+
58
+ ## Role-specific behavior
59
+
60
+ {charter}
61
+
62
+ Prime directives (non-negotiable):
63
+
64
+ - **Never mutate code.** Every code change flows through a delegated worker.
65
+ - **WIP = 1**, shared with the human-drivable Momo via the driver lease
66
+ (`.scripts/momo-wip-lock.py` → `runtime/wip-driver.lock`) — acquire before driving,
67
+ back off if Momo holds it fresh; never double-drive one board.
68
+ - **Reviewer ≠ implementer** — independent adversarial review is the normal path.
69
+ - **Evidence over status** — a board column is a claim; repo evidence is proof.
70
+ - **Everything is an event** — record consequential calls as Bloodbank decision events.
71
+ - **Anti-stall** — never park a pass on operator sign-off.
72
+
73
+ Default execution: {default_execution}.
74
+
75
+ ## Memory hygiene
76
+
77
+ Your durable memory is the shared **Hindsight bank `{repo}`** — one bank per PROJECT,
78
+ shared with the human-drivable Momo twin. Honcho and the per-agent `runtime/memories/`
79
+ store are **neutralized**: do not rely on `MEMORY.md`/`USER.md`. Retain with
80
+ `hindsight memory retain {repo} "…" --context <cat>`; recall with
81
+ `hindsight memory recall {repo} "…"`.
82
+
83
+ ## Doctrine
84
+
85
+ Decide on the operator's behalf using **`~/code/33GOD/momo/PILLARS.md`** (canonical,
86
+ priority-ordered). This soul **references** that file; it does not copy it. Cite the
87
+ pillar(s) that drove a consequential call in its decision event.
88
+ """
89
+
90
+ LEASE_BLOCK = '''# Coexistence WIP=1 lease (momo E2/S2.3): don't full-drive if the human-drivable
91
+ # Momo holds it. A crashed holder's lease expires (ttl) so the board is never wedged.
92
+ WIP_LOCK="$RUNTIME/wip-driver.lock"
93
+ if ! python3 "$ROLE_DIR/.scripts/momo-wip-lock.py" acquire "$WIP_LOCK" "hermes:$AGENT_ID" --ttl 3600 >/dev/null 2>&1; then
94
+ printf '[heartbeat] WIP lease held by Momo — skipping full reconcile pass this tick\\n'
95
+ maybe_checkpoint
96
+ exit 0
97
+ fi
98
+ trap 'python3 "$ROLE_DIR/.scripts/momo-wip-lock.py" release "$WIP_LOCK" "hermes:$AGENT_ID" >/dev/null 2>&1 || true' EXIT
99
+
100
+ '''
101
+
102
+ SPAWN_ANCHOR = 'prompt="$(<"$PROMPT_FILE")"'
103
+
104
+
105
+ def load_role(role_dir: Path):
106
+ """role.yaml at the role dir, else inside runtime/."""
107
+ for p in (role_dir / "role.yaml", role_dir / "runtime" / "role.yaml"):
108
+ if p.is_file():
109
+ return yaml.safe_load(p.read_text()) or {}
110
+ return None
111
+
112
+
113
+ def identity(r: dict, spec: dict) -> dict:
114
+ repo = r.get("repo", "?"); role = r.get("role", "pm")
115
+ agent_id = r.get("agent_id", f"{repo}-{role}")
116
+ disp = r.get("display_name") or f"{repo.title()} {role.upper() if len(role) <= 3 else role.title()}"
117
+ tel = (r.get("telegram") or {}).get("bot_username", "") if isinstance(r.get("telegram"), dict) else ""
118
+ tone_key = r.get("soul_tone") or spec["personality"]["default_tone"]
119
+ rs = spec["roles"].get(role, spec["roles"]["pm"])
120
+ return {
121
+ "repo": repo, "role": role, "agent_id": agent_id, "display_name": disp,
122
+ "telegram": f"@{tel}" if tel else "—",
123
+ "purpose": rs["purpose_template"].format(repo=repo),
124
+ "tone": spec["personality"]["tones"].get(tone_key, spec["personality"]["tones"]["direct"]),
125
+ "charter": rs.get("charter", "").strip(),
126
+ "default_execution": rs.get("default_execution", "subagent-driven-development (WIP=1, spec + quality gates)"),
127
+ }
128
+
129
+
130
+ def neutralize_config(cfg: Path, apply: bool) -> str:
131
+ if not cfg.exists():
132
+ return "no config.yaml (skip honcho)"
133
+ base = yaml.safe_load(cfg.read_text()) or {}
134
+ already = (isinstance(base.get("memory"), dict) and base["memory"].get("provider") == ""
135
+ and "memory" in (base.get("agent", {}) or {}).get("disabled_toolsets", []))
136
+ if already:
137
+ return "honcho: already neutralized"
138
+ overlay = {"memory": {"provider": "", "memory_enabled": False, "user_profile_enabled": False},
139
+ "agent": {"disabled_toolsets": ["memory"]}}
140
+
141
+ def merge(b, o):
142
+ for k, v in o.items():
143
+ if isinstance(b.get(k), dict) and isinstance(v, dict):
144
+ merge(b[k], v)
145
+ else:
146
+ b[k] = v
147
+ merge(base, overlay)
148
+ if apply:
149
+ cfg.with_suffix(cfg.suffix + ".pre-unify.bak").write_text(cfg.read_text()) if not cfg.with_suffix(cfg.suffix + ".pre-unify.bak").exists() else None
150
+ yaml.safe_dump(base, cfg.open("w"), sort_keys=False)
151
+ return "honcho: neutralized"
152
+
153
+
154
+ def install_lease(role_dir: Path, apply: bool) -> str:
155
+ scripts = role_dir / ".scripts"
156
+ msgs = []
157
+ if not scripts.is_dir():
158
+ return "no .scripts (skip lease)"
159
+ dst = scripts / "momo-wip-lock.py"
160
+ if not dst.exists() or dst.read_bytes() != LOCK_SRC.read_bytes():
161
+ if apply:
162
+ dst.write_bytes(LOCK_SRC.read_bytes()); dst.chmod(0o755)
163
+ msgs.append("lock helper installed")
164
+ else:
165
+ msgs.append("lock helper present")
166
+ hb = scripts / "heartbeat.sh"
167
+ if not hb.is_file():
168
+ msgs.append("no heartbeat.sh (reactive agent — lease unused)")
169
+ return "; ".join(msgs)
170
+ txt = hb.read_text()
171
+ if "momo-wip-lock.py" in txt:
172
+ msgs.append("heartbeat already lease-wired")
173
+ elif SPAWN_ANCHOR in txt:
174
+ if apply:
175
+ hb.write_text(txt.replace(SPAWN_ANCHOR, LEASE_BLOCK + SPAWN_ANCHOR, 1))
176
+ msgs.append("heartbeat lease-wired")
177
+ else:
178
+ msgs.append("WARN: heartbeat spawn anchor not found — wire manually")
179
+ return "; ".join(msgs)
180
+
181
+
182
+ def unify(role_dir: Path, spec: dict, apply: bool, repo_override: str | None = None) -> None:
183
+ r = load_role(role_dir)
184
+ if r is None:
185
+ if not repo_override:
186
+ print(f" SKIP: no role.yaml at {role_dir} (bare-layout agent — pass --repo to unify it)"); return
187
+ r = {"repo": repo_override, "role": "pm", "agent_id": f"{repo_override}-pm"}
188
+ print(f" (synthesized identity for bare agent: {r['agent_id']})")
189
+ idt = identity(r, spec)
190
+ render = SOUL_TMPL.format(**idt)
191
+ # Write BOTH the role-dir source AND the LIVE runtime/SOUL.md that hermes
192
+ # actually loads (HERMES_HOME=runtime). Provisioning copies source->runtime,
193
+ # but for an already-deployed agent we must update the live copy directly.
194
+ for label, sp in (("src ", role_dir / "SOUL.md"),
195
+ ("live", role_dir / "runtime" / "SOUL.md")):
196
+ if not sp.parent.exists():
197
+ continue
198
+ cur = sp.read_text() if sp.is_file() else ""
199
+ if cur.strip() == render.strip():
200
+ print(f" SOUL[{label}]: already unified ({idt['agent_id']})")
201
+ else:
202
+ if apply:
203
+ sp.write_text(render)
204
+ print(f" SOUL[{label}]: {'written' if apply else 'would write'} ({idt['agent_id']})")
205
+ print(" " + neutralize_config(role_dir / "runtime" / "config.yaml", apply))
206
+ print(" " + install_lease(role_dir, apply))
207
+
208
+
209
+ def main() -> int:
210
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
211
+ ap.add_argument("--role-dir", required=True)
212
+ ap.add_argument("--spec", default=str(SPEC_DEFAULT))
213
+ ap.add_argument("--repo", default=None, help="synthesize identity for a bare agent with no role.yaml")
214
+ ap.add_argument("--apply", action="store_true")
215
+ a = ap.parse_args()
216
+ spec = yaml.safe_load(Path(a.spec).read_text())
217
+ role_dir = Path(a.role_dir).expanduser().resolve()
218
+ print(f"== unify ({'APPLY' if a.apply else 'dry-run'}) :: {role_dir}")
219
+ unify(role_dir, spec, a.apply, a.repo)
220
+ return 0
221
+
222
+
223
+ if __name__ == "__main__":
224
+ raise SystemExit(main())
@@ -1,122 +1,10 @@
1
1
  #!/usr/bin/env bash
2
- # Repair a failing runtime-checkpoint service caused by an UNPOPULATED runtime
3
- # submodule (`git add -A` -> "fatal: in unpopulated submodule" -> exit 128).
4
- #
5
- # Dry-run by default (diagnose + print plan). Pass --apply to mutate. The mutate
6
- # path only ever runs `git init` / `fetch` / `reset --mixed` / `.gitignore` edits
7
- # / `rm --cached` — it NEVER rewrites working-tree files, so live runtime state
8
- # (incl. a large state.db) is preserved. It does NOT commit or push; run the
9
- # role's checkpoint.sh afterward.
10
- #
11
- # Docs: docs/runbooks/runtime-checkpoint-repair.md
12
- # Usage: repair-runtime-checkpoint.sh [--apply] <runtime-dir>
2
+ # Retained only so older automation fails with a clear compatibility message.
3
+ # The former checkpoint repair belongs to a retired persistence design.
13
4
  set -euo pipefail
14
5
 
15
- APPLY=0; RT=""
16
- for a in "$@"; do
17
- case "$a" in
18
- --apply) APPLY=1 ;;
19
- -h|--help) sed -n '2,12p' "$0"; exit 0 ;;
20
- *) RT="$a" ;;
21
- esac
22
- done
23
-
24
- die(){ printf 'ERROR: %s\n' "$*" >&2; exit 1; }
25
- note(){ printf ' %s\n' "$*"; }
26
-
27
- [ -n "$RT" ] || die "usage: $0 [--apply] <runtime-dir> (e.g. agents/hermes/pm/runtime)"
28
- [ -d "$RT" ] || die "no such directory: $RT"
29
-
30
- PARENT="$(git -C "$RT" rev-parse --show-superproject-working-tree 2>/dev/null || true)"
31
- [ -n "$PARENT" ] || PARENT="$(git -C "$RT" rev-parse --show-toplevel 2>/dev/null || true)"
32
- [ -n "$PARENT" ] || die "$RT is not inside a git repository"
33
- RTABS="$(cd "$RT" && pwd)"
34
- RELPATH="${RTABS#"$PARENT"/}"
35
-
36
- MODE="dry-run"; [ "$APPLY" -eq 1 ] && MODE="APPLY"
37
- printf '== runtime-checkpoint repair (%s) ==\n' "$MODE"
38
- note "runtime: $RTABS"
39
- note "parent: $PARENT"
40
- note "subpath: $RELPATH"
41
-
42
- if git -C "$RTABS" rev-parse --git-dir >/dev/null 2>&1 && [ -e "$RTABS/.git" ]; then
43
- POPULATED=1; note "state: runtime already has its own .git (re-attach not needed)"
44
- else
45
- POPULATED=0; note "state: UNPOPULATED submodule (no .git) — re-attach required"
46
- fi
47
-
48
- URL="$(git -C "$PARENT" config -f "$PARENT/.gitmodules" --get "submodule.$RELPATH.url" 2>/dev/null || true)"
49
- [ -n "$URL" ] || die "no .gitmodules url for submodule '$RELPATH'"
50
- note "remote: $URL"
51
- GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=15' git ls-remote "$URL" >/dev/null 2>&1 \
52
- || die "remote not reachable (check SSH/network): $URL"
53
- note "remote reachable: yes"
54
-
55
- IGNORE_MARK='NOT part of the durable brain backup'
56
- IGNORE_BLOCK='# Runtime working state — NOT part of the durable brain backup.
57
- *.db
58
- *.db-wal
59
- *.db-shm
60
- *.sqlite
61
- *.sqlite3
62
- *.lock
63
- .update_check
64
- lsp/
65
- checkpoints/
66
- sessions/
67
- cron/
68
- *_cache.json'
69
-
70
- if [ "$APPLY" -eq 0 ]; then
71
- cat <<EOF
72
-
73
- PLAN (dry-run; re-run with --apply):
74
- cd $RELPATH
75
- $( [ "$POPULATED" -eq 0 ] && echo "git init -q; git symbolic-ref HEAD refs/heads/main; git remote add origin <url>; git fetch origin; git reset --mixed origin/main" || echo "(already populated — skip re-init)" )
76
- git lfs install --local
77
- git checkout origin/main -- .gitattributes README.md # restore tracked config
78
- ensure runtime/.gitignore has the volatile-state block
79
- git rm -r --cached --ignore-unmatch state.db state.db-wal state.db-shm cron
80
- # safety gate: git add -A --dry-run must show no >1MB blobs / secrets / volatile paths
81
- then: run the role's checkpoint.sh and (re)start the systemd checkpoint unit.
82
- EOF
83
- exit 0
84
- fi
85
-
86
- cd "$RTABS"
87
- if [ "$POPULATED" -eq 0 ]; then
88
- git init -q
89
- git symbolic-ref HEAD refs/heads/main
90
- git remote add origin "$URL" 2>/dev/null || git remote set-url origin "$URL"
91
- GIT_SSH_COMMAND='ssh -o BatchMode=yes -o ConnectTimeout=20' git fetch -q origin
92
- git reset --mixed -q origin/main
93
- git branch --set-upstream-to=origin/main main >/dev/null 2>&1 || true
94
- note "re-attached HEAD -> $(git rev-parse --short HEAD) on $(git rev-parse --abbrev-ref HEAD)"
95
- fi
96
- git lfs install --local >/dev/null 2>&1 || true
97
- git checkout origin/main -- .gitattributes README.md 2>/dev/null || true
98
-
99
- touch .gitignore
100
- if grep -q "$IGNORE_MARK" .gitignore 2>/dev/null; then
101
- note ".gitignore already has the volatile-state block"
102
- else
103
- printf '\n%s\n' "$IGNORE_BLOCK" >> .gitignore
104
- note "appended volatile-state ignore block to .gitignore"
105
- fi
106
- git rm -r --cached --quiet --ignore-unmatch state.db state.db-wal state.db-shm cron 2>/dev/null || true
107
-
108
- # ---- mandatory safety gate ----
109
- staged(){ git add -A --dry-run 2>/dev/null | sed -E "s/^[a-z]+ '?//; s/'?$//"; }
110
- BIG="$(staged | while read -r f; do [ -f "$f" ] && s=$(stat -c%s "$f" 2>/dev/null) && [ "${s:-0}" -gt 1048576 ] && printf '%s (%sMB)\n' "$f" "$((s/1048576))"; done)"
111
- SECRETS="$(staged | grep -iE '(^|/)(\.env|\.env\.|auth\.json|auth\.lock)([./]|$)|\.(pem|key)$' || true)"
112
- VOL="$(staged | grep -E '(^|/)(state\.db|lsp|sessions|checkpoints)(/|$)' || true)"
113
- if [ -n "$BIG$SECRETS$VOL" ]; then
114
- printf 'SAFETY GATE FAILED — fix .gitignore before committing.\n' >&2
115
- [ -n "$BIG" ] && printf ' >1MB blobs:\n%s\n' "$BIG" >&2
116
- [ -n "$SECRETS" ] && printf ' secrets:\n%s\n' "$SECRETS" >&2
117
- [ -n "$VOL" ] && printf ' volatile state:\n%s\n' "$VOL" >&2
118
- exit 2
119
- fi
120
- note "safety gate passed: staged set is brain-only."
121
- note "DONE. Now run the role's checkpoint.sh, then:"
122
- note " systemctl --user reset-failed <checkpoint-service> && systemctl --user start <checkpoint-service>"
6
+ printf '%s\n' \
7
+ 'ERROR: repair-runtime-checkpoint.sh is retired and intentionally non-operational.' \
8
+ 'Current Hermes runtimes are ignored local state; this command changed nothing.' \
9
+ 'See docs/operations.md for backup, restore, and retention policy.' >&2
10
+ exit 64
@@ -1,6 +1,9 @@
1
1
  # Per-role ignores in agents/hermes/{{ role }}/
2
2
 
3
- # The runtime submodule manages its own ignores (.env, auth.json, etc.).
4
- # We just ignore the provisioning log so it doesn't end up in the project repo.
3
+ # The local runtime contains mutable agent state and secrets. It is never a
4
+ # project submodule or publishable project content.
5
+ runtime/
6
+
7
+ # Provisioning logs are host-local as well.
5
8
  .scripts/.provision.log
6
9
  .scripts/.last-run
@@ -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
 
@@ -13,7 +13,7 @@ cat >&2 <<EOF
13
13
  │ display $DISPLAY_NAME
14
14
  │ telegram @$BOT_HANDLE (one bot per agent)
15
15
  │ plane $PLANE_WORKSPACE workspace
16
- │ runtime gh:$RUNTIME_REPO (auto-checkpointed)
16
+ │ runtime $ROLE_DIR/runtime (pure-local, ignored)
17
17
  │ profile ~/.hermes/profiles/$PROFILE_NAME
18
18
  ╰────────────────────────────────────────────────────────────────────────╯
19
19
 
@@ -23,6 +23,9 @@ else
23
23
  [fleet]
24
24
  hermes_bin = "~/.hermes/hermes-agent/.venv/bin/hermes"
25
25
  hermes_repo = "~/.hermes/hermes-agent"
26
+ hermes_git_url = "https://github.com/delorenj/hermes-agent.git"
27
+ hermes_git_ref = "feature/PJAN-19-routing-publication"
28
+ hermes_git_sha = "113e1b182b6d72a7dd02a191f134a41668ceaf0e"
26
29
  fleet_env = "~/.hermes/fleet.env"
27
30
  registry_file = "~/.hermes/agents-registry.yaml"
28
31
  canonical_skills_dir = "/home/delorenj/.agents/skills"
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
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
3
+ # HERMES_HOME is the ignored ./runtime directory; this profile dir is a staging area
4
4
  # (cred-stripped .env + SOUL) that 20-runtime-repo.sh folds into the runtime and
5
5
  # then replaces with a symlink ~/.hermes/profiles/<name> -> ./runtime. That
6
6
  # symlink is load-bearing: hermes resolves `--profile <name>` invocations
@@ -57,6 +57,17 @@ rm -f "$PROFILE_HOME/gateway.pid" "$PROFILE_HOME/gateway_state.json" \
57
57
  # never borrow the parent's.
58
58
  PROFILE_ENV="$PROFILE_HOME/.env"
59
59
  if [[ -f "$PROFILE_ENV" ]]; then
60
+ # Some Hermes versions implement `profile create --clone` by symlinking the
61
+ # cloned .env back to the default profile. Never sanitize through that
62
+ # symlink: doing so would delete the operator's fleet-wide provider secrets.
63
+ # Break only the .env link into a private, mode-0600 copy first.
64
+ if [[ -L "$PROFILE_ENV" ]]; then
65
+ log " detaching cloned .env symlink before profile-local sanitization"
66
+ PROFILE_ENV_COPY="$PROFILE_HOME/.env.profile-local.$$"
67
+ cp -L "$PROFILE_ENV" "$PROFILE_ENV_COPY"
68
+ chmod 600 "$PROFILE_ENV_COPY"
69
+ mv -fT "$PROFILE_ENV_COPY" "$PROFILE_ENV"
70
+ fi
60
71
  log " stripping inherited platform credentials from profile .env"
61
72
  python3 - "$PROFILE_ENV" <<'PYEOF'
62
73
  import re, sys, pathlib