@sjawhar/claude-envoy 0.0.0 → 1.1.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.
Files changed (113) hide show
  1. package/.claude-plugin/plugin.json +6 -0
  2. package/.mcp.json +11 -0
  3. package/.omp-plugin/plugin.json +6 -0
  4. package/AGENTS.md +343 -0
  5. package/README.md +639 -0
  6. package/bin/dispatch +6 -0
  7. package/bin/envoy-channel.ts +9 -0
  8. package/bin/envoy-send.ts +33 -0
  9. package/dist/THIRD_PARTY_NOTICES +816 -0
  10. package/dist/dispatch.js +27707 -0
  11. package/dist/envoy-channel.js +57695 -0
  12. package/dist/session-hook.js +26328 -0
  13. package/dist/session-title-hook.js +116 -0
  14. package/hooks/hooks.json +60 -0
  15. package/hooks/session-hook.ts +512 -0
  16. package/hooks/session-title-hook.ts +46 -0
  17. package/legion/.claude-plugin/plugin.json +4 -0
  18. package/legion/agents/deep-worker.md +68 -0
  19. package/legion/agents/oracle.md +35 -0
  20. package/legion/agents/plan-gap-analyst.md +65 -0
  21. package/legion/agents/plan-reviewer.md +104 -0
  22. package/legion/agents/thermonuclear-code-quality.md +30 -0
  23. package/legion/agents/thermonuclear-deep-review.md +30 -0
  24. package/legion/hooks/hooks.json +89 -0
  25. package/legion/skills/ce-simplify-code/LICENSE +21 -0
  26. package/legion/skills/ce-simplify-code/SKILL.md +66 -0
  27. package/legion/skills/ce-simplify-code/references/personas/code-quality-reviewer.md +17 -0
  28. package/legion/skills/ce-simplify-code/references/personas/code-reuse-reviewer.md +7 -0
  29. package/legion/skills/ce-simplify-code/references/personas/efficiency-reviewer.md +11 -0
  30. package/legion/skills/leading-a-work-stream/SKILL.md +96 -0
  31. package/legion/skills/leading-a-work-stream/references/daily-plan.md +8 -0
  32. package/legion/skills/leading-a-work-stream/references/decision-rights.md +6 -0
  33. package/legion/skills/leading-a-work-stream/references/direction-and-coaching.md +13 -0
  34. package/legion/skills/leading-a-work-stream/references/dispatch-upkeep.md +8 -0
  35. package/legion/skills/leading-a-work-stream/references/sources.md +9 -0
  36. package/legion/skills/leading-a-work-stream/references/speed-tick.md +44 -0
  37. package/legion/skills/leading-a-work-stream/references/user-quotes.md +7 -0
  38. package/legion/skills/legion-architect/SKILL.md +86 -0
  39. package/legion/skills/legion-architect/references/acceptance.md +208 -0
  40. package/legion/skills/legion-architect/references/decomposition.md +129 -0
  41. package/legion/skills/legion-architect/references/design-gate.md +206 -0
  42. package/legion/skills/legion-architect/references/merge-notices.md +91 -0
  43. package/legion/skills/legion-architect/references/wake-routing.md +41 -0
  44. package/legion/skills/legion-controller/SKILL.md +151 -0
  45. package/legion/skills/legion-controller/references/admission.md +22 -0
  46. package/legion/skills/legion-controller/references/backlog-stewardship.md +101 -0
  47. package/legion/skills/legion-controller/references/daily-report.md +75 -0
  48. package/legion/skills/legion-controller/references/escalation.md +82 -0
  49. package/legion/skills/legion-controller/references/finding-asks.md +181 -0
  50. package/legion/skills/legion-controller/references/parked-decisions.md +198 -0
  51. package/legion/skills/legion-controller/references/session.md +100 -0
  52. package/legion/skills/legion-controller/references/slack-mentions.md +154 -0
  53. package/legion/skills/legion-controller/references/triage.md +96 -0
  54. package/legion/skills/legion-controller/references/wakes.md +132 -0
  55. package/legion/skills/legion-oracle/SKILL.md +48 -0
  56. package/legion/skills/legion-retro/SKILL.md +112 -0
  57. package/legion/skills/legion-retro/references/learnings.md +60 -0
  58. package/legion/skills/legion-retro/references/pr-body-content.md +56 -0
  59. package/legion/skills/legion-worker/SKILL.md +69 -0
  60. package/legion/skills/legion-worker/references/cleanup-deletion.md +22 -0
  61. package/legion/skills/legion-worker/references/conflicts-and-rewrites.md +164 -0
  62. package/legion/skills/legion-worker/references/default-branch-check.md +53 -0
  63. package/legion/skills/legion-worker/references/findings.md +199 -0
  64. package/legion/skills/legion-worker/references/merge-gate.md +353 -0
  65. package/legion/skills/legion-worker/references/pr-body.md +218 -0
  66. package/legion/skills/legion-worker/references/review-threads.md +77 -0
  67. package/legion/skills/legion-worker/references/systematic-rename.md +19 -0
  68. package/legion/skills/legion-worker/references/tools.md +708 -0
  69. package/legion/skills/thermonuclear-code-quality/LICENSE +21 -0
  70. package/legion/skills/thermonuclear-code-quality/SKILL.md +60 -0
  71. package/legion/skills/thermonuclear-deep-review/LICENSE +21 -0
  72. package/legion/skills/thermonuclear-deep-review/SKILL.md +80 -0
  73. package/package.json +49 -1
  74. package/scripts/build-inputs.txt +84 -0
  75. package/scripts/build.ts +415 -0
  76. package/scripts/check-packed-skills.sh +50 -0
  77. package/scripts/pack-skills.sh +146 -0
  78. package/scripts/smoke-channel.sh +178 -0
  79. package/scripts/smoke-clear-rebind.sh +164 -0
  80. package/scripts/smoke-session-title.sh +245 -0
  81. package/scripts/verify-build.sh +88 -0
  82. package/skills/agent-secrets/SKILL.md +180 -0
  83. package/skills/asd-ste100/LICENSE +21 -0
  84. package/skills/asd-ste100/SKILL.md +140 -0
  85. package/skills/asd-ste100/examples/before-after.md +92 -0
  86. package/skills/asd-ste100/references/writing-rules.md +59 -0
  87. package/skills/dispatch/SKILL.md +89 -0
  88. package/skills/dispatch-first/SKILL.md +46 -0
  89. package/skills/dispatch-writing-specs/SKILL.md +80 -0
  90. package/skills/envoy/SKILL.md +82 -0
  91. package/skills/envoy/references/check-settlement.md +62 -0
  92. package/skills/envoy/references/deliveries.md +67 -0
  93. package/skills/envoy/references/legion.md +14 -0
  94. package/skills/envoy/references/roles.md +12 -0
  95. package/skills/envoy/references/sources.md +41 -0
  96. package/skills/outside-message/SKILL.md +111 -0
  97. package/skills/plain-words/SKILL.md +88 -0
  98. package/src/channel-forwarder.ts +262 -0
  99. package/src/claude-channel.ts +705 -0
  100. package/src/claude-session.ts +38 -0
  101. package/src/envoy-channel-server.ts +2115 -0
  102. package/src/envoy-client.ts +26 -0
  103. package/src/envoy-off.ts +62 -0
  104. package/src/legion-claim.ts +211 -0
  105. package/src/legion-inbox.ts +110 -0
  106. package/src/legion-phase-hook.ts +554 -0
  107. package/src/legion-tool.ts +69 -0
  108. package/src/replay-state.ts +97 -0
  109. package/src/send-arguments.ts +13 -0
  110. package/src/session-identity.ts +165 -0
  111. package/src/session-state.ts +142 -0
  112. package/src/session-title.ts +251 -0
  113. package/src/subscription-topics.ts +16 -0
@@ -0,0 +1,88 @@
1
+ ---
2
+ name: plain-words
3
+ description: "Use before you send or publish any text a person reads: an ask, a spec, a Dispatch comment, a message to a colleague, a pull-request body, a commit message, a report, a status update. It removes empty verb phrases and replaces each with the literal thing. Use it after you write and before you send."
4
+ ---
5
+
6
+ # Say the literal thing
7
+
8
+ The operator made this a rule: delete the empty verb phrases agents keep using, and say the literal
9
+ thing instead.
10
+
11
+ Run this over your text before you send it. It takes a minute and it is not optional.
12
+
13
+ ## The test
14
+
15
+ Take the verb phrase out and ask: **what literally happens?** Write that. If you cannot say what
16
+ happens, you did not know, and the phrase hid it from you as well as from the reader.
17
+
18
+ ## The phrases, in the order agents actually use them
19
+
20
+ Counted on 2026-10-30, in one day's 198,898 assistant messages across one fleet of agents:
21
+
22
+ | Never | Used that day | What to write |
23
+ |---|---:|---|
24
+ | land the change, land the PR | **1,053** | merge it, release it, deploy it, apply it — whichever one it is |
25
+ | unblock the work | 151 | X can start once Y merges |
26
+ | name the problem, name the owner, name a date | 23 | say the problem; who owns it is X; it is due on 11-02 |
27
+ | it pays for itself, that pays | 13 | it costs $16,306 and returns $60,379 a month |
28
+ | carry it to X, carry the finding | 11 | tell X; send it to X; put it on the issue |
29
+ | this shows the gap, the number shows | 5 | the gap is 12 of 30; the number is 1.17 |
30
+
31
+ **The trigger for the commonest one:** you reach for "land" when you cannot say whether it merged,
32
+ released or deployed. If you can say which, say it. If you cannot, that is the sentence — "it is
33
+ approved and not merged" — and it is more useful than any verb.
34
+
35
+ **"name the X" is the one an instruction does not fix.** It survived at the same rate with this
36
+ skill and without it (11 drafts against 10, over 72). Use the test rather than the row: if deleting
37
+ "name" leaves a sentence that says what happens, delete it; if it does not, you were about to write
38
+ a verb for an action you had not decided on.
39
+
40
+ Three phrases were cut from this table because the same count found zero uses of them in a day:
41
+ "move the needle", "drive it to zero", "surface the issue". A rule against a phrase nobody writes
42
+ costs a reader attention and buys nothing.
43
+
44
+ ## Untested, and here because the fault is the same
45
+
46
+ These were measured at 0 of 72 drafts in both arms, so this skill has no evidence that naming them
47
+ changes anything. They stay as examples, not as rules:
48
+
49
+ - **A verb with no actor.** "The fix is being prioritised." Who is doing it, and when?
50
+ - **A noun that should be a number.** "Significant improvement." From what to what?
51
+ - **A promise with no object.** "We will follow up." On what, with whom, by when?
52
+ - **A hedge that costs the reader a second read.** "It may be worth considering whether we should."
53
+ Either recommend it or do not raise it.
54
+
55
+ **Where this skill helps, measured:** over 72 drafts of six real status messages, when the writer
56
+ works from notes it went from 16 of 18 clean to 18 of 18. When the task was already prose, both arms
57
+ were 18 of 18. So it earns its place where an agent writes up its own work, which is where these
58
+ phrases live.
59
+
60
+ ## Do not shout
61
+
62
+ The operator, on an ask written in capitalised headers: why do you keep writing your asks in this
63
+ obnoxious format with all caps headers and stuff?
64
+
65
+ No capitalised headers. No capitalised phrases for emphasis. Bold only where a number, a name or a
66
+ choice needs to be found again.
67
+
68
+ The reason it appears: an agent writes a long passage, cannot find its point, and puts a handle on
69
+ it so a reader can. **The handle is a symptom. Cut the passage instead.** A paragraph that needs a
70
+ shouted header to be skimmable is a paragraph nobody should have to skim.
71
+
72
+ The same goes for a message to another agent. A capitalised line in a brief reads as an instruction
73
+ shouted twice, and a reader who sees three of them stops seeing any.
74
+
75
+ ## What stays
76
+
77
+ Plain verbs that describe a real action stay: merge, delete, send, read, measure, kill a process by
78
+ pid, repoint a link. The rule is against phrases that sound like an action and name none.
79
+
80
+ A term of art stays when it is the literal name of the thing: a merge queue merges, a garbage
81
+ collector collects, a sweep sweeps.
82
+
83
+ ## Where this fits
84
+
85
+ `skill://asd-ste100` constrains grammar and vocabulary for a reader whose first language may not be
86
+ English. This skill removes verb phrases that carry no fact. Neither removes AI writing tells, so
87
+ where your session has a pass for those it is a third one. Run them all on an outside message
88
+ (`skill://outside-message`); run this one on everything.
@@ -0,0 +1,262 @@
1
+ import type { RelayHeaders } from "@legion/envoy-client/consumer-delivery"
2
+ import {
3
+ createDeliveryDedupe,
4
+ type DedupeIdentity,
5
+ DedupeIdentitySchema,
6
+ type DeliveryDedupe,
7
+ readArrivals,
8
+ } from "@legion/envoy-client/delivery"
9
+ import { messageFor } from "@legion/envoy-client/errors"
10
+ import { expandSubscriptionTopics } from "@legion/envoy-client/transport"
11
+ import { z } from "zod"
12
+
13
+ /** A message as the broker yields it (a nats.js `Msg` satisfies this). */
14
+ export interface ChannelBrokerMessage {
15
+ readonly subject: string
16
+ readonly data: Uint8Array
17
+ readonly reply?: string
18
+ /** The message's NATS headers; only a relay frame (consumer delivery) carries any read here. */
19
+ readonly headers?: RelayHeaders
20
+ }
21
+
22
+ /** A broker message as the forwarder delivers it: decoded once and classified. */
23
+ export interface ChannelInboundMessage extends ChannelBrokerMessage {
24
+ /** `data` decoded as UTF-8 once, at the broker boundary. */
25
+ readonly raw: string
26
+ /**
27
+ * The event already reached the channel queue, or its relayed copy is the one shown
28
+ * (`ChannelForwarderOptions.relayed`); direct request-reply still needs its receipt.
29
+ */
30
+ readonly duplicate?: true
31
+ /**
32
+ * The topic the envelope itself names. It differs from `subject` when the listener forwarded
33
+ * a lane (a role topic) onto the direct subject and is waiting for a receipt.
34
+ */
35
+ readonly envelopeTopic?: string
36
+ }
37
+
38
+ export interface ChannelTopicSubscription extends AsyncIterable<ChannelBrokerMessage> {
39
+ unsubscribe(): void
40
+ }
41
+
42
+ /** A nats.js connection status event; only its `type` is read. */
43
+ export interface ChannelConnectionStatus {
44
+ readonly type: string
45
+ }
46
+
47
+ /** The NATS connection surface used by a channel session. */
48
+ export interface ChannelForwarderConnection {
49
+ subscribe(subject: string): ChannelTopicSubscription
50
+ publish(subject: string, data: Uint8Array): void
51
+ flush(): Promise<void>
52
+ isClosed(): boolean
53
+ drain(): Promise<void>
54
+ close(): Promise<void>
55
+ /** The connection's status events (nats.js `status()`), which end when it closes. */
56
+ status(): AsyncIterable<ChannelConnectionStatus>
57
+ }
58
+
59
+ export interface ChannelForwarder {
60
+ /** Start delivering a topic; a topic already followed is left alone. */
61
+ follow(topic: string): void
62
+ /** Stop delivering the given topics, or every topic when the list is empty. */
63
+ unfollow(topics: readonly string[]): Promise<void>
64
+ /** Topics currently delivered, in follow order. */
65
+ topics(): readonly string[]
66
+ /** True once the connection is closed for good. */
67
+ isClosed(): boolean
68
+ /** Stop every subscription and drain the NATS connection; never rejects. */
69
+ close(): Promise<void>
70
+ }
71
+
72
+ /** The replay ledger as the live path sees it (`@legion/envoy-client/replay`). */
73
+ export interface ChannelForwarderLedger {
74
+ /** Whether the session was shown the frame already, before this process started included. */
75
+ delivered(frame: DedupeIdentity): boolean
76
+ /** Records a frame Claude Code was handed, so no replay shows it again; never rejects. */
77
+ recordDelivered(frame: DedupeIdentity): Promise<void>
78
+ }
79
+
80
+ export interface ChannelForwarderOptions {
81
+ /**
82
+ * Hands one frame on and resolves whether Claude Code got it; `false`, or a rejection, releases
83
+ * the frame's dedupe claim, so a re-send of it still arrives.
84
+ */
85
+ readonly deliver: (message: ChannelInboundMessage) => Promise<boolean>
86
+ /** How long close waits for a broker drain before closing outright. */
87
+ readonly drainTimeoutMs?: number
88
+ /** The claims of the live path; a replay engine reads them to leave a frame in flight alone. */
89
+ readonly dedupe?: DeliveryDedupe
90
+ readonly ledger?: ChannelForwarderLedger
91
+ /**
92
+ * A frame whose delivery rejected, after its claim was released: `topic` is the subscription it
93
+ * came on, which says whether a replay will show it again, and `receivedAt` when the
94
+ * subscription yielded it (`Date.now`, `readArrivals`), however long the frames before it took
95
+ * to deliver, which says whether a replay cursor started past it.
96
+ */
97
+ readonly failed?: (
98
+ message: ChannelInboundMessage,
99
+ topic: string,
100
+ error: unknown,
101
+ receivedAt: number,
102
+ ) => void
103
+ /**
104
+ * Whether the listener relays the subscription `topic`'s subject from the session's consumer
105
+ * (consumer delivery), which shows its frames: a core frame on it claims nothing and goes on as
106
+ * a duplicate, only to answer a lane's receipt. Read before the claim, so the core copy never
107
+ * holds the claim its relayed copy needs.
108
+ */
109
+ readonly relayed?: (topic: string) => boolean
110
+ }
111
+
112
+ interface Following {
113
+ readonly subscription: ChannelTopicSubscription
114
+ readonly done: Promise<void>
115
+ }
116
+
117
+ // A delivery's identity fields parse independently: a field the shared rule refuses identifies
118
+ // nothing, and its valid sibling can still recognise a repeat. `topic` follows the same rule
119
+ // before it is exposed as the envelope topic.
120
+ const DeliveryIdentity = DedupeIdentitySchema.extend({
121
+ event_id: DedupeIdentitySchema.shape.event_id.catch(undefined),
122
+ dedupe_key: DedupeIdentitySchema.shape.dedupe_key.catch(undefined),
123
+ source: DedupeIdentitySchema.shape.source.catch(undefined),
124
+ source_event_id: DedupeIdentitySchema.shape.source_event_id.catch(undefined),
125
+ topic: z.string().min(1).optional().catch(undefined),
126
+ })
127
+
128
+ const decoder = new TextDecoder()
129
+ const DEFAULT_DRAIN_TIMEOUT_MS = 1_000
130
+
131
+ /** A frame's identity fields, each kept only when valid; undefined for a frame that is not JSON. */
132
+ export function deliveryIdentity(raw: string): z.infer<typeof DeliveryIdentity> | undefined {
133
+ let parsed: unknown
134
+ try {
135
+ parsed = JSON.parse(raw)
136
+ } catch {
137
+ return undefined
138
+ }
139
+ const identity = DeliveryIdentity.safeParse(parsed)
140
+ return identity.success ? identity.data : undefined
141
+ }
142
+
143
+ function report(what: string, error: unknown): void {
144
+ process.stderr.write(`envoy-channel: ${what} — ${messageFor(error)}\n`)
145
+ }
146
+
147
+ /**
148
+ * One channel process receives both its direct Envoy route and every followed
149
+ * topic. It deduplicates at the broker boundary, by event id and by dedupe key,
150
+ * so an event covered by several patterns, and a re-send of one already
151
+ * delivered, still becomes one Claude Code channel notification. With a replay
152
+ * ledger, a frame the session was shown before this process started is dropped
153
+ * too, and each frame handed on is recorded there before its claim settles.
154
+ */
155
+ export function createChannelForwarder(
156
+ connection: ChannelForwarderConnection,
157
+ options: ChannelForwarderOptions,
158
+ ): ChannelForwarder {
159
+ const following = new Map<string, Following>()
160
+ const dedupe = options.dedupe ?? createDeliveryDedupe()
161
+ const drainTimeoutMs = options.drainTimeoutMs ?? DEFAULT_DRAIN_TIMEOUT_MS
162
+
163
+ const deliver = async (topic: string, subscription: ChannelTopicSubscription): Promise<void> => {
164
+ try {
165
+ for await (const { message, receivedAt } of readArrivals(subscription)) {
166
+ const raw = decoder.decode(message.data)
167
+ const identity = deliveryIdentity(raw)
168
+ // The relay shows this subject's frames: the core copy answers only a lane's receipt.
169
+ let claim = options.relayed?.(topic) === true ? undefined : dedupe.claim(identity)
170
+ // A repeat of a frame the session was shown before this process started, which only the
171
+ // replay ledger knows of: it goes on as a duplicate, which still answers a lane's receipt.
172
+ if (claim !== undefined && identity !== undefined && options.ledger?.delivered(identity)) {
173
+ claim.settle()
174
+ claim = undefined
175
+ }
176
+ // A nats.js Msg exposes subject/data/reply through prototype getters,
177
+ // which an object spread would silently drop; copy the fields by name.
178
+ const inbound: ChannelInboundMessage = {
179
+ subject: message.subject,
180
+ data: message.data,
181
+ raw,
182
+ // nats.js MsgImpl.reply is a getter returning "" when unset; absent means no reply address.
183
+ ...(message.reply ? { reply: message.reply } : {}),
184
+ ...(identity?.topic === undefined ? {} : { envelopeTopic: identity.topic }),
185
+ ...(claim === undefined ? { duplicate: true } : {}),
186
+ }
187
+ let handed: boolean
188
+ try {
189
+ handed = await options.deliver(inbound)
190
+ } catch (error) {
191
+ claim?.release()
192
+ report(`could not deliver a message on ${message.subject}`, error)
193
+ // A duplicate was never being shown; only its receipt can have failed.
194
+ if (claim !== undefined) options.failed?.(inbound, topic, error, receivedAt)
195
+ continue
196
+ }
197
+ if (claim === undefined) continue
198
+ if (!handed) {
199
+ claim.release()
200
+ continue
201
+ }
202
+ // Recorded before the claim settles, so a replay never finds the frame neither pending
203
+ // nor in the ledger and shows it a second time. The next frame does not wait for the
204
+ // write: a repeat of this one meanwhile still finds its claim.
205
+ const taken = claim
206
+ const settle = () => taken.settle()
207
+ if (identity === undefined || options.ledger === undefined) settle()
208
+ else void options.ledger.recordDelivered(identity).then(settle, settle)
209
+ }
210
+ } catch (error) {
211
+ report(`delivery from ${topic} stopped`, error)
212
+ }
213
+ }
214
+
215
+ const stop = async (topics: readonly string[]): Promise<void> => {
216
+ const stopping = topics.flatMap((topic) => {
217
+ const entry = following.get(topic)
218
+ if (entry === undefined) return []
219
+ following.delete(topic)
220
+ entry.subscription.unsubscribe()
221
+ return [entry.done]
222
+ })
223
+ await Promise.all(stopping)
224
+ }
225
+
226
+ return {
227
+ follow(topic) {
228
+ for (const subject of expandSubscriptionTopics([topic])) {
229
+ if (following.has(subject)) continue
230
+ const subscription = connection.subscribe(subject)
231
+ following.set(subject, { subscription, done: deliver(subject, subscription) })
232
+ }
233
+ },
234
+ unfollow(topics) {
235
+ return stop(topics.length === 0 ? [...following.keys()] : expandSubscriptionTopics(topics))
236
+ },
237
+ topics() {
238
+ return [...following.keys()]
239
+ },
240
+ isClosed() {
241
+ return connection.isClosed()
242
+ },
243
+ async close() {
244
+ await stop([...following.keys()])
245
+ if (connection.isClosed()) return
246
+ const timeout = Promise.withResolvers<void>()
247
+ const timer = setTimeout(timeout.resolve, drainTimeoutMs)
248
+ try {
249
+ await Promise.race([connection.drain(), timeout.promise])
250
+ } catch (error) {
251
+ report("draining the broker connection failed", error)
252
+ }
253
+ clearTimeout(timer)
254
+ if (connection.isClosed()) return
255
+ try {
256
+ await connection.close()
257
+ } catch (error) {
258
+ report("closing the broker connection failed", error)
259
+ }
260
+ },
261
+ }
262
+ }