@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.
Files changed (57) hide show
  1. package/dist/index.js +884 -295
  2. package/dist/mcp-server.js +892 -303
  3. package/package.json +8 -2
  4. package/templates/commonproject/AGENTS.md +3 -3
  5. package/templates/commonproject/README.md +11 -12
  6. package/templates/commonproject/copier.yml +11 -31
  7. package/templates/commonproject/template/.agents/hooks/README.md +13 -26
  8. package/templates/commonproject/template/.agents/hooks/lib/local-config.sh +4 -14
  9. package/templates/commonproject/template/.agents/hooks/sync.py +5 -6
  10. package/templates/commonproject/template/.agents/local.example.json +2 -8
  11. package/templates/commonproject/template/.agents/skills.json +6 -0
  12. package/templates/commonproject/template/.project.json.jinja +9 -13
  13. package/templates/commonproject/template/mise.toml.jinja +9 -16
  14. package/templates/hermes-agent/README.md +9 -9
  15. package/templates/hermes-agent/config.example.toml +1 -66
  16. package/templates/hermes-agent/copier.yml +4 -3
  17. package/templates/hermes-agent/docs/architecture.md +9 -12
  18. package/templates/hermes-agent/docs/fleet-control-plane/README.md +1 -1
  19. package/templates/hermes-agent/docs/operations.md +6 -5
  20. package/templates/hermes-agent/docs/sentinel/README.md +9 -7
  21. package/templates/hermes-agent/docs/sentinel/architecture.md +1 -2
  22. package/templates/hermes-agent/docs/sentinel/development.md +12 -13
  23. package/templates/hermes-agent/docs/sentinel/providers.md +34 -15
  24. package/templates/hermes-agent/install-local.sh +15 -14
  25. package/templates/hermes-agent/runtime-scaffold/README.md +1 -1
  26. package/templates/hermes-agent/runtime-scaffold/bloodbank-consumer.py +46 -14
  27. package/templates/hermes-agent/runtime-scaffold/memories/MEMORY.md +2 -2
  28. package/templates/hermes-agent/scripts/fleet-sync.sh +1 -64
  29. package/templates/hermes-agent/template/.gitignore.jinja +0 -2
  30. package/templates/hermes-agent/template/.runtime-scaffold/README.md +1 -1
  31. package/templates/hermes-agent/template/.runtime-scaffold/bloodbank-consumer.py +43 -9
  32. package/templates/hermes-agent/template/.runtime-scaffold/memories/MEMORY.md +2 -2
  33. package/templates/hermes-agent/template/.scripts/01-config.sh +0 -1
  34. package/templates/hermes-agent/template/.scripts/05-fleet-env.sh +0 -9
  35. package/templates/hermes-agent/template/.scripts/10-hermes-profile.sh +3 -22
  36. package/templates/hermes-agent/template/.scripts/20-runtime-repo.sh +0 -22
  37. package/templates/hermes-agent/template/.scripts/40-plane.sh +51 -0
  38. package/templates/hermes-agent/template/.scripts/42-ticket-provider.sh +59 -21
  39. package/templates/hermes-agent/template/.scripts/60-bloodbank.sh +2 -1
  40. package/templates/hermes-agent/template/.scripts/70-systemd.sh +1 -10
  41. package/templates/hermes-agent/template/.scripts/_lib.sh +3 -61
  42. package/templates/hermes-agent/template/.scripts/config.example.toml +0 -5
  43. package/templates/hermes-agent/template/.scripts/heartbeat.sh +33 -66
  44. package/templates/hermes-agent/template/.scripts/lib/ticket-provider.sh +5 -9
  45. package/templates/hermes-agent/template/.scripts/momo-wip-lock.py +137 -0
  46. package/templates/hermes-agent/template/.scripts/providers/linear.sh +176 -0
  47. package/templates/hermes-agent/template/.scripts/providers/plane.sh +9 -29
  48. package/templates/hermes-agent/template/.scripts/sentinel/docs/autonomous-delegated-review.md +2 -2
  49. package/templates/hermes-agent/template/.scripts/sentinel/docs/continuous-ticket-orchestration.md +1 -33
  50. package/templates/hermes-agent/template/.scripts/sentinel.prompt.md.jinja +22 -27
  51. package/templates/hermes-agent/template/SOUL.md.jinja +49 -30
  52. package/templates/hermes-agent/template/role.yaml.jinja +35 -4
  53. package/templates/hermes-agent/tests/test_bloodbank_consumer_contract.py +138 -0
  54. package/templates/commonproject/template/.mise/scripts/link-project-skills-to-clis.sh +0 -110
  55. package/templates/commonproject/template/.mise/scripts/unlink-project-skills-from-clis.sh +0 -45
  56. package/templates/hermes-agent/docs/bloodbank-gateway.md +0 -57
  57. package/templates/hermes-agent/docs/fleet-control-plane/n8n-service-hub.md +0 -60
@@ -92,23 +92,20 @@ happen to share a token, the second one's startup fails fast. The N×M cost
92
92
  (N BotFather sessions per fleet) is the price we accept for zero custom
93
93
  routing code.
94
94
 
95
- ## One Ticket Board Per Repo
95
+ ## One Plane project per agent
96
96
 
97
- The configured ticket board is the natural unit of work isolation. The PM owns
98
- one board per repo and routes worker agents through that shared source of truth,
99
- so decisions, filters, and review state stay coherent.
97
+ A Plane "project" is the natural unit of work isolation. Mixing agents into a
98
+ shared project would conflate decisions and break filters. 1:1 also makes
99
+ archive-on-retire clean.
100
100
 
101
101
  ## Bloodbank wiring
102
102
 
103
103
  Each consumer subscribes to two lanes:
104
- - `bloodbank.evt.v1.repo.<repo>.>` — events affecting this repo
105
- - `bloodbank.cmd.v1.agent.<agent_id>.>` — commands targeting THIS agent specifically
104
+ - `bloodbank.evt.v1.repo.>` canonical repo-domain events, filtered by `data.repo`
105
+ - `bloodbank.cmd.v1.agent.>`canonical agent-domain commands, filtered by `data.target_agent_id`
106
106
 
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
- 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).
109
+ contract is owned by Bloodbank (`~/code/33GOD/bloodbank/docs/event-naming.md`).
110
+ Repo and agent identifiers belong in envelope data, actor, or source fields,
111
+ never in type or subject tokens.
@@ -24,7 +24,6 @@ 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.
28
27
  - [implementation-readiness-report-2026-06-27.md](./implementation-readiness-report-2026-06-27.md) - Readiness assessment and launch gates.
29
28
 
30
29
  ## Adapted BMAD Note
@@ -33,3 +32,4 @@ The formal BMAD workflow expects a project-local `_bmad` runtime, PRD, and
33
32
  step-by-step user confirmations. This repo does not currently contain `_bmad`.
34
33
  These artifacts preserve the BMAD intent and structure while using the live
35
34
  architecture review and advanced elicitation decisions as input.
35
+
@@ -29,7 +29,7 @@ invocations).
29
29
  | 10 hermes profile | `hermes profile create <repo>-<role> --clone --no-alias` + mirror skills/plugins/hooks from default + symlink canonical runtime skills (`delonet-conventions`, `delonet-dotenv`, `hermes-pm-template-maintenance`, `hindsight`, `subagent-driven-development`) from `/home/delorenj/.agents/skills`; PM roles also seed `VOX_URL` in profile `.env` | n/a |
30
30
  | 20 runtime repo | Create gh:delorenj/agent-hm-<repo>-<role> (private), push scaffold from role-local `.runtime-scaffold/`, submodule-add into ./runtime/, symlink ~/.hermes/profiles/<id> → runtime; PM roles also link the Voxxy plugin and set `tts.provider: voxxy` | `SKIP_RUNTIME_REPO=1` |
31
31
  | 30 telegram | Capture BotFather token, write to runtime/.env, enable hermes-telegram toolset | `SKIP_TELEGRAM=1` |
32
- | 42 ticket provider | Create or bind the configured ticket board, patch identifier into role.yaml | `SKIP_PLANE=1` |
32
+ | 40 plane | Create Plane project in 33god workspace (1:1 with agent), patch identifier into role.yaml | `SKIP_PLANE=1` |
33
33
  | 60 bloodbank | Install consumer (renders from scaffold w/ agent values), health-check NATS, install nats-py via uv if missing | `SKIP_BLOODBANK=1` |
34
34
  | 70 systemd | Install user units: gateway, consumer, heartbeat timer (board-reconciliation sentinel pass + gated runtime checkpoint, one tick) | `SKIP_SYSTEMD=1` |
35
35
  | 80 registry | Append entry to ~/.hermes/agents-registry.yaml | n/a |
@@ -118,7 +118,7 @@ systemctl --user start hermes-${AGENT}-gateway.service
118
118
  | --- | --- |
119
119
  | Telegram | DM `@<repo>_<role>_bot` (once Telegram is wired) |
120
120
  | Local CLI | `./agents/hermes/<role>/hermes chat "..."` |
121
- | Bloodbank | Publish to subject `bloodbank.cmd.v1.agent.<agent_id>.<verb>.requested` |
121
+ | Bloodbank | Publish to `bloodbank.cmd.v1.agent.task.assign` with `data.target_agent_id = <agent_id>` |
122
122
 
123
123
  ## Inspect fleet state
124
124
 
@@ -187,10 +187,10 @@ systemctl --user disable --now hermes-${AGENT}-heartbeat.timer
187
187
  # that's what you want!)
188
188
  hermes profile delete ${AGENT}
189
189
 
190
- # 3. Archive the ticket board if the provider supports archival
190
+ # 3. Archive Plane project (Plane UI or API)
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_API_KEY:-${PLANE_33GOD_API_KEY}}"
193
+ -H "X-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)
@@ -219,7 +219,8 @@ rm -rf agents/hermes/<role>
219
219
  ### Consumer not seeing events
220
220
  - Verify NATS is up: `docker compose -f ~/code/33GOD/bloodbank/compose/docker-compose.yml ps`
221
221
  - Tail consumer: `journalctl --user -fu hermes-<agent>-consumer.service`
222
- - Make sure something is actually publishing to `bloodbank.evt.v1.repo.<repo>.*`
222
+ - Make sure something is publishing canonical repo events such as
223
+ `bloodbank.evt.v1.repo.issue.updated` with `data.repo = <repo>`
223
224
 
224
225
  ### Heartbeat not checkpointing (runtime not pushing)
225
226
  The checkpoint runs inside the heartbeat tick (after the board-reconciliation
@@ -26,7 +26,7 @@ that does the board-reconciliation pass and then a gated runtime checkpoint
26
26
  `.scripts/sentinel/`.
27
27
 
28
28
  It talks to the ticket board through a pluggable adapter, so the same engine
29
- runs on Plane or Trello. The engine lives once in this template (the
29
+ runs on Linear, Plane, or Trello. The engine lives once in this template (the
30
30
  single source of truth) and propagates to every deployment with
31
31
  `copier update`.
32
32
 
@@ -37,6 +37,8 @@ The following is true as of June 1, 2026.
37
37
  - The engine, the provider abstraction, and the autonomous adversarial-review
38
38
  enforcement are built, syntax-clean, and validated offline with a mock
39
39
  provider.
40
+ - The **Linear** adapter is verified live against a real board (a real
41
+ `DEL` team: `resolve`, `list_issues` returning 93 issues, and `get_issue`).
40
42
  - The **Plane** adapter is verified live against a real workspace
41
43
  (`resolve`, `list_issues`, `get_issue`, `comment`, and a
42
44
  `transition` to `completed` on a disposable issue).
@@ -52,7 +54,7 @@ For a local, single-machine install (no GitHub runtime repo, no Telegram, no
52
54
  NATS), use the one-command bootstrap. From inside the target project:
53
55
 
54
56
  ```bash
55
- export PLANE_33GOD_API_KEY=<key> # or PLANE_API_KEY / TRELLO_KEY + TRELLO_TOKEN
57
+ export PLANE_API_KEY=<key> # or LINEAR_API_KEY / TRELLO_KEY + TRELLO_TOKEN
56
58
  curl -fsSL https://raw.githubusercontent.com/delorenj/hermes-agent-template/main/install-local.sh | sh
57
59
  ```
58
60
 
@@ -64,7 +66,7 @@ macOS). See [Development guide: local install](development.md#local-install-one-
64
66
  <!-- prettier-ignore -->
65
67
  > [!IMPORTANT]
66
68
  > Full (non-local) provisioning is outward-facing. It can create a GitHub
67
- > runtime repo, a Telegram bot, and a ticket board, and the Telegram step is
69
+ > runtime repo, a Telegram bot, and a Plane project, and the Telegram step is
68
70
  > interactive. Use `install-local.sh` or the `SKIP_*` flags described in
69
71
  > [Development guide:
70
72
  > provisioning](development.md#provisioning-the-pm-manual) for local or lean
@@ -77,9 +79,9 @@ All paths are relative to the repository root.
77
79
  | Path | What it is |
78
80
  | --- | --- |
79
81
  | `copier.yml` | Questions (`role`, `ticket_provider`) and the `_tasks` provisioning chain. |
80
- | `template/role.yaml.jinja` | The rendered role identity/personality manifest. Repo-owned board binding and reconciliation knobs live in repo-root `.project.json`. |
82
+ | `template/role.yaml.jinja` | The rendered role manifest, including the `ticket_provider` binding and the `reconcile` knobs (`grace_hours`, `auto_review`). |
81
83
  | `template/.scripts/lib/ticket-provider.sh` | The adapter dispatcher (`tp`). The engine's only seam to a ticket system. |
82
- | `template/.scripts/providers/{plane,trello}.sh` | The provider adapters. |
84
+ | `template/.scripts/providers/{linear,plane,trello}.sh` | The provider adapters. |
83
85
  | `template/.scripts/42-ticket-provider.sh` | Provisioning step that resolves or creates the board. |
84
86
  | `template/.scripts/70-systemd.sh` | Provisioning step that installs the gateway, consumer, and the fused `heartbeat` timer (board-reconciliation sentinel pass + gated runtime checkpoint). |
85
87
  | `install-local.sh` | One-command local install (no cloud, macOS + Linux). |
@@ -107,8 +109,8 @@ the agent to read at run time. Keep the two in sync when behavior changes.
107
109
 
108
110
  The highest-value open work, in order:
109
111
 
110
- 1. Live-verify the **Trello** adapter against a real board (Plane is done).
111
- See [Providers: verifying an
112
+ 1. Live-verify the **Trello** adapter against a real board (Linear and Plane are
113
+ done). See [Providers: verifying an
112
114
  adapter](providers.md#verifying-an-adapter-against-a-live-board).
113
115
  2. Confirm `install-local.sh` on a real macOS machine. The Linux path and the
114
116
  Plane adapter are verified; the macOS `launchd` agent and `mkdir` lock get
@@ -71,8 +71,7 @@ function, `tp`, that dispatches to one provider implementation under
71
71
  through the `ticket_provider` question and recorded in `role.yaml` under
72
72
  `ticket_provider.name`.
73
73
 
74
- This is what lets one engine serve the supported ticket back ends: Plane and
75
- Trello.
74
+ This is what lets one engine serve three back ends: Trello, Plane, and Linear.
76
75
  For the full contract and the per-provider details, see
77
76
  [Providers](providers.md).
78
77
 
@@ -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_33GOD_API_KEY=<key> # or PLANE_API_KEY / TRELLO_KEY + TRELLO_TOKEN
115
+ export PLANE_API_KEY=<key> # or LINEAR_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
 
@@ -133,8 +133,8 @@ The script:
133
133
  7. smoke-tests the board connection through the adapter.
134
134
 
135
135
  Useful environment overrides (skip the prompts): `HAT_REPO`, `HAT_PROVIDER`,
136
- `HAT_ROLES`, `HAT_PLANE_WORKSPACE`, `HAT_PLANE_PROJECT`, `HAT_TRELLO_BOARD`,
137
- and `HAT_DRY_RUN=1` to preview without changing anything.
136
+ `HAT_ROLES`, `HAT_PLANE_WORKSPACE`, `HAT_PLANE_PROJECT`, `HAT_LINEAR_TEAM`,
137
+ `HAT_TRELLO_BOARD`, and `HAT_DRY_RUN=1` to preview without changing anything.
138
138
 
139
139
  This local path does not wire Telegram or email. Those are convenience layers in
140
140
  the `pjangler` provisioner, not requirements for a working agent — talk to the
@@ -151,7 +151,7 @@ cd /path/to/your-project
151
151
  copier copy gh:delorenj/hermes-agent-template ./agents/hermes/pm \
152
152
  --data role=pm \
153
153
  --data target_repo=<repo> \
154
- --data ticket_provider=<plane|trello>
154
+ --data ticket_provider=<linear|plane|trello>
155
155
  ```
156
156
 
157
157
  The `_tasks` chain in `copier.yml` runs the numbered provisioning scripts in
@@ -162,7 +162,7 @@ order. Several of them reach outside the repository.
162
162
  > Full provisioning is outward-facing and partly interactive.
163
163
  > `20-runtime-repo.sh` creates a private GitHub repo with `gh repo create`,
164
164
  > `30-telegram.sh` prompts for a BotFather token and blocks waiting for input,
165
- > and `42-ticket-provider.sh` can create a ticket board. Don't run full
165
+ > and `42-ticket-provider.sh` can create a Plane project. Don't run full
166
166
  > provisioning unattended.
167
167
 
168
168
  Use the `SKIP_*` environment flags for a local or lean install. Each numbered
@@ -172,7 +172,7 @@ script checks its flag and skips cleanly.
172
172
  | --- | --- |
173
173
  | `SKIP_TELEGRAM` | The interactive BotFather token step. |
174
174
  | `SKIP_RUNTIME_REPO` | Creating the GitHub runtime repo. |
175
- | `SKIP_PLANE` | Creating or linking the ticket board. |
175
+ | `SKIP_PLANE` | Creating a Plane project. |
176
176
  | `SKIP_BLOODBANK` | Installing the BloodBank consumer. |
177
177
  | `SKIP_SYSTEMD` | Installing `systemd` units (gateway, consumer, heartbeat timer). |
178
178
 
@@ -181,14 +181,13 @@ For example, a local install that creates no cloud resources:
181
181
  ```bash
182
182
  SKIP_TELEGRAM=1 SKIP_RUNTIME_REPO=1 SKIP_PLANE=1 SKIP_BLOODBANK=1 \
183
183
  copier copy gh:delorenj/hermes-agent-template ./agents/hermes/pm \
184
- --data role=pm --data target_repo=<repo> --data ticket_provider=trello
184
+ --data role=pm --data target_repo=<repo> --data ticket_provider=linear
185
185
  ```
186
186
 
187
187
  After provisioning, set the board binding in
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
- Make the provider key available to the heartbeat's environment: on Linux through
191
- a `systemd` `EnvironmentFile` (for example
188
+ `agents/hermes/pm/role.yaml`. For Linear, set `ticket_provider.team`
189
+ to the team key. Make the provider key available to the heartbeat's environment:
190
+ on Linux through a `systemd` `EnvironmentFile` (for example
192
191
  `~/.hermes/<agent_id>.env`); on macOS the `launchd` agent sources that same
193
192
  per-agent env file, so write the key there.
194
193
 
@@ -257,8 +256,8 @@ These cost real debugging time. Watch for them.
257
256
 
258
257
  The following work is open for the incoming agent, roughly in priority order.
259
258
 
260
- 1. **Live-verify the Trello adapter.** Plane is verified live
261
- (including `transition` and `comment`). Trello is implemented against
259
+ 1. **Live-verify the Trello adapter.** Linear and Plane are verified live
260
+ (Plane includes `transition` and `comment`). Trello is implemented against
262
261
  the contract but unverified. Follow [Providers: verifying an
263
262
  adapter](providers.md#verifying-an-adapter-against-a-live-board) with Trello
264
263
  credentials, and fix any endpoint or field mismatches.
@@ -1,7 +1,7 @@
1
1
  # Provider adapters
2
2
 
3
3
  This guide is the reference for the ticket-provider abstraction: the contract
4
- every adapter implements, the two adapters that ship today, and a
4
+ every adapter implements, the three adapters that ship today, and a
5
5
  step-by-step process for adding a new provider or verifying an existing one
6
6
  against a live board. Read [Architecture](architecture.md) first for how the
7
7
  adapter fits into the engine.
@@ -43,8 +43,12 @@ maps each to its back end's concrete state.
43
43
 
44
44
  ## The adapters that ship today
45
45
 
46
- The repository includes two supported adapters with different verification status.
46
+ The repository includes three adapters with different verification status.
47
47
 
48
+ - **Linear** (`providers/linear.sh`) is the reference implementation. It uses
49
+ the Linear GraphQL API. It's verified live against a real board. Linear's
50
+ `issue(id:)` field accepts both the UUID and the human identifier (for
51
+ example, `DEL-42`), so the engine can pass identifiers through.
48
52
  - **Plane** (`providers/plane.sh`) uses the Plane REST API with `X-API-Key`
49
53
  authentication. A Plane project maps to the board, a cycle maps to the
50
54
  milestone, and `state.group` maps to the state type. It's verified live. Note
@@ -60,12 +64,26 @@ The repository includes two supported adapters with different verification statu
60
64
 
61
65
  Set credentials in the environment and the board binding in `role.yaml`.
62
66
 
67
+ <details>
68
+ <summary>Linear</summary>
69
+
70
+ - Credentials: `LINEAR_API_KEY`.
71
+ - Binding:
72
+
73
+ ```yaml
74
+ ticket_provider:
75
+ name: linear
76
+ team: DEL # Linear team key
77
+ project: "" # optional, scopes milestone and issue queries
78
+ ```
79
+
80
+ </details>
81
+
63
82
  <details>
64
83
  <summary>Plane</summary>
65
84
 
66
- - Credentials: `PLANE_API_KEY` or workspace-specific
67
- `PLANE_<WORKSPACE>_API_KEY` from the shared fleet env. Endpoint:
68
- `PLANE_BASE` (default `https://plane.delo.sh`).
85
+ - Credentials: `PLANE_API_KEY`. Endpoint: `PLANE_BASE` (default
86
+ `https://plane.delo.sh`).
69
87
  - Binding:
70
88
 
71
89
  ```yaml
@@ -96,9 +114,8 @@ Set credentials in the environment and the board binding in `role.yaml`.
96
114
 
97
115
  To add a fourth provider, for example GitHub Issues or Jira, follow these steps.
98
116
 
99
- 1. Create `template/.scripts/providers/<name>.sh`. Start from `plane.sh` if it
100
- uses REST with a single API key, or `trello.sh` if it has a board/list/card
101
- model.
117
+ 1. Create `template/.scripts/providers/<name>.sh`. Start from `linear.sh` if the
118
+ back end uses GraphQL, or `plane.sh` if it uses REST with a single API key.
102
119
  2. Implement every operation in [the contract](#the-contract). Return the exact
103
120
  JSON shapes shown in the table. Map your back end's states to the five
104
121
  normalized states.
@@ -110,7 +127,8 @@ To add a fourth provider, for example GitHub Issues or Jira, follow these steps.
110
127
  5. Extend `template/role.yaml.jinja` with a provider-specific binding block
111
128
  under the `{% if ticket_provider == '<name>' %}` branch.
112
129
  6. Teach `42-ticket-provider.sh` how to resolve or create the board for your
113
- provider, if it differs from the existing `plane` and `trello` cases.
130
+ provider, if it differs from the existing `linear`, `plane`, and `trello`
131
+ cases.
114
132
  7. Validate and verify with the steps below.
115
133
 
116
134
  <!-- prettier-ignore -->
@@ -140,7 +158,7 @@ Before you touch a live board, confirm the adapter parses and fails cleanly.
140
158
  exit, with no Python traceback:
141
159
 
142
160
  ```bash
143
- ( unset PLANE_API_KEY PLANE_33GOD_API_KEY; sh template/.scripts/providers/plane.sh resolve )
161
+ ( unset LINEAR_API_KEY; sh template/.scripts/providers/linear.sh resolve )
144
162
  ```
145
163
 
146
164
  ## Verifying an adapter against a live board
@@ -151,15 +169,15 @@ never closes or modifies a ticket, so it's safe to run.
151
169
 
152
170
  ```bash
153
171
  # 1. Load the credential into the environment only (never print it).
154
- export 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/"$//')"
172
+ export LINEAR_API_KEY="$(grep -E '^(export )?LINEAR_API_KEY=' \
173
+ ~/.hermes/<agent>.env | head -1 | sed -E 's/^(export )?LINEAR_API_KEY=//; s/^"//; s/"$//')"
156
174
 
157
175
  # 2. Stage a role directory bound to the real board.
158
176
  T=$(mktemp -d); RD="$T/agents/hermes/pm"
159
177
  mkdir -p "$RD/.scripts/lib" "$RD/.scripts/providers"
160
178
  cp template/.scripts/lib/ticket-provider.sh "$RD/.scripts/lib/"
161
- cp template/.scripts/providers/plane.sh "$RD/.scripts/providers/"
162
- printf 'repo: demo\nrole: pm\nticket_provider:\n name: plane\n workspace: 33god\n project: <project-uuid>\n' \
179
+ cp template/.scripts/providers/linear.sh "$RD/.scripts/providers/"
180
+ printf 'repo: demo\nrole: pm\nticket_provider:\n name: linear\n team: DEL\n' \
163
181
  > "$RD/role.yaml"
164
182
 
165
183
  # 3. Run the read operations.
@@ -181,4 +199,5 @@ verification.
181
199
  ## Read next
182
200
 
183
201
  - [Development guide](development.md): the full edit, validate, and propagate
184
- workflow, plus the open roadmap, which includes live-verifying Trello.
202
+ workflow, plus the open roadmap, which includes live-verifying Plane and
203
+ Trello.
@@ -15,11 +15,10 @@
15
15
  #
16
16
  # Environment overrides (skip the prompts):
17
17
  # HAT_REPO=<name> project/repo name (default: basename of CWD)
18
- # HAT_PROVIDER=plane|trello (default: plane)
18
+ # HAT_PROVIDER=linear|plane|trello (default: plane)
19
19
  # HAT_ROLES="pm" roles to install (default: pm)
20
20
  # HAT_DRY_RUN=1 print actions, change nothing
21
- # Provider creds: PLANE_API_KEY or PLANE_<WORKSPACE>_API_KEY + PLANE_BASE
22
- # | TRELLO_KEY+TRELLO_TOKEN
21
+ # Provider creds: LINEAR_API_KEY | PLANE_API_KEY+PLANE_BASE | TRELLO_KEY+TRELLO_TOKEN
23
22
  set -eu
24
23
 
25
24
  say() { printf '\033[36m%s\033[0m\n' "$*"; }
@@ -101,7 +100,7 @@ TOML
101
100
  fi
102
101
 
103
102
  # --- 4. Provider credentials + board binding ---------------------------------
104
- ask HAT_PROVIDER "Ticket provider (plane|trello)" "plane"
103
+ ask HAT_PROVIDER "Ticket provider (linear|plane|trello)" "plane"
105
104
  PROVIDER="$HAT_PROVIDER"
106
105
  ask HAT_REPO "Project/repo name" "$(basename "$PROJECT_DIR" | tr '[:upper:]' '[:lower:]')"
107
106
  REPO="$HAT_REPO"
@@ -110,17 +109,13 @@ PM_ENV="$HOME/.hermes/${REPO}-pm.env"
110
109
  # These get written into the pm role.yaml binding after render.
111
110
  # Pre-seed from optional env knobs so the install can run non-interactively.
112
111
  TP_WORKSPACE="${HAT_PLANE_WORKSPACE:-}"; TP_PROJECT="${HAT_PLANE_PROJECT:-}"
113
- TP_BOARD="${HAT_TRELLO_BOARD:-}"
112
+ TP_TEAM="${HAT_LINEAR_TEAM:-}"; TP_BOARD="${HAT_TRELLO_BOARD:-}"
114
113
  case "$PROVIDER" in
115
114
  plane)
116
115
  ask PLANE_BASE "Plane base URL" "https://app.plane.so"
116
+ : "${PLANE_API_KEY:?Set PLANE_API_KEY in your environment, then re-run}"
117
117
  ask TP_WORKSPACE "Plane workspace slug" ""
118
118
  [ -n "$TP_WORKSPACE" ] || die "workspace is required"
119
- TP_WORKSPACE_KEY="$(printf '%s' "$TP_WORKSPACE" | tr '[:lower:]' '[:upper:]' | sed 's/[^A-Z0-9]/_/g')"
120
- TP_WORKSPACE_KEY="PLANE_${TP_WORKSPACE_KEY}_API_KEY"
121
- eval "TP_WORKSPACE_API_KEY=\${$TP_WORKSPACE_KEY:-}"
122
- PLANE_API_KEY="${PLANE_API_KEY:-$TP_WORKSPACE_API_KEY}"
123
- : "${PLANE_API_KEY:?Set PLANE_API_KEY or $TP_WORKSPACE_KEY in your environment, then re-run}"
124
119
  say " Plane projects in '$TP_WORKSPACE':"
125
120
  _pj="$(mktemp)"
126
121
  curl -fsS "$PLANE_BASE/api/v1/workspaces/$TP_WORKSPACE/projects/?per_page=100" \
@@ -138,9 +133,15 @@ PY
138
133
  rm -f "$_pj"
139
134
  ask TP_PROJECT "Plane project UUID to manage" ""
140
135
  [ -n "$TP_PROJECT" ] || die "project UUID is required"
141
- CRED_LINES="$TP_WORKSPACE_KEY=$PLANE_API_KEY
136
+ CRED_LINES="PLANE_API_KEY=$PLANE_API_KEY
142
137
  PLANE_BASE=$PLANE_BASE"
143
138
  ;;
139
+ linear)
140
+ : "${LINEAR_API_KEY:?Set LINEAR_API_KEY in your environment, then re-run}"
141
+ ask TP_TEAM "Linear team key (for example DEL)" ""
142
+ [ -n "$TP_TEAM" ] || die "team key is required"
143
+ CRED_LINES="LINEAR_API_KEY=$LINEAR_API_KEY"
144
+ ;;
144
145
  trello)
145
146
  : "${TRELLO_KEY:?Set TRELLO_KEY in your environment, then re-run}"
146
147
  : "${TRELLO_TOKEN:?Set TRELLO_TOKEN in your environment, then re-run}"
@@ -164,13 +165,12 @@ fi
164
165
  export SKIP_TELEGRAM=1 SKIP_EMAIL=1 SKIP_RUNTIME_REPO=1 SKIP_PLANE=1 \
165
166
  SKIP_BLOODBANK=1 SKIP_SYSTEMD=1
166
167
  ROLES="${HAT_ROLES:-pm}"
167
- SCRUB_PLANE_KEY="${TP_WORKSPACE_KEY:-PLANE_DEFAULT_API_KEY}"
168
168
  for ROLE in $ROLES; do
169
169
  say "5. provisioning role: $ROLE"
170
170
  DEST="$PROJECT_DIR/agents/hermes/$ROLE"
171
171
  # Scrub provider creds from copier's environment so 42-ticket-provider skips
172
172
  # board CREATION; we bind to the existing board in step 6 instead.
173
- run "env -u PLANE_API_KEY -u '$SCRUB_PLANE_KEY' \
173
+ run "env -u PLANE_API_KEY -u PLANE_33GOD_API_KEY -u LINEAR_API_KEY \
174
174
  -u TRELLO_KEY -u TRELLO_TOKEN \
175
175
  copier copy '$TEMPLATE_SRC' '$DEST' --trust --defaults --overwrite \
176
176
  --data target_repo='$REPO' --data role='$ROLE' --data ticket_provider='$PROVIDER'"
@@ -180,7 +180,7 @@ done
180
180
  PM_ROLE="$PROJECT_DIR/agents/hermes/pm/role.yaml"
181
181
  if [ -f "$PM_ROLE" ] && [ "${HAT_DRY_RUN:-0}" != "1" ]; then
182
182
  say "6. binding pm to your $PROVIDER board"
183
- TP_WORKSPACE="$TP_WORKSPACE" TP_PROJECT="$TP_PROJECT" \
183
+ TP_WORKSPACE="$TP_WORKSPACE" TP_PROJECT="$TP_PROJECT" TP_TEAM="$TP_TEAM" \
184
184
  TP_BOARD="$TP_BOARD" PROVIDER="$PROVIDER" python3 - "$PM_ROLE" <<'PY'
185
185
  import os, re, sys, pathlib
186
186
  p = pathlib.Path(sys.argv[1]); t = p.read_text()
@@ -190,6 +190,7 @@ def setleaf(text, key, val):
190
190
  return new if n else text
191
191
  t = setleaf(t, "workspace", os.environ.get("TP_WORKSPACE",""))
192
192
  t = setleaf(t, "project", os.environ.get("TP_PROJECT",""))
193
+ t = setleaf(t, "team", os.environ.get("TP_TEAM",""))
193
194
  t = setleaf(t, "board", os.environ.get("TP_BOARD",""))
194
195
  p.write_text(t)
195
196
  print(" bound:", {k:v for k,v in os.environ.items() if k.startswith("TP_") and v})
@@ -13,7 +13,7 @@ and recoverable on failure.
13
13
  | `memories/MEMORY.md` | yes | The condensed mental-model summary loaded each session |
14
14
  | `memories/USER.md` | yes | The operator's persona (Jarad DeLorenzo, ...) |
15
15
  | `sessions/sessions.db` | yes (LFS) | SQLite store of every conversation |
16
- | `bloodbank-consumer.py` | yes | NATS subscriber for repo-scoped events |
16
+ | `bloodbank-consumer.py` | yes | NATS subscriber using canonical subjects plus envelope routing data |
17
17
  | `decisions/` | yes | Agent-emitted decisions, one file per important call |
18
18
  | `.env` | **no** | API keys + Telegram bot token (per-machine secret) |
19
19
  | `auth.json` | **no** | Deprecated local OAuth store; fleet auth defaults to `HERMES_OAUTH_FILE=~/.hermes/auth.json` |
@@ -3,14 +3,16 @@
3
3
  Bloodbank consumer for hermes agent {{agent_id}}.
4
4
 
5
5
  Subscribes to:
6
- bloodbank.evt.v1.repo.{{repo}}.>
7
- bloodbank.cmd.v1.agent.{{agent_id}}.>
6
+ bloodbank.evt.v1.repo.>
7
+ bloodbank.cmd.v1.agent.>
8
8
 
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.
9
+ The Bloodbank subject identifies the fixed domain/entity/action route. Repo and
10
+ agent identifiers stay in envelope data, so this consumer filters repo events
11
+ by ``data.repo`` and agent commands by ``data.target_agent_id``.
12
+
13
+ For each event/command received, writes it to the agent's notification queue at
14
+ $HERMES_HOME/bloodbank-inbox/<timestamp>.json so the gateway can ingest it on
15
+ the next tick. Reconnects on broker churn; idempotent across restarts.
14
16
 
15
17
  Generated by hermes-agent-template/runtime-scaffold/bloodbank-consumer.py and
16
18
  rendered with role-specific values at provision time. Re-render by re-running
@@ -49,8 +51,8 @@ PRODUCER = f"hermes-agent:{AGENT_ID}"
49
51
  SOURCE = f"hermes://agent/{AGENT_ID}"
50
52
 
51
53
  SUBJECTS = [
52
- f"bloodbank.evt.v1.repo.{REPO}.>",
53
- f"bloodbank.cmd.v1.agent.{AGENT_ID}.>",
54
+ "bloodbank.evt.v1.repo.>",
55
+ "bloodbank.cmd.v1.agent.>",
54
56
  ]
55
57
 
56
58
  KIND_MARKERS = {"event": "evt", "command": "cmd", "reply": "rpy"}
@@ -62,8 +64,8 @@ def _now():
62
64
 
63
65
  def _subject_and_domain(ce_type, kind):
64
66
  parts = ce_type.split(".")
65
- if len(parts) != 5 or parts[0] != "bloodbank" or not parts[1].startswith("v"):
66
- raise ValueError(f"type {ce_type!r} must match bloodbank.vN.<domain>.<entity>.<action>")
67
+ if len(parts) != 5 or parts[0] != "bloodbank" or parts[1] != "v1":
68
+ raise ValueError(f"type {ce_type!r} must match bloodbank.v1.<domain>.<entity>.<action>")
67
69
  if kind not in KIND_MARKERS:
68
70
  raise ValueError(f"unknown envelope kind {kind!r}")
69
71
  _vendor, version, domain, entity, action = parts
@@ -95,6 +97,33 @@ def build_envelope(ce_type, data, *, kind="event", correlationid=None, causation
95
97
  return env
96
98
 
97
99
 
100
+ def _is_for_consumer(subject, payload):
101
+ """Route canonical subjects using identifiers carried in envelope data."""
102
+ if not isinstance(payload, dict):
103
+ return False
104
+ parts = subject.split(".")
105
+ if len(parts) != 6 or parts[0] != "bloodbank" or parts[2] != "v1":
106
+ return False
107
+
108
+ _vendor, kind, version, domain, entity, action = parts
109
+ if kind not in KIND_MARKERS.values():
110
+ return False
111
+ if KIND_MARKERS.get(payload.get("kind")) != kind:
112
+ return False
113
+ if payload.get("subject") != subject:
114
+ return False
115
+ if payload.get("type") != f"bloodbank.{version}.{domain}.{entity}.{action}":
116
+ return False
117
+ data = payload.get("data")
118
+ if not isinstance(data, dict):
119
+ return False
120
+ if kind == "evt" and domain == "repo":
121
+ return data.get("repo") == REPO
122
+ if kind == "cmd" and domain == "agent":
123
+ return data.get("target_agent_id") == AGENT_ID
124
+ return False
125
+
126
+
98
127
  async def main():
99
128
  nc = await nats.connect(NATS_URL, name=f"hermes-{AGENT_ID}")
100
129
  stop = asyncio.Event()
@@ -103,7 +132,10 @@ async def main():
103
132
  try:
104
133
  payload = json.loads(msg.data.decode())
105
134
  except Exception:
106
- payload = {"_raw": msg.data.decode(errors="replace")}
135
+ sys.stderr.write(f"[bloodbank-consumer:{AGENT_ID}] skipped unroutable payload on {msg.subject}\n")
136
+ return
137
+ if not _is_for_consumer(msg.subject, payload):
138
+ return
107
139
  entry = {
108
140
  "received_at": _now(),
109
141
  "subject": msg.subject,
@@ -125,7 +157,7 @@ async def main():
125
157
  "bloodbank.v1.agent.online.changed",
126
158
  {"agent_id": AGENT_ID, "repo": REPO, "role": ROLE, "state": "online"},
127
159
  )
128
- await nc.publish("bloodbank.evt.v1.agent.online.changed", json.dumps(online).encode())
160
+ await nc.publish(online["subject"], json.dumps(online).encode())
129
161
 
130
162
  def _shutdown(*_):
131
163
  stop.set()
@@ -138,7 +170,7 @@ async def main():
138
170
  "bloodbank.v1.agent.online.changed",
139
171
  {"agent_id": AGENT_ID, "repo": REPO, "role": ROLE, "state": "offline"},
140
172
  )
141
- await nc.publish("bloodbank.evt.v1.agent.online.changed", json.dumps(offline).encode())
173
+ await nc.publish(offline["subject"], json.dumps(offline).encode())
142
174
  await nc.drain()
143
175
 
144
176
 
@@ -7,8 +7,8 @@ short and high-signal — anything that should ALWAYS be in context.
7
7
 
8
8
  You are **{{display_name}}**, the `{{role}}` agent for the `{{repo}}` repo.
9
9
  Your scope is the repo at the project root. You communicate via Telegram
10
- (`@{{repo}}_{{role}}_bot`) and Bloodbank events on the
11
- `bloodbank.evt.v1.repo.{{repo}}.*` lane.
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