@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.
Files changed (32) hide show
  1. package/dist/index.js +498 -204
  2. package/dist/mcp-server.js +488 -194
  3. package/package.json +1 -1
  4. package/templates/commonproject/copier.yml +4 -1
  5. package/templates/commonproject/template/.project.json.jinja +9 -12
  6. package/templates/commonproject/template/mise.toml.jinja +1 -1
  7. package/templates/hermes-agent/README.md +1 -1
  8. package/templates/hermes-agent/config.example.toml +66 -0
  9. package/templates/hermes-agent/docs/architecture.md +5 -0
  10. package/templates/hermes-agent/docs/bloodbank-gateway.md +57 -0
  11. package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
  12. package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +60 -0
  13. package/templates/hermes-agent/docs/operations.md +1 -1
  14. package/templates/hermes-agent/docs/sentinel/README.md +2 -2
  15. package/templates/hermes-agent/docs/sentinel/development.md +3 -3
  16. package/templates/hermes-agent/docs/sentinel/providers.md +6 -5
  17. package/templates/hermes-agent/install-local.sh +10 -4
  18. package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +5 -3
  19. package/templates/hermes-agent/scripts/fleet-sync.sh +64 -1
  20. package/templates/hermes-agent/template/.gitignore.jinja +2 -0
  21. package/templates/hermes-agent/template/.scripts/01-config.sh +1 -0
  22. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +9 -0
  23. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +22 -3
  24. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +22 -0
  25. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +18 -33
  26. package/templates/hermes-agent/template/.scripts/70-systemd.sh +8 -1
  27. package/templates/hermes-agent/template/.scripts/_lib.sh +61 -3
  28. package/templates/hermes-agent/template/.scripts/config.example.toml +5 -0
  29. package/templates/hermes-agent/template/.scripts/heartbeat.sh +66 -22
  30. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +3 -3
  31. package/templates/hermes-agent/template/.scripts/providers/plane.sh +29 -9
  32. package/templates/hermes-agent/template/role.yaml.jinja +2 -27
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@delorenj/pjangler",
3
- "version": "1.2.9",
3
+ "version": "1.2.13",
4
4
  "description": "Project subsystem bootstrapper CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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
- "board_url": {{ resolved_board_url | tojson }}
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` | Skills mirrored into each profile |
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 PLANE_API_KEY=<key> # or TRELLO_KEY + TRELLO_TOKEN
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, including the `ticket_provider` binding and the `reconcile` knobs (`grace_hours`, `auto_review`). |
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 PLANE_API_KEY=<key> # or TRELLO_KEY + TRELLO_TOKEN
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
- `agents/hermes/pm/role.yaml`. For Trello, set `ticket_provider.board` to the
189
- board id; for Plane, set `ticket_provider.workspace` and `ticket_provider.project`.
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`. Endpoint: `PLANE_BASE` (default
67
- `https://plane.delo.sh`).
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 )?PLANE_API_KEY=' \
154
- ~/.hermes/<agent>.env | head -1 | sed -E 's/^(export )?PLANE_API_KEY=//; s/^"//; s/"$//')"
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+PLANE_BASE | TRELLO_KEY+TRELLO_TOKEN
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="PLANE_API_KEY=$PLANE_API_KEY
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 PLANE_33GOD_API_KEY \
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
- For each event/command received, writes it to the agent's notification queue at
10
- $HERMES_HOME/bloodbank-inbox/<timestamp>.json so the gateway can ingest it on
11
- the next tick. Reconnects on broker churn; idempotent across restarts.
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
- print(os.path.expanduser(str(cur)), end="")
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
@@ -4,3 +4,5 @@
4
4
  # We just ignore the provisioning log so it doesn't end up in the project repo.
5
5
  .scripts/.provision.log
6
6
  .scripts/.last-run
7
+ runtime/
8
+
@@ -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 [[ -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"
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