@delorenj/pjangler 1.2.18 → 1.2.21
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 +884 -295
- package/dist/mcp-server.js +892 -303
- package/package.json +8 -2
- package/templates/commonproject/AGENTS.md +3 -3
- package/templates/commonproject/README.md +11 -12
- package/templates/commonproject/copier.yml +11 -31
- package/templates/commonproject/template/.agents/hooks/README.md +13 -26
- package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
- package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
- package/templates/commonproject/template/.agents/local.example.json +2 -8
- package/templates/commonproject/template/.agents/skills.json +6 -0
- package/templates/commonproject/template/.project.json.jinja +9 -13
- package/templates/commonproject/template/mise.toml.jinja +9 -16
- 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 +33 -66
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
- package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
- 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 -33
- package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +22 -27
- package/templates/hermes-agent/template/SOUL.md.jinja +49 -30
- package/templates/hermes-agent/template/role.yaml.jinja +35 -4
- package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -0
- package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
- package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
- package/templates/hermes-agent/docs/bloodbank-gateway.md +0 -57
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +0 -60
|
@@ -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
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Create a Plane project in the configured workspace (one project per agent).
|
|
3
|
+
# Workspace + base URL come from role.yaml / config.toml (see _lib.sh).
|
|
4
|
+
# shellcheck source=_lib.sh
|
|
5
|
+
source "$(dirname "$0")/_lib.sh"
|
|
6
|
+
load_role_env
|
|
7
|
+
|
|
8
|
+
already_done 40-plane && { log "[40] plane already set up — skipping"; exit 0; }
|
|
9
|
+
[[ "${SKIP_PLANE:-0}" == "1" ]] && { log "[40] plane — SKIPPED"; mark_done 40-plane; exit 0; }
|
|
10
|
+
|
|
11
|
+
[[ -n "$PLANE_API_KEY" ]] || { warn "[40] PLANE_API_KEY not set; skipping. set PLANE_33GOD_API_KEY and re-run ./.scripts/40-plane.sh"; exit 0; }
|
|
12
|
+
|
|
13
|
+
# Build the 5-char identifier: <repo[0..2]><role[0..1]> uppercased
|
|
14
|
+
RAW=$(printf '%s%s' "${REPO:0:3}" "${ROLE:0:2}" | tr -cd '[:alnum:]' | tr '[:lower:]' '[:upper:]')
|
|
15
|
+
while (( ${#RAW} < 3 )); do RAW="${RAW}X"; done
|
|
16
|
+
IDENT="${RAW:0:5}"
|
|
17
|
+
|
|
18
|
+
# Project name = display_name (already smart-cased: "Bloodbank PM", "Hermes-Agent Dev").
|
|
19
|
+
# Plane forbids hyphens in names — substitute spaces.
|
|
20
|
+
NAME="${DISPLAY_NAME//-/ }"
|
|
21
|
+
|
|
22
|
+
log "[40] creating plane project '$NAME' [$IDENT]"
|
|
23
|
+
|
|
24
|
+
# Check for existing project with this identifier
|
|
25
|
+
EXISTING=$(curl -sS "$PLANE_BASE/api/v1/workspaces/$PLANE_WORKSPACE/projects/?per_page=200" \
|
|
26
|
+
-H "X-API-Key: $PLANE_API_KEY" \
|
|
27
|
+
| python3 -c "
|
|
28
|
+
import sys, json
|
|
29
|
+
d = json.load(sys.stdin)
|
|
30
|
+
ident = '$IDENT'
|
|
31
|
+
for p in d.get('results', []):
|
|
32
|
+
if (p.get('identifier') or '').upper() == ident:
|
|
33
|
+
print(p['id']); break")
|
|
34
|
+
|
|
35
|
+
if [[ -n "$EXISTING" ]]; then
|
|
36
|
+
log " plane project $IDENT already exists (id=$EXISTING) — reusing"
|
|
37
|
+
PROJECT_ID="$EXISTING"
|
|
38
|
+
else
|
|
39
|
+
RESP=$(curl -sS -X POST "$PLANE_BASE/api/v1/workspaces/$PLANE_WORKSPACE/projects/" \
|
|
40
|
+
-H "X-API-Key: $PLANE_API_KEY" \
|
|
41
|
+
-H "Content-Type: application/json" \
|
|
42
|
+
-d "$(python3 -c "import json,sys; print(json.dumps({'name': sys.argv[1], 'identifier': sys.argv[2], 'description': sys.argv[3]}))" \
|
|
43
|
+
"$NAME" "$IDENT" "Hermes agent board for $AGENT_ID")")
|
|
44
|
+
PROJECT_ID=$(echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('id') or '')")
|
|
45
|
+
[[ -n "$PROJECT_ID" ]] || die "plane create failed: $RESP"
|
|
46
|
+
log " plane project created id=$PROJECT_ID"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
yaml_set plane.identifier "$IDENT"
|
|
50
|
+
echo "$PROJECT_ID" > "$ROLE_DIR/.scripts/.plane-project-id"
|
|
51
|
+
mark_done 40-plane
|
|
@@ -10,12 +10,11 @@
|
|
|
10
10
|
# 2. Otherwise (hermes run on a repo with no CommonProject board yet)
|
|
11
11
|
# -> create ONE repo-named board and write it back into .project.json so
|
|
12
12
|
# it becomes the SOT for every agent in this repo.
|
|
13
|
-
# Either way we register this agent under .project.json `agents
|
|
14
|
-
#
|
|
13
|
+
# Either way we register this agent under .project.json `agents` and mirror the
|
|
14
|
+
# binding into role.yaml for back-compat (80-registry.sh / 99-summary.sh).
|
|
15
15
|
# shellcheck source=_lib.sh
|
|
16
16
|
source "$(dirname "$0")/_lib.sh"
|
|
17
17
|
load_role_env
|
|
18
|
-
resolve_plane_api_key "$PLANE_WORKSPACE"
|
|
19
18
|
# shellcheck source=lib/ticket-provider.sh
|
|
20
19
|
source "$(dirname "$0")/lib/ticket-provider.sh"
|
|
21
20
|
|
|
@@ -47,13 +46,13 @@ PY
|
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
# pj_write — merge board binding (optional) + this agent into .project.json.
|
|
50
|
-
# args: set_provider(0|1) provider board_id workspace identifier
|
|
49
|
+
# args: set_provider(0|1) provider board_id board_url workspace identifier team
|
|
51
50
|
pj_write() {
|
|
52
51
|
REPO="$REPO" REPO_ROOT="$REPO_ROOT" AGENT_ID="$AGENT_ID" ROLE="$ROLE" \
|
|
53
52
|
ROLE_DIR_REL="$ROLE_DIR_REL" PROJECT_DESC="${PROJECT_DESC:-}" \
|
|
54
53
|
python3 - "$PROJECT_JSON" "$@" <<'PY'
|
|
55
54
|
import sys, os, json, pathlib
|
|
56
|
-
(path, set_provider, provider, board_id, workspace, identifier) = sys.argv[1:
|
|
55
|
+
(path, set_provider, provider, board_id, board_url, workspace, identifier, team) = sys.argv[1:9]
|
|
57
56
|
p = pathlib.Path(path)
|
|
58
57
|
try:
|
|
59
58
|
d = json.loads(p.read_text())
|
|
@@ -72,30 +71,47 @@ if set_provider == "1":
|
|
|
72
71
|
if workspace: tp["workspace"] = workspace
|
|
73
72
|
if identifier: tp["identifier"] = identifier
|
|
74
73
|
if board_id: tp["board_id"] = board_id
|
|
75
|
-
tp
|
|
76
|
-
tp["
|
|
74
|
+
if board_url: tp["board_url"] = board_url
|
|
75
|
+
if team: tp["team"] = team
|
|
77
76
|
ag = d.setdefault("agents", {})
|
|
78
77
|
ag[os.environ["AGENT_ID"]] = {"role": os.environ["ROLE"], "role_dir": os.environ["ROLE_DIR_REL"]}
|
|
79
|
-
automation = d.setdefault("automation", {})
|
|
80
|
-
automation.setdefault("reconcile", {"enabled": False, "grace_hours": 0, "auto_review": True})
|
|
81
78
|
p.write_text(json.dumps(d, indent=2) + "\n")
|
|
82
79
|
PY
|
|
83
80
|
log " .project.json updated (agent=$AGENT_ID)"
|
|
84
81
|
}
|
|
85
82
|
|
|
86
|
-
#
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
local provider="$1"
|
|
83
|
+
# Mirror the binding into role.yaml so legacy consumers keep working.
|
|
84
|
+
mirror_to_role_yaml() {
|
|
85
|
+
# mirror_to_role_yaml <provider> <board_id> <board_url> <workspace> <identifier> <team>
|
|
86
|
+
local provider="$1" bid="$2" burl="$3" ws="$4" ident="$5" team="$6"
|
|
90
87
|
yaml_set ticket_provider.name "$provider" 2>/dev/null || true
|
|
88
|
+
[ -n "$bid" ] && yaml_set ticket_provider.board_id "$bid" 2>/dev/null || true
|
|
89
|
+
[ -n "$burl" ] && yaml_set ticket_provider.board_url "$burl" 2>/dev/null || true
|
|
90
|
+
case "$provider" in
|
|
91
|
+
plane)
|
|
92
|
+
[ -n "$bid" ] && echo "$bid" > "$ROLE_DIR/.scripts/.plane-project-id"
|
|
93
|
+
[ -n "$ws" ] && yaml_set ticket_provider.workspace "$ws" 2>/dev/null || true
|
|
94
|
+
[ -n "$bid" ] && yaml_set ticket_provider.project "$bid" 2>/dev/null || true
|
|
95
|
+
[ -n "$ws" ] && yaml_set plane.workspace "$ws" 2>/dev/null || true
|
|
96
|
+
[ -n "$ident" ] && yaml_set plane.identifier "$ident" 2>/dev/null || true
|
|
97
|
+
;;
|
|
98
|
+
trello)
|
|
99
|
+
[ -n "$bid" ] && yaml_set ticket_provider.board "$bid" 2>/dev/null || true
|
|
100
|
+
;;
|
|
101
|
+
linear)
|
|
102
|
+
[ -n "$team" ] && yaml_set ticket_provider.team "$team" 2>/dev/null || true
|
|
103
|
+
;;
|
|
104
|
+
esac
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
# ── Provider resolution ──────────────────────────────────────────────────
|
|
94
108
|
# An existing repo board (in .project.json) wins — every agent binds to it.
|
|
95
109
|
SOT_TYPE="$(pj ticket_provider.type)"
|
|
96
110
|
SOT_BOARD_ID="$(pj ticket_provider.board_id)"
|
|
111
|
+
SOT_URL="$(pj ticket_provider.board_url)"
|
|
97
112
|
SOT_WS="$(pj ticket_provider.workspace)"
|
|
98
113
|
SOT_IDENT="$(pj ticket_provider.identifier)"
|
|
114
|
+
SOT_TEAM="$(pj ticket_provider.team)"
|
|
99
115
|
|
|
100
116
|
# role.yaml provider comes from copier --data (the operator's pjangler choice).
|
|
101
117
|
ROLE_PROVIDER="$(yaml_get ticket_provider.name)"
|
|
@@ -106,10 +122,9 @@ if [ -n "$SOT_BOARD_ID" ]; then
|
|
|
106
122
|
if [ -n "$ROLE_PROVIDER" ] && [ "$ROLE_PROVIDER" != "$PROVIDER" ]; then
|
|
107
123
|
warn "[42] requested provider '$ROLE_PROVIDER' but repo board is '$PROVIDER' (.project.json wins); binding to existing board"
|
|
108
124
|
fi
|
|
109
|
-
case "$PROVIDER" in plane|trello) ;; *) die "unsupported ticket provider in .project.json: $PROVIDER (expected plane|trello)" ;; esac
|
|
110
125
|
log "[42] binding $AGENT_ID to existing repo board (provider=$PROVIDER, id=$SOT_BOARD_ID)"
|
|
111
|
-
|
|
112
|
-
pj_write
|
|
126
|
+
mirror_to_role_yaml "$PROVIDER" "$SOT_BOARD_ID" "$SOT_URL" "$SOT_WS" "$SOT_IDENT" "$SOT_TEAM"
|
|
127
|
+
pj_write 0 "$PROVIDER" "" "" "" "" "" # register agent only; board already recorded
|
|
113
128
|
mark_done 42-ticket-provider
|
|
114
129
|
exit 0
|
|
115
130
|
fi
|
|
@@ -128,22 +143,45 @@ NAME="$(printf '%s' "$REPO" | tr '_-' ' ' | python3 -c 'import sys; print(" ".j
|
|
|
128
143
|
DESC="Ticket board for $REPO"
|
|
129
144
|
|
|
130
145
|
case "$PROVIDER" in
|
|
146
|
+
linear)
|
|
147
|
+
if [[ -z "${LINEAR_API_KEY:-}" ]]; then
|
|
148
|
+
warn "[42] LINEAR_API_KEY not set; set role.yaml/.project.json ticket_provider.team and re-run ./.scripts/42-ticket-provider.sh"
|
|
149
|
+
pj_write 1 linear "" "" "" "$IDENT" "$SOT_TEAM"
|
|
150
|
+
mark_done 42-ticket-provider; exit 0
|
|
151
|
+
fi
|
|
152
|
+
OUT="$(tp resolve 2>/dev/null || true)"
|
|
153
|
+
BID="$(printf '%s' "$OUT" | python3 -c 'import sys,json
|
|
154
|
+
try: print(json.load(sys.stdin).get("board_id",""))
|
|
155
|
+
except Exception: print("")')"
|
|
156
|
+
BURL="$(printf '%s' "$OUT" | python3 -c 'import sys,json
|
|
157
|
+
try: print(json.load(sys.stdin).get("board_url",""))
|
|
158
|
+
except Exception: print("")')"
|
|
159
|
+
if [ -n "$BID" ]; then
|
|
160
|
+
mirror_to_role_yaml linear "$BID" "$BURL" "" "$IDENT" "$SOT_TEAM"
|
|
161
|
+
pj_write 1 linear "$BID" "$BURL" "" "$IDENT" "$SOT_TEAM"
|
|
162
|
+
else
|
|
163
|
+
warn "[42] linear resolve returned no board; set ticket_provider.team and re-run"
|
|
164
|
+
pj_write 1 linear "" "" "" "$IDENT" "$SOT_TEAM"
|
|
165
|
+
fi
|
|
166
|
+
;;
|
|
167
|
+
|
|
131
168
|
plane|trello)
|
|
132
169
|
KEYVAR=PLANE_API_KEY; [ "$PROVIDER" = trello ] && KEYVAR=TRELLO_KEY
|
|
133
170
|
if [[ -z "${!KEYVAR:-}" ]]; then
|
|
134
171
|
warn "[42] $KEYVAR not set; skipping board creation. Set creds and re-run ./.scripts/42-ticket-provider.sh"
|
|
135
|
-
pj_write 1 "$PROVIDER" "" "${SOT_WS:-$
|
|
172
|
+
pj_write 1 "$PROVIDER" "" "" "${SOT_WS:-$PLANE_WORKSPACE}" "$IDENT" ""
|
|
136
173
|
mark_done 42-ticket-provider; exit 0
|
|
137
174
|
fi
|
|
138
175
|
OUT="$(tp create_board "$NAME" "$IDENT" "$DESC")" || die "create_board failed for $PROVIDER"
|
|
139
176
|
BID="$(printf '%s' "$OUT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("board_id",""))')"
|
|
140
|
-
|
|
177
|
+
BURL="$(printf '%s' "$OUT" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("board_url",""))')"
|
|
178
|
+
WS="${SOT_WS:-$PLANE_WORKSPACE}"
|
|
141
179
|
[ "$PROVIDER" = trello ] && WS=""
|
|
142
|
-
|
|
143
|
-
pj_write 1 "$PROVIDER" "$BID" "$WS" "$IDENT"
|
|
180
|
+
mirror_to_role_yaml "$PROVIDER" "$BID" "$BURL" "$WS" "$IDENT" ""
|
|
181
|
+
pj_write 1 "$PROVIDER" "$BID" "$BURL" "$WS" "$IDENT" ""
|
|
144
182
|
;;
|
|
145
183
|
|
|
146
|
-
*) die "unknown ticket provider: $PROVIDER (expected plane|trello)" ;;
|
|
184
|
+
*) die "unknown ticket provider: $PROVIDER (expected linear|plane|trello)" ;;
|
|
147
185
|
esac
|
|
148
186
|
|
|
149
187
|
mark_done 42-ticket-provider
|
|
@@ -20,7 +20,8 @@ log "[60] installing bloodbank consumer for $AGENT_ID"
|
|
|
20
20
|
# placeholders. If $RUNTIME/bloodbank-consumer.py was ever lost (runtime
|
|
21
21
|
# submodule wiped, manual rm, etc.) and then someone restored it by
|
|
22
22
|
# copying from $RUNTIME_SCAFFOLD_DIR, they'd end up with an un-rendered
|
|
23
|
-
# consumer
|
|
23
|
+
# consumer whose envelope routing filter compares against literal `{{repo}}`
|
|
24
|
+
# and `{{agent_id}}` values.
|
|
24
25
|
# This block handles both failure modes idempotently.
|
|
25
26
|
CONSUMER="$RUNTIME/bloodbank-consumer.py"
|
|
26
27
|
SCAFFOLD_CONSUMER="$RUNTIME_SCAFFOLD_DIR/bloodbank-consumer.py"
|
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
source "$(dirname "$0")/_lib.sh"
|
|
6
6
|
load_role_env
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
log "[70] systemd already installed — skipping"
|
|
10
|
-
exit 0
|
|
11
|
-
fi
|
|
8
|
+
already_done 70-systemd && { log "[70] systemd already installed — skipping"; exit 0; }
|
|
12
9
|
[[ "${SKIP_SYSTEMD:-0}" == "1" ]] && { log "[70] systemd — SKIPPED"; mark_done 70-systemd; exit 0; }
|
|
13
10
|
|
|
14
11
|
RUNTIME="$ROLE_DIR/runtime"
|
|
@@ -35,9 +32,6 @@ Type=simple
|
|
|
35
32
|
Environment=HERMES_HOME=$RUNTIME
|
|
36
33
|
Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
|
|
37
34
|
Environment=CODEX_HOME=$CODEX_HOME
|
|
38
|
-
Environment=HERMES_KANBAN_DISPATCH_IN_GATEWAY=0
|
|
39
|
-
Environment=HERMES_KANBAN_SLASH_ENABLED=0
|
|
40
|
-
EnvironmentFile=-$FLEET_ENV
|
|
41
35
|
EnvironmentFile=-$RUNTIME/.env
|
|
42
36
|
ExecStart=$HERMES_BIN gateway run --replace
|
|
43
37
|
Restart=on-failure
|
|
@@ -63,7 +57,6 @@ WorkingDirectory=$RUNTIME
|
|
|
63
57
|
Environment=HERMES_HOME=$RUNTIME
|
|
64
58
|
Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
|
|
65
59
|
Environment=CODEX_HOME=$CODEX_HOME
|
|
66
|
-
EnvironmentFile=-$FLEET_ENV
|
|
67
60
|
EnvironmentFile=-$RUNTIME/.env
|
|
68
61
|
ExecStart=$HERMES_AGENT_REPO/.venv/bin/python $RUNTIME/bloodbank-consumer.py
|
|
69
62
|
Restart=on-failure
|
|
@@ -93,7 +86,6 @@ WorkingDirectory=$REPO_ROOT
|
|
|
93
86
|
Environment=HERMES_HOME=$RUNTIME
|
|
94
87
|
Environment=HERMES_OAUTH_FILE=$HERMES_OAUTH_FILE
|
|
95
88
|
Environment=CODEX_HOME=$CODEX_HOME
|
|
96
|
-
EnvironmentFile=-$FLEET_ENV
|
|
97
89
|
EnvironmentFile=-%h/.config/hermes-agent/env
|
|
98
90
|
EnvironmentFile=-%h/.hermes/env
|
|
99
91
|
EnvironmentFile=-%h/.hermes/hermes-agent.env
|
|
@@ -120,7 +112,6 @@ UNIT
|
|
|
120
112
|
|
|
121
113
|
if systemd_user_available; then
|
|
122
114
|
systemctl --user daemon-reload
|
|
123
|
-
systemctl --user disable --now "hermes-${AGENT_ID}-checkpoint.timer" "hermes-${AGENT_ID}-checkpoint.service" >/dev/null 2>&1 || true
|
|
124
115
|
# `enable --now` both enables (persist across login) AND starts the unit now, so a
|
|
125
116
|
# freshly provisioned agent comes up live instead of dormant. Units with missing
|
|
126
117
|
# creds (e.g. a gateway with no Telegram token yet) fail softly via Restart=on-failure.
|