@sjawhar/opencode-legion-envoy 1.8.0 → 1.10.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.
@@ -13633,10 +13633,20 @@ var CommentEventPayloadSchema = object({
13633
13633
  var MessageEventPayloadSchema = object({
13634
13634
  id: string2().optional(),
13635
13635
  body: string2().optional(),
13636
- reply_to: string2().nullish(),
13636
+ target: string2().nullish(),
13637
+ in_reply_to: string2().nullish(),
13637
13638
  reply_body: string2().optional(),
13638
13639
  author: object({ kind: string2(), id: string2() }).optional()
13639
13640
  });
13641
+ var MessageDeliveryEventPayloadSchema = object({
13642
+ message_id: string2().optional(),
13643
+ attempt: number2().int().positive().optional(),
13644
+ delivery: _enum2(["btw", "aside", "steer"]).optional(),
13645
+ session_id: string2().optional(),
13646
+ title: string2().optional(),
13647
+ state: _enum2(["sent", "failed"]).optional(),
13648
+ error: string2().optional()
13649
+ });
13640
13650
  var ChildStatusEventPayloadSchema = object({
13641
13651
  child_key: string2().optional(),
13642
13652
  from: string2().optional(),
@@ -13835,7 +13845,7 @@ var dispatchToolSpecs = [
13835
13845
  arguments: (z) => ({
13836
13846
  issue: z.string().describe(ISSUE_REFERENCE),
13837
13847
  body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
13838
- reply_to: z.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation.").optional()
13848
+ in_reply_to: z.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation.").optional()
13839
13849
  })
13840
13850
  },
13841
13851
  {
@@ -14998,13 +15008,13 @@ function askId(args) {
14998
15008
  }
14999
15009
  return reference.id;
15000
15010
  }
15001
- function messageReplyTo(args) {
15002
- const replyTo = optionalString(args, "reply_to");
15003
- if (replyTo === undefined || !replyTo.startsWith("dispatch://"))
15004
- return replyTo;
15005
- const reference = parseDispatchRef(replyTo);
15011
+ function messageInReplyTo(args) {
15012
+ const inReplyTo = optionalString(args, "in_reply_to");
15013
+ if (inReplyTo === undefined || !inReplyTo.startsWith("dispatch://"))
15014
+ return inReplyTo;
15015
+ const reference = parseDispatchRef(inReplyTo);
15006
15016
  if (reference?.kind !== "message") {
15007
- throw new Error("reply_to must be a bare message id or a dispatch://.../message/<id> reference");
15017
+ throw new Error("in_reply_to must be a bare message id or a dispatch://.../message/<id> reference");
15008
15018
  }
15009
15019
  return reference.id;
15010
15020
  }
@@ -15452,10 +15462,10 @@ async function executeDispatchTool(input) {
15452
15462
  };
15453
15463
  }
15454
15464
  case "dispatch_message": {
15455
- const replyTo = messageReplyTo(args);
15465
+ const inReplyTo = messageInReplyTo(args);
15456
15466
  const message = await client.message(issue(), {
15457
15467
  body: stringArg(args, "body"),
15458
- ...replyTo === undefined ? {} : { reply_to: replyTo },
15468
+ ...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
15459
15469
  actor
15460
15470
  });
15461
15471
  const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
@@ -15790,6 +15800,7 @@ var SessionWireSchema = object({
15790
15800
  title: string2(),
15791
15801
  topics: array(string2()),
15792
15802
  roles: array(string2()).optional(),
15803
+ capabilities: array(string2()).optional(),
15793
15804
  self_subscribed: boolean2(),
15794
15805
  last_seen: number2().int().optional()
15795
15806
  });
@@ -15898,6 +15909,7 @@ function createEnvoyClient(config) {
15898
15909
  topics: expandSubscriptionTopics(input.topics),
15899
15910
  port: input.port,
15900
15911
  title: input.title,
15912
+ ...input.capabilities === undefined ? {} : { capabilities: input.capabilities },
15901
15913
  driving: input.driving,
15902
15914
  ...input.selfSubscribed === undefined ? {} : { self_subscribed: input.selfSubscribed }
15903
15915
  }))),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
package/skills/AGENTS.md CHANGED
@@ -8,9 +8,8 @@ event intake, process lifecycle, credentials, and role delivery.
8
8
 
9
9
  ```
10
10
  skills/
11
- ├── dispatch/ # Raising a durable human question as a GitHub-issue thread
12
- ├── github/ # GitHub issue and pull-request operations
13
- ├── linear/ # Linear adapter instructions
11
+ ├── dispatch/ # Writing specs, asks, comments, and artifacts on native Dispatch
12
+ ├── envoy/ # Envoy subscriptions, agent-to-agent messages, and topic formats
14
13
  ├── legion-architect/ # Tree ownership, decomposition, gates, and scheduling
15
14
  ├── legion-controller/ # Derived-verdict control-plane operation
16
15
  ├── legion-oracle/ # Repository-grounded research
@@ -27,9 +26,10 @@ returns that schema to the architect. It writes the same phase-specific payload
27
26
  The committed predecessor handoff wins after revival or re-creation.
28
27
 
29
28
  Workers do not run a controller loop or mutate lifecycle labels. Workers coordinate
30
- lifecycle, scope, and cross-phase decisions with the owning architect through hub, sending
31
- the verified observation and decision needed. A worker may use the `dispatch` MCP tool
32
- directly for a durable human question; replies come back to the worker's own session.
29
+ lifecycle, scope, and cross-phase decisions with the owning architect by `envoy_publish` to its
30
+ role topic, sending the verified observation and decision needed (`hub` reaches only subagents
31
+ inside the worker's own process). A worker may call the native `dispatch_*` tools directly for a
32
+ durable human question; replies come back to the worker's own session.
33
33
 
34
34
  ## Durable artifacts
35
35
 
@@ -222,6 +222,20 @@ and deleting a cell's quoted text removes only that text.
222
222
  Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated target; re-read a missing or ambiguous target before
223
223
  retrying. Pass `summary` to name the version when recording a decision.
224
224
 
225
+ ## Typed blocks
226
+
227
+ The server declares typed document blocks at `GET /api/v1/schema/blocks`. Write one only with the
228
+ container-directive form `:::name{#block-id key="value"}` on its own line, ordinary block children,
229
+ and a closing `:::` at the same nesting. An unclosed typed block at document level is rejected. For
230
+ a new typed block, omit `#block-id`; Dispatch mints it. When editing an existing typed block, retain
231
+ its id and every rendered attribute.
232
+
233
+ Use only the type names, content rule, attributes, and enum values returned by the schema. Values are
234
+ quoted: `:::callout{kind="warning" title="Risk"}`. Do not write Pandoc-style `::: {.callout}`, leaf
235
+ `::name` directives, or text `:name` directives; those strings are literal when quoted inside a code
236
+ block. Do not set attributes the schema marks `server: true`; the server ignores them and reasserts
237
+ its authoritative value at settlement.
238
+
225
239
  ## Comments and suggestions
226
240
 
227
241
  Add feedback with:
@@ -284,6 +298,24 @@ dispatch_message({ issue, body })
284
298
  It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 characters. A message is not a decision
285
299
  (`dispatch_ask`) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
286
300
 
301
+ ### Targeted agent messages
302
+
303
+ A human can target the issue message at a live Envoy session or role as **BTW**, **Aside**, or
304
+ **Steer**. The incoming Dispatch frame names the issue and includes a `reply_with` instruction;
305
+ reply on the same open issue with the existing tool, never a new targeted send:
306
+
307
+ ```ts
308
+ dispatch_message({
309
+ issue: "CORE-1",
310
+ in_reply_to: "<targeted-message-id>",
311
+ body: "The requested answer.",
312
+ })
313
+ ```
314
+
315
+ `in_reply_to` correlates the answer under the human's message in its Conversation card. A BTW
316
+ delivery can post its answer automatically; use this call when the frame asks the primary agent to
317
+ reply. Agent bearers cannot set `target` or `delivery`.
318
+
287
319
  ## What comes back
288
320
 
289
321
  A write result's `details.topic` subscribes the host to its owner, and the first such subscription on an issue or document also tells
@@ -10,11 +10,6 @@ with human-created children; either way you own its complete outcome. Work from
10
10
  wakes and current artifacts. Do not perform code work yourself and do not rely on a
11
11
  separate coordinator to finish necessary work.
12
12
 
13
- This skill documents the target Dispatch-native contract. The `legion` tool's
14
- `register_gate`/`release_wave`/`set_status` ops, the Dispatch key format, and the
15
- `dispatch_*` tool family land with PR B (#TBD); until that PR merges, this skill's contract
16
- is not yet runnable on `main`.
17
-
18
13
  ## Tool and ownership boundaries
19
14
 
20
15
  - Use the `legion` tool for lifecycle writes. Its issue key is the Dispatch key
@@ -105,9 +100,10 @@ idea what the fuck you're talking about."
105
100
  Then park. Do not release a wave or spawn a Legion role until a later delivered wake
106
101
  shows `design-approved` on the root. On a deployment whose design gate is off
107
102
  (`gates.design: off` in its `legion.yaml`), the daemon satisfies the gate as you register it
108
- and `design-approved` arrives immediately — proceed; the ask remains open on Dispatch as the
109
- record and needs no answer. Approval covers the entire tree: later waves, re-scopes, and
110
- integration-failure children do not repeat this sequence.
103
+ and `design-approved` arrives immediately — proceed. The daemon then closes the ask on Dispatch
104
+ (you will see `ask.resolved` for it); that is expected and needs nothing from you. Approval
105
+ covers the entire tree: later waves, re-scopes, and integration-failure children do not repeat
106
+ this sequence.
111
107
 
112
108
  ## 2. Children in flight
113
109
 
@@ -9,10 +9,6 @@ The controller is the one persistent, wake-driven session for a Legion project.
9
9
  triage, escalation, and human-interaction judgments; it never does phase-worker work or
10
10
  routes raw events into an architect.
11
11
 
12
- This skill documents the target Dispatch-native contract. The `legion` tool's `set_status`
13
- op, `dispatch_read`/`dispatch_issue`, and the Dispatch key format land with PR B (#TBD);
14
- until that PR merges, this skill's contract is not yet runnable on `main`.
15
-
16
12
  ## Start and claim the controller role
17
13
 
18
14
  The Legion extension claims `legion-<project>-controller` and registers controller readiness
@@ -36,14 +36,18 @@ digraph oracle_decision {
36
36
 
37
37
  ## Research Strategy
38
38
 
39
- Run steps 1-2 first (parallel OK), then 3-4 if needed:
39
+ If the deployment instructions name a librarian role, ask it first (publish to its
40
+ `notifications.role.<name>` topic with `expects_reply: required`). Then run steps 1-2 (parallel
41
+ OK), and 3-4 if needed, with tools a Legion pane actually has: `read`, `grep`, `web_search`, and
42
+ `task(agent="scout")` (fast read-only codebase search) or `task(agent="oracle")` (deeper
43
+ read-only analysis when the answer needs judgment across many files). Do not name any other agent.
40
44
 
41
45
  | Step | Tool | Query |
42
46
  |------|------|-------|
43
- | 1. Institutional learnings | `Task learnings-researcher` | Search docs/solutions/ for [question] |
44
- | 2. Codebase patterns | `Task Explore` | Find how src/ handles [topic] |
45
- | 3. Framework docs | Context7 MCP | resolve-library-id query-docs |
46
- | 4. External practices | `Task best-practices-researcher` or `WebSearch` | Current best practices for [topic] |
47
+ | 1. Institutional learnings | `grep` then `read` over `docs/solutions/` (front-matter `tags`, then the body) | [question]'s keywords |
48
+ | 2. Codebase patterns | `task(agent="scout")`; `task(agent="oracle")` when judgment across many files is needed | Find how [module] handles [topic] |
49
+ | 3. Framework docs | `read` the library's documentation URL | [library] [topic] |
50
+ | 4. External practices | `web_search`, then `read` the primary source | Current best practices for [topic] |
47
51
 
48
52
  ## Output
49
53
 
@@ -54,10 +58,11 @@ Run steps 1-2 first (parallel OK), then 3-4 if needed:
54
58
  ## Example
55
59
 
56
60
  ```
57
- /legion-oracle How should I handle GraphQL pagination?
61
+ Question: How should I paginate a GraphQL connection?
58
62
 
59
- [learnings-researcher]No matches
60
- [Explore] Found src/legion/state/fetch.py uses cursor-based pagination
63
+ grep pagination docs/solutions/ no matches
64
+ task(agent="scout") packages/daemon/src/state/fetch.ts loops on
65
+ pageInfo.hasNextPage / endCursor (contextsPage)
61
66
 
62
- Answer: Use cursor-based pagination per src/legion/state/fetch.py:42
67
+ Answer: cursor-based, per the `while (page.hasNextPage)` loop in packages/daemon/src/state/fetch.ts
63
68
  ```
@@ -11,10 +11,6 @@ phase gets its own long-lived process against the same jj workspace, run in turn
11
11
  the phase assigned to you, report its completion to the architect, and leave the durable
12
12
  copy the next phase can trust.
13
13
 
14
- This skill documents the target Dispatch-native contract. `LEGION_ISSUE` as a Dispatch key,
15
- the `dispatch_ask` tool, and the `Dispatch: <KEY>` PR-body linkage land with PR B (#TBD);
16
- until that PR merges, this skill's contract is not yet runnable on `main`.
17
-
18
14
  ## Identity, scope, and role
19
15
 
20
16
  The daemon spawns you as a separate `omp --mode rpc` process (behind `legion worker-shim`,
@@ -95,12 +91,15 @@ committed predecessor handoffs in lifecycle order from `$LEGION_WORKSPACE/.legio
95
91
  4. `test.json`
96
92
  5. `review.json`
97
93
 
98
- Read only files that precede the assigned phase. There is no handoff schema (rejected
99
- design — no schema validation runs anywhere in this pipeline): write the phase-specific
100
- fields the next phase and the architect need, consistent with what predecessor phases
101
- already wrote. The durable copy lives in `$LEGION_WORKSPACE/.legion/<phase>.json`. If a
102
- committed handoff conflicts with memory or a prior transcript, the committed file wins: it
103
- is the copy that survived.
94
+ Read only files that precede the assigned phase. Every handoff is validated when it is read:
95
+ `validatePhaseHandoff` (`packages/contracts/src/handoff-schema.ts`) checks the file, and the
96
+ ledger (`packages/daemon/src/handoff/ledger.ts`) treats a file that fails validation as missing.
97
+ Undeclared fields pass validation untouched and reach the next worker; a declared field of the
98
+ wrong type fails the whole file, so `legion handoff read` returns null for that phase.
99
+ Write the phase-specific fields the next phase and the architect need, consistent with what
100
+ predecessor phases already wrote. The durable copy lives in
101
+ `$LEGION_WORKSPACE/.legion/<phase>.json`. If a committed handoff conflicts with memory or a prior
102
+ transcript, the committed file wins: it is the copy that survived.
104
103
 
105
104
  ## jj Safety Rules
106
105
 
@@ -138,6 +137,14 @@ capability it needs; invoke GitHub through the credential helper:
138
137
  legion gh -- <gh args…>
139
138
  ```
140
139
 
140
+ Three facts about `gh` in a worker pane. The `gh` on your `PATH` is a shim
141
+ (`packages/pi-envoy/src/legion/gh-shim.ts`) that execs `legion gh -- "$@"`, so `gh …` and
142
+ `legion gh -- …` are the same call, and each call redeems a fresh token from your session's
143
+ grant — identity is supplied per call, never stored. Never run `gh auth login` or
144
+ `gh auth setup-git`; there is no login state to create. The shim refuses `pr merge` (and a raw
145
+ `gh api …/merge`): no worker role merges a pull request — the merge queue does, under its own
146
+ authority.
147
+
141
148
  ## GitHub PR comment attribution
142
149
 
143
150
  Append this exact structured footer to **every** pull-request comment and review that this
@@ -158,6 +165,15 @@ legion gh -- pr comment <pr-number> \
158
165
  --repo <owner>/<repo>
159
166
  ```
160
167
 
168
+ ## Planner artifact
169
+
170
+ The plan lives in `.legion/plan.json` and the Dispatch issue document; never commit a plan or spec file to the repository.
171
+ No `docs/plans/*`, `docs/superpowers/plans/*`, or spec markdown goes into the pull request: plan
172
+ and spec content goes into the issue, never into a PR (the root `AGENTS.md`'s `docs/plans/` row
173
+ is human-authored design history, not a Legion artifact). A skill step that says "save the plan
174
+ to a file" is satisfied by the handoff write in the completion gate below; the planner's only
175
+ commit is `plan: record handoff`.
176
+
161
177
  ## Implementer push and pull request
162
178
 
163
179
  Only the implementer creates the issue bookmark, pushes it, and opens the pull request.
@@ -212,17 +228,36 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
212
228
  changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
213
229
  Findings about naming, duplication, or wording are batched into the single `Fast-follow`
214
230
  line instead of iterating per push.
231
+ - **Rebase only on a real conflict.** Sami, 2026-09-11, verbatim:
232
+ "Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
233
+ The implementer rebases the issue branch only when GitHub reports it `CONFLICTING` or the
234
+ controller asks because of a conflict — never to pick up `main` or to refresh CI. A single
235
+ failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
236
+ pushing a new commit.
237
+ - **No deferrals.** Sami, 2026-09-11, verbatim: "My rule is no deferrals." The `Fast-follow:`
238
+ field names naming, duplication, or wording cleanup only; anything that changes behaviour,
239
+ hides an error, or breaks a gate lands in this PR.
215
240
  - The tester fills in the `E2E` section: the real surface a user reaches the criterion
216
241
  through, the exact command or run id, what was observed, the head SHA, and one negative
217
242
  control — a deliberately broken input and the refusal or failure it produced. A unit or
218
- integration test is a regression lock, never proof of a criterion.
243
+ integration test is a regression lock, never proof of a criterion. Environment or
244
+ secret-scrub evidence (e.g. "`LEGION_*`/`DISPATCH_*`/`ENVOY_*` unset") is recorded once, in
245
+ `.legion/test.json`, and only when the issue's acceptance criteria call for it — never
246
+ re-pasted into the PR body each round.
219
247
  - The reviewer verifies the `CI`, `Threads`, and `E2E` facts against GitHub directly —
220
248
  never from a handoff — then runs `task(agent="thermonuclear-deep-review")` and
221
249
  `task(agent="thermonuclear-code-quality")` once at that head and records the verdict.
222
- Skip the `Thermo` line entirely on a docs-only PR. Post every correctness finding as a
223
- PR review comment and return the issue to the architect; when clean, have the architect send
224
- the implementer back to push the `.legion/` deletion (the review App cannot push), then
225
- review **that** head and approve it by name.
250
+ Skip the `Thermo` line entirely on a docs-only PR. Submit **one review per round**
251
+ `REQUEST_CHANGES` when any correctness finding stands, otherwise `COMMENT` while the head
252
+ still carries `.legion/`; `APPROVE` only for the head that differs from the reviewed one by
253
+ the `.legion/` deletion alone, named by SHA — carrying every inline comment in that single
254
+ call: `legion gh -- api --method POST repos/{owner}/{repo}/pulls/{number}/reviews --input body.json`
255
+ with `commit_id`, `event` (`REQUEST_CHANGES`, `COMMENT`, or `APPROVE`), `body` (with the
256
+ Legion footer), and a `comments[]` array of `{path, line, side, body}`, one entry per
257
+ finding — never one `pr review` call per finding (each submission fires a `pr-review` wake).
258
+ Then return the issue to the architect; when clean, have the architect send the implementer
259
+ back to push the `.legion/` deletion (the review App cannot push), then review **that** head
260
+ and approve it by name.
226
261
  - Once a base is frozen for others to stack on, never rewrite it — fixes land as new
227
262
  commits on top, and the `Chain` line records what is frozen.
228
263
  - The merger confirms the approved head still equals the current head, then publishes
@@ -1,208 +0,0 @@
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
- **Inside a Legion worker workspace**, skip `gh auth login`/`gh auth setup-git`: `gh` on `PATH`
18
- is a shim that execs `legion gh -- <args>` (`packages/pi-envoy/src/legion/gh-shim.ts`), which
19
- redeems a fresh token from the daemon's grant on every invocation and hands it straight to the
20
- real `gh` binary — it is already authenticated per call, independent of any local login state.
21
-
22
- ## Operations
23
-
24
- ### Search/List Issues (via project)
25
-
26
- List all items in a GitHub Project V2:
27
-
28
- ```bash
29
- gh project item-list $PROJECT_NUM --owner $OWNER --format json
30
- ```
31
-
32
- **Parameters:**
33
- - `$PROJECT_NUM`: Project number (from `LEGION_ID` format: `owner/project-number`)
34
- - `$OWNER`: Repository owner
35
- - `--format json`: Returns structured data for parsing
36
-
37
- **Example:**
38
- ```bash
39
- gh project item-list 42 --owner acme --format json | jq '.items[] | {id, title, status}'
40
- ```
41
-
42
- ### Get Issue Details
43
-
44
- Fetch full issue metadata:
45
-
46
- ```bash
47
- gh issue view $ISSUE_NUMBER --json title,body,labels,comments,state -R $OWNER/$REPO
48
- ```
49
-
50
- **Parameters:**
51
- - `$ISSUE_NUMBER`: Issue number (e.g., `123`)
52
- - `-R $OWNER/$REPO`: Repository (required for multi-repo support)
53
- - `--json`: Fields to return (title, body, labels, comments, state, etc.)
54
-
55
- **Example:**
56
- ```bash
57
- gh issue view 123 --json title,body,labels,state -R acme/backend
58
- ```
59
-
60
- ### Update Status (Projects V2 — GraphQL)
61
-
62
- Update issue status in a GitHub Project V2. Requires field and option IDs from project schema:
63
-
64
- ```bash
65
- gh api graphql -f query='mutation {
66
- updateProjectV2ItemFieldValue(input: {
67
- projectId: "$PROJECT_ID"
68
- itemId: "$ITEM_ID"
69
- fieldId: "$STATUS_FIELD_ID"
70
- value: { singleSelectOptionId: "$OPTION_ID" }
71
- }) { projectV2Item { id } }
72
- }'
73
- ```
74
-
75
- **Parameters:**
76
- - `$PROJECT_ID`: GraphQL ID of the project (not the number)
77
- - `$ITEM_ID`: GraphQL ID of the issue in the project
78
- - `$STATUS_FIELD_ID`: GraphQL ID of the Status field
79
- - `$OPTION_ID`: GraphQL ID of the status option (e.g., "In Progress", "Done")
80
-
81
- **Note:** Field and option IDs must be resolved from the project schema. The controller caches these after first query.
82
-
83
- **Resolve IDs (one-time):**
84
- ```bash
85
- gh api graphql -f query='query {
86
- repository(owner: "$OWNER", name: "$REPO") {
87
- projectV2(number: $PROJECT_NUM) {
88
- fields(first: 20) {
89
- nodes {
90
- ... on ProjectV2SingleSelectField {
91
- id
92
- name
93
- options { id name }
94
- }
95
- }
96
- }
97
- }
98
- }
99
- }'
100
- ```
101
-
102
- ### Add Label
103
-
104
- Add a label to an issue (additive — does not remove existing labels):
105
-
106
- ```bash
107
- gh issue edit $ISSUE_NUMBER --add-label "bug" -R $OWNER/$REPO
108
- ```
109
-
110
- **Parameters:**
111
- - `$ISSUE_NUMBER`: Issue number
112
- - `--add-label`: Label to add (can be used multiple times)
113
- - `-R $OWNER/$REPO`: Repository
114
-
115
- **Example:**
116
- ```bash
117
- gh issue edit 123 --add-label "bug" -R acme/backend
118
- ```
119
-
120
- ### Remove Label
121
-
122
- Remove a label from an issue:
123
-
124
- ```bash
125
- gh issue edit $ISSUE_NUMBER --remove-label "wontfix" -R $OWNER/$REPO
126
- ```
127
-
128
- **Parameters:**
129
- - `$ISSUE_NUMBER`: Issue number
130
- - `--remove-label`: Label to remove (can be used multiple times)
131
- - `-R $OWNER/$REPO`: Repository
132
-
133
- **Example:**
134
- ```bash
135
- gh issue edit 123 --remove-label "wontfix" -R acme/backend
136
- ```
137
-
138
- ### Comment on Issue
139
-
140
- Add a comment to an issue:
141
-
142
- ```bash
143
- gh issue comment $ISSUE_NUMBER --body "Fixed in commit abc123" -R $OWNER/$REPO
144
- ```
145
-
146
- **Parameters:**
147
- - `$ISSUE_NUMBER`: Issue number
148
- - `--body`: Comment text (supports Markdown)
149
- - `-R $OWNER/$REPO`: Repository
150
-
151
- **Example:**
152
- ```bash
153
- gh issue comment 123 --body "Implemented in PR #456" -R acme/backend
154
- ```
155
-
156
- ### Create Issue
157
-
158
- Create a new issue:
159
-
160
- ```bash
161
- gh issue create --title "Bug: Login fails" --body "Details" -R $OWNER/$REPO
162
- ```
163
-
164
- **Parameters:**
165
- - `--title`: Issue title (required)
166
- - `--body`: Issue description (optional, supports Markdown)
167
- - `-R $OWNER/$REPO`: Repository
168
-
169
- **Example:**
170
- ```bash
171
- gh issue create --title "Feature: Add dark mode" --body "User request from #789" -R acme/backend
172
- ```
173
-
174
- ## Key Differences from Linear
175
-
176
- | Aspect | Linear | GitHub |
177
- |--------|--------|--------|
178
- | **Labels** | Replace all (read-modify-write) | Additive (`--add-label`, `--remove-label`) |
179
- | **Status** | Direct field update | Projects V2 GraphQL mutation |
180
- | **PR Association** | Attachment field | Native (issue ↔ PR link) |
181
- | **API** | MCP tool dispatch | Direct `gh` CLI |
182
- | **Multi-repo** | Single team | `-R owner/repo` per command |
183
-
184
- ## Important Notes
185
-
186
- - **Always specify `-R $OWNER/$REPO`** for multi-repo project support
187
- - **Labels are additive**: Use `--add-label` and `--remove-label` separately (unlike Linear which replaces all)
188
- - **Status updates require Projects V2 GraphQL** — not just issue labels
189
- - **PR association is automatic** — GitHub links issues and PRs natively
190
- - **`$OWNER` and `$REPO` come from `LEGION_ID`** (format: `owner/project-number`)
191
- - **Field/option IDs must be cached** by the controller after first resolution
192
-
193
- ## Error Handling
194
-
195
- Common errors and solutions:
196
-
197
- | Error | Cause | Solution |
198
- |-------|-------|----------|
199
- | `Could not resolve to a Repository` | Wrong `-R` format | Use `-R owner/repo` (not `owner-repo`) |
200
- | `Could not resolve to an Issue` | Issue doesn't exist | Verify issue number is correct |
201
- | `GraphQL error: Field not found` | Wrong field ID | Re-resolve field IDs from project schema |
202
- | `Not authenticated` | `gh` not logged in | Run `gh auth login` |
203
-
204
- ## Reference
205
-
206
- - **Project number**: Visible in GitHub UI (e.g., `https://github.com/orgs/acme/projects/42` → `42`)
207
- - **Issue number**: Visible in URL (e.g., `https://github.com/acme/backend/issues/123` → `123`)
208
- - **GraphQL IDs**: Base64-encoded, returned by GraphQL queries (not human-readable)
@@ -1,76 +0,0 @@
1
- ---
2
- name: linear
3
- description: Manage Linear issues. Use when working with tasks, tickets, bugs, or Linear.
4
- mcp:
5
- linear:
6
- command: npx
7
- args: ["-y", "github:obra/streamlinear"]
8
- env:
9
- LINEAR_API_TOKEN: ${LINEAR_API_TOKEN}
10
- ---
11
-
12
- # Linear (Stream Linear)
13
-
14
- Single-tool MCP with action dispatch. All operations go through `linear_linear`.
15
-
16
- ## Actions
17
-
18
- ### Search Issues
19
-
20
- ```
21
- linear_linear(action="search") # Your active issues
22
- linear_linear(action="search", query="auth bug") # Text search
23
- linear_linear(action="search", query={state: "In Progress"}) # Filter
24
- linear_linear(action="search", query={team: "ENG", assignee: "me"})
25
- ```
26
-
27
- ### Get Issue Details
28
-
29
- ```
30
- linear_linear(action="get", id="ABC-123") # By short ID
31
- linear_linear(action="get", id="https://linear.app/...") # By URL
32
- ```
33
-
34
- Returns: title, description, status, labels, comments, attachments.
35
-
36
- ### Update Issue
37
-
38
- ```
39
- linear_linear(action="update", id="ABC-123", state="Done")
40
- linear_linear(action="update", id="ABC-123", priority=1)
41
- linear_linear(action="update", id="ABC-123", assignee="me")
42
- linear_linear(action="update", id="ABC-123", labels=["bug", "existing-label"])
43
- ```
44
-
45
- **Labels array replaces all labels.** Fetch current labels first, then append.
46
-
47
- ### Comment on Issue
48
-
49
- ```
50
- linear_linear(action="comment", id="ABC-123", body="Fixed in commit abc123")
51
- ```
52
-
53
- ### Create Issue
54
-
55
- ```
56
- linear_linear(action="create", title="Bug: Login fails", team="ENG")
57
- linear_linear(action="create", title="Bug", team="ENG", body="Details", priority=2)
58
- ```
59
-
60
- ### Raw GraphQL
61
-
62
- ```
63
- linear_linear(action="graphql", graphql="query { viewer { name } }")
64
- ```
65
-
66
- ### Help
67
-
68
- ```
69
- linear_linear(action="help")
70
- ```
71
-
72
- ## Reference
73
-
74
- - Priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low
75
- - State matching is fuzzy: "done" → "Done", "in prog" → "In Progress"
76
- - IDs accept: `ABC-123`, Linear URLs, or UUIDs