@sjawhar/pi-legion-envoy 0.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.
@@ -0,0 +1,37 @@
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, mutate lifecycle labels, or create human dispatch threads.
29
+ When an issue needs a product, scope, cross-phase, or human decision, they send the owning
30
+ architect the verified observation and decision needed through hub.
31
+
32
+ ## Durable artifacts
33
+
34
+ Phase handoffs are committed in lifecycle order: architect, plan, implement, test, and review.
35
+ The reviewer removes `.legion/` as its final commit; retro records its learning in
36
+ `docs/solutions/` and writes no handoff. GitHub comments and reviews carry the required Legion
37
+ 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.
@@ -0,0 +1,203 @@
1
+ ---
2
+ name: github
3
+ description: Manage GitHub issues via Projects V2. Use when LEGION_ISSUE_BACKEND=github.
4
+ ---
5
+
6
+ # GitHub (gh CLI)
7
+
8
+ Direct CLI operations via `gh`. No embedded MCP — all commands are shell invocations.
9
+
10
+ ## Setup
11
+
12
+ Requires `gh` CLI installed and authenticated:
13
+ ```bash
14
+ gh auth login
15
+ ```
16
+
17
+ ## Operations
18
+
19
+ ### Search/List Issues (via project)
20
+
21
+ List all items in a GitHub Project V2:
22
+
23
+ ```bash
24
+ gh project item-list $PROJECT_NUM --owner $OWNER --format json
25
+ ```
26
+
27
+ **Parameters:**
28
+ - `$PROJECT_NUM`: Project number (from `LEGION_ID` format: `owner/project-number`)
29
+ - `$OWNER`: Repository owner
30
+ - `--format json`: Returns structured data for parsing
31
+
32
+ **Example:**
33
+ ```bash
34
+ gh project item-list 42 --owner acme --format json | jq '.items[] | {id, title, status}'
35
+ ```
36
+
37
+ ### Get Issue Details
38
+
39
+ Fetch full issue metadata:
40
+
41
+ ```bash
42
+ gh issue view $ISSUE_NUMBER --json title,body,labels,comments,state -R $OWNER/$REPO
43
+ ```
44
+
45
+ **Parameters:**
46
+ - `$ISSUE_NUMBER`: Issue number (e.g., `123`)
47
+ - `-R $OWNER/$REPO`: Repository (required for multi-repo support)
48
+ - `--json`: Fields to return (title, body, labels, comments, state, etc.)
49
+
50
+ **Example:**
51
+ ```bash
52
+ gh issue view 123 --json title,body,labels,state -R acme/backend
53
+ ```
54
+
55
+ ### Update Status (Projects V2 — GraphQL)
56
+
57
+ Update issue status in a GitHub Project V2. Requires field and option IDs from project schema:
58
+
59
+ ```bash
60
+ gh api graphql -f query='mutation {
61
+ updateProjectV2ItemFieldValue(input: {
62
+ projectId: "$PROJECT_ID"
63
+ itemId: "$ITEM_ID"
64
+ fieldId: "$STATUS_FIELD_ID"
65
+ value: { singleSelectOptionId: "$OPTION_ID" }
66
+ }) { projectV2Item { id } }
67
+ }'
68
+ ```
69
+
70
+ **Parameters:**
71
+ - `$PROJECT_ID`: GraphQL ID of the project (not the number)
72
+ - `$ITEM_ID`: GraphQL ID of the issue in the project
73
+ - `$STATUS_FIELD_ID`: GraphQL ID of the Status field
74
+ - `$OPTION_ID`: GraphQL ID of the status option (e.g., "In Progress", "Done")
75
+
76
+ **Note:** Field and option IDs must be resolved from the project schema. The controller caches these after first query.
77
+
78
+ **Resolve IDs (one-time):**
79
+ ```bash
80
+ gh api graphql -f query='query {
81
+ repository(owner: "$OWNER", name: "$REPO") {
82
+ projectV2(number: $PROJECT_NUM) {
83
+ fields(first: 20) {
84
+ nodes {
85
+ ... on ProjectV2SingleSelectField {
86
+ id
87
+ name
88
+ options { id name }
89
+ }
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }'
95
+ ```
96
+
97
+ ### Add Label
98
+
99
+ Add a label to an issue (additive — does not remove existing labels):
100
+
101
+ ```bash
102
+ gh issue edit $ISSUE_NUMBER --add-label "needs-approval" -R $OWNER/$REPO
103
+ ```
104
+
105
+ **Parameters:**
106
+ - `$ISSUE_NUMBER`: Issue number
107
+ - `--add-label`: Label to add (can be used multiple times)
108
+ - `-R $OWNER/$REPO`: Repository
109
+
110
+ **Example:**
111
+ ```bash
112
+ gh issue edit 123 --add-label "needs-approval" -R acme/backend
113
+ ```
114
+
115
+ ### Remove Label
116
+
117
+ Remove a label from an issue:
118
+
119
+ ```bash
120
+ gh issue edit $ISSUE_NUMBER --remove-label "needs-approval" -R $OWNER/$REPO
121
+ ```
122
+
123
+ **Parameters:**
124
+ - `$ISSUE_NUMBER`: Issue number
125
+ - `--remove-label`: Label to remove (can be used multiple times)
126
+ - `-R $OWNER/$REPO`: Repository
127
+
128
+ **Example:**
129
+ ```bash
130
+ gh issue edit 123 --remove-label "legion-backlog" -R acme/backend
131
+ ```
132
+
133
+ ### Comment on Issue
134
+
135
+ Add a comment to an issue:
136
+
137
+ ```bash
138
+ gh issue comment $ISSUE_NUMBER --body "Fixed in commit abc123" -R $OWNER/$REPO
139
+ ```
140
+
141
+ **Parameters:**
142
+ - `$ISSUE_NUMBER`: Issue number
143
+ - `--body`: Comment text (supports Markdown)
144
+ - `-R $OWNER/$REPO`: Repository
145
+
146
+ **Example:**
147
+ ```bash
148
+ gh issue comment 123 --body "Implemented in PR #456" -R acme/backend
149
+ ```
150
+
151
+ ### Create Issue
152
+
153
+ Create a new issue:
154
+
155
+ ```bash
156
+ gh issue create --title "Bug: Login fails" --body "Details" -R $OWNER/$REPO
157
+ ```
158
+
159
+ **Parameters:**
160
+ - `--title`: Issue title (required)
161
+ - `--body`: Issue description (optional, supports Markdown)
162
+ - `-R $OWNER/$REPO`: Repository
163
+
164
+ **Example:**
165
+ ```bash
166
+ gh issue create --title "Feature: Add dark mode" --body "User request from #789" -R acme/backend
167
+ ```
168
+
169
+ ## Key Differences from Linear
170
+
171
+ | Aspect | Linear | GitHub |
172
+ |--------|--------|--------|
173
+ | **Labels** | Replace all (read-modify-write) | Additive (`--add-label`, `--remove-label`) |
174
+ | **Status** | Direct field update | Projects V2 GraphQL mutation |
175
+ | **PR Association** | Attachment field | Native (issue ↔ PR link) |
176
+ | **API** | MCP tool dispatch | Direct `gh` CLI |
177
+ | **Multi-repo** | Single team | `-R owner/repo` per command |
178
+
179
+ ## Important Notes
180
+
181
+ - **Always specify `-R $OWNER/$REPO`** for multi-repo project support
182
+ - **Labels are additive**: Use `--add-label` and `--remove-label` separately (unlike Linear which replaces all)
183
+ - **Status updates require Projects V2 GraphQL** — not just issue labels
184
+ - **PR association is automatic** — GitHub links issues and PRs natively
185
+ - **`$OWNER` and `$REPO` come from `LEGION_ID`** (format: `owner/project-number`)
186
+ - **Field/option IDs must be cached** by the controller after first resolution
187
+
188
+ ## Error Handling
189
+
190
+ Common errors and solutions:
191
+
192
+ | Error | Cause | Solution |
193
+ |-------|-------|----------|
194
+ | `Could not resolve to a Repository` | Wrong `-R` format | Use `-R owner/repo` (not `owner-repo`) |
195
+ | `Could not resolve to an Issue` | Issue doesn't exist | Verify issue number is correct |
196
+ | `GraphQL error: Field not found` | Wrong field ID | Re-resolve field IDs from project schema |
197
+ | `Not authenticated` | `gh` not logged in | Run `gh auth login` |
198
+
199
+ ## Reference
200
+
201
+ - **Project number**: Visible in GitHub UI (e.g., `https://github.com/orgs/acme/projects/42` → `42`)
202
+ - **Issue number**: Visible in URL (e.g., `https://github.com/acme/backend/issues/123` → `123`)
203
+ - **GraphQL IDs**: Base64-encoded, returned by GraphQL queries (not human-readable)