@delorenj/pjangler 1.2.9 → 1.2.13
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 +498 -204
- package/dist/mcp-server.js +488 -194
- package/package.json +1 -1
- package/templates/commonproject/copier.yml +4 -1
- package/templates/commonproject/template/.project.json.jinja +9 -12
- package/templates/commonproject/template/mise.toml.jinja +1 -1
- package/templates/hermes-agent/README.md +1 -1
- package/templates/hermes-agent/config.example.toml +66 -0
- package/templates/hermes-agent/docs/architecture.md +5 -0
- package/templates/hermes-agent/docs/bloodbank-gateway.md +57 -0
- package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
- package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +60 -0
- package/templates/hermes-agent/docs/operations.md +1 -1
- package/templates/hermes-agent/docs/sentinel/README.md +2 -2
- package/templates/hermes-agent/docs/sentinel/development.md +3 -3
- package/templates/hermes-agent/docs/sentinel/providers.md +6 -5
- package/templates/hermes-agent/install-local.sh +10 -4
- package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +5 -3
- package/templates/hermes-agent/scripts/fleet-sync.sh +64 -1
- package/templates/hermes-agent/template/.gitignore.jinja +2 -0
- package/templates/hermes-agent/template/.scripts/01-config.sh +1 -0
- package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +9 -0
- package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +22 -3
- package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +22 -0
- package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -33
- package/templates/hermes-agent/template/.scripts/70-systemd.sh +8 -1
- package/templates/hermes-agent/template/.scripts/_lib.sh +61 -3
- package/templates/hermes-agent/template/.scripts/config.example.toml +5 -0
- package/templates/hermes-agent/template/.scripts/heartbeat.sh +66 -22
- package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +3 -3
- package/templates/hermes-agent/template/.scripts/providers/plane.sh +29 -9
- package/templates/hermes-agent/template/role.yaml.jinja +2 -27
package/package.json
CHANGED
|
@@ -72,12 +72,15 @@ plane_base:
|
|
|
72
72
|
|
|
73
73
|
# Provider-agnostic board binding — supplied by pjangler's --data values after
|
|
74
74
|
# the board is created or linked. Defaults keep a bare `copier copy` rendering
|
|
75
|
-
# valid, and fall back to the legacy plane_* aliases above.
|
|
75
|
+
# valid, and fall back to the legacy plane_* aliases above. Board URLs are
|
|
76
|
+
# derived from provider/workspace/board_id and are intentionally not persisted.
|
|
76
77
|
board_id:
|
|
77
78
|
type: str
|
|
78
79
|
default: "{{ plane_project_id }}"
|
|
79
80
|
when: false
|
|
80
81
|
|
|
82
|
+
# Deprecated no-op accepted so older pjangler/CommonProject callers do not fail
|
|
83
|
+
# when they still pass `--data board_url=...`.
|
|
81
84
|
board_url:
|
|
82
85
|
type: str
|
|
83
86
|
default: ""
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
{% set ticket_board_id = board_id | default(plane_project_id) -%}
|
|
2
|
-
{% set ticket_board_url = board_url | default('') -%}
|
|
3
2
|
{% set workspace = ticket_workspace | default(plane_workspace if ticket_provider == 'plane' else '') -%}
|
|
4
|
-
{% if ticket_board_url -%}
|
|
5
|
-
{% set resolved_board_url = ticket_board_url -%}
|
|
6
|
-
{% elif ticket_provider == 'plane' and ticket_board_id -%}
|
|
7
|
-
{% set resolved_board_url = plane_base ~ '/' ~ workspace ~ '/projects/' ~ ticket_board_id ~ '/issues/' -%}
|
|
8
|
-
{% elif ticket_provider == 'trello' and ticket_board_id -%}
|
|
9
|
-
{% set resolved_board_url = 'https://trello.com/b/' ~ ticket_board_id -%}
|
|
10
|
-
{% else -%}
|
|
11
|
-
{% set resolved_board_url = '' -%}
|
|
12
|
-
{% endif -%}
|
|
13
3
|
{
|
|
14
4
|
"project_name": {{ project_name | tojson }},
|
|
15
5
|
"project_description": {{ project_description | tojson }},
|
|
@@ -20,7 +10,14 @@
|
|
|
20
10
|
"workspace": {{ workspace | tojson }},
|
|
21
11
|
"identifier": {{ project_identifier | tojson }},
|
|
22
12
|
"board_id": {{ ticket_board_id | tojson }},
|
|
23
|
-
"
|
|
13
|
+
"state": {{ ("linked" if ticket_board_id else "planned") | tojson }}
|
|
24
14
|
},
|
|
25
|
-
"agents": {}
|
|
15
|
+
"agents": {},
|
|
16
|
+
"automation": {
|
|
17
|
+
"reconcile": {
|
|
18
|
+
"enabled": false,
|
|
19
|
+
"grace_hours": 0,
|
|
20
|
+
"auto_review": true
|
|
21
|
+
}
|
|
22
|
+
}
|
|
26
23
|
}
|
|
@@ -15,7 +15,7 @@ _.file = [".env"]
|
|
|
15
15
|
enter = [
|
|
16
16
|
"{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-agentfiles.sh",
|
|
17
17
|
"op inject -i .env.op > .env",
|
|
18
|
-
"{% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh",
|
|
18
|
+
"[ -f {% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh ] && {% raw %}{{config_root}}{% endraw %}/.mise/scripts/codegraph.sh || true",
|
|
19
19
|
{%- if agent_hooks_layer %}
|
|
20
20
|
"{% raw %}{{config_root}}{% endraw %}/.mise/scripts/link-project-skills-to-clis.sh",
|
|
21
21
|
"{% raw %}{{config_root}}{% endraw %}/.agents/hooks/sync.py --install --quiet",
|
|
@@ -68,7 +68,7 @@ you to review it. Keys:
|
|
|
68
68
|
| `fleet` | `fleet_env`, `registry_file` | Fleet source-of-truth + registry locations |
|
|
69
69
|
| `fleet` | `oauth_file`, `codex_home` | Shared Hermes OAuth store + Codex CLI/app-server auth home |
|
|
70
70
|
| `fleet` | `runtime_scaffold_dir` | Fallback scaffold (if agent-local one is missing) |
|
|
71
|
-
| `fleet` | `canonical_skills_dir`, `symlinked_runtime_skills` |
|
|
71
|
+
| `fleet` | `canonical_skills_dir`, `pm_external_skill_dirs`, `symlinked_runtime_skills` | Shared runtime skills plus PM external skill libraries |
|
|
72
72
|
| `github` | `runtime_repo_owner` | Owner of the `agent-hm-*` runtime repos |
|
|
73
73
|
| `plane` | `base`, `workspace` | Plane URL + workspace slug |
|
|
74
74
|
| `bloodbank` | `nats_host`, `nats_port`, `compose_dir` | NATS endpoint + compose dir hint |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# hermes-agent-template — distributable configuration
|
|
2
|
+
#
|
|
3
|
+
# This file holds every environment-specific default the provisioning scripts
|
|
4
|
+
# need. Copy it to ~/.config/hermes-agent-template/config.toml (the first
|
|
5
|
+
# provisioning run does this for you) and edit the values for YOUR machine.
|
|
6
|
+
#
|
|
7
|
+
# Resolution precedence for each value (highest wins):
|
|
8
|
+
# explicit env var > ~/.hermes/fleet.env > this file > built-in fallback
|
|
9
|
+
#
|
|
10
|
+
# Override the config location with $HERMES_TEMPLATE_CONFIG if you keep it
|
|
11
|
+
# somewhere other than $XDG_CONFIG_HOME/hermes-agent-template/config.toml.
|
|
12
|
+
|
|
13
|
+
[fleet]
|
|
14
|
+
# Absolute path to the shared Hermes executable every agent launcher execs.
|
|
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.
|
|
17
|
+
hermes_repo = "~/.hermes/hermes-agent"
|
|
18
|
+
# Fallback runtime scaffold dir — used only when an agent's vendored
|
|
19
|
+
# ./.runtime-scaffold is missing.
|
|
20
|
+
runtime_scaffold_dir = "~/code/hermes-agent-template/runtime-scaffold"
|
|
21
|
+
# Shared fleet source-of-truth env file + fleet registry. ~ is expanded.
|
|
22
|
+
fleet_env = "~/.hermes/fleet.env"
|
|
23
|
+
registry_file = "~/.hermes/agents-registry.yaml"
|
|
24
|
+
# Shared auth roots. HERMES_OAUTH_FILE is the Hermes provider OAuth store
|
|
25
|
+
# (including openai-codex); CODEX_HOME is for Codex CLI/app-server auth.
|
|
26
|
+
oauth_file = "~/.hermes/auth.json"
|
|
27
|
+
codex_home = "~/.codex"
|
|
28
|
+
# Canonical external skills dir mirrored into each agent profile.
|
|
29
|
+
canonical_skills_dir = "/home/delorenj/.agents/skills"
|
|
30
|
+
# PM-only external skill libraries surfaced alongside Hermes built-ins.
|
|
31
|
+
pm_external_skill_dirs = [
|
|
32
|
+
"~/code/skillex/skill-sets/global/.system",
|
|
33
|
+
"~/code/skillex/packs/bmad/6.10.2",
|
|
34
|
+
]
|
|
35
|
+
# Voxxy plugin + service defaults for Hermes PM agents.
|
|
36
|
+
voxxy_plugin_dir = "~/code/voxxy/plugins/tts/voxxy"
|
|
37
|
+
vox_url = "https://vox.delo.sh"
|
|
38
|
+
# Canonical PM config.yaml the provisioner seeds each runtime's config.yaml from.
|
|
39
|
+
# This is the fleet-wide Hermes inference/skill config (model, toolsets, etc.)
|
|
40
|
+
# every agent runtime starts from. ~ is expanded.
|
|
41
|
+
# canonical_pm_config = "~/.hermes/config.yaml"
|
|
42
|
+
# Skills symlinked into every agent's runtime skills root.
|
|
43
|
+
symlinked_runtime_skills = [
|
|
44
|
+
"delonet-conventions",
|
|
45
|
+
"delonet-dotenv",
|
|
46
|
+
"hermes-pm-template-maintenance",
|
|
47
|
+
"hindsight",
|
|
48
|
+
"33god-projects",
|
|
49
|
+
"subagent-driven-development",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[github]
|
|
53
|
+
# Owner of the per-agent runtime repos (creates <owner>/agent-hm-<repo>-<role>).
|
|
54
|
+
runtime_repo_owner = "delorenj"
|
|
55
|
+
|
|
56
|
+
[plane]
|
|
57
|
+
# Plane instance base URL and workspace slug (one project per agent).
|
|
58
|
+
base = "https://plane.delo.sh"
|
|
59
|
+
workspace = "33god"
|
|
60
|
+
|
|
61
|
+
[bloodbank]
|
|
62
|
+
# NATS endpoint the consumer connects to.
|
|
63
|
+
nats_host = "127.0.0.1"
|
|
64
|
+
nats_port = 4222
|
|
65
|
+
# Where the bloodbank docker compose lives (used only for a hint message). ~ ok.
|
|
66
|
+
compose_dir = "~/code/33GOD/bloodbank"
|
|
@@ -107,3 +107,8 @@ Each consumer subscribes to two lanes:
|
|
|
107
107
|
Each agent emits CloudEvents 1.0 envelopes with `actor.agent_id`,
|
|
108
108
|
`producer = hermes-agent:<id>`, `source = hermes://agent/<id>`. The naming
|
|
109
109
|
spec is owned by Holyfields (`~/code/33GOD/bloodbank/docs/event-naming.md`).
|
|
110
|
+
|
|
111
|
+
The current generated consumer is a transitional NATS receiver and audit
|
|
112
|
+
capture. The target architecture is a first-class Hermes Bloodbank gateway where
|
|
113
|
+
command events enter the same turn queue as Telegram/web messages; see
|
|
114
|
+
[Bloodbank Gateway](bloodbank-gateway.md).
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Bloodbank Gateway
|
|
2
|
+
|
|
3
|
+
Bloodbank command events should be a first-class Hermes gateway, not an inbox
|
|
4
|
+
directory that a later heartbeat drains.
|
|
5
|
+
|
|
6
|
+
## Goal
|
|
7
|
+
|
|
8
|
+
Make this true:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
Telegram message -> Hermes gateway -> agent turn queue -> agent acts
|
|
12
|
+
Bloodbank command -> Hermes gateway -> agent turn queue -> agent acts
|
|
13
|
+
Web message -> Hermes gateway -> agent turn queue -> agent acts
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
The transport changes; the agent turn contract does not.
|
|
17
|
+
|
|
18
|
+
## Current State
|
|
19
|
+
|
|
20
|
+
`runtime-scaffold/bloodbank-consumer.py` subscribes to NATS subjects and writes
|
|
21
|
+
JSON files under `bloodbank-inbox/`. That proves subscription, envelope parsing,
|
|
22
|
+
and durable audit capture, but it is not command execution. It can accumulate
|
|
23
|
+
files while the agent never acts.
|
|
24
|
+
|
|
25
|
+
## Target Contract
|
|
26
|
+
|
|
27
|
+
- The Bloodbank gateway subscribes to `bloodbank.cmd.v1.agent.<agent_id>.>`.
|
|
28
|
+
- Each command event is converted into the same internal Hermes message/turn
|
|
29
|
+
shape used by Telegram and web gateways.
|
|
30
|
+
- Hermes owns backpressure: if the agent is busy, the command waits in the
|
|
31
|
+
message queue until the agent can act.
|
|
32
|
+
- The gateway emits lifecycle events:
|
|
33
|
+
- received
|
|
34
|
+
- accepted or rejected
|
|
35
|
+
- turn_started
|
|
36
|
+
- turn_completed or turn_failed
|
|
37
|
+
- Replies use the incoming `correlationid` and `reply_to`/reply subject when
|
|
38
|
+
present.
|
|
39
|
+
- Idempotency uses the CloudEvent `id`; duplicate events do not create duplicate
|
|
40
|
+
turns.
|
|
41
|
+
|
|
42
|
+
## Required Work
|
|
43
|
+
|
|
44
|
+
1. Add a Hermes transport adapter for Bloodbank/NATS.
|
|
45
|
+
2. Define a command payload schema that maps cleanly to a user prompt plus
|
|
46
|
+
metadata (`actor`, `source`, `priority`, `correlationid`, `reply_to`).
|
|
47
|
+
3. Replace the file-inbox consumer service with `hermes gateway bloodbank run`
|
|
48
|
+
or equivalent.
|
|
49
|
+
4. Keep optional audit persistence, but make it observational only.
|
|
50
|
+
5. Add smoke tests that publish a NATS command and assert that a Hermes turn is
|
|
51
|
+
created, processed, and acknowledged.
|
|
52
|
+
|
|
53
|
+
## Non-Goals
|
|
54
|
+
|
|
55
|
+
- No heartbeat inbox drain.
|
|
56
|
+
- No polling directory as the command execution mechanism.
|
|
57
|
+
- No per-agent one-off consumer logic.
|
|
@@ -24,6 +24,7 @@ The Fleet remains a set of independently useful parts:
|
|
|
24
24
|
- [prd.md](./prd.md) - Functional and non-functional requirements.
|
|
25
25
|
- [architecture.md](./architecture.md) - Technical architecture and ADRs.
|
|
26
26
|
- [epics-and-stories.md](./epics-and-stories.md) - Implementation backlog with acceptance criteria.
|
|
27
|
+
- [n8n-service-hub.md](./n8n-service-hub.md) - Concrete service-hub shape for n8n plus systemd fallback.
|
|
27
28
|
- [implementation-readiness-report-2026-06-27.md](./implementation-readiness-report-2026-06-27.md) - Readiness assessment and launch gates.
|
|
28
29
|
|
|
29
30
|
## Adapted BMAD Note
|
|
@@ -32,4 +33,3 @@ The formal BMAD workflow expects a project-local `_bmad` runtime, PRD, and
|
|
|
32
33
|
step-by-step user confirmations. This repo does not currently contain `_bmad`.
|
|
33
34
|
These artifacts preserve the BMAD intent and structure while using the live
|
|
34
35
|
architecture review and advanced elicitation decisions as input.
|
|
35
|
-
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# n8n Service Hub
|
|
2
|
+
|
|
3
|
+
n8n should become the visible service hub for fleet operations while systemd
|
|
4
|
+
remains the local survival layer.
|
|
5
|
+
|
|
6
|
+
## Shape
|
|
7
|
+
|
|
8
|
+
- systemd owns process supervision on each host.
|
|
9
|
+
- n8n owns visibility, operator controls, scheduling, and multi-service flows.
|
|
10
|
+
- pjangler owns deterministic generation and reconciliation of both sides.
|
|
11
|
+
|
|
12
|
+
## Service Model
|
|
13
|
+
|
|
14
|
+
Each installed Hermes agent service is represented as an n8n node/workflow item:
|
|
15
|
+
|
|
16
|
+
- gateway service
|
|
17
|
+
- Bloodbank gateway or transitional consumer
|
|
18
|
+
- heartbeat timer/service
|
|
19
|
+
- runtime checkpoint status
|
|
20
|
+
- ticket-provider health check
|
|
21
|
+
|
|
22
|
+
The node does not replace systemd. It calls stable commands such as:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
systemctl --user status hermes-<agent>-heartbeat.timer
|
|
26
|
+
systemctl --user restart hermes-<agent>-gateway.service
|
|
27
|
+
pj fleet status --agent <agent_id> --json
|
|
28
|
+
pj fleet reconcile --agent <agent_id> --apply
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Heartbeat v2
|
|
32
|
+
|
|
33
|
+
Checkpoint era means the only recurring task was an hourly runtime
|
|
34
|
+
commit/push. It preserved state but did not manage work.
|
|
35
|
+
|
|
36
|
+
Heartbeat v2 is a frequent, cheap tick:
|
|
37
|
+
|
|
38
|
+
- always performs a gated runtime checkpoint
|
|
39
|
+
- optionally performs autonomous board reconciliation when
|
|
40
|
+
`.project.json automation.reconcile.enabled` is true
|
|
41
|
+
- records status in runtime state files for fleet status readers
|
|
42
|
+
- can be triggered by systemd locally or by n8n centrally
|
|
43
|
+
|
|
44
|
+
## First Implementation Pass
|
|
45
|
+
|
|
46
|
+
1. Extend `pj fleet status --json` to include service unit state, last heartbeat,
|
|
47
|
+
last checkpoint, and ticket-provider resolution.
|
|
48
|
+
2. Generate an n8n workflow from `~/.hermes/agents-registry.yaml`.
|
|
49
|
+
3. Add n8n actions for status, restart, reconcile dry-run, reconcile apply, and
|
|
50
|
+
log tail.
|
|
51
|
+
4. Keep systemd timers installed and enabled as fallback.
|
|
52
|
+
5. Add drift detection when n8n and systemd disagree about an agent.
|
|
53
|
+
|
|
54
|
+
## Open Questions
|
|
55
|
+
|
|
56
|
+
- Whether n8n should invoke local commands through SSH, an agent-side webhook,
|
|
57
|
+
or a small host-local control API.
|
|
58
|
+
- How much service history should live in n8n versus Hermes runtime state.
|
|
59
|
+
- Whether Bloodbank gateway events should also drive n8n workflows for visible
|
|
60
|
+
operator traces.
|
|
@@ -190,7 +190,7 @@ hermes profile delete ${AGENT}
|
|
|
190
190
|
# 3. Archive the ticket board if the provider supports archival
|
|
191
191
|
PROJECT_ID=$(python3 -c "import yaml,pathlib; print(yaml.safe_load(pathlib.Path.home().joinpath('.hermes/agents-registry.yaml').read_text())['agents']['${AGENT}']['plane']['project_id'])")
|
|
192
192
|
curl -X POST "https://plane.delo.sh/api/v1/workspaces/33god/projects/${PROJECT_ID}/archive/" \
|
|
193
|
-
-H "X-API-Key: ${PLANE_33GOD_API_KEY}"
|
|
193
|
+
-H "X-API-Key: ${PLANE_API_KEY:-${PLANE_33GOD_API_KEY}}"
|
|
194
194
|
|
|
195
195
|
# 4. BotFather: /deletebot @<repo>_<role>_bot
|
|
196
196
|
# 5. Archive runtime repo (GitHub UI; we don't have delete_repo scope by default)
|
|
@@ -52,7 +52,7 @@ For a local, single-machine install (no GitHub runtime repo, no Telegram, no
|
|
|
52
52
|
NATS), use the one-command bootstrap. From inside the target project:
|
|
53
53
|
|
|
54
54
|
```bash
|
|
55
|
-
export
|
|
55
|
+
export PLANE_33GOD_API_KEY=<key> # or PLANE_API_KEY / TRELLO_KEY + TRELLO_TOKEN
|
|
56
56
|
curl -fsSL https://raw.githubusercontent.com/delorenj/hermes-agent-template/main/install-local.sh | sh
|
|
57
57
|
```
|
|
58
58
|
|
|
@@ -77,7 +77,7 @@ All paths are relative to the repository root.
|
|
|
77
77
|
| Path | What it is |
|
|
78
78
|
| --- | --- |
|
|
79
79
|
| `copier.yml` | Questions (`role`, `ticket_provider`) and the `_tasks` provisioning chain. |
|
|
80
|
-
| `template/role.yaml.jinja` | The rendered role manifest
|
|
80
|
+
| `template/role.yaml.jinja` | The rendered role identity/personality manifest. Repo-owned board binding and reconciliation knobs live in repo-root `.project.json`. |
|
|
81
81
|
| `template/.scripts/lib/ticket-provider.sh` | The adapter dispatcher (`tp`). The engine's only seam to a ticket system. |
|
|
82
82
|
| `template/.scripts/providers/{plane,trello}.sh` | The provider adapters. |
|
|
83
83
|
| `template/.scripts/42-ticket-provider.sh` | Provisioning step that resolves or creates the board. |
|
|
@@ -112,7 +112,7 @@ repository root instead of driving Copier by hand. From inside the target
|
|
|
112
112
|
project:
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
|
-
export
|
|
115
|
+
export PLANE_33GOD_API_KEY=<key> # or PLANE_API_KEY / TRELLO_KEY + TRELLO_TOKEN
|
|
116
116
|
curl -fsSL https://raw.githubusercontent.com/delorenj/hermes-agent-template/main/install-local.sh | sh
|
|
117
117
|
```
|
|
118
118
|
|
|
@@ -185,8 +185,8 @@ SKIP_TELEGRAM=1 SKIP_RUNTIME_REPO=1 SKIP_PLANE=1 SKIP_BLOODBANK=1 \
|
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
After provisioning, set the board binding in
|
|
188
|
-
|
|
189
|
-
board id; for Plane, set `ticket_provider.workspace` and `ticket_provider.
|
|
188
|
+
repo-root `.project.json`. For Trello, set `ticket_provider.board_id` to the
|
|
189
|
+
board id; for Plane, set `ticket_provider.workspace` and `ticket_provider.board_id`.
|
|
190
190
|
Make the provider key available to the heartbeat's environment: on Linux through
|
|
191
191
|
a `systemd` `EnvironmentFile` (for example
|
|
192
192
|
`~/.hermes/<agent_id>.env`); on macOS the `launchd` agent sources that same
|
|
@@ -63,8 +63,9 @@ Set credentials in the environment and the board binding in `role.yaml`.
|
|
|
63
63
|
<details>
|
|
64
64
|
<summary>Plane</summary>
|
|
65
65
|
|
|
66
|
-
- Credentials: `PLANE_API_KEY
|
|
67
|
-
`
|
|
66
|
+
- Credentials: `PLANE_API_KEY` or workspace-specific
|
|
67
|
+
`PLANE_<WORKSPACE>_API_KEY` from the shared fleet env. Endpoint:
|
|
68
|
+
`PLANE_BASE` (default `https://plane.delo.sh`).
|
|
68
69
|
- Binding:
|
|
69
70
|
|
|
70
71
|
```yaml
|
|
@@ -139,7 +140,7 @@ Before you touch a live board, confirm the adapter parses and fails cleanly.
|
|
|
139
140
|
exit, with no Python traceback:
|
|
140
141
|
|
|
141
142
|
```bash
|
|
142
|
-
( unset PLANE_API_KEY; sh template/.scripts/providers/plane.sh resolve )
|
|
143
|
+
( unset PLANE_API_KEY PLANE_33GOD_API_KEY; sh template/.scripts/providers/plane.sh resolve )
|
|
143
144
|
```
|
|
144
145
|
|
|
145
146
|
## Verifying an adapter against a live board
|
|
@@ -150,8 +151,8 @@ never closes or modifies a ticket, so it's safe to run.
|
|
|
150
151
|
|
|
151
152
|
```bash
|
|
152
153
|
# 1. Load the credential into the environment only (never print it).
|
|
153
|
-
export PLANE_API_KEY="$(grep -E '^(export )?
|
|
154
|
-
~/.hermes/<agent>.env | head -1 | sed -E 's/^(export )?
|
|
154
|
+
export PLANE_API_KEY="$(grep -E '^(export )?PLANE(_[A-Z0-9_]+)?_API_KEY=' \
|
|
155
|
+
~/.hermes/<agent>.env | head -1 | sed -E 's/^(export )?PLANE(_[A-Z0-9_]+)?_API_KEY=//; s/^"//; s/"$//')"
|
|
155
156
|
|
|
156
157
|
# 2. Stage a role directory bound to the real board.
|
|
157
158
|
T=$(mktemp -d); RD="$T/agents/hermes/pm"
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
# HAT_PROVIDER=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
|
|
21
|
+
# Provider creds: PLANE_API_KEY or PLANE_<WORKSPACE>_API_KEY + PLANE_BASE
|
|
22
|
+
# | TRELLO_KEY+TRELLO_TOKEN
|
|
22
23
|
set -eu
|
|
23
24
|
|
|
24
25
|
say() { printf '\033[36m%s\033[0m\n' "$*"; }
|
|
@@ -113,9 +114,13 @@ TP_BOARD="${HAT_TRELLO_BOARD:-}"
|
|
|
113
114
|
case "$PROVIDER" in
|
|
114
115
|
plane)
|
|
115
116
|
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}"
|
|
119
124
|
say " Plane projects in '$TP_WORKSPACE':"
|
|
120
125
|
_pj="$(mktemp)"
|
|
121
126
|
curl -fsS "$PLANE_BASE/api/v1/workspaces/$TP_WORKSPACE/projects/?per_page=100" \
|
|
@@ -133,7 +138,7 @@ PY
|
|
|
133
138
|
rm -f "$_pj"
|
|
134
139
|
ask TP_PROJECT "Plane project UUID to manage" ""
|
|
135
140
|
[ -n "$TP_PROJECT" ] || die "project UUID is required"
|
|
136
|
-
CRED_LINES="
|
|
141
|
+
CRED_LINES="$TP_WORKSPACE_KEY=$PLANE_API_KEY
|
|
137
142
|
PLANE_BASE=$PLANE_BASE"
|
|
138
143
|
;;
|
|
139
144
|
trello)
|
|
@@ -159,12 +164,13 @@ fi
|
|
|
159
164
|
export SKIP_TELEGRAM=1 SKIP_EMAIL=1 SKIP_RUNTIME_REPO=1 SKIP_PLANE=1 \
|
|
160
165
|
SKIP_BLOODBANK=1 SKIP_SYSTEMD=1
|
|
161
166
|
ROLES="${HAT_ROLES:-pm}"
|
|
167
|
+
SCRUB_PLANE_KEY="${TP_WORKSPACE_KEY:-PLANE_DEFAULT_API_KEY}"
|
|
162
168
|
for ROLE in $ROLES; do
|
|
163
169
|
say "5. provisioning role: $ROLE"
|
|
164
170
|
DEST="$PROJECT_DIR/agents/hermes/$ROLE"
|
|
165
171
|
# Scrub provider creds from copier's environment so 42-ticket-provider skips
|
|
166
172
|
# board CREATION; we bind to the existing board in step 6 instead.
|
|
167
|
-
run "env -u PLANE_API_KEY -u
|
|
173
|
+
run "env -u PLANE_API_KEY -u '$SCRUB_PLANE_KEY' \
|
|
168
174
|
-u TRELLO_KEY -u TRELLO_TOKEN \
|
|
169
175
|
copier copy '$TEMPLATE_SRC' '$DEST' --trust --defaults --overwrite \
|
|
170
176
|
--data target_repo='$REPO' --data role='$ROLE' --data ticket_provider='$PROVIDER'"
|
|
@@ -6,9 +6,11 @@ Subscribes to:
|
|
|
6
6
|
bloodbank.evt.v1.repo.{{repo}}.>
|
|
7
7
|
bloodbank.cmd.v1.agent.{{agent_id}}.>
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
$HERMES_HOME/bloodbank-inbox/<timestamp>.json
|
|
11
|
-
the
|
|
9
|
+
This scaffold is a transitional receiver: it proves NATS subscription and keeps
|
|
10
|
+
a durable audit copy at $HERMES_HOME/bloodbank-inbox/<timestamp>.json. It is
|
|
11
|
+
not the final command path. Bloodbank command subjects should become a Hermes
|
|
12
|
+
gateway/transport so each consumed event enters the same turn queue as Telegram
|
|
13
|
+
or web input, with backpressure handled by the queue instead of by a file drain.
|
|
12
14
|
|
|
13
15
|
Generated by hermes-agent-template/runtime-scaffold/bloodbank-consumer.py and
|
|
14
16
|
rendered with role-specific values at provision time. Re-render by re-running
|
|
@@ -42,7 +42,11 @@ 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
|
-
|
|
45
|
+
if isinstance(cur, list):
|
|
46
|
+
cur = " ".join(str(x) for x in cur)
|
|
47
|
+
else:
|
|
48
|
+
cur = str(cur)
|
|
49
|
+
print(os.path.expanduser(cur), end="")
|
|
46
50
|
PYEOF
|
|
47
51
|
return 0
|
|
48
52
|
}
|
|
@@ -56,6 +60,7 @@ REGISTRY_FILE="${HERMES_FLEET_REGISTRY_FILE:-$(cfg fleet.registry_file "$HOME/.h
|
|
|
56
60
|
PROFILES_DIR="${HERMES_FLEET_HOME:-$HOME/.hermes}/profiles"
|
|
57
61
|
VOXXY_PLUGIN_DIR="${VOXXY_PLUGIN_DIR:-$(cfg fleet.voxxy_plugin_dir "$HOME/code/voxxy/plugins/tts/voxxy")}"
|
|
58
62
|
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")}"
|
|
59
64
|
|
|
60
65
|
APPLY=0
|
|
61
66
|
RESTART=1
|
|
@@ -136,6 +141,42 @@ path.write_text("\n".join(lines) + "\n")
|
|
|
136
141
|
PYEOF
|
|
137
142
|
}
|
|
138
143
|
|
|
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
|
+
|
|
139
180
|
wanted_agent() {
|
|
140
181
|
[[ ${#ONLY_AGENTS[@]} -eq 0 ]] && return 0
|
|
141
182
|
local a
|
|
@@ -238,6 +279,28 @@ while IFS=$'\t' read -r agent_id role_dir profile_name gateway_unit consumer_uni
|
|
|
238
279
|
# 5. PM fleet voice contract: runtime plugin symlink + Voxxy config + VOX_URL.
|
|
239
280
|
role_name="$(basename "$role_dir")"
|
|
240
281
|
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
|
+
|
|
241
304
|
plugin_link="$runtime/plugins/tts/voxxy"
|
|
242
305
|
if [[ -d "$VOXXY_PLUGIN_DIR" ]]; then
|
|
243
306
|
if [[ -L "$plugin_link" ]]; then
|
|
@@ -26,6 +26,7 @@ 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"]
|
|
29
30
|
|
|
30
31
|
[github]
|
|
31
32
|
runtime_repo_owner = "delorenj"
|
|
@@ -47,6 +47,15 @@ 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
|
|
50
59
|
chmod 600 "$FLEET_ENV"
|
|
51
60
|
|
|
52
61
|
mark_done 05-fleet-env
|
|
@@ -110,10 +110,29 @@ 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 [[
|
|
114
|
-
|
|
115
|
-
|
|
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
|
|
116
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
|
+
if [[ -d "$CANONICAL_SKILLS_DIR" ]]; then
|
|
117
136
|
# Ensure key PM/local-ops skills are symlinked into runtime/profile skills root.
|
|
118
137
|
# This preserves canonical ownership and keeps updates instant across agents.
|
|
119
138
|
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,6 +144,28 @@ 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
|
+
|
|
147
169
|
if [[ "$ROLE" == "pm" ]]; then
|
|
148
170
|
VOXXY_PLUGIN_DIR="${VOXXY_PLUGIN_DIR:-$(config_get fleet.voxxy_plugin_dir "$HOME/code/voxxy/plugins/tts/voxxy")}"
|
|
149
171
|
if [[ -d "$VOXXY_PLUGIN_DIR" ]]; then
|