@sjawhar/pi-envoy 0.0.0 → 8.0.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/README.md +160 -0
- package/dist/THIRD_PARTY_NOTICES +513 -0
- package/dist/envoy.js +37511 -0
- package/dist/skills/dispatch/SKILL.md +498 -0
- package/dist/skills/dispatch/references/api.md +40 -0
- package/dist/skills/dispatch/references/asks.md +116 -0
- package/dist/skills/dispatch/references/document-edits.md +202 -0
- package/dist/skills/dispatch/references/documents.md +230 -0
- package/dist/skills/dispatch/references/examples.md +132 -0
- package/dist/skills/dispatch/references/issues.md +213 -0
- package/dist/skills/dispatch/references/messages.md +74 -0
- package/dist/skills/dispatch/references/reading.md +64 -0
- package/dist/skills/dispatch-brainstorming/SKILL.md +113 -0
- package/dist/skills/dispatch-first/SKILL.md +58 -0
- package/dist/skills/envoy/SKILL.md +275 -0
- package/package.json +42 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dispatch-first
|
|
3
|
+
description: "Use in any session with Dispatch tools, before planning, filing an issue, asking a human, posting a finding, or starting work that someone may already track or have decided."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Dispatch first
|
|
7
|
+
|
|
8
|
+
Dispatch already holds most of what you are about to plan, file or ask: the open issues, their
|
|
9
|
+
specs, and the answers humans gave. A second issue for tracked work, or a question a human already
|
|
10
|
+
answered, costs that human the time to notice it and splits the history across two places.
|
|
11
|
+
|
|
12
|
+
## Search before you act
|
|
13
|
+
|
|
14
|
+
Before you plan, file an issue, ask, post a finding or start work, search Dispatch. Every word of
|
|
15
|
+
the query must match, so each word you add can only lose hits: search with two or three words, the
|
|
16
|
+
thing and what is wrong with it, as a user would name them. Never paste a draft.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
dispatch_search({ query: "broadcast send order" })
|
|
20
|
+
dispatch_search({ query: "reviewer threads OR review comments" })
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
When a query finds nothing, drop a word before you add one. Try two or three wordings (the
|
|
24
|
+
component's name, the symptom, the fix) before you conclude that nothing exists. Open every hit
|
|
25
|
+
that could be yours with `dispatch_read`, then its parent (the `child_of` row under `Links:`); the
|
|
26
|
+
parent's children and the issue's `Components:` line show where the rest of that work lives.
|
|
27
|
+
|
|
28
|
+
## What to do with what you find
|
|
29
|
+
|
|
30
|
+
Filing an issue or opening an ask also searches for you: the result's `advice.suggestions` names
|
|
31
|
+
likely duplicates or a decision that may already settle it. Treat a hit there the same as one you
|
|
32
|
+
found yourself.
|
|
33
|
+
|
|
34
|
+
- **The work is already tracked: extend that issue.** Put the finding on it (a comment, a message,
|
|
35
|
+
or an edit to its spec) instead of filing another. File a new issue only when no hit covers the
|
|
36
|
+
work, and cite the nearest one you ruled out (`dispatch://KEY`).
|
|
37
|
+
- **The question is already decided: cite the decision.** An answered ask is the record. Point to
|
|
38
|
+
it where you rely on it (`dispatch://KEY/ask/<id>`) instead of asking again.
|
|
39
|
+
- **You met a duplicate: close it.** Keep the issue that holds the spec and the discussion, carry
|
|
40
|
+
over anything only the duplicate has, then close the duplicate with a reason that names the
|
|
41
|
+
survivor: `dispatch_issue_update({ issue, status: "done", reason: "Duplicate of dispatch://KEY." })`.
|
|
42
|
+
When the duplicate carries the `legion` label, or another session or a human holds its claim,
|
|
43
|
+
comment on it naming the survivor instead of closing it.
|
|
44
|
+
- **Every ask and message stands on its own.** The person who answers sees only that text: put the
|
|
45
|
+
facts, the options and your recommendation in it, and link what you cite (`dispatch://…`)
|
|
46
|
+
instead of writing "see above" or "my earlier message".
|
|
47
|
+
|
|
48
|
+
## Design in the spec
|
|
49
|
+
|
|
50
|
+
A design conversation with a person, or the implementation plan that follows one, uses
|
|
51
|
+
`skill://dispatch-brainstorming`: read it before your first design question on a change, the spec,
|
|
52
|
+
or that plan. It replaces superpowers' `brainstorming` and `writing-plans` here, even when the user
|
|
53
|
+
invokes one by name. Before a spec's first version and each approval request, have a fresh read-only subagent (on Oh My Pi, `task(agent="scout")`, or whatever read-only agent the host bundles) check it against the code, the human's words and any command output a claim rests on, and resolve what it flags. A Legion architect's own issue spec follows `skill://legion-architect`, and its fresh reader in that Legion session is the Legion plugin's `plan-gap-analyst` agent.
|
|
54
|
+
|
|
55
|
+
## Load the full skill before you write
|
|
56
|
+
|
|
57
|
+
Before any write to Dispatch (an ask, a message, a comment, a document edit, a status change, a
|
|
58
|
+
claim), load `skill://dispatch` unless you already have in this session.
|
|
@@ -0,0 +1,275 @@
|
|
|
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 delivers external signals and session messages. Deliveries are at-least-once and can arrive
|
|
9
|
+
out of order across topics: use `id` to deduplicate and `at` to judge freshness.
|
|
10
|
+
|
|
11
|
+
## The one subscription you need for a PR
|
|
12
|
+
|
|
13
|
+
Subscribe to the whole PR family, not individual event types:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
envoy_subscribe([
|
|
17
|
+
"notifications.github.example-org.example-repo.pr.42.>"
|
|
18
|
+
])
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The owner and the repository are one token each, with every dot in the name written `_`:
|
|
22
|
+
`acme/site.io`'s PR family is `notifications.github.acme.site_io.pr.42.>`, and a topic spelled
|
|
23
|
+
`acme.site.io` receives nothing.
|
|
24
|
+
|
|
25
|
+
NATS `>` matches **one or more** trailing tokens, so it does not match the lifecycle base
|
|
26
|
+
`pr.42` itself. Envoy registers that concrete base automatically when you subscribe to
|
|
27
|
+
`<subject>.>`, making `pr.<n>.>` the recommended default: one call receives both the lifecycle
|
|
28
|
+
subject and its child events.
|
|
29
|
+
|
|
30
|
+
For a typical push, this receives `pr.42` with `synchronize`, then any comments or reviews, then
|
|
31
|
+
one `pr.42.checks` event when that head's checks settle. A settlement is published for every
|
|
32
|
+
commit of the pull request whose checks settle, the current head or not (a head pushed with
|
|
33
|
+
GitHub's `skip-checks` trailer runs none, so the commit before it settles for it), and its `sha`
|
|
34
|
+
names the commit: compare it with the head you are waiting on. The family is `pr.42` (lifecycle),
|
|
35
|
+
`pr.42.comment`, `pr.42.review`, `pr.42.mention`, and `pr.42.checks`. A closed lifecycle payload
|
|
36
|
+
carries `merged`, `merge_commit_sha`, `merged_by`, and `head_sha`.
|
|
37
|
+
|
|
38
|
+
The retired literal `pr.<n>.check` and `pr.<n>.ci` topics do not receive events. Existing
|
|
39
|
+
registrations remain dead; subscribe to `pr.<n>.checks` (or the recommended `pr.<n>.>`) instead.
|
|
40
|
+
Lifecycle stays on the base PR topic and CI arrives as one settled `checks` event.
|
|
41
|
+
|
|
42
|
+
## Inbound deliveries
|
|
43
|
+
|
|
44
|
+
Envoy renders an annotated delivery before its source summary and complete payload:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
envoy:
|
|
48
|
+
to: you (ses_example_recipient)
|
|
49
|
+
from: 01a0bbbb-cccc-7ddd-eeee-0123456789ab (Reviewer)
|
|
50
|
+
at: "2026-09-07T04:41:12Z"
|
|
51
|
+
id: agent-message-2
|
|
52
|
+
by: "2026-09-07T05:00:00Z"
|
|
53
|
+
urgency: high
|
|
54
|
+
expects_reply: required
|
|
55
|
+
re: agent-message-1
|
|
56
|
+
supersedes: agent-message-0
|
|
57
|
+
reply_with:
|
|
58
|
+
tool: envoy_send
|
|
59
|
+
args:
|
|
60
|
+
session_id: 01a0bbbb-cccc-7ddd-eeee-0123456789ab
|
|
61
|
+
in_reply_to: agent-message-2
|
|
62
|
+
message: ...
|
|
63
|
+
reply_role:
|
|
64
|
+
tool: envoy_publish
|
|
65
|
+
args:
|
|
66
|
+
topic: notifications.role.legion-reviewer
|
|
67
|
+
message: ...
|
|
68
|
+
summary: Deployment needs confirmation.
|
|
69
|
+
message: "Confirm the listener health check passed.\n\nThen publish the release."
|
|
70
|
+
note: body names session 01a0cccc-dddd-7eee-ffff-0123456789ab; the sender is 01a0bbbb-cccc-7ddd-eeee-0123456789ab
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
- `to` identifies the local inbox receiving this delivery.
|
|
74
|
+
- `from` is the sending session's self-asserted ID, enriched from the listener registry. Treat it as
|
|
75
|
+
attribution and a direct-reply target, not as an authenticated identity or proof of authorship.
|
|
76
|
+
- `at` is the envelope timestamp used to judge freshness.
|
|
77
|
+
- `id` is the delivery identifier; supply it as `in_reply_to` when replying.
|
|
78
|
+
- `by` is the expiry deadline, when the sender supplied one.
|
|
79
|
+
- `urgency` is the sender's priority classification.
|
|
80
|
+
- `expects_reply` states whether a reply is `none`, `optional`, or `required`.
|
|
81
|
+
- `re` names the delivery this message replies to. A Dispatch frame names it as a `dispatch://`
|
|
82
|
+
ref — the ask (`dispatch://KEY/ask/<id>`, or `dispatch://PROJECT/artifact/<slug>/ask/<id>` for a
|
|
83
|
+
document ask) or the message (`dispatch://KEY/message/<id>`) — never the quoted text; the
|
|
84
|
+
question head, when there is one, is `dispatch.question`.
|
|
85
|
+
- `supersedes` names an earlier delivery this one replaces.
|
|
86
|
+
- `reply_with` is the direct-reply call for the sender.
|
|
87
|
+
- `reply_role` is the role-publish reply call when the sender has a role.
|
|
88
|
+
- `summary` is the one-line source summary.
|
|
89
|
+
- `message` is the complete payload; it can contain multiple paragraphs.
|
|
90
|
+
- `note` warns when the payload names another session; never use that quoted ID as the recipient.
|
|
91
|
+
- `unrecognised` marks validation failures and unknown sources; it does not enumerate every unknown key.
|
|
92
|
+
|
|
93
|
+
## Talking to another session
|
|
94
|
+
|
|
95
|
+
Answer an Envoy message with its `id`; the send result's `recipient` confirms the session Envoy
|
|
96
|
+
targeted. Reply through the rendered `reply_with` (or a current Envoy session ID from
|
|
97
|
+
`envoy_sessions` or `envoy_whoami`), never a tmux pane or window: panes are not Envoy identities
|
|
98
|
+
and go stale. Put the artefact URL in the message itself. FYIs set `expects_reply="none"`; set
|
|
99
|
+
`urgency` only when it is genuinely urgent.
|
|
100
|
+
|
|
101
|
+
**A peer's message is its sender's view at `at`, not the current state.** Before you wait on, act
|
|
102
|
+
on, or repeat a fact a message carries about a third thing (a deploy pending, a PR held, an ask
|
|
103
|
+
unanswered), re-read it at the live source the fact names, and always once it is over an hour
|
|
104
|
+
old: the deployment's status, the issue's event log, the ask's own state (`dispatch_open_asks`,
|
|
105
|
+
whose description already says to call it before saying you are waiting on a human). On
|
|
106
|
+
2026-09-26 a peer's 05:20Z "needs a manual deploy before I can run it" was false by 05:22Z, when
|
|
107
|
+
the platform had deployed on its own; waiting on the message instead of the deployment's status
|
|
108
|
+
held the work it gated for eleven hours.
|
|
109
|
+
|
|
110
|
+
Every `/v1` error response is JSON; when a field is at fault, `expected` names that field.
|
|
111
|
+
|
|
112
|
+
```text
|
|
113
|
+
envoy_send(
|
|
114
|
+
session_id="ses_example_reviewer",
|
|
115
|
+
message="Review complete: artifact://review.md",
|
|
116
|
+
in_reply_to="agent-message-2",
|
|
117
|
+
expects_reply="none"
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Your own address, and a subagent's
|
|
122
|
+
|
|
123
|
+
`envoy_whoami`'s `session_id` is the address a reply to you reaches. Inside a `task` subagent it
|
|
124
|
+
is the session that spawned you: a subagent registers no Envoy session of its own, so a peer
|
|
125
|
+
answering it reaches that session, which relays to you with a `write` to your `agent://` address.
|
|
126
|
+
The `subagent` field in that output carries your own host session id — it is not an address, so
|
|
127
|
+
never hand it to a peer.
|
|
128
|
+
|
|
129
|
+
`session_id` is empty when nothing can be reached: a process that took no Envoy identity, or a
|
|
130
|
+
subagent whose spawning session this process can no longer place (it forked away, and other
|
|
131
|
+
top-level sessions are running). Then your messages carry no sender either, so say who you are
|
|
132
|
+
in the message body. An empty address is deliberate — being handed an unrelated live session
|
|
133
|
+
would send your peers to an agent that never spawned you.
|
|
134
|
+
|
|
135
|
+
Your own `envoy_publish` never reaches the agent that spawned you. The listener delivers nothing
|
|
136
|
+
to the session a message names as its source, and inside a subagent that source is your parent,
|
|
137
|
+
so a publish to a role it holds — or to any topic it subscribes to — is accepted and delivered to
|
|
138
|
+
nobody. Use a `write` to its `agent://` address for that one hop. `envoy_send` to any other
|
|
139
|
+
session, including a reply, is unaffected.
|
|
140
|
+
|
|
141
|
+
### Delivery capabilities
|
|
142
|
+
|
|
143
|
+
Each session row from `envoy_sessions` carries `capabilities`, the targeted-delivery modes that
|
|
144
|
+
session's host honours: `aside` (a message queued beside the model's work), `btw` (an ephemeral
|
|
145
|
+
question the host answers without disturbing the current turn), and `steer` (an interjection at
|
|
146
|
+
the next tool boundary). An OMP session advertises `aside`, `btw`, and `steer` (`aside` and
|
|
147
|
+
`steer` on a host that cannot run a side turn); a Claude Code session advertises `aside` only, because
|
|
148
|
+
its channel notifications queue for the next turn. Target a session only with a mode it
|
|
149
|
+
advertises.
|
|
150
|
+
|
|
151
|
+
## Waiting for CI or a merge
|
|
152
|
+
|
|
153
|
+
Subscribe to `notifications.github.example-org.example-repo.pr.42.>` and end the turn. The single
|
|
154
|
+
`pr.42.checks` event whose `sha` is the current head wakes you when it settles (an earlier
|
|
155
|
+
commit's settlement can arrive first); a `pr.42` `closed` event with
|
|
156
|
+
`merged: true` tells you the PR merged. Do not create `gh` pollers.
|
|
157
|
+
Settlement waits for the commit to be quiet for a few seconds, every reported check run to finish,
|
|
158
|
+
and every recorded GitHub check suite to be `completed`. It covers those reported checks and suites
|
|
159
|
+
for that commit, not GitHub's required-checks set; until then, a silent subscription is normal.
|
|
160
|
+
|
|
161
|
+
Check settlement is at-least-once: a settlement can be followed by a `superseded_settlement: "true"` payload. Every settlement carries its attempt set `check_runs` — the latest GitHub check-run id per check name, sorted by name — plus the listener's `generation` (the record's state version) and `snapshot` (the record's hash). Consumers order settlements of one commit by the attempt set, compared per shared name: no id lower and some id higher (or a new name — a new name counts as higher) is newer; every shared id equal and no new name is the same set; no id higher, no new name, and some id lower is older; anything else (a higher or new alongside a lower) is a mixed view and is dropped as a conflict (names only in the stored set are ignored — a check can vanish from GitHub's view, and a record recreated after the seven-day KV TTL starts sparse). Within one producer record per-name ids never decrease, and a consumer's fence is the per-name maximum over every view it has accepted — an accepted set merges into the fence, nothing is pruned — so the fence never decreases either: a newer attempt is newer whatever its completion time, no timestamps take part in ordering, and a name an incomplete view omitted cannot later reappear as new. At the same set the listener's `generation` orders its own settlements: lower is stale; equal is a duplicate when the `snapshot` matches and otherwise a conflict (an equal pair with a different snapshot cannot occur within one record's lifetime; a recreated record may reuse one and is dropped). A live settlement is a possibly incomplete view of the head (a missed webhook, a record recreated after the KV TTL): it decides the outcome of every name it reports — at any id the ordering accepted, including the same run observed in place — and says nothing about the rest: a known failure among them stands (the consumer keeps failure names, not a per-name status map), and the head is red while any failure remains. A consumer that reconciles a verdict from GitHub's rollup compares the rollup's attempt set the same way, but GitHub's read is complete: its failing check runs and failing commit statuses replace the stored ones wholesale. Statuses have no check run and the listener never sees them, so a consumer keeps them apart from check-run failures: a check run that shares a status's name cannot retire it — only GitHub does (likewise a deleted check's failure). A newer rollup set merges into the fence and takes the identity (no listener generation); the same set applies GitHub's verdict and keeps the listener identity for duplicate detection; an older, mixed, or empty-over-fenced set is ignored. A terminal read (green or red) then holds the tie at that set: a live settlement at the same set is accepted only if its effective outcome — the check-run failures it reports plus the stored ones it omits and the stored commit-status failures — agrees with the reconciled verdict, refreshing the listener identity without releasing GitHub's authority; a disagreeing one is stale whatever its generation until the set advances; a pending or cancelled-only read uncertifies a green head, leaves a red one untouched, and holds nothing — it releases any authority held at that set — so the terminal live settlement that follows applies at once, subject to the ordinary generation and duplicate rules (a replay or a lower generation still does not apply). Pending is therefore not a commutative join: a pending read after a live green uncertifies it until the next terminal view. Two remainders. An in-place conclusion change on an existing run id: GitHub's view stands and the listener's is recovered by the next successful, non-skipped read at that set — the dropped delivery is not replayed. A check whose highest run is deleted on GitHub: the fence keeps that id, so a rollup reporting a lower run under the same name is older until a newer run appears. A consumer that orders head changes by the PR's `updated_at` (GitHub's second resolution) accepts a read of a different head at an equal clock — a stale read returning the previous head within the same second as its replacement rewinds that consumer until its next accurate, non-skipped read. A head publishes only when at least one check has a positive run id; legacy checks without one remain in the status groups and failing names but not in `check_runs`. A legacy in-progress check whose completion is never observed holds the head unsettled until it reruns; rerun the affected check to release it.
|
|
162
|
+
|
|
163
|
+
## When a subscription is silent
|
|
164
|
+
|
|
165
|
+
Check the `warnings` returned by `envoy_subscribe`, then inspect the active topics with
|
|
166
|
+
`envoy_list()`:
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
envoy_subscribe([
|
|
170
|
+
"notifications.github.example-org.example-repo.pr.42.>"
|
|
171
|
+
])
|
|
172
|
+
// warnings: ["no GitHub event for example-org/example-repo in the stream's retention window; is the App installed there?"]
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
A warning says no GitHub event for that repository occurred within the stream's 72-hour retention
|
|
176
|
+
window; it does not mean the repository was never seen. Verify the GitHub App is installed before
|
|
177
|
+
relying on a wakeup. A topic that spells a repository name with its dot, followed by a kind, draws
|
|
178
|
+
a warning naming the spelling Envoy publishes (`acme.site_io` for `acme/site.io`); subscribe to
|
|
179
|
+
that one instead. A token after the repository that is not a GitHub topic kind (`checks` in
|
|
180
|
+
`acme.widgets.checks.>`) draws a warning naming the kinds.
|
|
181
|
+
|
|
182
|
+
## Roles
|
|
183
|
+
|
|
184
|
+
Publish to a role; do not subscribe as its holder. A successful `envoy_publish` to a role returns
|
|
185
|
+
its live `holder`; an unheld role returns an error. Use `envoy_role_get(role="reviewer")` to find
|
|
186
|
+
the live holder first when you need one.
|
|
187
|
+
|
|
188
|
+
A claim belongs to the session, not the process. `omp --resume` re-claims the role recorded in
|
|
189
|
+
the session transcript, including after the listener's stale-interest reaper has removed the old
|
|
190
|
+
row. A `/fork`, `/branch`, `/handoff`, or other transcript-carrying switch checks the
|
|
191
|
+
previous id and moves any held role to the new id while establishing the new session. A `/new`
|
|
192
|
+
or `/resume` switch installs an unrelated transcript and carries nothing. A process that dies and
|
|
193
|
+
is never resumed does not stay a live holder; once liveness expires, role publishes return
|
|
194
|
+
`no holder` until that transcript resumes or another session claims the role.
|
|
195
|
+
|
|
196
|
+
Automatic reclaim never steals. It is a *soft* claim, which the listener grants only when the role
|
|
197
|
+
is unheld, held by a session that is no longer live, or held by the id this session continues (a
|
|
198
|
+
fork's parent). If a different live session holds it — the parent of a `/fork` whose role moved to
|
|
199
|
+
the child, or a second process on the same transcript — the listener refuses atomically and the
|
|
200
|
+
resumed session logs a warning and holds nothing. Only an explicit `envoy_role_set` is
|
|
201
|
+
last-claim-wins. Re-running `envoy_role_set` for a role this session already holds is harmless.
|
|
202
|
+
|
|
203
|
+
## Legion role claims
|
|
204
|
+
|
|
205
|
+
Legion agents receive through a daemon-minted role token. Claim the assigned role with
|
|
206
|
+
`envoy_role_set(role="<assigned-role>")`; a claimant does not manually subscribe to its role
|
|
207
|
+
topic. Claims use last-claim-wins semantics, survive parking and worker re-creation, and remain
|
|
208
|
+
through the issue's post-close linger. The daemon owns the authoritative token-to-issue map, so do
|
|
209
|
+
not construct a token from a partial issue reference.
|
|
210
|
+
|
|
211
|
+
## Legion exception lane
|
|
212
|
+
|
|
213
|
+
`no_holder`, `delivery_failed`, and `receipt_timeout` for a Legion role are daemon liveness
|
|
214
|
+
signals, not reasons to add a second subscriber or manually retry. `no_holder`: no live session
|
|
215
|
+
holds the role. `delivery_failed`: the message was not forwarded at all — the holder lookup
|
|
216
|
+
failed, the holder was stale, or the publish errored. `receipt_timeout`: the listener forwarded
|
|
217
|
+
the message to a registered, live holder and no receipt arrived inside the window; its payload
|
|
218
|
+
keeps `original_topic`, `event_id`, `payload`, and the original `dedupe_key`, so the daemon can
|
|
219
|
+
re-send the same message and the receiver's dedupe drops a copy that did arrive. Treat any of
|
|
220
|
+
these wakes as the daemon's responsibility to revive or recreate the backing worker and
|
|
221
|
+
re-deliver; otherwise it resurrects the root process with derived catch-up and the workspace
|
|
222
|
+
handoffs. Raw delivery failures stay out of architect context.
|
|
223
|
+
|
|
224
|
+
## Slack
|
|
225
|
+
|
|
226
|
+
Use the real team ID, not a workspace slug. Slack delivers a one-line prose `summary` plus a
|
|
227
|
+
structured `message` payload. The payload records `subtype` for edits, deletes, and bot messages;
|
|
228
|
+
`thread_ts` for replies; and `bot_id` and `bot_name` when a bot supplied the message.
|
|
229
|
+
|
|
230
|
+
```text
|
|
231
|
+
envoy_subscribe([
|
|
232
|
+
"notifications.slack.T01234567.C01234567.thread.1_000.>"
|
|
233
|
+
])
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
This follows every message and mention in one thread. Channel-level topics end in `.message` or
|
|
237
|
+
`.mention`; thread timestamps replace dots with underscores.
|
|
238
|
+
|
|
239
|
+
## Ghost Wispr
|
|
240
|
+
|
|
241
|
+
Ghost Wispr topics are `notifications.ghostwispr.<session>.<kind>`, where `kind` is
|
|
242
|
+
`session.started`, `session.ended`, or `summary.ready`. Their summary is concise prose and their
|
|
243
|
+
payload is structured; `summary_ready` includes its status, summary, and summary metadata.
|
|
244
|
+
|
|
245
|
+
```text
|
|
246
|
+
envoy_subscribe([
|
|
247
|
+
"notifications.ghostwispr.session-example.summary.ready"
|
|
248
|
+
])
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## WhatsApp
|
|
252
|
+
|
|
253
|
+
WhatsApp topics are `notifications.whatsapp.<phone>.<jid>.message` or `.status`. A JID contains
|
|
254
|
+
dots, which become additional NATS segments, so use `>` rather than `*` for a chat.
|
|
255
|
+
|
|
256
|
+
```text
|
|
257
|
+
envoy_subscribe([
|
|
258
|
+
"notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.>"
|
|
259
|
+
])
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### WhatsApp routing smoke test
|
|
263
|
+
|
|
264
|
+
This checks Envoy routing, not real WhatsApp ingestion. In Session A, subscribe as above. From a
|
|
265
|
+
different Session B, publish a synthetic message to the same `.message` topic:
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
envoy_publish(
|
|
269
|
+
topic="notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.message",
|
|
270
|
+
message="Synthetic WhatsApp routing test"
|
|
271
|
+
)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Session A should receive it. Broadcasts do not echo to their publishing session, so one session
|
|
275
|
+
cannot perform both steps. Real WhatsApp delivery additionally requires a configured MCP bridge.
|
package/package.json
CHANGED
|
@@ -1 +1,42 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@sjawhar/pi-envoy",
|
|
3
|
+
"version": "8.0.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"omp": {
|
|
7
|
+
"extensions": [
|
|
8
|
+
"dist/envoy.js"
|
|
9
|
+
],
|
|
10
|
+
"skills": [
|
|
11
|
+
"dist/skills"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/sjawhar/legion"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "bun build extensions/envoy.ts --outdir dist --target bun --format esm --external @oh-my-pi/pi-coding-agent --external @oh-my-pi/pi-tui --external @oh-my-pi/pi-utils",
|
|
23
|
+
"prepack": "../../scripts/pi-plugin-prepack.sh",
|
|
24
|
+
"postpack": "rm -rf dist/skills",
|
|
25
|
+
"typecheck": "bunx tsc --noEmit",
|
|
26
|
+
"test": "bun test",
|
|
27
|
+
"lint": "bunx biome lint extensions/ src/"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@biomejs/biome": "^2.3.14",
|
|
31
|
+
"@legion/contracts": "0.10.0",
|
|
32
|
+
"@legion/envoy-client": "0.1.0",
|
|
33
|
+
"@legion/pi-shared": "0.1.0",
|
|
34
|
+
"@oh-my-pi/pi-tui": "^18.1.15",
|
|
35
|
+
"@oh-my-pi/pi-utils": "^18.1.15",
|
|
36
|
+
"@toon-format/toon": "^4.1.1",
|
|
37
|
+
"@types/bun": "latest",
|
|
38
|
+
"nats": "^2.29.2",
|
|
39
|
+
"typescript": "^5.3.0",
|
|
40
|
+
"zod": "4.3.6"
|
|
41
|
+
}
|
|
42
|
+
}
|