@sjawhar/opencode-legion-envoy 0.7.0 → 0.8.0
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/src/server.js +17 -0
- package/package.json +6 -4
- package/skills/AGENTS.md +38 -0
- package/skills/envoy/SKILL.md +396 -0
- package/skills/github/SKILL.md +203 -0
- package/skills/legion-architect/SKILL.md +201 -0
- package/skills/legion-controller/SKILL.md +136 -0
- package/skills/legion-oracle/SKILL.md +63 -0
- package/skills/legion-retro/SKILL.md +87 -0
- package/skills/legion-worker/SKILL.md +177 -0
- package/skills/legion-worker/references/config.md +259 -0
- package/skills/legion-worker/references/knowledge-injection.md +98 -0
- package/skills/legion-worker/resources/strategies/cleanup-deletion.md +22 -0
- package/skills/legion-worker/resources/strategies/systematic-rename.md +19 -0
- package/skills/linear/SKILL.md +76 -0
- package/src/server.ts +29 -1
package/dist/src/server.js
CHANGED
|
@@ -14,6 +14,11 @@ var __export = (target, all) => {
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// src/server.ts
|
|
18
|
+
import { existsSync as existsSync3 } from "fs";
|
|
19
|
+
import path4 from "path";
|
|
20
|
+
import { fileURLToPath } from "url";
|
|
21
|
+
|
|
17
22
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
18
23
|
var exports_external = {};
|
|
19
24
|
__export(exports_external, {
|
|
@@ -14235,6 +14240,11 @@ async function resolvePort(serverUrl, exec = defaultExec) {
|
|
|
14235
14240
|
}
|
|
14236
14241
|
|
|
14237
14242
|
// src/server.ts
|
|
14243
|
+
var moduleDirectory = path4.dirname(fileURLToPath(import.meta.url));
|
|
14244
|
+
var skillsDirectory = [
|
|
14245
|
+
path4.resolve(moduleDirectory, "../../skills"),
|
|
14246
|
+
path4.resolve(moduleDirectory, "../../../skills")
|
|
14247
|
+
].find((dir) => existsSync3(dir));
|
|
14238
14248
|
var [
|
|
14239
14249
|
subscribeSpec,
|
|
14240
14250
|
unsubscribeSpec,
|
|
@@ -14320,6 +14330,13 @@ var server_default = async (input) => {
|
|
|
14320
14330
|
});
|
|
14321
14331
|
return {
|
|
14322
14332
|
config: (cfg) => {
|
|
14333
|
+
if (skillsDirectory) {
|
|
14334
|
+
cfg.skills ??= {};
|
|
14335
|
+
cfg.skills.paths ??= [];
|
|
14336
|
+
if (!cfg.skills.paths.includes(skillsDirectory)) {
|
|
14337
|
+
cfg.skills.paths.push(skillsDirectory);
|
|
14338
|
+
}
|
|
14339
|
+
}
|
|
14323
14340
|
const entry = buildDispatchMcpEntry({
|
|
14324
14341
|
dispatch: config2.dispatch
|
|
14325
14342
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/opencode-legion-envoy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/src/server.js",
|
|
6
6
|
"exports": {
|
|
@@ -21,14 +21,16 @@
|
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
23
|
"src",
|
|
24
|
-
"!src/**/__tests__"
|
|
24
|
+
"!src/**/__tests__",
|
|
25
|
+
"skills"
|
|
25
26
|
],
|
|
26
27
|
"scripts": {
|
|
27
28
|
"build": "bun build src/server.ts bin/dispatch-mcp-shim.ts --outdir dist --target bun --format esm --external '@opencode-ai/*'",
|
|
28
|
-
"prepack": "bun run build",
|
|
29
|
+
"prepack": "bun run build && rm -rf skills && cp -r ../../skills skills",
|
|
29
30
|
"typecheck": "bunx tsc --noEmit",
|
|
30
31
|
"test": "bun test",
|
|
31
|
-
"lint": "bunx biome check src/"
|
|
32
|
+
"lint": "bunx biome check src/",
|
|
33
|
+
"postpack": "rm -rf skills"
|
|
32
34
|
},
|
|
33
35
|
"dependencies": {
|
|
34
36
|
"@opencode-ai/plugin": "~1.14.46"
|
package/skills/AGENTS.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Skills Layer
|
|
2
|
+
|
|
3
|
+
Legion skills guide the architect and its sequential phase workers in a shared issue workspace.
|
|
4
|
+
They are Markdown instructions loaded by Oh My Pi sessions; the daemon and OMP extension own
|
|
5
|
+
event intake, process lifecycle, credentials, and role delivery.
|
|
6
|
+
|
|
7
|
+
## Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
skills/
|
|
11
|
+
├── github/ # GitHub issue and pull-request operations
|
|
12
|
+
├── linear/ # Linear adapter instructions
|
|
13
|
+
├── legion-architect/ # Tree ownership, decomposition, gates, and scheduling
|
|
14
|
+
├── legion-controller/ # Derived-verdict control-plane operation
|
|
15
|
+
├── legion-oracle/ # Repository-grounded research
|
|
16
|
+
├── legion-retro/ # Post-review retrospective
|
|
17
|
+
└── legion-worker/ # Sequential architect, plan, implement, test, and review phases
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Phase workers
|
|
21
|
+
|
|
22
|
+
The extension supplies a phase worker with its issue, workspace, role token, and structured
|
|
23
|
+
output schema. The worker claims its supplied role, works only on its phase artifact, and
|
|
24
|
+
returns that schema to the architect. It writes the same phase-specific payload to
|
|
25
|
+
`.legion/<phase>.json`, verifies it exists, and commits the handoff before reporting completion.
|
|
26
|
+
The committed predecessor handoff wins after revival or re-creation.
|
|
27
|
+
|
|
28
|
+
Workers do not run a controller loop or mutate lifecycle labels. Workers coordinate
|
|
29
|
+
lifecycle, scope, and cross-phase decisions with the owning architect through hub, sending
|
|
30
|
+
the verified observation and decision needed. A worker may use the `dispatch` MCP tool
|
|
31
|
+
directly for a durable human question; replies come back to the worker's own session.
|
|
32
|
+
|
|
33
|
+
## Durable artifacts
|
|
34
|
+
|
|
35
|
+
Phase handoffs are committed in lifecycle order: architect, plan, implement, test, and review.
|
|
36
|
+
The reviewer removes `.legion/` as its final commit; retro records its learning in
|
|
37
|
+
`docs/solutions/` and writes no handoff. GitHub comments and reviews carry the required Legion
|
|
38
|
+
footer so the daemon can attribute artifacts to their worker session.
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: envoy
|
|
3
|
+
description: Use when subscribing sessions to Envoy topics, sending agent-to-agent messages, or reasoning about topic formats for Slack/GitHub/agent routing.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Envoy
|
|
7
|
+
|
|
8
|
+
Envoy is Legion's event-routing subsystem. It delivers Slack, GitHub, and agent-to-agent events to OpenCode sessions.
|
|
9
|
+
|
|
10
|
+
## What the tools do
|
|
11
|
+
|
|
12
|
+
- `envoy_subscribe(topics)` — make the current session RECEIVE future events on those topics
|
|
13
|
+
- `envoy_unsubscribe(topics?)` — stop receiving some or all topics
|
|
14
|
+
- `envoy_list()` — show the union of live local and persisted registry subscriptions, with each topic marked `live`, `registry`, or `both`
|
|
15
|
+
- `envoy_send(session_id, message)` — SEND a message directly to another session
|
|
16
|
+
|
|
17
|
+
## Topic formats
|
|
18
|
+
|
|
19
|
+
### Agent-to-agent
|
|
20
|
+
|
|
21
|
+
- Direct session route:
|
|
22
|
+
- `notifications.agent.<session_id>`
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
|
|
26
|
+
- `notifications.agent.ses_2e6ca3034ffejVikSZ8mDwk0mR`
|
|
27
|
+
|
|
28
|
+
### Legion role claims
|
|
29
|
+
|
|
30
|
+
- Role topic:
|
|
31
|
+
- `notifications.role.<role>`
|
|
32
|
+
|
|
33
|
+
Legion agents receive through a daemon-minted role token. Claim the role for the current
|
|
34
|
+
session with `envoy_role_set(role="<role>")`; claiming transfers its holder with
|
|
35
|
+
last-claim-wins semantics. A claimant does not manually subscribe to its role topic.
|
|
36
|
+
|
|
37
|
+
Legion role tokens must satisfy `^[a-z0-9][a-z0-9_-]*$` and are unique across repositories:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
legion-<project>-controller
|
|
41
|
+
legion-<project>-<enc(owner)>__<enc(repo)>-<number>-<role>
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`<project>` matches `[a-z0-9]+`. In owner and repository components, the injective escape
|
|
45
|
+
encoding is `_` → `_u`, `.` → `_d`, and `-` → `_h`; `__` separates owner from repository.
|
|
46
|
+
The daemon owns the authoritative token-to-issue map and hands the token to each process.
|
|
47
|
+
Do not construct a token from a partial issue reference.
|
|
48
|
+
|
|
49
|
+
Claims last through the issue's post-close linger. They survive parking and worker
|
|
50
|
+
re-creation; a re-claim re-points the role to the backing session. The daemon publishes to an
|
|
51
|
+
issue role only while the issue is active. Inactive issue events become daemon state and later
|
|
52
|
+
surface as derived catch-up, never as raw event replay.
|
|
53
|
+
|
|
54
|
+
### Legion exception lane
|
|
55
|
+
|
|
56
|
+
`no_holder` and `delivery_failed` on a Legion role are daemon liveness signals, not a prompt
|
|
57
|
+
for a second subscriber or a manual retry. The daemon probes the process that owns the tree:
|
|
58
|
+
|
|
59
|
+
1. If it is alive, the daemon sends a control-topic directive. The extension revives or
|
|
60
|
+
re-creates the backing worker in code, then the daemon re-delivers the message.
|
|
61
|
+
2. If it is dead, the daemon resurrects the root process behind a generation lock and supplies
|
|
62
|
+
derived catch-up plus the shared workspace handoffs.
|
|
63
|
+
|
|
64
|
+
This keeps raw delivery failures out of architect context. Controller exception wakes are
|
|
65
|
+
handled by the controller's wake routing table; every other role follows the liveness path.
|
|
66
|
+
|
|
67
|
+
### GitHub
|
|
68
|
+
|
|
69
|
+
GitHub topics are **resource-scoped** — every event includes the resource type and number (or, for push/workflow events, the ref or workflow filename).
|
|
70
|
+
|
|
71
|
+
**Topic structure:** `notifications.github.<owner>.<repo>.<resource_type>.<number>.<event_kind>`
|
|
72
|
+
|
|
73
|
+
- PR opened/closed/merged/ready:
|
|
74
|
+
- `notifications.github.<owner>.<repo>.pr.<number>`
|
|
75
|
+
- Issue opened/closed/labeled:
|
|
76
|
+
- `notifications.github.<owner>.<repo>.issue.<number>`
|
|
77
|
+
- Comment on a PR:
|
|
78
|
+
- `notifications.github.<owner>.<repo>.pr.<number>.comment`
|
|
79
|
+
- Comment on an issue:
|
|
80
|
+
- `notifications.github.<owner>.<repo>.issue.<number>.comment`
|
|
81
|
+
- PR review submitted:
|
|
82
|
+
- `notifications.github.<owner>.<repo>.pr.<number>.review`
|
|
83
|
+
- Raw CI check observation (per-PR, immediate):
|
|
84
|
+
- `notifications.github.<owner>.<repo>.pr.<number>.check`
|
|
85
|
+
- Every PR-associated `check_run` publishes one raw observation per associated PR. `Payload` is JSON with `sha`, `name`, `status`, and `conclusion`; `PayloadSummary` names the check and its state.
|
|
86
|
+
- CI/check summary (per-PR, per-commit, debounced):
|
|
87
|
+
- `notifications.github.<owner>.<repo>.pr.<number>.ci`
|
|
88
|
+
- The same `check_run` updates ingest-side per-commit state in JetStream KV bucket `envoy_ci_state`. Once the check set has been quiet for `ENVOY_CI_DEBOUNCE` (default `5s`), Envoy publishes one JSON summary. A new summary is emitted only when the check set changes; a new push starts a fresh tally, and a re-run that returns to running changes the tally.
|
|
89
|
+
- `PayloadSummary` is a compact JSON object (`Payload` unused): `{"kind":"ci_summary","repo":"<o>/<r>","number":"<n>","sha":"<sha>","failed":{"count":N,"checks":[...]},"running":{...},"passed":{...},"queued":{...},"skipped":{...}}`. Each status is `{count, checks}` with the full sorted name list; every status is present (`{"count":0,"checks":[]}` when empty).
|
|
90
|
+
- `check_suite` is ignored (it is a per-app rollup without a per-check name). Checks not tied to a PR are dropped, so there is no repo-wide CI topic. For non-PR visibility, use `workflow.<filename>.<action>`; individual GitHub Actions `workflow_job` events are not routed.
|
|
91
|
+
- Mention events (per-resource):
|
|
92
|
+
- `notifications.github.<owner>.<repo>.pr.<number>.mention`
|
|
93
|
+
- `notifications.github.<owner>.<repo>.issue.<number>.mention`
|
|
94
|
+
- Mention events (repo-wide — catches all mentions):
|
|
95
|
+
- `notifications.github.<owner>.<repo>.mention`
|
|
96
|
+
- Push events (per branch/tag):
|
|
97
|
+
- `notifications.github.<owner>.<repo>.push.branch.<branch>`
|
|
98
|
+
- `notifications.github.<owner>.<repo>.push.tag.<tag>`
|
|
99
|
+
Branch and tag names with dots are sanitized to underscores (`v1.0.0` → `v1_0_0`).
|
|
100
|
+
Push events for refs other than `refs/heads/...` and `refs/tags/...` are not routed.
|
|
101
|
+
- Workflow run events (per workflow file):
|
|
102
|
+
- `notifications.github.<owner>.<repo>.workflow.<filename>.<action>`
|
|
103
|
+
Filename is the basename of `workflow_run.path` with dots sanitized (`ci.yml` → `ci_yml`).
|
|
104
|
+
`action` is one of `requested`, `in_progress`, `completed`.
|
|
105
|
+
|
|
106
|
+
**Using wildcards to subscribe broadly:**
|
|
107
|
+
- All events in a repo: `notifications.github.<owner>.<repo>.>`
|
|
108
|
+
- All PR events in a repo: `notifications.github.<owner>.<repo>.pr.>`
|
|
109
|
+
- All events for a specific PR: `notifications.github.<owner>.<repo>.pr.<number>.>`
|
|
110
|
+
- All issue events in a repo: `notifications.github.<owner>.<repo>.issue.>`
|
|
111
|
+
- All events for a specific issue: `notifications.github.<owner>.<repo>.issue.<number>.>`
|
|
112
|
+
- All push events in a repo: `notifications.github.<owner>.<repo>.push.>`
|
|
113
|
+
- Pushes to main only: `notifications.github.<owner>.<repo>.push.branch.main`
|
|
114
|
+
- All branch pushes: `notifications.github.<owner>.<repo>.push.branch.>`
|
|
115
|
+
- All tag pushes: `notifications.github.<owner>.<repo>.push.tag.>`
|
|
116
|
+
- All workflow events: `notifications.github.<owner>.<repo>.workflow.>`
|
|
117
|
+
- All events for a specific workflow: `notifications.github.<owner>.<repo>.workflow.ci_yml.>`
|
|
118
|
+
- Every workflow completion: `notifications.github.<owner>.<repo>.workflow.*.completed`
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
|
|
122
|
+
- `notifications.github.trajectory-labs-pbc.agent-c.pr.9880` (PR #9880 state changes)
|
|
123
|
+
- `notifications.github.trajectory-labs-pbc.agent-c.pr.9880.comment` (comments on PR #9880)
|
|
124
|
+
- `notifications.github.trajectory-labs-pbc.agent-c.issue.9909.>` (all events on issue #9909)
|
|
125
|
+
- `notifications.github.sjawhar.legion.pr.>` (all PR events across all PRs)
|
|
126
|
+
- `notifications.github.sjawhar.legion.mention` (all @mentions repo-wide)
|
|
127
|
+
- `notifications.github.sjawhar.legion.push.branch.main` (pushes to main)
|
|
128
|
+
- `notifications.github.sjawhar.legion.workflow.ci_yml.in_progress` (CI workflow starts)
|
|
129
|
+
|
|
130
|
+
### Slack
|
|
131
|
+
|
|
132
|
+
- Channel message events:
|
|
133
|
+
- `notifications.slack.<team_id>.<channel_id>.message`
|
|
134
|
+
- App mention events:
|
|
135
|
+
- `notifications.slack.<team_id>.<channel_id>.mention`
|
|
136
|
+
- Thread message events:
|
|
137
|
+
- `notifications.slack.<team_id>.<channel_id>.thread.<normalized_ts>.message`
|
|
138
|
+
- Thread mention events:
|
|
139
|
+
- `notifications.slack.<team_id>.<channel_id>.thread.<normalized_ts>.mention`
|
|
140
|
+
|
|
141
|
+
Thread timestamps are normalized: `1234567890.123456` → `1234567890_123456`
|
|
142
|
+
(dots replaced with underscores to make the thread ID a single NATS segment).
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
|
|
146
|
+
- `notifications.slack.T09FRELLTS8.C0A0DHVU8HE.message`
|
|
147
|
+
- `notifications.slack.T09FRELLTS8.C0A0DHVU8HE.mention`
|
|
148
|
+
- `notifications.slack.T09FRELLTS8.C0A0DHVU8HE.thread.1234567890_123456.message`
|
|
149
|
+
- `notifications.slack.T09FRELLTS8.C0A0DHVU8HE.thread.1234567890_123456.mention`
|
|
150
|
+
|
|
151
|
+
### Ghost Wispr
|
|
152
|
+
|
|
153
|
+
- Session started events:
|
|
154
|
+
- `notifications.ghostwispr.<session_id>.session.started`
|
|
155
|
+
- Session ended events:
|
|
156
|
+
- `notifications.ghostwispr.<session_id>.session.ended`
|
|
157
|
+
- Summary ready events:
|
|
158
|
+
- `notifications.ghostwispr.<session_id>.summary.ready`
|
|
159
|
+
|
|
160
|
+
**Parameters:**
|
|
161
|
+
- `<session_id>`: Ghost Wispr session timestamp string (e.g., `20260326041405`). Alphanumeric only, safe for NATS topic segments.
|
|
162
|
+
- Supported kinds: `session.started`, `session.ended`, `summary.ready`
|
|
163
|
+
|
|
164
|
+
Examples:
|
|
165
|
+
|
|
166
|
+
- `notifications.ghostwispr.20260326041405.session.ended` (session ended)
|
|
167
|
+
- `notifications.ghostwispr.20260326041629.summary.ready` (summary ready)
|
|
168
|
+
|
|
169
|
+
### WhatsApp
|
|
170
|
+
|
|
171
|
+
- Chat message events:
|
|
172
|
+
- `notifications.whatsapp.<phone>.<jid>.message`
|
|
173
|
+
- Status/receipt events:
|
|
174
|
+
- `notifications.whatsapp.<phone>.<jid>.status`
|
|
175
|
+
|
|
176
|
+
**Parameters:**
|
|
177
|
+
- `<phone>`: Connected WhatsApp account phone number in E.164 digits-only format (no `+` prefix). Example: `15551234567`. This identifies **which WhatsApp account** the events belong to — not the remote contact.
|
|
178
|
+
- `<jid>`: Remote chat's WhatsApp JID. Individual: `PHONE@s.whatsapp.net`. Group: `ID@g.us`.
|
|
179
|
+
- Supported kinds: `message`, `status`
|
|
180
|
+
|
|
181
|
+
> **⚠️ JID dot expansion:** JID dots (`.`) become additional NATS subject tokens. For example, `5551234567@s.whatsapp.net` splits into tokens `5551234567@s`, `whatsapp`, `net`. This means individual chat topics produce 7 tokens and group chat topics produce 6 tokens. **Always use `>` (multi-level wildcard), never `*` (single-token wildcard)**, when subscribing to a chat or phone number.
|
|
182
|
+
|
|
183
|
+
Examples:
|
|
184
|
+
|
|
185
|
+
- `notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.message` (individual chat messages — note this expands to 7 NATS tokens)
|
|
186
|
+
- `notifications.whatsapp.15551234567.120363XXX@g.us.message` (group chat messages — 6 NATS tokens)
|
|
187
|
+
|
|
188
|
+
## When to use what
|
|
189
|
+
|
|
190
|
+
### To receive future Slack/GitHub/WhatsApp events
|
|
191
|
+
|
|
192
|
+
1. Decide the exact topic(s)
|
|
193
|
+
2. Call `envoy_subscribe([...])`
|
|
194
|
+
3. Optionally call `envoy_list()` to confirm
|
|
195
|
+
|
|
196
|
+
### To talk directly to another agent/session
|
|
197
|
+
|
|
198
|
+
1. Get the target session ID
|
|
199
|
+
2. Call `envoy_send(session_id, message)`
|
|
200
|
+
|
|
201
|
+
You do NOT need to subscribe in order to send or publish.
|
|
202
|
+
|
|
203
|
+
### To wait for CI, PR checks, or other async work
|
|
204
|
+
|
|
205
|
+
**Don't `sleep`-poll. Don't "check back in N minutes."** Subscribe to the event and continue with productive work — the system will wake the session when the event arrives.
|
|
206
|
+
|
|
207
|
+
1. Identify the relevant topic (e.g., `notifications.github.<owner>.<repo>.pr.<num>.>` for all PR events; `.pr.<num>.check` for immediate per-check state; `.pr.<num>.ci` for the debounced PR summary; `.workflow.<filename>.completed` for a workflow run finishing)
|
|
208
|
+
2. Call `envoy_subscribe([...])`
|
|
209
|
+
3. Move on to other work, or end the response and let the watcher wake you
|
|
210
|
+
4. The next response is triggered by the event, with the payload available in your context
|
|
211
|
+
|
|
212
|
+
If you have nothing else to do, end the response. The user is not your alarm clock; do not loop with `sleep`.
|
|
213
|
+
|
|
214
|
+
### Tools
|
|
215
|
+
|
|
216
|
+
- `envoy_subscribe(topics)` — receive future events on those topics
|
|
217
|
+
- `envoy_unsubscribe(topics?)` — stop receiving some or all topics
|
|
218
|
+
- `envoy_list()` — show live and persisted subscriptions, marked by source
|
|
219
|
+
- `envoy_send(session_id, message)` — send directly to a specific session (point-to-point)
|
|
220
|
+
- `envoy_publish(topic, message)` — publish a normal topic to matching subscribers or route a role topic to its current holder
|
|
221
|
+
- `envoy_role_set(role)` — claim a named role for the current session (exactly-one-holder)
|
|
222
|
+
|
|
223
|
+
## Patterns
|
|
224
|
+
|
|
225
|
+
### Subscribe controller to a specific Slack channel mentions
|
|
226
|
+
|
|
227
|
+
```text
|
|
228
|
+
envoy_subscribe([
|
|
229
|
+
"notifications.slack.T09FRELLTS8.C0A0DHVU8HE.mention"
|
|
230
|
+
])
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Subscribe to all events in a specific Slack thread
|
|
234
|
+
|
|
235
|
+
```text
|
|
236
|
+
envoy_subscribe([
|
|
237
|
+
"notifications.slack.T09FRELLTS8.C0A0DHVU8HE.thread.1234567890_123456.>"
|
|
238
|
+
])
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Subscribe to only messages in a Slack thread (not mentions)
|
|
242
|
+
|
|
243
|
+
```text
|
|
244
|
+
envoy_subscribe([
|
|
245
|
+
"notifications.slack.T09FRELLTS8.C0A0DHVU8HE.thread.1234567890_123456.message"
|
|
246
|
+
])
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Subscribe to all threads in a Slack channel
|
|
250
|
+
|
|
251
|
+
```text
|
|
252
|
+
envoy_subscribe([
|
|
253
|
+
"notifications.slack.T09FRELLTS8.C0A0DHVU8HE.thread.>"
|
|
254
|
+
])
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Subscribe to all PR events for agent-c
|
|
258
|
+
|
|
259
|
+
```text
|
|
260
|
+
envoy_subscribe([
|
|
261
|
+
"notifications.github.trajectory-labs-pbc.agent-c.pr.>"
|
|
262
|
+
])
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Subscribe controller to GitHub @mentions for agent-c
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
envoy_subscribe([
|
|
269
|
+
"notifications.github.trajectory-labs-pbc.agent-c.mention"
|
|
270
|
+
])
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Message another session directly
|
|
274
|
+
|
|
275
|
+
```text
|
|
276
|
+
envoy_send(
|
|
277
|
+
session_id="ses_2e6ca3034ffejVikSZ8mDwk0mR",
|
|
278
|
+
message="Please continue the smoke test"
|
|
279
|
+
)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Subscribe to a specific WhatsApp contact (1:1 chat)
|
|
283
|
+
|
|
284
|
+
```text
|
|
285
|
+
envoy_subscribe([
|
|
286
|
+
"notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.>"
|
|
287
|
+
])
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Use `>` (not `*`) to catch all event kinds despite JID dot expansion into multiple NATS tokens.
|
|
291
|
+
|
|
292
|
+
### Subscribe to a WhatsApp group
|
|
293
|
+
|
|
294
|
+
```text
|
|
295
|
+
envoy_subscribe([
|
|
296
|
+
"notifications.whatsapp.15551234567.120363XXX@g.us.>"
|
|
297
|
+
])
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Subscribe to all WhatsApp events for an account
|
|
301
|
+
|
|
302
|
+
```text
|
|
303
|
+
envoy_subscribe([
|
|
304
|
+
"notifications.whatsapp.15551234567.>"
|
|
305
|
+
])
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Catches all conversations and event kinds for the specified phone number.
|
|
309
|
+
|
|
310
|
+
**When to use which:**
|
|
311
|
+
- **1:1 chat** — when monitoring a specific contact conversation (e.g., a bot handling customer queries)
|
|
312
|
+
- **Group chat** — when monitoring a specific group for commands or events
|
|
313
|
+
- **All chats for a phone** — when building a general WhatsApp event handler or dashboard for an account
|
|
314
|
+
|
|
315
|
+
## Important notes
|
|
316
|
+
|
|
317
|
+
- Sessions choose their own Slack/GitHub subscriptions
|
|
318
|
+
- Different sessions can subscribe to different channels/repos
|
|
319
|
+
- Agent-to-agent delivery uses exact session IDs
|
|
320
|
+
- `envoy_list()` distinguishes `live`, `registry`, and `both`; a `live` topic is receiving now even when the listener registry has not caught up.
|
|
321
|
+
- For Slack, use the real `team_id` in topics (for example `T09FRELLTS8`), not a workspace slug like `trajectorylabs`
|
|
322
|
+
- GitHub mention routing is body-based because GitHub has no dedicated app mention webhook event
|
|
323
|
+
|
|
324
|
+
## Synthetic Smoke Test (WhatsApp — NATS Routing Only)
|
|
325
|
+
|
|
326
|
+
> **Important:** This procedure validates Envoy's NATS → listener → session delivery path using `envoy_publish`. It does **not** test real WhatsApp message ingestion. The generic MCP bridge (`packages/envoy/cmd/mcp/`) can bridge real WhatsApp events, but requires production configuration. See "Current Limitations" below.
|
|
327
|
+
>
|
|
328
|
+
> **Two sessions required:** `envoy_publish` sets `source_session` to the publishing session's ID. The listener skips delivering broadcasts back to the sender (`packages/envoy/cmd/listener/main.go`). You must subscribe in one session and publish from a different session.
|
|
329
|
+
|
|
330
|
+
### Step 1: Subscribe to a WhatsApp topic (Session A)
|
|
331
|
+
|
|
332
|
+
```text
|
|
333
|
+
envoy_subscribe([
|
|
334
|
+
"notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.>"
|
|
335
|
+
])
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Step 2: Verify subscription is active (Session A)
|
|
339
|
+
|
|
340
|
+
```text
|
|
341
|
+
envoy_list()
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Confirm `notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.>` appears as `live` or `both`.
|
|
345
|
+
|
|
346
|
+
### Step 3: Publish a synthetic test envelope (Session B — a different session)
|
|
347
|
+
|
|
348
|
+
```text
|
|
349
|
+
envoy_publish(
|
|
350
|
+
topic="notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.message",
|
|
351
|
+
message="Synthetic WhatsApp smoke test: hello from envoy_publish"
|
|
352
|
+
)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Step 4: Verify delivery (Session A)
|
|
356
|
+
|
|
357
|
+
Session A should receive a notification containing the text "Synthetic WhatsApp smoke test: hello from envoy_publish". This confirms:
|
|
358
|
+
- The topic pattern matches the subscription
|
|
359
|
+
- NATS routes the message to the listener
|
|
360
|
+
- The listener delivers to the subscribed session (Session A ≠ the publishing session)
|
|
361
|
+
|
|
362
|
+
**If the notification does not arrive:** Check `envoy_list()` in Session A. The topic should be `live` or `both`; `registry` alone does not confirm the local subscription is receiving. Verify the topic in `envoy_publish` matches the subscription pattern. Ensure you are publishing from a **different** session than the one subscribed.
|
|
363
|
+
|
|
364
|
+
### Reference: Real WhatsApp Envelope Shape
|
|
365
|
+
|
|
366
|
+
When the MCP bridge (`packages/envoy/internal/mcpbridge/envelope.go`) publishes a real WhatsApp event, the Envoy envelope has this structure:
|
|
367
|
+
|
|
368
|
+
```json
|
|
369
|
+
{
|
|
370
|
+
"event_id": "<generated unique ID>",
|
|
371
|
+
"source": "whatsapp",
|
|
372
|
+
"source_event_id": "whatsapp://messages/15551234567/5551234567@s.whatsapp.net",
|
|
373
|
+
"topic": "notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.message",
|
|
374
|
+
"dedupe_key": "whatsapp.<event_id value>",
|
|
375
|
+
"issued_at": 1712345678000,
|
|
376
|
+
"payload_summary": "Hello from WhatsApp",
|
|
377
|
+
"payload_ref": "whatsapp://messages/15551234567/5551234567@s.whatsapp.net",
|
|
378
|
+
"trace_id": "<generated unique ID>"
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Field notes:**
|
|
383
|
+
- `source` is `"whatsapp"` — in contrast, `envoy_publish` sets `source: "agent"` for synthetic messages
|
|
384
|
+
- `issued_at` is in **milliseconds** (Unix epoch ms), not seconds
|
|
385
|
+
- `dedupe_key` is `source + "." + event_id` (e.g., `"whatsapp.cuid_abc123"`)
|
|
386
|
+
- `payload_summary` is the actual message text from the MCP resource read (truncated to 200 chars), or fallback `"whatsapp event from <uri>"` if no text content
|
|
387
|
+
- `payload_ref` and `source_event_id` are both the MCP resource notification URI
|
|
388
|
+
- `source_session` is **omitted** (empty) — the MCP bridge is not an OpenCode session, so no echo-skip occurs
|
|
389
|
+
- `expires_at` is **omitted** — the bridge does not set message expiry
|
|
390
|
+
|
|
391
|
+
## Current Limitations (WhatsApp)
|
|
392
|
+
|
|
393
|
+
- **No production WhatsApp event ingestion configured.** The repo contains a generic MCP→NATS bridge (`packages/envoy/cmd/mcp/` + `packages/envoy/internal/mcpbridge/`) that already supports WhatsApp topic patterns (tested in `packages/envoy/internal/integration/delivery_test.go`). However, it is not yet configured/deployed to connect to the `@sjawhar/whatsapp-mcp` server in production.
|
|
394
|
+
- **Synthetic testing only.** The smoke test above uses `envoy_publish` to inject test messages into NATS. It validates Envoy delivery mechanics (NATS → listener → session), not true WhatsApp end-to-end delivery.
|
|
395
|
+
- **Production wiring needed.** To receive real WhatsApp events, the MCP bridge needs to be configured with the `@sjawhar/whatsapp-mcp` server connection details (similar to how `packages/envoy/cmd/github/` and `packages/envoy/cmd/slack/` are configured for their respective platforms). The bridge would then subscribe to WhatsApp MCP resource notifications and publish Envoy envelopes to NATS automatically.
|
|
396
|
+
- **Subscription + routing + delivery path is ready.** The contracts layer (`whatsappSubject` helper), NATS topic format, generic listener routing, and MCP bridge infrastructure all work. Only the production configuration connecting the bridge to the WhatsApp MCP server is missing.
|