@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.
- package/dist/index.js +1852 -368
- package/dist/mcp-server.js +1782 -296
- package/package.json +14 -3
- package/templates/commonproject/copier.yml +3 -0
- package/templates/commonproject/template/.mise/scripts/provision-bmad-skills.py +480 -0
- package/templates/commonproject/template/.mise/scripts/sync-skills.py +449 -0
- package/templates/commonproject/template/mise.toml.jinja +9 -2
- package/templates/commonproject/tests/test_provision_bmad_skills.py +203 -0
- package/templates/commonproject/tests/test_sync_skills_topology.py +120 -0
- package/templates/hermes-agent/README.md +70 -29
- package/templates/hermes-agent/copier.yml +9 -6
- package/templates/hermes-agent/docs/architecture.md +75 -51
- package/templates/hermes-agent/docs/fleet-control-plane/architecture.md +11 -6
- package/templates/hermes-agent/docs/fleet-control-plane/epics-and-stories.md +2 -2
- package/templates/hermes-agent/docs/fleet-control-plane/prd.md +2 -3
- package/templates/hermes-agent/docs/operations.md +179 -81
- package/templates/hermes-agent/docs/runbooks/runtime-checkpoint-repair.md +31 -171
- package/templates/hermes-agent/docs/sentinel/README.md +10 -10
- package/templates/hermes-agent/docs/sentinel/architecture.md +6 -8
- package/templates/hermes-agent/docs/sentinel/development.md +12 -15
- package/templates/hermes-agent/docs/sentinel.md +1 -1
- package/templates/hermes-agent/install-local.sh +41 -4
- package/templates/hermes-agent/runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/scripts/degit-runtime.sh +192 -0
- package/templates/hermes-agent/scripts/fleet-prune-debris.sh +128 -0
- package/templates/hermes-agent/scripts/fleet-sync.sh +164 -7
- package/templates/hermes-agent/scripts/hermes-runtime-templatize.py +321 -0
- package/templates/hermes-agent/scripts/migrate-profile-scoped-chat-secrets.py +136 -0
- package/templates/hermes-agent/scripts/momo-unify-agent.py +224 -0
- package/templates/hermes-agent/scripts/repair-runtime-checkpoint.sh +7 -119
- package/templates/hermes-agent/template/.gitignore.jinja +5 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +4 -2
- package/templates/hermes-agent/template/.scripts/00-banner.sh +1 -1
- package/templates/hermes-agent/template/.scripts/01-config.sh +3 -0
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +12 -1
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +55 -129
- package/templates/hermes-agent/template/.scripts/30-telegram.sh +276 -22
- package/templates/hermes-agent/template/.scripts/31-slack.sh +362 -0
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +8 -66
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +44 -30
- package/templates/hermes-agent/template/.scripts/80-registry.sh +93 -13
- package/templates/hermes-agent/template/.scripts/99-summary.sh +1 -4
- package/templates/hermes-agent/template/.scripts/_lib.sh +66 -1
- package/templates/hermes-agent/template/.scripts/checkpoint.sh +3 -2
- package/templates/hermes-agent/template/.scripts/config.example.toml +10 -10
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +1 -1
- package/templates/hermes-agent/template/SOUL.md.jinja +2 -2
- package/templates/hermes-agent/template/hermes.jinja +6 -3
- package/templates/hermes-agent/template/role.yaml.jinja +24 -13
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +429 -130
- package/templates/hermes-agent/tests/test_fleet_upgrade_contract.py +176 -0
- package/templates/hermes-agent/tests/test_launcher_chat_secret_isolation.py +85 -0
- package/templates/hermes-agent/tests/test_local_runtime_provisioning.py +130 -0
- package/templates/hermes-agent/tests/test_operator_docs_local_runtime_contract.py +132 -0
- package/templates/hermes-agent/tests/test_profile_env_detach_contract.py +14 -0
- package/templates/hermes-agent/tests/test_profile_scoped_chat_secrets.py +89 -0
- package/templates/hermes-agent/tests/test_repository_privacy_contract.py +32 -0
- package/templates/hermes-agent/tests/test_slack_provisioning.py +344 -0
- package/templates/hermes-agent/tests/test_telegram_provisioning.py +475 -0
- package/templates/hermes-agent/.codegraph/daemon.pid +0 -6
- package/templates/hermes-agent/.omo/run-continuation/ses_0e5b28303ffeXxL53hZjKggXDW.json +0 -10
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +0 -181
- 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
|
-
#
|
|
3
|
-
#
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
|
4
|
-
#
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|