@delorenj/pjangler 1.2.17 → 1.2.19
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 +244 -6
- package/dist/mcp-server.js +245 -7
- package/package.json +8 -2
- package/templates/commonproject/AGENTS.md +3 -3
- package/templates/commonproject/README.md +11 -12
- package/templates/commonproject/copier.yml +8 -27
- package/templates/commonproject/template/.project.json.jinja +9 -13
- package/templates/hermes-agent/README.md +9 -9
- package/templates/hermes-agent/config.example.toml +1 -66
- package/templates/hermes-agent/copier.yml +4 -3
- package/templates/hermes-agent/docs/architecture.md +9 -12
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
- package/templates/hermes-agent/docs/operations.md +6 -5
- package/templates/hermes-agent/docs/sentinel/README.md +9 -7
- package/templates/hermes-agent/docs/sentinel/architecture.md +1 -2
- package/templates/hermes-agent/docs/sentinel/development.md +12 -13
- package/templates/hermes-agent/docs/sentinel/providers.md +34 -15
- package/templates/hermes-agent/install-local.sh +15 -14
- package/templates/hermes-agent/runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +46 -14
- package/templates/hermes-agent/runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/scripts/fleet-sync.sh +1 -64
- package/templates/hermes-agent/template/.gitignore.jinja +0 -2
- package/templates/hermes-agent/template/.runtime-scaffold/README.md +1 -1
- package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +43 -9
- package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +2 -2
- package/templates/hermes-agent/template/.scripts/01-config.sh +0 -1
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -9
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +3 -22
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -22
- package/templates/hermes-agent/template/.scripts/40-plane.sh +51 -0
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +59 -21
- package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +2 -1
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -10
- package/templates/hermes-agent/template/.scripts/_lib.sh +3 -61
- package/templates/hermes-agent/template/.scripts/config.example.toml +0 -5
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +22 -66
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
- package/templates/hermes-agent/template/.scripts/providers/linear.sh +176 -0
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -29
- package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
- package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -1
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +1 -6
- package/templates/hermes-agent/template/SOUL.md.jinja +19 -21
- package/templates/hermes-agent/template/role.yaml.jinja +35 -4
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -0
- package/templates/hermes-agent/docs/bloodbank-gateway.md +0 -57
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +0 -60
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Provider adapters
|
|
2
2
|
|
|
3
3
|
This guide is the reference for the ticket-provider abstraction: the contract
|
|
4
|
-
every adapter implements, the
|
|
4
|
+
every adapter implements, the three adapters that ship today, and a
|
|
5
5
|
step-by-step process for adding a new provider or verifying an existing one
|
|
6
6
|
against a live board. Read [Architecture](architecture.md) first for how the
|
|
7
7
|
adapter fits into the engine.
|
|
@@ -43,8 +43,12 @@ maps each to its back end's concrete state.
|
|
|
43
43
|
|
|
44
44
|
## The adapters that ship today
|
|
45
45
|
|
|
46
|
-
The repository includes
|
|
46
|
+
The repository includes three adapters with different verification status.
|
|
47
47
|
|
|
48
|
+
- **Linear** (`providers/linear.sh`) is the reference implementation. It uses
|
|
49
|
+
the Linear GraphQL API. It's verified live against a real board. Linear's
|
|
50
|
+
`issue(id:)` field accepts both the UUID and the human identifier (for
|
|
51
|
+
example, `DEL-42`), so the engine can pass identifiers through.
|
|
48
52
|
- **Plane** (`providers/plane.sh`) uses the Plane REST API with `X-API-Key`
|
|
49
53
|
authentication. A Plane project maps to the board, a cycle maps to the
|
|
50
54
|
milestone, and `state.group` maps to the state type. It's verified live. Note
|
|
@@ -60,12 +64,26 @@ The repository includes two supported adapters with different verification statu
|
|
|
60
64
|
|
|
61
65
|
Set credentials in the environment and the board binding in `role.yaml`.
|
|
62
66
|
|
|
67
|
+
<details>
|
|
68
|
+
<summary>Linear</summary>
|
|
69
|
+
|
|
70
|
+
- Credentials: `LINEAR_API_KEY`.
|
|
71
|
+
- Binding:
|
|
72
|
+
|
|
73
|
+
```yaml
|
|
74
|
+
ticket_provider:
|
|
75
|
+
name: linear
|
|
76
|
+
team: DEL # Linear team key
|
|
77
|
+
project: "" # optional, scopes milestone and issue queries
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
</details>
|
|
81
|
+
|
|
63
82
|
<details>
|
|
64
83
|
<summary>Plane</summary>
|
|
65
84
|
|
|
66
|
-
- Credentials: `PLANE_API_KEY`
|
|
67
|
-
`
|
|
68
|
-
`PLANE_BASE` (default `https://plane.delo.sh`).
|
|
85
|
+
- Credentials: `PLANE_API_KEY`. Endpoint: `PLANE_BASE` (default
|
|
86
|
+
`https://plane.delo.sh`).
|
|
69
87
|
- Binding:
|
|
70
88
|
|
|
71
89
|
```yaml
|
|
@@ -96,9 +114,8 @@ Set credentials in the environment and the board binding in `role.yaml`.
|
|
|
96
114
|
|
|
97
115
|
To add a fourth provider, for example GitHub Issues or Jira, follow these steps.
|
|
98
116
|
|
|
99
|
-
1. Create `template/.scripts/providers/<name>.sh`. Start from `
|
|
100
|
-
|
|
101
|
-
model.
|
|
117
|
+
1. Create `template/.scripts/providers/<name>.sh`. Start from `linear.sh` if the
|
|
118
|
+
back end uses GraphQL, or `plane.sh` if it uses REST with a single API key.
|
|
102
119
|
2. Implement every operation in [the contract](#the-contract). Return the exact
|
|
103
120
|
JSON shapes shown in the table. Map your back end's states to the five
|
|
104
121
|
normalized states.
|
|
@@ -110,7 +127,8 @@ To add a fourth provider, for example GitHub Issues or Jira, follow these steps.
|
|
|
110
127
|
5. Extend `template/role.yaml.jinja` with a provider-specific binding block
|
|
111
128
|
under the `{% if ticket_provider == '<name>' %}` branch.
|
|
112
129
|
6. Teach `42-ticket-provider.sh` how to resolve or create the board for your
|
|
113
|
-
provider, if it differs from the existing `plane
|
|
130
|
+
provider, if it differs from the existing `linear`, `plane`, and `trello`
|
|
131
|
+
cases.
|
|
114
132
|
7. Validate and verify with the steps below.
|
|
115
133
|
|
|
116
134
|
<!-- prettier-ignore -->
|
|
@@ -140,7 +158,7 @@ Before you touch a live board, confirm the adapter parses and fails cleanly.
|
|
|
140
158
|
exit, with no Python traceback:
|
|
141
159
|
|
|
142
160
|
```bash
|
|
143
|
-
( unset
|
|
161
|
+
( unset LINEAR_API_KEY; sh template/.scripts/providers/linear.sh resolve )
|
|
144
162
|
```
|
|
145
163
|
|
|
146
164
|
## Verifying an adapter against a live board
|
|
@@ -151,15 +169,15 @@ never closes or modifies a ticket, so it's safe to run.
|
|
|
151
169
|
|
|
152
170
|
```bash
|
|
153
171
|
# 1. Load the credential into the environment only (never print it).
|
|
154
|
-
export
|
|
155
|
-
~/.hermes/<agent>.env | head -1 | sed -E 's/^(export )?
|
|
172
|
+
export LINEAR_API_KEY="$(grep -E '^(export )?LINEAR_API_KEY=' \
|
|
173
|
+
~/.hermes/<agent>.env | head -1 | sed -E 's/^(export )?LINEAR_API_KEY=//; s/^"//; s/"$//')"
|
|
156
174
|
|
|
157
175
|
# 2. Stage a role directory bound to the real board.
|
|
158
176
|
T=$(mktemp -d); RD="$T/agents/hermes/pm"
|
|
159
177
|
mkdir -p "$RD/.scripts/lib" "$RD/.scripts/providers"
|
|
160
178
|
cp template/.scripts/lib/ticket-provider.sh "$RD/.scripts/lib/"
|
|
161
|
-
cp template/.scripts/providers/
|
|
162
|
-
printf 'repo: demo\nrole: pm\nticket_provider:\n name:
|
|
179
|
+
cp template/.scripts/providers/linear.sh "$RD/.scripts/providers/"
|
|
180
|
+
printf 'repo: demo\nrole: pm\nticket_provider:\n name: linear\n team: DEL\n' \
|
|
163
181
|
> "$RD/role.yaml"
|
|
164
182
|
|
|
165
183
|
# 3. Run the read operations.
|
|
@@ -181,4 +199,5 @@ verification.
|
|
|
181
199
|
## Read next
|
|
182
200
|
|
|
183
201
|
- [Development guide](development.md): the full edit, validate, and propagate
|
|
184
|
-
workflow, plus the open roadmap, which includes live-verifying
|
|
202
|
+
workflow, plus the open roadmap, which includes live-verifying Plane and
|
|
203
|
+
Trello.
|
|
@@ -15,11 +15,10 @@
|
|
|
15
15
|
#
|
|
16
16
|
# Environment overrides (skip the prompts):
|
|
17
17
|
# HAT_REPO=<name> project/repo name (default: basename of CWD)
|
|
18
|
-
# HAT_PROVIDER=plane|trello (default: plane)
|
|
18
|
+
# HAT_PROVIDER=linear|plane|trello (default: plane)
|
|
19
19
|
# HAT_ROLES="pm" roles to install (default: pm)
|
|
20
20
|
# HAT_DRY_RUN=1 print actions, change nothing
|
|
21
|
-
# Provider creds: PLANE_API_KEY
|
|
22
|
-
# | TRELLO_KEY+TRELLO_TOKEN
|
|
21
|
+
# Provider creds: LINEAR_API_KEY | PLANE_API_KEY+PLANE_BASE | TRELLO_KEY+TRELLO_TOKEN
|
|
23
22
|
set -eu
|
|
24
23
|
|
|
25
24
|
say() { printf '\033[36m%s\033[0m\n' "$*"; }
|
|
@@ -101,7 +100,7 @@ TOML
|
|
|
101
100
|
fi
|
|
102
101
|
|
|
103
102
|
# --- 4. Provider credentials + board binding ---------------------------------
|
|
104
|
-
ask HAT_PROVIDER "Ticket provider (plane|trello)" "plane"
|
|
103
|
+
ask HAT_PROVIDER "Ticket provider (linear|plane|trello)" "plane"
|
|
105
104
|
PROVIDER="$HAT_PROVIDER"
|
|
106
105
|
ask HAT_REPO "Project/repo name" "$(basename "$PROJECT_DIR" | tr '[:upper:]' '[:lower:]')"
|
|
107
106
|
REPO="$HAT_REPO"
|
|
@@ -110,17 +109,13 @@ PM_ENV="$HOME/.hermes/${REPO}-pm.env"
|
|
|
110
109
|
# These get written into the pm role.yaml binding after render.
|
|
111
110
|
# Pre-seed from optional env knobs so the install can run non-interactively.
|
|
112
111
|
TP_WORKSPACE="${HAT_PLANE_WORKSPACE:-}"; TP_PROJECT="${HAT_PLANE_PROJECT:-}"
|
|
113
|
-
TP_BOARD="${HAT_TRELLO_BOARD:-}"
|
|
112
|
+
TP_TEAM="${HAT_LINEAR_TEAM:-}"; TP_BOARD="${HAT_TRELLO_BOARD:-}"
|
|
114
113
|
case "$PROVIDER" in
|
|
115
114
|
plane)
|
|
116
115
|
ask PLANE_BASE "Plane base URL" "https://app.plane.so"
|
|
116
|
+
: "${PLANE_API_KEY:?Set PLANE_API_KEY in your environment, then re-run}"
|
|
117
117
|
ask TP_WORKSPACE "Plane workspace slug" ""
|
|
118
118
|
[ -n "$TP_WORKSPACE" ] || die "workspace is required"
|
|
119
|
-
TP_WORKSPACE_KEY="$(printf '%s' "$TP_WORKSPACE" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g')"
|
|
120
|
-
TP_WORKSPACE_KEY="PLANE_${TP_WORKSPACE_KEY}_API_KEY"
|
|
121
|
-
eval "TP_WORKSPACE_API_KEY=\${$TP_WORKSPACE_KEY:-}"
|
|
122
|
-
PLANE_API_KEY="${PLANE_API_KEY:-$TP_WORKSPACE_API_KEY}"
|
|
123
|
-
: "${PLANE_API_KEY:?Set PLANE_API_KEY or $TP_WORKSPACE_KEY in your environment, then re-run}"
|
|
124
119
|
say " Plane projects in '$TP_WORKSPACE':"
|
|
125
120
|
_pj="$(mktemp)"
|
|
126
121
|
curl -fsS "$PLANE_BASE/api/v1/workspaces/$TP_WORKSPACE/projects/?per_page=100" \
|
|
@@ -138,9 +133,15 @@ PY
|
|
|
138
133
|
rm -f "$_pj"
|
|
139
134
|
ask TP_PROJECT "Plane project UUID to manage" ""
|
|
140
135
|
[ -n "$TP_PROJECT" ] || die "project UUID is required"
|
|
141
|
-
CRED_LINES="
|
|
136
|
+
CRED_LINES="PLANE_API_KEY=$PLANE_API_KEY
|
|
142
137
|
PLANE_BASE=$PLANE_BASE"
|
|
143
138
|
;;
|
|
139
|
+
linear)
|
|
140
|
+
: "${LINEAR_API_KEY:?Set LINEAR_API_KEY in your environment, then re-run}"
|
|
141
|
+
ask TP_TEAM "Linear team key (for example DEL)" ""
|
|
142
|
+
[ -n "$TP_TEAM" ] || die "team key is required"
|
|
143
|
+
CRED_LINES="LINEAR_API_KEY=$LINEAR_API_KEY"
|
|
144
|
+
;;
|
|
144
145
|
trello)
|
|
145
146
|
: "${TRELLO_KEY:?Set TRELLO_KEY in your environment, then re-run}"
|
|
146
147
|
: "${TRELLO_TOKEN:?Set TRELLO_TOKEN in your environment, then re-run}"
|
|
@@ -164,13 +165,12 @@ fi
|
|
|
164
165
|
export SKIP_TELEGRAM=1 SKIP_EMAIL=1 SKIP_RUNTIME_REPO=1 SKIP_PLANE=1 \
|
|
165
166
|
SKIP_BLOODBANK=1 SKIP_SYSTEMD=1
|
|
166
167
|
ROLES="${HAT_ROLES:-pm}"
|
|
167
|
-
SCRUB_PLANE_KEY="${TP_WORKSPACE_KEY:-PLANE_DEFAULT_API_KEY}"
|
|
168
168
|
for ROLE in $ROLES; do
|
|
169
169
|
say "5. provisioning role: $ROLE"
|
|
170
170
|
DEST="$PROJECT_DIR/agents/hermes/$ROLE"
|
|
171
171
|
# Scrub provider creds from copier's environment so 42-ticket-provider skips
|
|
172
172
|
# board CREATION; we bind to the existing board in step 6 instead.
|
|
173
|
-
run "env -u PLANE_API_KEY -u
|
|
173
|
+
run "env -u PLANE_API_KEY -u PLANE_33GOD_API_KEY -u LINEAR_API_KEY \
|
|
174
174
|
-u TRELLO_KEY -u TRELLO_TOKEN \
|
|
175
175
|
copier copy '$TEMPLATE_SRC' '$DEST' --trust --defaults --overwrite \
|
|
176
176
|
--data target_repo='$REPO' --data role='$ROLE' --data ticket_provider='$PROVIDER'"
|
|
@@ -180,7 +180,7 @@ done
|
|
|
180
180
|
PM_ROLE="$PROJECT_DIR/agents/hermes/pm/role.yaml"
|
|
181
181
|
if [ -f "$PM_ROLE" ] && [ "${HAT_DRY_RUN:-0}" != "1" ]; then
|
|
182
182
|
say "6. binding pm to your $PROVIDER board"
|
|
183
|
-
TP_WORKSPACE="$TP_WORKSPACE" TP_PROJECT="$TP_PROJECT" \
|
|
183
|
+
TP_WORKSPACE="$TP_WORKSPACE" TP_PROJECT="$TP_PROJECT" TP_TEAM="$TP_TEAM" \
|
|
184
184
|
TP_BOARD="$TP_BOARD" PROVIDER="$PROVIDER" python3 - "$PM_ROLE" <<'PY'
|
|
185
185
|
import os, re, sys, pathlib
|
|
186
186
|
p = pathlib.Path(sys.argv[1]); t = p.read_text()
|
|
@@ -190,6 +190,7 @@ def setleaf(text, key, val):
|
|
|
190
190
|
return new if n else text
|
|
191
191
|
t = setleaf(t, "workspace", os.environ.get("TP_WORKSPACE",""))
|
|
192
192
|
t = setleaf(t, "project", os.environ.get("TP_PROJECT",""))
|
|
193
|
+
t = setleaf(t, "team", os.environ.get("TP_TEAM",""))
|
|
193
194
|
t = setleaf(t, "board", os.environ.get("TP_BOARD",""))
|
|
194
195
|
p.write_text(t)
|
|
195
196
|
print(" bound:", {k:v for k,v in os.environ.items() if k.startswith("TP_") and v})
|
|
@@ -13,7 +13,7 @@ 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
|
|
16
|
+
| `bloodbank-consumer.py` | yes | NATS subscriber using canonical subjects plus envelope routing data |
|
|
17
17
|
| `decisions/` | yes | Agent-emitted decisions, one file per important call |
|
|
18
18
|
| `.env` | **no** | API keys + Telegram bot token (per-machine secret) |
|
|
19
19
|
| `auth.json` | **no** | Deprecated local OAuth store; fleet auth defaults to `HERMES_OAUTH_FILE=~/.hermes/auth.json` |
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
Bloodbank consumer for hermes agent {{agent_id}}.
|
|
4
4
|
|
|
5
5
|
Subscribes to:
|
|
6
|
-
bloodbank.evt.v1.repo
|
|
7
|
-
bloodbank.cmd.v1.agent
|
|
6
|
+
bloodbank.evt.v1.repo.>
|
|
7
|
+
bloodbank.cmd.v1.agent.>
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
The Bloodbank subject identifies the fixed domain/entity/action route. Repo and
|
|
10
|
+
agent identifiers stay in envelope data, so this consumer filters repo events
|
|
11
|
+
by ``data.repo`` and agent commands by ``data.target_agent_id``.
|
|
12
|
+
|
|
13
|
+
For each event/command received, writes it to the agent's notification queue at
|
|
14
|
+
$HERMES_HOME/bloodbank-inbox/<timestamp>.json so the gateway can ingest it on
|
|
15
|
+
the next tick. Reconnects on broker churn; idempotent across restarts.
|
|
14
16
|
|
|
15
17
|
Generated by hermes-agent-template/runtime-scaffold/bloodbank-consumer.py and
|
|
16
18
|
rendered with role-specific values at provision time. Re-render by re-running
|
|
@@ -49,8 +51,8 @@ PRODUCER = f"hermes-agent:{AGENT_ID}"
|
|
|
49
51
|
SOURCE = f"hermes://agent/{AGENT_ID}"
|
|
50
52
|
|
|
51
53
|
SUBJECTS = [
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
"bloodbank.evt.v1.repo.>",
|
|
55
|
+
"bloodbank.cmd.v1.agent.>",
|
|
54
56
|
]
|
|
55
57
|
|
|
56
58
|
KIND_MARKERS = {"event": "evt", "command": "cmd", "reply": "rpy"}
|
|
@@ -62,8 +64,8 @@ def _now():
|
|
|
62
64
|
|
|
63
65
|
def _subject_and_domain(ce_type, kind):
|
|
64
66
|
parts = ce_type.split(".")
|
|
65
|
-
if len(parts) != 5 or parts[0] != "bloodbank" or
|
|
66
|
-
raise ValueError(f"type {ce_type!r} must match bloodbank.
|
|
67
|
+
if len(parts) != 5 or parts[0] != "bloodbank" or parts[1] != "v1":
|
|
68
|
+
raise ValueError(f"type {ce_type!r} must match bloodbank.v1.<domain>.<entity>.<action>")
|
|
67
69
|
if kind not in KIND_MARKERS:
|
|
68
70
|
raise ValueError(f"unknown envelope kind {kind!r}")
|
|
69
71
|
_vendor, version, domain, entity, action = parts
|
|
@@ -95,6 +97,33 @@ def build_envelope(ce_type, data, *, kind="event", correlationid=None, causation
|
|
|
95
97
|
return env
|
|
96
98
|
|
|
97
99
|
|
|
100
|
+
def _is_for_consumer(subject, payload):
|
|
101
|
+
"""Route canonical subjects using identifiers carried in envelope data."""
|
|
102
|
+
if not isinstance(payload, dict):
|
|
103
|
+
return False
|
|
104
|
+
parts = subject.split(".")
|
|
105
|
+
if len(parts) != 6 or parts[0] != "bloodbank" or parts[2] != "v1":
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
_vendor, kind, version, domain, entity, action = parts
|
|
109
|
+
if kind not in KIND_MARKERS.values():
|
|
110
|
+
return False
|
|
111
|
+
if KIND_MARKERS.get(payload.get("kind")) != kind:
|
|
112
|
+
return False
|
|
113
|
+
if payload.get("subject") != subject:
|
|
114
|
+
return False
|
|
115
|
+
if payload.get("type") != f"bloodbank.{version}.{domain}.{entity}.{action}":
|
|
116
|
+
return False
|
|
117
|
+
data = payload.get("data")
|
|
118
|
+
if not isinstance(data, dict):
|
|
119
|
+
return False
|
|
120
|
+
if kind == "evt" and domain == "repo":
|
|
121
|
+
return data.get("repo") == REPO
|
|
122
|
+
if kind == "cmd" and domain == "agent":
|
|
123
|
+
return data.get("target_agent_id") == AGENT_ID
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
98
127
|
async def main():
|
|
99
128
|
nc = await nats.connect(NATS_URL, name=f"hermes-{AGENT_ID}")
|
|
100
129
|
stop = asyncio.Event()
|
|
@@ -103,7 +132,10 @@ async def main():
|
|
|
103
132
|
try:
|
|
104
133
|
payload = json.loads(msg.data.decode())
|
|
105
134
|
except Exception:
|
|
106
|
-
payload
|
|
135
|
+
sys.stderr.write(f"[bloodbank-consumer:{AGENT_ID}] skipped unroutable payload on {msg.subject}\n")
|
|
136
|
+
return
|
|
137
|
+
if not _is_for_consumer(msg.subject, payload):
|
|
138
|
+
return
|
|
107
139
|
entry = {
|
|
108
140
|
"received_at": _now(),
|
|
109
141
|
"subject": msg.subject,
|
|
@@ -125,7 +157,7 @@ async def main():
|
|
|
125
157
|
"bloodbank.v1.agent.online.changed",
|
|
126
158
|
{"agent_id": AGENT_ID, "repo": REPO, "role": ROLE, "state": "online"},
|
|
127
159
|
)
|
|
128
|
-
await nc.publish(
|
|
160
|
+
await nc.publish(online["subject"], json.dumps(online).encode())
|
|
129
161
|
|
|
130
162
|
def _shutdown(*_):
|
|
131
163
|
stop.set()
|
|
@@ -138,7 +170,7 @@ async def main():
|
|
|
138
170
|
"bloodbank.v1.agent.online.changed",
|
|
139
171
|
{"agent_id": AGENT_ID, "repo": REPO, "role": ROLE, "state": "offline"},
|
|
140
172
|
)
|
|
141
|
-
await nc.publish("
|
|
173
|
+
await nc.publish(offline["subject"], json.dumps(offline).encode())
|
|
142
174
|
await nc.drain()
|
|
143
175
|
|
|
144
176
|
|
|
@@ -7,8 +7,8 @@ short and high-signal — anything that should ALWAYS be in context.
|
|
|
7
7
|
|
|
8
8
|
You are **{{display_name}}**, the `{{role}}` agent for the `{{repo}}` repo.
|
|
9
9
|
Your scope is the repo at the project root. You communicate via Telegram
|
|
10
|
-
(`@{{repo}}_{{role}}_bot`) and Bloodbank events on
|
|
11
|
-
`bloodbank.evt.v1.repo.{{repo}}
|
|
10
|
+
(`@{{repo}}_{{role}}_bot`) and canonical Bloodbank repo events on
|
|
11
|
+
`bloodbank.evt.v1.repo.>`, filtered by `data.repo = {{repo}}`.
|
|
12
12
|
|
|
13
13
|
## Operator
|
|
14
14
|
|
|
@@ -42,11 +42,7 @@ for p in sys.argv[2].split("."):
|
|
|
42
42
|
cur = cur[p]
|
|
43
43
|
else:
|
|
44
44
|
print(sys.argv[3], end=""); sys.exit(0)
|
|
45
|
-
|
|
46
|
-
cur = " ".join(str(x) for x in cur)
|
|
47
|
-
else:
|
|
48
|
-
cur = str(cur)
|
|
49
|
-
print(os.path.expanduser(cur), end="")
|
|
45
|
+
print(os.path.expanduser(str(cur)), end="")
|
|
50
46
|
PYEOF
|
|
51
47
|
return 0
|
|
52
48
|
}
|
|
@@ -60,7 +56,6 @@ REGISTRY_FILE="${HERMES_FLEET_REGISTRY_FILE:-$(cfg fleet.registry_file "$HOME/.h
|
|
|
60
56
|
PROFILES_DIR="${HERMES_FLEET_HOME:-$HOME/.hermes}/profiles"
|
|
61
57
|
VOXXY_PLUGIN_DIR="${VOXXY_PLUGIN_DIR:-$(cfg fleet.voxxy_plugin_dir "$HOME/code/voxxy/plugins/tts/voxxy")}"
|
|
62
58
|
VOX_URL_VALUE="${VOX_URL:-$(cfg fleet.vox_url 'https://vox.delo.sh')}"
|
|
63
|
-
PM_EXTERNAL_SKILL_DIRS="${PM_EXTERNAL_SKILL_DIRS:-$(cfg fleet.pm_external_skill_dirs "$HOME/code/skillex/skill-sets/global/.system $HOME/code/skillex/packs/bmad/6.10.2")}"
|
|
64
59
|
|
|
65
60
|
APPLY=0
|
|
66
61
|
RESTART=1
|
|
@@ -141,42 +136,6 @@ path.write_text("\n".join(lines) + "\n")
|
|
|
141
136
|
PYEOF
|
|
142
137
|
}
|
|
143
138
|
|
|
144
|
-
pm_skill_dirs_status() { # pm_skill_dirs_status <config-path>
|
|
145
|
-
python3 - "$1" "$PM_EXTERNAL_SKILL_DIRS" <<'PYEOF'
|
|
146
|
-
import pathlib
|
|
147
|
-
import sys
|
|
148
|
-
|
|
149
|
-
import yaml
|
|
150
|
-
|
|
151
|
-
path = pathlib.Path(sys.argv[1])
|
|
152
|
-
desired = [entry for entry in sys.argv[2].split() if entry]
|
|
153
|
-
if not path.exists():
|
|
154
|
-
print('missing')
|
|
155
|
-
raise SystemExit(0)
|
|
156
|
-
data = yaml.safe_load(path.read_text()) or {}
|
|
157
|
-
skills = data.get('skills') or {}
|
|
158
|
-
current = list(skills.get('external_dirs') or [])
|
|
159
|
-
print('ok' if current == desired else 'mismatch')
|
|
160
|
-
PYEOF
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
set_pm_skill_dirs() { # set_pm_skill_dirs <config-path>
|
|
164
|
-
python3 - "$1" "$PM_EXTERNAL_SKILL_DIRS" <<'PYEOF'
|
|
165
|
-
import pathlib
|
|
166
|
-
import sys
|
|
167
|
-
|
|
168
|
-
import yaml
|
|
169
|
-
|
|
170
|
-
path = pathlib.Path(sys.argv[1])
|
|
171
|
-
desired = [entry for entry in sys.argv[2].split() if entry]
|
|
172
|
-
data = yaml.safe_load(path.read_text()) or {}
|
|
173
|
-
skills = data.get('skills') or {}
|
|
174
|
-
skills['external_dirs'] = desired
|
|
175
|
-
data['skills'] = skills
|
|
176
|
-
path.write_text(yaml.safe_dump(data, sort_keys=False))
|
|
177
|
-
PYEOF
|
|
178
|
-
}
|
|
179
|
-
|
|
180
139
|
wanted_agent() {
|
|
181
140
|
[[ ${#ONLY_AGENTS[@]} -eq 0 ]] && return 0
|
|
182
141
|
local a
|
|
@@ -279,28 +238,6 @@ while IFS=$'\t' read -r agent_id role_dir profile_name gateway_unit consumer_uni
|
|
|
279
238
|
# 5. PM fleet voice contract: runtime plugin symlink + Voxxy config + VOX_URL.
|
|
280
239
|
role_name="$(basename "$role_dir")"
|
|
281
240
|
if [[ "$role_name" == "pm" && -d "$runtime" ]]; then
|
|
282
|
-
runtime_config="$runtime/config.yaml"
|
|
283
|
-
if [[ -n "$PM_EXTERNAL_SKILL_DIRS" ]]; then
|
|
284
|
-
skill_dir_status="$(pm_skill_dirs_status "$runtime_config")"
|
|
285
|
-
case "$skill_dir_status" in
|
|
286
|
-
ok) ;;
|
|
287
|
-
missing)
|
|
288
|
-
note "$agent_id" DRIFT "runtime config missing: $runtime_config"
|
|
289
|
-
DRIFT=$((DRIFT + 1))
|
|
290
|
-
;;
|
|
291
|
-
*)
|
|
292
|
-
if [[ $APPLY -eq 1 ]]; then
|
|
293
|
-
set_pm_skill_dirs "$runtime_config"
|
|
294
|
-
note "$agent_id" FIXED "PM runtime skills.external_dirs synchronized"
|
|
295
|
-
changed=1; FIXED=$((FIXED + 1))
|
|
296
|
-
else
|
|
297
|
-
note "$agent_id" DRIFT "PM runtime skills.external_dirs differ from fleet contract"
|
|
298
|
-
DRIFT=$((DRIFT + 1))
|
|
299
|
-
fi
|
|
300
|
-
;;
|
|
301
|
-
esac
|
|
302
|
-
fi
|
|
303
|
-
|
|
304
241
|
plugin_link="$runtime/plugins/tts/voxxy"
|
|
305
242
|
if [[ -d "$VOXXY_PLUGIN_DIR" ]]; then
|
|
306
243
|
if [[ -L "$plugin_link" ]]; then
|
|
@@ -13,7 +13,7 @@ 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
|
|
16
|
+
| `bloodbank-consumer.py` | yes | NATS subscriber using canonical subjects plus envelope routing data |
|
|
17
17
|
| `decisions/` | yes | Agent-emitted decisions, one file per important call |
|
|
18
18
|
| `.env` | **no** | API keys + Telegram bot token (per-machine secret) |
|
|
19
19
|
| `auth.json` | **no** | Deprecated local OAuth store; fleet auth defaults to `HERMES_OAUTH_FILE=~/.hermes/auth.json` |
|
|
@@ -3,8 +3,12 @@
|
|
|
3
3
|
Bloodbank consumer for hermes agent {{agent_id}}.
|
|
4
4
|
|
|
5
5
|
Subscribes to:
|
|
6
|
-
bloodbank.evt.v1.repo
|
|
7
|
-
bloodbank.cmd.v1.agent
|
|
6
|
+
bloodbank.evt.v1.repo.>
|
|
7
|
+
bloodbank.cmd.v1.agent.>
|
|
8
|
+
|
|
9
|
+
The Bloodbank subject identifies the fixed domain/entity/action route. Repo and
|
|
10
|
+
agent identifiers stay in envelope data, so this consumer filters repo events
|
|
11
|
+
by ``data.repo`` and agent commands by ``data.target_agent_id``.
|
|
8
12
|
|
|
9
13
|
For each event/command received, writes it to the agent's notification queue at
|
|
10
14
|
$HERMES_HOME/bloodbank-inbox/<timestamp>.json so the gateway can ingest it on
|
|
@@ -47,8 +51,8 @@ PRODUCER = f"hermes-agent:{AGENT_ID}"
|
|
|
47
51
|
SOURCE = f"hermes://agent/{AGENT_ID}"
|
|
48
52
|
|
|
49
53
|
SUBJECTS = [
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
"bloodbank.evt.v1.repo.>",
|
|
55
|
+
"bloodbank.cmd.v1.agent.>",
|
|
52
56
|
]
|
|
53
57
|
|
|
54
58
|
KIND_MARKERS = {"event": "evt", "command": "cmd", "reply": "rpy"}
|
|
@@ -60,8 +64,8 @@ def _now():
|
|
|
60
64
|
|
|
61
65
|
def _subject_and_domain(ce_type, kind):
|
|
62
66
|
parts = ce_type.split(".")
|
|
63
|
-
if len(parts) != 5 or parts[0] != "bloodbank" or
|
|
64
|
-
raise ValueError(f"type {ce_type!r} must match bloodbank.
|
|
67
|
+
if len(parts) != 5 or parts[0] != "bloodbank" or parts[1] != "v1":
|
|
68
|
+
raise ValueError(f"type {ce_type!r} must match bloodbank.v1.<domain>.<entity>.<action>")
|
|
65
69
|
if kind not in KIND_MARKERS:
|
|
66
70
|
raise ValueError(f"unknown envelope kind {kind!r}")
|
|
67
71
|
_vendor, version, domain, entity, action = parts
|
|
@@ -93,6 +97,33 @@ def build_envelope(ce_type, data, *, kind="event", correlationid=None, causation
|
|
|
93
97
|
return env
|
|
94
98
|
|
|
95
99
|
|
|
100
|
+
def _is_for_consumer(subject, payload):
|
|
101
|
+
"""Route canonical subjects using identifiers carried in envelope data."""
|
|
102
|
+
if not isinstance(payload, dict):
|
|
103
|
+
return False
|
|
104
|
+
parts = subject.split(".")
|
|
105
|
+
if len(parts) != 6 or parts[0] != "bloodbank" or parts[2] != "v1":
|
|
106
|
+
return False
|
|
107
|
+
|
|
108
|
+
_vendor, kind, version, domain, entity, action = parts
|
|
109
|
+
if kind not in KIND_MARKERS.values():
|
|
110
|
+
return False
|
|
111
|
+
if KIND_MARKERS.get(payload.get("kind")) != kind:
|
|
112
|
+
return False
|
|
113
|
+
if payload.get("subject") != subject:
|
|
114
|
+
return False
|
|
115
|
+
if payload.get("type") != f"bloodbank.{version}.{domain}.{entity}.{action}":
|
|
116
|
+
return False
|
|
117
|
+
data = payload.get("data")
|
|
118
|
+
if not isinstance(data, dict):
|
|
119
|
+
return False
|
|
120
|
+
if kind == "evt" and domain == "repo":
|
|
121
|
+
return data.get("repo") == REPO
|
|
122
|
+
if kind == "cmd" and domain == "agent":
|
|
123
|
+
return data.get("target_agent_id") == AGENT_ID
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
96
127
|
async def main():
|
|
97
128
|
nc = await nats.connect(NATS_URL, name=f"hermes-{AGENT_ID}")
|
|
98
129
|
stop = asyncio.Event()
|
|
@@ -101,7 +132,10 @@ async def main():
|
|
|
101
132
|
try:
|
|
102
133
|
payload = json.loads(msg.data.decode())
|
|
103
134
|
except Exception:
|
|
104
|
-
payload
|
|
135
|
+
sys.stderr.write(f"[bloodbank-consumer:{AGENT_ID}] skipped unroutable payload on {msg.subject}\n")
|
|
136
|
+
return
|
|
137
|
+
if not _is_for_consumer(msg.subject, payload):
|
|
138
|
+
return
|
|
105
139
|
entry = {
|
|
106
140
|
"received_at": _now(),
|
|
107
141
|
"subject": msg.subject,
|
|
@@ -123,7 +157,7 @@ async def main():
|
|
|
123
157
|
"bloodbank.v1.agent.online.changed",
|
|
124
158
|
{"agent_id": AGENT_ID, "repo": REPO, "role": ROLE, "state": "online"},
|
|
125
159
|
)
|
|
126
|
-
await nc.publish(
|
|
160
|
+
await nc.publish(online["subject"], json.dumps(online).encode())
|
|
127
161
|
|
|
128
162
|
def _shutdown(*_):
|
|
129
163
|
stop.set()
|
|
@@ -136,7 +170,7 @@ async def main():
|
|
|
136
170
|
"bloodbank.v1.agent.online.changed",
|
|
137
171
|
{"agent_id": AGENT_ID, "repo": REPO, "role": ROLE, "state": "offline"},
|
|
138
172
|
)
|
|
139
|
-
await nc.publish("
|
|
173
|
+
await nc.publish(offline["subject"], json.dumps(offline).encode())
|
|
140
174
|
await nc.drain()
|
|
141
175
|
|
|
142
176
|
|
|
@@ -7,8 +7,8 @@ short and high-signal — anything that should ALWAYS be in context.
|
|
|
7
7
|
|
|
8
8
|
You are **{{display_name}}**, the `{{role}}` agent for the `{{repo}}` repo.
|
|
9
9
|
Your scope is the repo at the project root. You communicate via Telegram
|
|
10
|
-
(`@{{repo}}_{{role}}_bot`) and Bloodbank events on
|
|
11
|
-
`bloodbank.evt.v1.repo.{{repo}}
|
|
10
|
+
(`@{{repo}}_{{role}}_bot`) and canonical Bloodbank repo events on
|
|
11
|
+
`bloodbank.evt.v1.repo.>`, filtered by `data.repo = {{repo}}`.
|
|
12
12
|
|
|
13
13
|
## Operator
|
|
14
14
|
|
|
@@ -26,7 +26,6 @@ hermes_repo = "~/.hermes/hermes-agent"
|
|
|
26
26
|
fleet_env = "~/.hermes/fleet.env"
|
|
27
27
|
registry_file = "~/.hermes/agents-registry.yaml"
|
|
28
28
|
canonical_skills_dir = "/home/delorenj/.agents/skills"
|
|
29
|
-
pm_external_skill_dirs = ["~/code/skillex/skill-sets/global/.system", "~/code/skillex/packs/bmad/6.10.2"]
|
|
30
29
|
|
|
31
30
|
[github]
|
|
32
31
|
runtime_repo_owner = "delorenj"
|
|
@@ -47,15 +47,6 @@ PYEOF
|
|
|
47
47
|
|
|
48
48
|
upsert_fleet_env HERMES_FLEET_OAUTH_FILE "$HERMES_OAUTH_FILE"
|
|
49
49
|
upsert_fleet_env HERMES_FLEET_CODEX_HOME "$CODEX_HOME"
|
|
50
|
-
|
|
51
|
-
PLANE_WORKSPACE_KEY="$(plane_workspace_key "$PLANE_WORKSPACE")"
|
|
52
|
-
PLANE_WORKSPACE_VALUE="${!PLANE_WORKSPACE_KEY:-}"
|
|
53
|
-
if [[ -z "$PLANE_WORKSPACE_VALUE" && -n "${PLANE_API_KEY:-}" ]]; then
|
|
54
|
-
PLANE_WORKSPACE_VALUE="$PLANE_API_KEY"
|
|
55
|
-
fi
|
|
56
|
-
if [[ -n "$PLANE_WORKSPACE_VALUE" ]]; then
|
|
57
|
-
upsert_fleet_env "$PLANE_WORKSPACE_KEY" "$PLANE_WORKSPACE_VALUE"
|
|
58
|
-
fi
|
|
59
50
|
chmod 600 "$FLEET_ENV"
|
|
60
51
|
|
|
61
52
|
mark_done 05-fleet-env
|
|
@@ -110,29 +110,10 @@ CANONICAL_SKILLS_DIR="${CANONICAL_SKILLS_DIR:-$(config_get fleet.canonical_skill
|
|
|
110
110
|
CANONICAL_PM_SKILL_SRC="$CANONICAL_SKILLS_DIR/subagent-driven-development"
|
|
111
111
|
LOCAL_PM_SKILL_DST="$PROFILE_HOME/skills/software-development/subagent-driven-development"
|
|
112
112
|
|
|
113
|
-
if [[ "$ROLE" == "pm" ]]; then
|
|
114
|
-
PM_EXTERNAL_SKILL_DIRS="${PM_EXTERNAL_SKILL_DIRS:-$(config_get fleet.pm_external_skill_dirs "$HOME/code/skillex/skill-sets/global/.system $HOME/code/skillex/packs/bmad/6.10.2")}"
|
|
115
|
-
read -r -a RESOLVED_PM_EXTERNAL_SKILL_DIRS <<< "$PM_EXTERNAL_SKILL_DIRS"
|
|
116
|
-
if [[ ${#RESOLVED_PM_EXTERNAL_SKILL_DIRS[@]} -gt 0 ]]; then
|
|
117
|
-
log " setting PM skills.external_dirs = ${RESOLVED_PM_EXTERNAL_SKILL_DIRS[*]}"
|
|
118
|
-
python3 - "$PROFILE_HOME/config.yaml" "${RESOLVED_PM_EXTERNAL_SKILL_DIRS[@]}" <<'PYEOF'
|
|
119
|
-
import pathlib
|
|
120
|
-
import sys
|
|
121
|
-
|
|
122
|
-
import yaml
|
|
123
|
-
|
|
124
|
-
path = pathlib.Path(sys.argv[1])
|
|
125
|
-
external_dirs = sys.argv[2:]
|
|
126
|
-
data = yaml.safe_load(path.read_text()) or {}
|
|
127
|
-
skills = data.get("skills") or {}
|
|
128
|
-
skills["external_dirs"] = external_dirs
|
|
129
|
-
data["skills"] = skills
|
|
130
|
-
path.write_text(yaml.safe_dump(data, sort_keys=False))
|
|
131
|
-
PYEOF
|
|
132
|
-
fi
|
|
133
|
-
fi
|
|
134
|
-
|
|
135
113
|
if [[ -d "$CANONICAL_SKILLS_DIR" ]]; then
|
|
114
|
+
log " setting skills.external_dirs[0] = $CANONICAL_SKILLS_DIR"
|
|
115
|
+
env HERMES_HOME="$PROFILE_HOME" "$HERMES_BIN" config set skills.external_dirs.0 "$CANONICAL_SKILLS_DIR"
|
|
116
|
+
|
|
136
117
|
# Ensure key PM/local-ops skills are symlinked into runtime/profile skills root.
|
|
137
118
|
# This preserves canonical ownership and keeps updates instant across agents.
|
|
138
119
|
read -r -a SYMLINKED_RUNTIME_SKILLS <<< "${SYMLINKED_RUNTIME_SKILLS:-$(config_get fleet.symlinked_runtime_skills 'delonet-conventions delonet-dotenv hermes-pm-template-maintenance hindsight subagent-driven-development')}"
|
|
@@ -144,28 +144,6 @@ log " profile symlink $PROFILE_HOME -> $RUNTIME_LOCAL"
|
|
|
144
144
|
# Apply the one genuine per-repo config delta directly to the runtime config.
|
|
145
145
|
env HERMES_HOME="$RUNTIME_LOCAL" "$HERMES_BIN" config set terminal.cwd "$PROJECT_PATH" >/dev/null 2>&1 || true
|
|
146
146
|
|
|
147
|
-
if [[ "$ROLE" == "pm" ]]; then
|
|
148
|
-
PM_EXTERNAL_SKILL_DIRS="${PM_EXTERNAL_SKILL_DIRS:-$(config_get fleet.pm_external_skill_dirs "$HOME/code/skillex/skill-sets/global/.system $HOME/code/skillex/packs/bmad/6.10.2")}"
|
|
149
|
-
read -r -a RESOLVED_PM_EXTERNAL_SKILL_DIRS <<< "$PM_EXTERNAL_SKILL_DIRS"
|
|
150
|
-
if [[ ${#RESOLVED_PM_EXTERNAL_SKILL_DIRS[@]} -gt 0 ]]; then
|
|
151
|
-
python3 - "$RUNTIME_LOCAL/config.yaml" "${RESOLVED_PM_EXTERNAL_SKILL_DIRS[@]}" <<'PYEOF'
|
|
152
|
-
import pathlib
|
|
153
|
-
import sys
|
|
154
|
-
|
|
155
|
-
import yaml
|
|
156
|
-
|
|
157
|
-
path = pathlib.Path(sys.argv[1])
|
|
158
|
-
external_dirs = sys.argv[2:]
|
|
159
|
-
data = yaml.safe_load(path.read_text()) or {}
|
|
160
|
-
skills = data.get("skills") or {}
|
|
161
|
-
skills["external_dirs"] = external_dirs
|
|
162
|
-
data["skills"] = skills
|
|
163
|
-
path.write_text(yaml.safe_dump(data, sort_keys=False))
|
|
164
|
-
PYEOF
|
|
165
|
-
log " set PM runtime skills.external_dirs -> ${RESOLVED_PM_EXTERNAL_SKILL_DIRS[*]}"
|
|
166
|
-
fi
|
|
167
|
-
fi
|
|
168
|
-
|
|
169
147
|
if [[ "$ROLE" == "pm" ]]; then
|
|
170
148
|
VOXXY_PLUGIN_DIR="${VOXXY_PLUGIN_DIR:-$(config_get fleet.voxxy_plugin_dir "$HOME/code/voxxy/plugins/tts/voxxy")}"
|
|
171
149
|
if [[ -d "$VOXXY_PLUGIN_DIR" ]]; then
|