@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
@@ -5,7 +5,6 @@ source "$(dirname "$0")/_lib.sh"
5
5
  load_role_env
6
6
 
7
7
  GW_UNIT="hermes-${AGENT_ID}-gateway.service"
8
- CSM_UNIT="hermes-${AGENT_ID}-consumer.service"
9
8
  HB_TIMER="hermes-${AGENT_ID}-heartbeat.timer"
10
9
 
11
10
  cat >&2 <<EOF
@@ -15,18 +14,16 @@ cat >&2 <<EOF
15
14
  │ Talk: @$BOT_HANDLE (Telegram DM)
16
15
  │ Shell: $ROLE_DIR/hermes chat "status"
17
16
  │ Board: $PLANE_BASE/$PLANE_WORKSPACE/projects/$(cat "$ROLE_DIR/.scripts/.plane-project-id" 2>/dev/null || echo "(skipped)")
18
- │ Runtime: gh:$RUNTIME_REPO (reconcile + checkpoint heartbeat)
17
+ │ Runtime: $ROLE_DIR/runtime (pure-local, ignored)
19
18
  │ Hermes bin: $HERMES_BIN
20
19
  │ Fleet env: $FLEET_ENV
21
20
 
22
21
  │ Start fleet daemons:
23
22
  │ systemctl --user start $GW_UNIT
24
- │ systemctl --user start $CSM_UNIT
25
23
  │ systemctl --user start $HB_TIMER
26
24
 
27
25
  │ Tail logs:
28
26
  │ journalctl --user -fu $GW_UNIT
29
- │ journalctl --user -fu $CSM_UNIT
30
27
 
31
28
  │ Fleet status:
32
29
  │ python3 -c "import yaml,pathlib; print(yaml.safe_dump(yaml.safe_load(pathlib.Path('$REGISTRY_FILE').read_text())['agents']))"
@@ -145,10 +145,17 @@ if [[ -f "$FLEET_ENV" ]]; then
145
145
  # shellcheck disable=SC1090
146
146
  source "$FLEET_ENV"
147
147
  fi
148
+ # Identity-bearing chat credentials are never fleet-scoped. Platform wiring
149
+ # steps capture explicit invocation values before sourcing this library and
150
+ # restore only those values afterward; all other provisioning steps stay clean.
151
+ unset TELEGRAM_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN
148
152
 
149
153
  # Tools we expect on the host
150
154
  HERMES_BIN="${HERMES_BIN:-${HERMES_FLEET_BIN:-$(config_get fleet.hermes_bin "$HOME/.hermes/hermes-agent/.venv/bin/hermes")}}"
151
155
  HERMES_AGENT_REPO="${HERMES_AGENT_REPO:-${HERMES_FLEET_REPO:-$(config_get fleet.hermes_repo "$HOME/.hermes/hermes-agent")}}"
156
+ HERMES_RUNTIME_GIT_URL="${HERMES_RUNTIME_GIT_URL:-$(config_get fleet.hermes_git_url 'https://github.com/delorenj/hermes-agent.git')}"
157
+ HERMES_RUNTIME_GIT_REF="${HERMES_RUNTIME_GIT_REF:-$(config_get fleet.hermes_git_ref 'feature/PJAN-19-routing-publication')}"
158
+ HERMES_RUNTIME_GIT_SHA="${HERMES_RUNTIME_GIT_SHA:-$(config_get fleet.hermes_git_sha '113e1b182b6d72a7dd02a191f134a41668ceaf0e')}"
152
159
  HERMES_OAUTH_FILE="${HERMES_OAUTH_FILE:-${HERMES_FLEET_OAUTH_FILE:-$(config_get fleet.oauth_file "$HOME/.hermes/auth.json")}}"
153
160
  CODEX_HOME="${CODEX_HOME:-${HERMES_FLEET_CODEX_HOME:-$(config_get fleet.codex_home "$HOME/.codex")}}"
154
161
  # Prefer a scaffold vendored into this agent directory; fall back to the configured template path.
@@ -158,6 +165,29 @@ if [[ ! -d "$RUNTIME_SCAFFOLD_DIR" ]]; then
158
165
  fi
159
166
  REGISTRY_FILE="${REGISTRY_FILE:-${HERMES_FLEET_REGISTRY_FILE:-$(config_get fleet.registry_file "$HOME/.hermes/agents-registry.yaml")}}"
160
167
 
168
+ # Cross-process serialization for fleet identity claims and registry updates.
169
+ # The lock file may remain on disk; flock ownership is kernel-scoped, so a
170
+ # crashed provisioner cannot leave a permanently stale lock.
171
+ FLEET_LOCK_FD=""
172
+ fleet_lock_acquire() {
173
+ command -v flock >/dev/null 2>&1 || die "flock is required for safe fleet registry updates"
174
+ local lock_file="${REGISTRY_FILE}.lock"
175
+ mkdir -p "$(dirname "$lock_file")"
176
+ [[ ! -L "$lock_file" ]] || die "refusing fleet lock symlink: $lock_file"
177
+ exec {FLEET_LOCK_FD}>"$lock_file"
178
+ chmod 600 "$lock_file"
179
+ flock -w "${FLEET_LOCK_TIMEOUT_SECONDS:-30}" "$FLEET_LOCK_FD" \
180
+ || die "timed out waiting for fleet registry lock: $lock_file"
181
+ }
182
+
183
+ fleet_lock_release() {
184
+ if [[ "${FLEET_LOCK_FD:-}" =~ ^[0-9]+$ ]]; then
185
+ flock -u "$FLEET_LOCK_FD" 2>/dev/null || true
186
+ eval "exec ${FLEET_LOCK_FD}>&-"
187
+ fi
188
+ FLEET_LOCK_FD=""
189
+ }
190
+
161
191
  # Bloodbank / NATS
162
192
  BLOODBANK_NATS_HOST="${BLOODBANK_NATS_HOST:-$(config_get bloodbank.nats_host '127.0.0.1')}"
163
193
  BLOODBANK_NATS_PORT="${BLOODBANK_NATS_PORT:-$(config_get bloodbank.nats_port '4222')}"
@@ -167,7 +197,8 @@ BLOODBANK_COMPOSE_DIR="${BLOODBANK_COMPOSE_DIR:-$(config_get bloodbank.compose_d
167
197
  PLANE_BASE="${PLANE_BASE:-$(config_get plane.base 'https://plane.delo.sh')}"
168
198
  PLANE_API_KEY="${PLANE_API_KEY:-${PLANE_33GOD_API_KEY:-}}"
169
199
 
170
- export FLEET_ENV HERMES_BIN HERMES_AGENT_REPO HERMES_OAUTH_FILE CODEX_HOME \
200
+ export FLEET_ENV HERMES_BIN HERMES_AGENT_REPO HERMES_RUNTIME_GIT_URL \
201
+ HERMES_RUNTIME_GIT_REF HERMES_RUNTIME_GIT_SHA HERMES_OAUTH_FILE CODEX_HOME \
171
202
  RUNTIME_SCAFFOLD_DIR REGISTRY_FILE \
172
203
  BLOODBANK_NATS_HOST BLOODBANK_NATS_PORT BLOODBANK_COMPOSE_DIR \
173
204
  PLANE_BASE PLANE_API_KEY
@@ -180,6 +211,40 @@ systemd_user_available() {
180
211
  [[ "$state" =~ ^(running|degraded|starting|maintenance)$ ]]
181
212
  }
182
213
 
214
+ # Query one systemd user-unit state without conflating every non-zero exit with
215
+ # an inactive/disabled unit. Output is `ok|<state>` only for documented
216
+ # state/exit-code pairs; D-Bus, manager, and arbitrary query failures become
217
+ # `error|<safe summary>` so retirement callers can fail closed.
218
+ systemctl_user_unit_state() {
219
+ local query="$1" unit="$2" output rc first_line
220
+ command -v systemctl >/dev/null 2>&1 \
221
+ || { printf 'error|systemctl unavailable'; return 0; }
222
+ set +e
223
+ output="$(LC_ALL=C systemctl --user "$query" "$unit" 2>&1)"
224
+ rc=$?
225
+ set -e
226
+ first_line="${output%%$'\n'*}"
227
+ first_line="${first_line//$'\t'/ }"
228
+ first_line="${first_line//|/}"
229
+ case "$query:$rc:$first_line" in
230
+ is-active:0:active|is-active:0:reloading|is-active:0:activating|is-active:0:deactivating)
231
+ printf 'ok|%s' "$first_line" ;;
232
+ is-active:3:inactive|is-active:3:failed)
233
+ printf 'ok|%s' "$first_line" ;;
234
+ is-active:4:inactive)
235
+ printf 'ok|not-found' ;;
236
+ is-enabled:0:enabled|is-enabled:0:enabled-runtime|is-enabled:0:linked|is-enabled:0:linked-runtime|is-enabled:0:alias|is-enabled:0:static|is-enabled:0:indirect|is-enabled:0:generated|is-enabled:0:transient)
237
+ printf 'ok|%s' "$first_line" ;;
238
+ is-enabled:1:disabled|is-enabled:1:masked|is-enabled:1:masked-runtime)
239
+ printf 'ok|%s' "$first_line" ;;
240
+ is-enabled:4:not-found)
241
+ printf 'ok|not-found' ;;
242
+ *)
243
+ [[ -n "$first_line" ]] || first_line="exit $rc with no state"
244
+ printf 'error|%s' "$first_line" ;;
245
+ esac
246
+ }
247
+
183
248
  # Resolve project repo path (the repo that holds agents/hermes/<role>/).
184
249
  # Walk up from $ROLE_DIR until we find a git root that isn't us.
185
250
  project_repo_path() {
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bash
2
- # Auto-checkpoint commit for the agent's runtime submodule.
2
+ # Legacy checkpoint compatibility for installations whose runtime is still a
3
+ # nested Git repository. Pure-local runtimes intentionally skip this script.
3
4
  # Idempotent — exits 0 with no commit if there are no changes.
4
5
  #
5
6
  # Secret-scan gate (PJAN): before committing, the staged diff is scanned for
@@ -11,7 +12,7 @@ set -euo pipefail
11
12
  RUNTIME_DIR="$(cd "$(dirname "$0")/../runtime" && pwd)"
12
13
  cd "$RUNTIME_DIR"
13
14
 
14
- # Skip if not a git repo (e.g. submodule not initialized)
15
+ # Skip when the pure-local runtime has no Git metadata.
15
16
  [[ -d .git || -f .git ]] || exit 0
16
17
 
17
18
  # Returns 0 when the staged diff looks clean, 1 when a likely secret is present.
@@ -13,8 +13,13 @@
13
13
  [fleet]
14
14
  # Absolute path to the shared Hermes executable every agent launcher execs.
15
15
  hermes_bin = "~/.hermes/hermes-agent/.venv/bin/hermes"
16
- # Checkout of the hermes-agent repo. Its .venv is used to install nats-py, etc.
16
+ # Checkout of the shared hermes-agent repo.
17
17
  hermes_repo = "~/.hermes/hermes-agent"
18
+ # Reviewed fleet runtime publication. Provisioning metadata and clean installs
19
+ # must agree on this fork/ref/SHA; do not substitute upstream main.
20
+ hermes_git_url = "https://github.com/delorenj/hermes-agent.git"
21
+ hermes_git_ref = "feature/PJAN-19-routing-publication"
22
+ hermes_git_sha = "113e1b182b6d72a7dd02a191f134a41668ceaf0e"
18
23
  # Fallback runtime scaffold dir — used only when an agent's vendored
19
24
  # ./.runtime-scaffold is missing.
20
25
  runtime_scaffold_dir = "~/code/hermes-agent-template/runtime-scaffold"
@@ -45,17 +50,12 @@ symlinked_runtime_skills = [
45
50
  ]
46
51
 
47
52
  [github]
48
- # Owner of the per-agent runtime repos (creates <owner>/agent-hm-<repo>-<role>).
49
- runtime_repo_owner = "delorenj"
53
+ # LEGACY INERT COMPATIBILITY METADATA ONLY. Older role manifests may still
54
+ # carry this owner field. Current provisioning does not create, attach,
55
+ # synchronize, restore, archive, or remove runtime storage from this value.
56
+ runtime_repo_owner = ""
50
57
 
51
58
  [plane]
52
59
  # Plane instance base URL and workspace slug (one project per agent).
53
60
  base = "https://plane.delo.sh"
54
61
  workspace = "33god"
55
-
56
- [bloodbank]
57
- # NATS endpoint the consumer connects to.
58
- nats_host = "127.0.0.1"
59
- nats_port = 4222
60
- # Where the bloodbank docker compose lives (used only for a hint message). ~ ok.
61
- compose_dir = "~/code/33GOD/bloodbank"
@@ -8,7 +8,7 @@
8
8
  # full pass executes the role's sentinel.prompt.md, which reasons about tickets
9
9
  # through the ticket-provider adapter (Linear | Plane | Trello) — never a
10
10
  # hardcoded backend. After the sentinel decision (skip OR full), it
11
- # opportunistically checkpoints the runtime submodule (commit+push) at most once
11
+ # opportunistically checkpoints only legacy nested-Git runtimes at most once
12
12
  # per HEARTBEAT_CHECKPOINT_MIN_INTERVAL_SECONDS, so memory/session state stays
13
13
  # durable without pushing every minute.
14
14
  set -euo pipefail
@@ -229,6 +229,17 @@ state.update({"source":"hermes-continuous-ticket-sentinel","agent_id":agent_id,"
229
229
  tmp = path.with_suffix(path.suffix + ".tmp"); tmp.write_text(json.dumps(state, indent=2, sort_keys=True)+"\n"); tmp.replace(path)
230
230
  PYEOF
231
231
 
232
+ # Coexistence WIP=1 lease (momo E2/S2.3): don't full-drive if the human-drivable
233
+ # Momo holds it — it's driving the same board. A crashed holder's lease expires
234
+ # (ttl) so the board is never wedged. Release on any exit.
235
+ WIP_LOCK="$RUNTIME/wip-driver.lock"
236
+ if ! python3 "$ROLE_DIR/.scripts/momo-wip-lock.py" acquire "$WIP_LOCK" "hermes:$AGENT_ID" --ttl 3600 >/dev/null 2>&1; then
237
+ printf '[heartbeat] WIP lease held by Momo — skipping full reconcile pass this tick\n'
238
+ maybe_checkpoint
239
+ exit 0
240
+ fi
241
+ trap 'python3 "$ROLE_DIR/.scripts/momo-wip-lock.py" release "$WIP_LOCK" "hermes:$AGENT_ID" >/dev/null 2>&1 || true' EXIT
242
+
232
243
  prompt="$(<"$PROMPT_FILE")"
233
244
  set +e
234
245
  env HERMES_HOME="$RUNTIME" "$HERMES_BIN" chat -Q --source cron --max-turns 90 -q "$prompt"
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env python3
2
+ """momo-wip-lock — shared WIP=1 driver lease so interactive Momo and the Hermes
3
+ sentinel never double-drive one board (momo E2/S2.3, the coexistence gate).
4
+
5
+ Both drivers acquire the SAME advisory lease (a JSON file, conventionally
6
+ <runtime>/wip-driver.lock) before a board-driving pass. The lease PERSISTS across
7
+ processes — it is deliberately NOT a held flock, because a Momo pass spans many
8
+ separate tool-call/bash invocations over minutes. flock is used only to serialize
9
+ the check-and-set so two acquirers can't race. A lease is respected while its
10
+ heartbeat is fresh (now - heartbeat_at < ttl); a stale lease (holder died without
11
+ releasing) can be stolen after it expires.
12
+
13
+ Owners are free strings by convention: "momo" (interactive) or
14
+ "hermes:<agent_id>" (the sentinel). WIP=1 is per BOARD == per runtime, so there
15
+ is one lease file per runtime.
16
+
17
+ Protocol:
18
+ * Driver start: acquire <lock> <me> -> exit 0 → drive; exit 1 → HELD, back off.
19
+ * While driving: refresh <lock> <me> periodically (< ttl) so the lease stays fresh.
20
+ * Driver end: release <lock> <me>.
21
+ * A holder that crashed leaves a lease that expires after `ttl`; the next driver
22
+ acquires it normally (or `--steal` to take an explicitly stale one immediately).
23
+
24
+ Commands (exit 0 = you hold it; 1 = someone else holds it fresh; 2 = usage/error):
25
+ acquire <lockfile> <owner> [--ttl S] [--steal]
26
+ refresh <lockfile> <owner>
27
+ release <lockfile> <owner>
28
+ status <lockfile> # always exit 0
29
+ """
30
+ from __future__ import annotations
31
+ import argparse, fcntl, json, os, socket, sys, tempfile, time
32
+
33
+
34
+ def _read(path: str):
35
+ try:
36
+ with open(path) as f:
37
+ return json.load(f)
38
+ except Exception:
39
+ return None
40
+
41
+
42
+ def _fresh(lease: dict | None, now: float) -> bool:
43
+ return bool(lease) and (now - lease.get("heartbeat_at", 0)) < lease.get("ttl", 300)
44
+
45
+
46
+ def _write_atomic(path: str, data: dict) -> None:
47
+ d = os.path.dirname(os.path.abspath(path)) or "."
48
+ fd, tmp = tempfile.mkstemp(dir=d)
49
+ with os.fdopen(fd, "w") as f:
50
+ json.dump(data, f)
51
+ os.replace(tmp, path)
52
+
53
+
54
+ def _guard(lockfile: str):
55
+ """Sibling .flock file held only for the read-modify-write critical section."""
56
+ g = open(lockfile + ".flock", "a")
57
+ fcntl.flock(g, fcntl.LOCK_EX)
58
+ return g
59
+
60
+
61
+ def acquire(lockfile: str, owner: str, ttl: int, steal: bool) -> int:
62
+ now = time.time(); g = _guard(lockfile)
63
+ try:
64
+ cur = _read(lockfile)
65
+ if cur and cur.get("owner") != owner and _fresh(cur, now) and not steal:
66
+ print(f"HELD by {cur['owner']} (fresh, {int(now - cur['heartbeat_at'])}s ago) — back off")
67
+ return 1
68
+ started = cur["started_at"] if (cur and cur.get("owner") == owner and "started_at" in cur) else now
69
+ _write_atomic(lockfile, {
70
+ "owner": owner, "pid": os.getpid(), "host": socket.gethostname(),
71
+ "started_at": started, "heartbeat_at": now, "ttl": ttl,
72
+ })
73
+ print(f"ACQUIRED by {owner}" + (" (stole stale lease)" if (cur and cur.get('owner') != owner) else ""))
74
+ return 0
75
+ finally:
76
+ fcntl.flock(g, fcntl.LOCK_UN); g.close()
77
+
78
+
79
+ def refresh(lockfile: str, owner: str) -> int:
80
+ now = time.time(); g = _guard(lockfile)
81
+ try:
82
+ cur = _read(lockfile)
83
+ if not cur or cur.get("owner") != owner:
84
+ print(f"NOT OWNER (held by {cur.get('owner') if cur else 'nobody'}) — cannot refresh")
85
+ return 1
86
+ cur["heartbeat_at"] = now
87
+ _write_atomic(lockfile, cur); print("REFRESHED"); return 0
88
+ finally:
89
+ fcntl.flock(g, fcntl.LOCK_UN); g.close()
90
+
91
+
92
+ def release(lockfile: str, owner: str) -> int:
93
+ g = _guard(lockfile)
94
+ try:
95
+ cur = _read(lockfile)
96
+ if cur and cur.get("owner") != owner:
97
+ print(f"NOT OWNER (held by {cur['owner']}) — not releasing"); return 1
98
+ try:
99
+ os.remove(lockfile)
100
+ except FileNotFoundError:
101
+ pass
102
+ print("RELEASED"); return 0
103
+ finally:
104
+ fcntl.flock(g, fcntl.LOCK_UN); g.close()
105
+
106
+
107
+ def status(lockfile: str) -> int:
108
+ now = time.time(); cur = _read(lockfile)
109
+ if not cur:
110
+ print("FREE (no lease)"); return 0
111
+ state = "fresh" if _fresh(cur, now) else "STALE"
112
+ print(f"{cur['owner']} — {state} (heartbeat {int(now - cur.get('heartbeat_at', 0))}s ago, "
113
+ f"ttl {cur.get('ttl')}s, pid {cur.get('pid')}@{cur.get('host')})")
114
+ return 0
115
+
116
+
117
+ def main() -> int:
118
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
119
+ ap.add_argument("cmd", choices=["acquire", "refresh", "release", "status"])
120
+ ap.add_argument("lockfile")
121
+ ap.add_argument("owner", nargs="?")
122
+ ap.add_argument("--ttl", type=int, default=300, help="freshness window in seconds (default 300)")
123
+ ap.add_argument("--steal", action="store_true", help="take an explicitly stale lease immediately")
124
+ a = ap.parse_args()
125
+ if a.cmd in ("acquire", "refresh", "release") and not a.owner:
126
+ print("owner required", file=sys.stderr); return 2
127
+ if a.cmd == "acquire":
128
+ return acquire(a.lockfile, a.owner, a.ttl, a.steal)
129
+ if a.cmd == "refresh":
130
+ return refresh(a.lockfile, a.owner)
131
+ if a.cmd == "release":
132
+ return release(a.lockfile, a.owner)
133
+ return status(a.lockfile)
134
+
135
+
136
+ if __name__ == "__main__":
137
+ raise SystemExit(main())
@@ -31,6 +31,12 @@ on Linear, Plane, or Trello.
31
31
 
32
32
  ## Pass
33
33
 
34
+ **Trigger.** This pass runs both on the cheap heartbeat timer AND on a live
35
+ **Plane board event** — a `bloodbank.evt.v1.repo.<repo>.ticket_*` event delivered
36
+ by the `plane-webhook-bridge`. If a specific ticket event triggered you, FIRST
37
+ `tp get_issue <that ticket>`, read the change, and react to it (triage / refine /
38
+ comment / transition per the lifecycle) before the general reconcile below.
39
+
34
40
  1. Run or explicitly follow the project session-start ritual if one exists.
35
41
  2. Reconcile: active milestone (`tp active_milestone`), issues (`tp list_issues`),
36
42
  local evidence under `_bmad-output/implementation-artifacts/issue-evidence/`,
@@ -85,6 +91,21 @@ on Linear, Plane, or Trello.
85
91
  as done, move on), **held** (back to active), or a genuine **out-of-scope
86
92
  blocker** (recorded and waited on). There is no fourth "waiting for the
87
93
  operator's sign-off" state.
94
+
95
+ **Before going idle — always run this protocol (this is the point of the pass):**
96
+
97
+ - **(a) Unclaimed-work sweep.** If ANY ticket is claimable (`ready`/`unstarted`
98
+ in the active milestone, no active worker) and WIP allows, run the delegation
99
+ cycle (step 4) now — never go idle with claimable work sitting on the board.
100
+ - **(b) In-progress status sweep.** For EVERY `started` / `in_review` ticket:
101
+ request a status update — verify the worker is alive (its zellij session /
102
+ worktree / recent git activity) and post or refresh a concise status note on
103
+ the ticket (`tp comment`) with progress + next step. If a ticket is
104
+ **blocked**, actively try to UNBLOCK it: identify the blocker and act on
105
+ anything agent-doable (missing AC, a dependency you can drive, a stale worker
106
+ to re-drive); escalate ONLY a genuine external blocker (step 7). A silent or
107
+ stalled worker is re-driven or flagged — never left silent.
108
+
88
109
  9. Update `runtime/continuous-ticket-sentinel-state.json`: `active` /
89
110
  `blocked` / `idle` / `stalled` with the required fields (`source`, `agent_id`,
90
111
  `repo`, `ticket_provider`, `status`, `summary`, `reason`, `updated_at`,
@@ -17,9 +17,10 @@ You are **{{ display_name }}** — a Hermes agent provisioned to work inside the
17
17
 
18
18
  You operate **only** within the working directory of `{{ target_repo }}`. You do
19
19
  not touch files outside this repo unless the operator explicitly approves it.
20
- Your HERMES_HOME is the submodule at `./runtime/` (a separate git repo named
21
- `{{ runtime_repo_owner }}/{{ runtime_repo }}`); everything you change there is
22
- auto-checkpointed hourly + on session end.
20
+ Your HERMES_HOME is the local runtime at `./runtime/`; Hermes loads its
21
+ `config.yaml` directly. Secrets, SOUL, skills, sessions, and gateway state live
22
+ local to that runtime (pure-local state; durable memory is the shared Hindsight
23
+ bank — see Memory hygiene).
23
24
 
24
25
  ## Tone
25
26
 
@@ -43,8 +44,8 @@ question — not three vague ones.
43
44
  You **MUST** emit a Bloodbank event for every consequential action you take.
44
45
  Envelope shape: CloudEvents 1.0, type `bloodbank.v1.<domain>.<entity>.<action>`,
45
46
  `actor.agent_id = {{ agent_id }}`, `producer = hermes-agent:{{ agent_id }}`,
46
- `source = hermes://agent/{{ agent_id }}`. The consumer in `./runtime/` already
47
- imports the envelope helper.
47
+ `source = hermes://agent/{{ agent_id }}`. Inbound commands arrive through the
48
+ fleet-shared Hermes gateway and are routed by `data.target_agent_id`.
48
49
 
49
50
  You **MUST NOT** invent new event `type` values. Bloodbank owns the naming
50
51
  contract at `~/code/33GOD/bloodbank/docs/event-naming.md` —
@@ -53,12 +54,24 @@ read it before publishing a type you haven't published before.
53
54
  ## Role-specific behavior
54
55
 
55
56
  {% if role == "pm" -%}
56
- You are the **project manager**. You triage incoming requests from Telegram /
57
- Bloodbank command lanes, decompose them into discrete tasks on the
58
- Plane board, and route work to other agents in the fleet (e.g. the dev role
59
- on `bloodbank.cmd.v1.agent.task.assign` with
60
- `data.target_agent_id = {{ target_repo }}-dev`). You do not write application
61
- code. You do not approve merges.
57
+ You are the **project-manager ORCHESTRATOR** the autonomous Hermes carrier of
58
+ Momo, and the twin of the human-drivable Momo. You share ONE board and ONE
59
+ Hindsight bank with it; stay attributable and never split-brain the state. You
60
+ triage incoming requests from Telegram / Bloodbank command lanes, decompose them
61
+ into discrete tasks on the Plane board, and route work to other agents (e.g. the
62
+ dev role on `bloodbank.cmd.v1.agent.task.assign` with
63
+ `data.target_agent_id = {{ target_repo }}-dev`).
64
+
65
+ **Prime directives (non-negotiable):**
66
+ - **Never mutate code** — every code change flows through a delegated worker.
67
+ - **WIP = 1**, shared with the human-drivable Momo via the driver lease
68
+ (`.scripts/momo-wip-lock.py` → `runtime/wip-driver.lock`) — acquire before driving,
69
+ back off if Momo holds it fresh; never double-drive one board. (The heartbeat
70
+ enforces this automatically for the reconcile pass.)
71
+ - **Reviewer ≠ implementer** — independent adversarial review is the normal path.
72
+ - **Evidence over status** — a board column is a claim; repo evidence is proof.
73
+ - **Anti-stall** — never park a pass on operator sign-off.
74
+ - You do not write application code. You do not approve merges.
62
75
 
63
76
  Default execution workflow for implementation delivery: use
64
77
  `subagent-driven-development` in kanban-orchestrated codex mode
@@ -114,7 +127,15 @@ declared` event so the fleet knows what to route to you.
114
127
 
115
128
  ## Memory hygiene
116
129
 
117
- Your memory is the submodule at `./runtime/memories/`. Use Hindsight for
118
- durable cross-session facts (`hindsight memory retain {{ target_repo }} "…"
119
- --context conventions`). Edit `memories/MEMORY.md` directly for the
120
- condensed mental-model summary the gateway loads on every session.
130
+ Your durable memory is the shared **Hindsight bank `{{ target_repo }}`** — one
131
+ bank per PROJECT, shared with the human-drivable Momo twin. Honcho and the
132
+ per-agent `runtime/memories/` store are **neutralized** (see `config.yaml`
133
+ `memory.provider: ""`): do not rely on `MEMORY.md`/`USER.md`. Retain with
134
+ `hindsight memory retain {{ target_repo }} "…" --context <cat>`; recall with
135
+ `hindsight memory recall {{ target_repo }} "…"`.
136
+
137
+ ## Doctrine
138
+
139
+ Decide on the operator's behalf using **`~/code/33GOD/momo/PILLARS.md`**
140
+ (canonical, priority-ordered). This soul **references** that file; it does not
141
+ copy it. Cite the pillar(s) that drove a consequential call in its decision event.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env bash
2
- # Launcher for {{ agent_id }}. Resolves HERMES_HOME to the runtime submodule
2
+ # Launcher for {{ agent_id }}. Resolves HERMES_HOME to the local runtime
3
3
  # and execs the shared fleet Hermes binary configured in ~/.hermes/fleet.env.
4
4
 
5
5
  set -euo pipefail
@@ -12,6 +12,9 @@ if [[ -f "$FLEET_ENV" ]]; then
12
12
  # shellcheck disable=SC1090
13
13
  source "$FLEET_ENV"
14
14
  fi
15
+ # Never pass an identity-bearing chat credential from shared fleet state into
16
+ # a profile process. Hermes loads the profile's own runtime/.env itself.
17
+ unset TELEGRAM_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN
15
18
 
16
19
  CONFIG="${HERMES_TEMPLATE_CONFIG:-${XDG_CONFIG_HOME:-$HOME/.config}/hermes-agent-template/config.toml}"
17
20
  config_get() {
@@ -49,8 +52,8 @@ HERMES_OAUTH_FILE="${HERMES_OAUTH_FILE:-${HERMES_FLEET_OAUTH_FILE:-$(config_get
49
52
  CODEX_HOME="${CODEX_HOME:-${HERMES_FLEET_CODEX_HOME:-$(config_get fleet.codex_home "$HOME/.codex")}}"
50
53
 
51
54
  if [[ ! -d "$HERMES_HOME" ]]; then
52
- echo "hermes: runtime submodule not initialized at $HERMES_HOME" >&2
53
- echo " fix: git submodule update --init --recursive" >&2
55
+ echo "hermes: local runtime not provisioned at $HERMES_HOME" >&2
56
+ echo " fix: run $ROLE_DIR/.scripts/20-runtime-repo.sh" >&2
54
57
  exit 1
55
58
  fi
56
59
 
@@ -1,6 +1,6 @@
1
1
  # Hermes agent role manifest. Tracked in the PROJECT repository.
2
2
  # The agent's actual state (memory, sessions, evolving SOUL) lives in the
3
- # runtime submodule. This file is the immutable contract for this role.
3
+ # ignored local runtime. This file is the immutable contract for this role.
4
4
 
5
5
  repo: {{ target_repo }}
6
6
  role: {{ role }}
@@ -14,12 +14,24 @@ model:
14
14
  name: "{{ model_name }}"
15
15
 
16
16
  # Hermes profile name. ~/.hermes/profiles/<name> symlinks to ./runtime/, so both
17
- # HERMES_HOME and `hermes --profile <name>` resolve to the runtime submodule.
17
+ # HERMES_HOME and `hermes --profile <name>` resolve to the local runtime.
18
18
  profile: {{ agent_id }}
19
19
 
20
20
  # Telegram bot identity (one bot per agent — see docs/architecture.md).
21
21
  telegram:
22
+ provisioning_status: "deferred"
22
23
  bot_username: "{{ bot_handle }}"
24
+ bot_id: ""
25
+
26
+ # Slack is opt-in. Credentials live only in runtime/.env; this block records
27
+ # safe provisioning state and the verified auth.test identity.
28
+ slack:
29
+ provisioning_status: "deferred"
30
+ team_id: ""
31
+ team_name: ""
32
+ bot_user_id: ""
33
+ bot_id: ""
34
+ bot_username: ""
23
35
 
24
36
  # Ticket-provider binding (linear | plane | trello).
25
37
  # The heartbeat reconciliation pass talks ONLY to this via .scripts/lib/ticket-provider.sh;
@@ -54,25 +66,24 @@ plane:
54
66
  # identifier is set by .scripts/42-ticket-provider.sh after creation (plane only)
55
67
  identifier: ""
56
68
 
57
- # Bloodbank wiring (consumed + produced subjects).
69
+ # Bloodbank ingress is owned by one fleet-shared Hermes gateway. The gateway
70
+ # reads the fleet registry and routes canonical commands by target_agent_id;
71
+ # this profile does not install its own NATS consumer or inbox bridge.
58
72
  bloodbank:
59
- subscribe:
60
- - "bloodbank.evt.v1.repo.>"
61
- - "bloodbank.cmd.v1.agent.>"
62
- routing:
63
- repo: "{{ target_repo }}"
64
- target_agent_id: "{{ agent_id }}"
73
+ gateway_scope: fleet
74
+ target_agent_id: "{{ agent_id }}"
65
75
  producer: "hermes-agent:{{ agent_id }}"
66
76
 
67
- # Runtime repo (git-tracked HERMES_HOME, auto-checkpointed).
77
+ # Pure-local HERMES_HOME. The legacy GitHub identity remains metadata-only for
78
+ # locating old archives; provisioning never creates or attaches a submodule.
68
79
  runtime:
69
80
  # Empty owner -> resolved from config.toml [github].runtime_repo_owner.
70
81
  github_owner: "{{ runtime_repo_owner }}"
71
82
  github_repo: "{{ runtime_repo }}"
72
- submodule_path: "./runtime"
83
+ local_path: "./runtime"
73
84
  checkpoint:
74
- cadence: "heartbeat"
75
- on_session_end: true
85
+ cadence: "disabled"
86
+ on_session_end: false
76
87
 
77
88
  # Provenance — managed by the template, do not hand-edit.
78
89
  provisioned_at: "{{ '%Y-%m-%dT%H:%M:%SZ' | strftime }}"