@sjawhar/opencode-legion-envoy 0.30.0 → 0.32.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 CHANGED
@@ -32,8 +32,8 @@ the session working directory, stamps it with the OpenCode session id and title,
32
32
  `details.topic` as tool metadata so a successful mutation subscribes to that exact Dispatch topic.
33
33
 
34
34
  `dispatch_artifact` accepts exactly one upload source: a local `path`, or inline `content`.
35
- An architect can post a primary specification directly with
36
- `{ issue, name: "spec.md", content: "# Design", primary: true }`.
35
+ An architect can post a specification directly with
36
+ `{ issue, name: "spec.md", content: "# Design" }`.
37
37
 
38
38
  It also maintains the live session registry metadata needed for Envoy to discover OpenCode sessions and their API ports.
39
39
 
@@ -13603,6 +13603,16 @@ function dispatchToolSchema(spec, z, opts) {
13603
13603
  return spec.validation === undefined ? z.object(shape, opts) : z.refineObject(shape, spec.validation.check, spec.validation.message, opts);
13604
13604
  }
13605
13605
  var ISSUE_REFERENCE = "An issue is a native KEY or external owner/repo#n reference; an external reference creates its native issue in the repository's dashboard-configured project or, failing that, the default project (DISPATCH_DEFAULT_PROJECT).";
13606
+ var SPEC_SECTIONS = [
13607
+ "Decisions needed",
13608
+ "Acceptance",
13609
+ "Requirements",
13610
+ "Design",
13611
+ "Errors",
13612
+ "Testing",
13613
+ "Rejected"
13614
+ ];
13615
+ var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Every line is a fact, decision, or risk; use tables over prose; see skills/dispatch Writing a spec.";
13606
13616
  var ASK_URGENCIES = ["low", "med", "high", "blocking"];
13607
13617
  var DOC_EDIT_OPS = ["replace", "delete", "insert"];
13608
13618
  var dispatchToolSpecs = [
@@ -13614,7 +13624,7 @@ var dispatchToolSpecs = [
13614
13624
  title: z.string().describe("Concise issue title."),
13615
13625
  parent: z.string().describe("Optional parent issue.").optional(),
13616
13626
  external: z.string().describe("Optional external issue reference.").optional(),
13617
- spec: z.string().describe("Optional initial primary-document markdown.").optional()
13627
+ spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional()
13618
13628
  })
13619
13629
  },
13620
13630
  {
@@ -13680,7 +13690,7 @@ var dispatchToolSpecs = [
13680
13690
  },
13681
13691
  {
13682
13692
  name: "dispatch_doc_edit",
13683
- description: "Apply deterministic text edits to a document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${ISSUE_REFERENCE}`,
13693
+ description: "Apply deterministic text edits to a document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${ISSUE_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
13684
13694
  arguments: (z) => ({
13685
13695
  issue: z.string().describe(ISSUE_REFERENCE),
13686
13696
  artifact: z.string().describe("Artifact slug or id for the document."),
@@ -13714,7 +13724,6 @@ var dispatchToolSpecs = [
13714
13724
  name: z.string().describe("Artifact filename shown in Dispatch."),
13715
13725
  path: z.string().describe("Local path to the file to upload.").optional(),
13716
13726
  content: z.string().describe("Inline text to store as a Markdown document.").optional(),
13717
- primary: z.boolean().describe("Make this document the issue primary artifact.").optional(),
13718
13727
  summary: z.string().describe("Optional version summary.").optional()
13719
13728
  }),
13720
13729
  validation: {
@@ -13874,8 +13883,17 @@ var legionRole = _enum2(LEGION_ROLES);
13874
13883
  var requiredUnknown = unknown().refine((value) => value !== undefined, {
13875
13884
  message: "Required"
13876
13885
  });
13877
- var ARCHITECT_MUTABLE_LABELS = ["needs-approval"];
13878
- var architectMutableLabel = _enum2(ARCHITECT_MUTABLE_LABELS);
13886
+ var LIFECYCLE_STATUSES = [
13887
+ "triage",
13888
+ "icebox",
13889
+ "backlog",
13890
+ "todo",
13891
+ "in_progress",
13892
+ "testing",
13893
+ "needs_review",
13894
+ "retro",
13895
+ "done"
13896
+ ];
13879
13897
  var architectCapability = strictObject({
13880
13898
  tree: nonEmptyString,
13881
13899
  sessionId: nonEmptyString,
@@ -13928,33 +13946,10 @@ var LegionDaemonApi = {
13928
13946
  request: architectCapability.extend({ generation: number2().int() }),
13929
13947
  response: object({})
13930
13948
  },
13931
- IssueCreate: {
13932
- request: architectCapability.extend({
13933
- title: nonEmptyString,
13934
- body: nonEmptyString,
13935
- labels: array(architectMutableLabel).optional()
13936
- }),
13937
- response: object({ issue: nonEmptyString, url: nonEmptyString })
13938
- },
13939
13949
  WaveRelease: {
13940
- request: architectCapability.extend({ children: array(nonEmptyString).optional() }),
13950
+ request: architectCapability.extend({ issues: array(nonEmptyString).optional() }),
13941
13951
  response: object({ released: array(nonEmptyString) })
13942
13952
  },
13943
- Comment: {
13944
- request: architectCapability.extend({ issue: nonEmptyString, body: nonEmptyString }),
13945
- response: object({ commentId: number2().int(), url: nonEmptyString })
13946
- },
13947
- PostBody: {
13948
- request: architectCapability.extend({ issue: nonEmptyString, body: nonEmptyString }),
13949
- response: object({})
13950
- },
13951
- Labels: {
13952
- request: architectCapability.extend({
13953
- issue: nonEmptyString,
13954
- add: array(architectMutableLabel).optional()
13955
- }),
13956
- response: object({ labels: array(nonEmptyString) })
13957
- },
13958
13953
  Escalate: {
13959
13954
  request: architectCapability.extend({
13960
13955
  kind: _enum2(["re-file", "capacity", "cross-tree"]),
@@ -13962,10 +13957,6 @@ var LegionDaemonApi = {
13962
13957
  }),
13963
13958
  response: object({})
13964
13959
  },
13965
- IssueClose: {
13966
- request: architectCapability.extend({ issue: nonEmptyString, comment: string2().optional() }),
13967
- response: object({})
13968
- },
13969
13960
  ProvisioningCredential: {
13970
13961
  request: architectCapability.extend({ issue: nonEmptyString }),
13971
13962
  response: object({ token: nonEmptyString })
@@ -14028,16 +14019,16 @@ var LegionDaemonApi = {
14028
14019
  secret: nonEmptyString
14029
14020
  })
14030
14021
  },
14031
- GatesApprove: {
14032
- request: controllerIssue,
14022
+ IssueStatus: {
14023
+ request: controllerIssue.extend({
14024
+ status: _enum2(LIFECYCLE_STATUSES),
14025
+ tree: nonEmptyString.optional(),
14026
+ sessionId: nonEmptyString.optional()
14027
+ }),
14033
14028
  response: object({})
14034
14029
  },
14035
- Admission: {
14036
- request: controllerIssue,
14037
- response: object({ result: _enum2(["spawned", "queued"]) })
14038
- },
14039
- Backlog: {
14040
- request: controllerIssue.extend({ marker: nonEmptyString }),
14030
+ GatesRegister: {
14031
+ request: architectCapability.extend({ issue: nonEmptyString, askId: nonEmptyString }),
14041
14032
  response: object({})
14042
14033
  },
14043
14034
  Grant: {
@@ -14377,8 +14368,6 @@ class DispatchClient {
14377
14368
  return this.#json("POST", artifactPath, input);
14378
14369
  const form = new FormData;
14379
14370
  form.set("name", input.name);
14380
- if (input.primary !== undefined)
14381
- form.set("primary", String(input.primary));
14382
14371
  if (input.summary !== undefined)
14383
14372
  form.set("summary", input.summary);
14384
14373
  if (input.actor !== undefined)
@@ -14897,20 +14886,17 @@ Open anchored asks/comments: ${marks.join(", ")}`,
14897
14886
  };
14898
14887
  }
14899
14888
  case "dispatch_artifact": {
14900
- const primary = optionalBoolean(args, "primary");
14901
14889
  const summary = optionalString(args, "summary");
14902
14890
  const name = stringArg(args, "name");
14903
14891
  const content = optionalString(args, "content");
14904
14892
  const result = await client.artifact(issue(), content === undefined ? {
14905
14893
  name,
14906
14894
  file: Bun.file(resolvePath(input.cwd, stringArg(args, "path"))),
14907
- ...primary === undefined ? {} : { primary },
14908
14895
  ...summary === undefined ? {} : { summary },
14909
14896
  actor
14910
14897
  } : {
14911
14898
  name,
14912
14899
  content,
14913
- ...primary === undefined ? {} : { primary },
14914
14900
  ...summary === undefined ? {} : { summary },
14915
14901
  actor
14916
14902
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -12,6 +12,38 @@ The server enforces high signal: an ask question is at most 800 characters with
12
12
  options; comment and message bodies are at most 2,000 characters; an artifact is at most 25 MiB.
13
13
  It refuses over-limit input; it never truncates it. GitHub threads and markers no longer exist.
14
14
 
15
+ ## Writing a spec
16
+
17
+ A spec is a decision record for the human who decides and the implementer who builds, not a
18
+ transcript of your thinking. Use exactly these document headings in this order.
19
+
20
+ | Section | Required content | Form |
21
+ | --- | --- | --- |
22
+ | **Decisions needed** | Only decisions requiring human authority, taste, or risk appetite. Each states one question, two or three options with tradeoffs, and a recommendation. Every item is an anchored `dispatch_ask`. Answered items move into Requirements with provenance, then leave this section. No other section asks the reader anything. Empty means `None.` | One decision per line; anchor each ask to that line. |
23
+ | **Acceptance** | Every outcome names its check and user-facing surface. An outcome without a verification method is not acceptance criteria. | Numbered lines; browser scenario, API call, or CLI command. |
24
+ | **Requirements** | Provenance is a verbatim human quote or `inferred: <reasoning>`; readers treat inferred requirements as hypotheses. Do not restate the prompt in prose. | `requirement \| provenance` table. |
25
+ | **Design** | State the files, components, routes, and data flow that change. | Facts, not narrative; diagrams only for genuine structure. |
26
+ | **Errors** | Name the behaviour for every error condition; never specify a silent fallback. | `condition \| behaviour` table. |
27
+ | **Testing** | Map every acceptance line to the proof that exercises it. | Suite or scenario. |
28
+ | **Rejected** | Record each considered alternative and why it was rejected so it is not proposed again. | One alternative per line. |
29
+
30
+ ### Rules
31
+
32
+ - Every sentence is a fact, decision, or risk; delete the rest.
33
+ - Use tables over prose and keep one idea per line.
34
+ - Do not use Overview, Background, Introduction, Summary, or Conclusion sections.
35
+ - Do not hedge with “might” or “could consider.”
36
+ - Do not use TBD, TODO, or placeholders; an open item is a Decision needed.
37
+ - Keep each section to one screen; work that exceeds one screen per section is two specs.
38
+ - Update the spec in place as decisions land. The spec is the record; comments are the discussion.
39
+
40
+ ### Self-review
41
+
42
+ - [ ] No placeholders remain.
43
+ - [ ] No sections conflict.
44
+ - [ ] The spec covers one implementation plan's worth of work.
45
+ - [ ] Every requirement has exactly one reading.
46
+
15
47
  ## Your issue
16
48
 
17
49
  Every session works on an issue. Legion pre-fills `issue` from `LEGION_ISSUE`: use a native issue key
@@ -26,6 +58,7 @@ dispatch_issue({ project, title, parent?, external?, spec? })
26
58
  ```
27
59
  It returns `details` `{ issue, topic }`. Use `dispatch_issue` only to create an issue; never use
28
60
  it to park a question.
61
+ When `spec` is supplied, follow [Writing a spec](#writing-a-spec).
29
62
 
30
63
  ## Asking
31
64
 
@@ -65,6 +98,7 @@ answer. Use `reply_to_ask` on `dispatch_comment` to reply under your own ask; it
65
98
  exclusive with `reply_to`.
66
99
 
67
100
  ## The spec is where narrative goes
101
+ Write and update the issue specification according to [Writing a spec](#writing-a-spec).
68
102
 
69
103
  Read the current document before changing it:
70
104
 
@@ -72,7 +106,7 @@ Read the current document before changing it:
72
106
  dispatch_doc_read({ issue?, artifact?, version?, ref? })
73
107
  ```
74
108
  It returns live or versioned markdown with open marks and `details` `{ issue }`; omit `artifact`
75
- with `issue` to read the primary document. Then write narrative with:
109
+ with `issue` to read the issue specification. Then write narrative with:
76
110
 
77
111
  ```ts
78
112
  dispatch_doc_edit({ issue, artifact, ops, summary? })
@@ -119,27 +153,26 @@ dispatch_suggest({ issue, artifact, quote, replace_with, body?, occurrence? })
119
153
  It returns `details` `{ issue, topic, comment }`. A human accepts or rejects a suggestion. On
120
154
  `TARGET_AMBIGUOUS`, add zero-based `occurrence`. On `TARGET_NOT_FOUND`, re-read the document
121
155
  before retrying. `INVALID_OP` names a malformed edit; `CAP_EXCEEDED` never truncates;
122
- `ISSUE_CLOSED` rejects a write. `ACTOR_KIND`, `ROUTE_INVALID`, and `PRIMARY_NOT_DOC` reject an
123
- invalid actor, route, or primary artifact.
156
+ `ISSUE_CLOSED` rejects a write. `ACTOR_KIND` and `ROUTE_INVALID` reject an invalid actor or route.
124
157
 
125
158
  ## Artifacts
126
159
 
127
160
  Attach an image, diagram, or local file with:
128
161
 
129
162
  ```ts
130
- dispatch_artifact({ issue, name, path, primary?, summary? })
163
+ dispatch_artifact({ issue, name, path, summary? })
131
164
  ```
132
165
 
133
166
  Or, when the text is already in the call, post a Markdown document directly:
134
167
 
135
168
  ```ts
136
- dispatch_artifact({ issue, name: "spec.md", content: "# Design\n...", primary: true })
169
+ dispatch_artifact({ issue, name: "spec.md", content: "# Design\n..." })
137
170
  ```
138
171
 
139
172
  Exactly one of `path` and `content` is required. The inline form sends JSON with
140
173
  `Content-Type: application/json`. It returns `details` `{ issue, topic, artifact, version }`.
141
- Uploading the same `name` creates its next version. Use `content` for an architect's primary spec;
142
- set `primary: true` only for Markdown documents.
174
+ Uploading the same `name` creates its next version. Use `content` when the text is already in
175
+ the call.
143
176
 
144
177
  ## Messages
145
178
 
@@ -104,7 +104,7 @@ gh api graphql -f query='query {
104
104
  Add a label to an issue (additive — does not remove existing labels):
105
105
 
106
106
  ```bash
107
- gh issue edit $ISSUE_NUMBER --add-label "needs-approval" -R $OWNER/$REPO
107
+ gh issue edit $ISSUE_NUMBER --add-label "bug" -R $OWNER/$REPO
108
108
  ```
109
109
 
110
110
  **Parameters:**
@@ -114,7 +114,7 @@ gh issue edit $ISSUE_NUMBER --add-label "needs-approval" -R $OWNER/$REPO
114
114
 
115
115
  **Example:**
116
116
  ```bash
117
- gh issue edit 123 --add-label "needs-approval" -R acme/backend
117
+ gh issue edit 123 --add-label "bug" -R acme/backend
118
118
  ```
119
119
 
120
120
  ### Remove Label
@@ -122,7 +122,7 @@ gh issue edit 123 --add-label "needs-approval" -R acme/backend
122
122
  Remove a label from an issue:
123
123
 
124
124
  ```bash
125
- gh issue edit $ISSUE_NUMBER --remove-label "needs-approval" -R $OWNER/$REPO
125
+ gh issue edit $ISSUE_NUMBER --remove-label "wontfix" -R $OWNER/$REPO
126
126
  ```
127
127
 
128
128
  **Parameters:**
@@ -132,7 +132,7 @@ gh issue edit $ISSUE_NUMBER --remove-label "needs-approval" -R $OWNER/$REPO
132
132
 
133
133
  **Example:**
134
134
  ```bash
135
- gh issue edit 123 --remove-label "legion-backlog" -R acme/backend
135
+ gh issue edit 123 --remove-label "wontfix" -R acme/backend
136
136
  ```
137
137
 
138
138
  ### Comment on Issue
@@ -10,29 +10,31 @@ 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
+
13
18
  ## Tool and ownership boundaries
14
19
 
15
- - Use the `legion` tool for lifecycle writes. Its issue key format is
16
- `owner/repo#number`.
20
+ - Use the `legion` tool for lifecycle writes. Its issue key is the Dispatch key
21
+ (pattern `^[A-Z][A-Z0-9]*-[0-9]+$`, e.g. `LEGION-41`).
17
22
  - Use `legion({ op: "spawn_worker", issue, role, task })` for every Legion role spawn.
18
23
  Message a known phase worker with `envoy_publish` to `notifications.role.` followed by
19
24
  its encoded role token (the token `spawn_worker` returned for it); re-assign it by
20
25
  calling `spawn_worker` again on the same existing role, which resumes the same process
21
26
  instead of starting a fresh one. Phase workers escalate lifecycle, scope, and
22
27
  cross-phase matters the same way: `envoy_publish` to your own encoded token. Any role
23
- may use `dispatch` directly for a standalone human question; replies return to the
28
+ may use `dispatch_ask` directly for a standalone human question; replies return to the
24
29
  asking session.
25
30
  - The daemon spawns each role as its own process with the issue's context already in its
26
31
  environment. Never hand-format a role token: the daemon encodes one as
27
- `legion-<project>-<encoded-owner>__<encoded-repo>-<number>-<role>` (escaping `_`, `.`,
28
- and `-` within the owner/repo names); for example, project `acme`, issue
29
- `sjawhar/legion#41`, role `architect` encodes to `legion-acme-sjawhar__legion-41-architect`.
30
- Reuse a token you already hold (your own, or one `spawn_worker` returned) or compute
31
- another with the `roleToken` helper from `@legion/contracts` exactly the way the daemon
32
- does.
33
- - Use only the live label vocabulary: `needs-approval`, `human-approved`,
34
- `legion-child`, and `legion-backlog`. Do not attempt to apply a label whose ownership
35
- belongs to the controller or Sami.
32
+ `legion-<project>-<KEY>-<role>`; for example, project `acme`, issue `LEGION-41`, role
33
+ `architect` encodes to `legion-acme-LEGION-41-architect`. Reuse a token you already
34
+ hold (your own, or one `spawn_worker` returned) or compute another with the
35
+ `roleToken` helper from `@legion/contracts` exactly the way the daemon does.
36
+ - There is no label vocabulary. Dispatch status replaces the board, and the design gate
37
+ is a `dispatch_ask` answered `Approve`, not a label. Never attempt to apply a label.
36
38
  - Deferring necessary work is failure. The sole valid deferral is a new child issue you
37
39
  create and continue to own. Re-file a genuinely independent child through the
38
40
  controller rather than treating it as an abandoned dependency.
@@ -46,7 +48,7 @@ exercise a criterion end to end, building that path is a child issue of this tre
46
48
 
47
49
  - **Existing children:** adopt them. Do not replace or re-decompose human-created work.
48
50
  Put every adopted child into the initial wave. **You MUST call**
49
- `legion({ op: "wave_release", children: ["owner/repo#41", "owner/repo#42"] })`
51
+ `legion({ op: "release_wave", issues: ["LEGION-41", "LEGION-42"] })`
50
52
  **before any `spawn_worker` call for an adopted child.** Until release, the daemon
51
53
  holds that child's role activity. Then spawn each child's daemon-managed sub-architect
52
54
  owner.
@@ -54,16 +56,21 @@ exercise a criterion end to end, building that path is a child issue of this tre
54
56
  completed and integrated as one unit. Otherwise create complete child issues with:
55
57
 
56
58
  ```text
57
- legion({
58
- op: "issue_create",
59
+ dispatch_issue({
60
+ project: "<project>",
61
+ parent: "<root issue>",
59
62
  title: "<child outcome>",
60
- body: "<acceptance criteria, scope, and context>",
61
- labels: []
63
+ spec: "<acceptance criteria, scope, and context>"
62
64
  })
63
65
  ```
64
66
 
65
- The daemon establishes the sub-issue relationship and the `legion-child` label. Keep
66
- the returned issue keys in ordered waves; a child is inert until released.
67
+ `project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` `LEGSMOKE`) — not
68
+ the role-token `<project>` (the daemon's own project, e.g. `acme`), a different string. A
69
+ root session has `LEGION_TREE == LEGION_ISSUE`. The daemon establishes the sub-issue
70
+ relationship from `parent`. Keep the returned issue keys in ordered waves; a child is
71
+ inert until released.
72
+
73
+ Specifications written into Dispatch follow [`skills/dispatch`'s Writing a spec](../dispatch/SKILL.md#writing-a-spec).
67
74
 
68
75
  Write one root specification containing the accepted scope, adoption/decomposition,
69
76
  waves, acceptance criteria, and integration test. When the config-armed root design gate
@@ -71,20 +78,18 @@ applies, run this exact sequence **before any Legion-role spawn**, including a
71
78
  sub-architect:
72
79
 
73
80
  ```text
74
- legion({ op: "post_spec", issue: "<root issue>", body: "<root specification>" })
75
- legion({ op: "label_add", issue: "<root issue>", label: "needs-approval" })
76
- dispatch({
77
- parent: "<root issue>",
78
- subject: "Legion design approval requested",
79
- context: "<what the tree is, what triggered the gate>",
80
- question: "<specification summary and the decision requested>"
81
+ dispatch_artifact({ issue: "<root issue>", name: "spec.md", content: "<root specification>", summary: "<one-line summary>" })
82
+ askId = dispatch_ask({
83
+ issue: "<root issue>",
84
+ question: "<specification summary and the decision requested>",
85
+ options: [{ label: "Approve" }, ...]
81
86
  })
87
+ legion({ op: "register_gate", issue: "<root issue>", askId })
82
88
  ```
83
89
 
84
90
  Then park. Do not release a wave or spawn a Legion role until a later delivered wake
85
- shows `human-approved` on the root. You never add that label yourself. Approval covers
86
- the entire tree: later waves, re-scopes, and integration-failure children do not repeat
87
- this sequence.
91
+ shows `design-approved` on the root. Approval covers the entire tree: later waves,
92
+ re-scopes, and integration-failure children do not repeat this sequence.
88
93
 
89
94
  ## 2. Children in flight
90
95
 
@@ -92,7 +97,7 @@ Release only the next useful wave, then give its owners their work. A release is
92
97
  explicit lifecycle write:
93
98
 
94
99
  ```text
95
- legion({ op: "wave_release", children: ["owner/repo#41", "owner/repo#42"] })
100
+ legion({ op: "release_wave", issues: ["LEGION-41", "LEGION-42"] })
96
101
  ```
97
102
 
98
103
  After release, spawn each relevant owner; for example:
@@ -100,7 +105,7 @@ After release, spawn each relevant owner; for example:
100
105
  ```text
101
106
  legion({
102
107
  op: "spawn_worker",
103
- issue: "owner/repo#41",
108
+ issue: "LEGION-41",
104
109
  role: "architect",
105
110
  task: "Own this child through its lifecycle and report its evidence."
106
111
  })
@@ -122,7 +127,7 @@ and current `main` integration surface:
122
127
  ```text
123
128
  legion({
124
129
  op: "spawn_worker",
125
- issue: "owner/repo#40",
130
+ issue: "LEGION-40",
126
131
  role: "tester",
127
132
  task: "Verify this parent issue against its acceptance criteria on current main; return reproducible integration evidence."
128
133
  })
@@ -181,15 +186,12 @@ If anything changes the approved head, return to review; do not let the merger p
181
186
 
182
187
  ## 7. Close
183
188
 
184
- After the merge result and sign-off are recorded, close this issue through the Legion
185
- write surface and include the sign-off comment:
189
+ After the merge result and sign-off are recorded, post the sign-off and close this issue
190
+ through the Legion write surface:
186
191
 
187
192
  ```text
188
- legion({
189
- op: "issue_close",
190
- issue: "owner/repo#40",
191
- comment: "<sign-off: scope, integration evidence, review, retro, Sami approval, and merge>"
192
- })
193
+ dispatch_comment({ issue: "LEGION-40", body: "<sign-off: scope, integration evidence, review, retro, Sami approval, and merge>" })
194
+ legion({ op: "set_status", issue: "LEGION-40", status: "done" })
193
195
  ```
194
196
 
195
197
  Closing a child supplies the closure event to its parent. Do not close a parent until the
@@ -220,6 +222,6 @@ corresponding lifecycle procedure.
220
222
 
221
223
  Controller-actionable matters are exactly re-filing a genuinely independent child,
222
224
  capacity, and cross-tree conflict. Use the Legion escalation operation for those. Handle
223
- everything else in the tree, or use `dispatch` for a human question; workers may reach
224
- Sami directly with `dispatch` the same way. Do not create a wait loop for any wake
225
+ everything else in the tree, or use `dispatch_ask` for a human question; workers may reach
226
+ Sami directly with `dispatch_ask` the same way. Do not create a wait loop for any wake
225
227
  source.
@@ -9,6 +9,10 @@ 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
+
12
16
  ## Start and claim the controller role
13
17
 
14
18
  The Legion extension claims `legion-<project>-controller` and registers controller readiness
@@ -23,11 +27,11 @@ For an interactive takeover, start OMP with `LEGION_CONTROLLER_SECRET` and
23
27
 
24
28
  The command resolves the project from daemon state, claims the Envoy role for the current
25
29
  session, and posts readiness before controller commands can act. It retains the environment
26
- capability for `legion admit` and `legion backlog`. Never pass a secret as a command argument
30
+ capability for `legion({ op: "set_status", issue, status })`. Never pass a secret as a command argument
27
31
  or copy it into a transcript.
28
32
 
29
33
  This handshake lets the daemon redeliver held controller work. It does not turn the controller
30
- into a state holder: daemon state and GitHub artifacts remain authoritative.
34
+ into a state holder: daemon state and the Dispatch project remain authoritative.
31
35
 
32
36
  ## Turn discipline
33
37
 
@@ -36,7 +40,7 @@ into a state holder: daemon state and GitHub artifacts remain authoritative.
36
40
  - **One wake = one turn.** Handle exactly the wake's implication, then end the turn. Never
37
41
  poll, idle-loop, or wait for another event.
38
42
  - **Wakes are advisory.** Before any side effect, verify the current daemon state and the
39
- relevant GitHub artifact. A stale or duplicate wake may cost a read, never a wrong action.
43
+ relevant Dispatch issue. A stale or duplicate wake may cost a read, never a wrong action.
40
44
  - **Controller state is disposable.** Do not reconstruct or preserve local controller
41
45
  bookkeeping between turns.
42
46
 
@@ -44,52 +48,59 @@ into a state holder: daemon state and GitHub artifacts remain authoritative.
44
48
 
45
49
  | Wake | Content | Controller action |
46
50
  |---|---|---|
47
- | New issue added to the project board (webhook: issue opened / project item added; resync heals misses) | issue ref + triage context (incl. pre-existing children) | Triage: spawn root process via daemon admission, or park in the daemon-state backlog |
48
- | Backlog eligibility | slot freed / priority change | Reconsider parked items; deliberately-backlogged issues carry a marker so resync doesn't re-flag them |
49
- | Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; issue-scoped human Q&A goes through `dispatch` from the owning architect, not here |
50
- | Resync report | artifact-driven anomaly list (zero-owner trees, erroring issues) | Verify against fresh state, then dispatch/heal |
51
- | Mention | Slack/GitHub @mention text | Answer, or route to the owning issue's architect role |
52
- | Closed-tree activity (comment, review, CI on a closed tree) | issue, root, event summary | Read the artifact; if work should resume, `legion admit <root>`; otherwise no action — the event is not held or redelivered |
51
+ | New issue created in the Dispatch project (`issue.created`, status `triage`; resync heals misses) | issue key + triage context (incl. pre-existing children) | Triage: `legion({ op: "set_status", issue, status: "todo" })` to admit, or set `backlog`/`icebox` to park |
52
+ | Backlog eligibility | slot freed / priority change | Reconsider parked items and move the eligible root to `todo` |
53
+ | Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; issue-scoped human Q&A goes through `dispatch_ask` from the owning architect, not here |
54
+ | Resync report | artifact-driven anomaly list (zero-owner trees, untriaged-open, launch-failed) | Verify against fresh state, then heal |
55
+ | `child-status` | child key + status transition | Not controller-actionable by default; if the daemon could not route it to the parent's architect role, verify the transition and forward it with `envoy_publish` |
56
+ | Mention | Slack/GitHub PR @mention text | Answer, or route to the owning issue's architect role |
57
+ | Closed-tree activity (comment, review, CI on a closed tree) | issue, root, event summary | Read the artifact; if work should resume, `legion({ op: "set_status", issue: root, status: "todo" })`; otherwise no action — the event is not held or redelivered |
53
58
  | Direct user message | — | Always first |
54
59
 
55
60
  ## New issue triage
56
61
 
57
- 1. Read `legion state --json`, then inspect the reported GitHub issue with `gh issue view`.
58
- Verify the issue is on this project board, is eligible for a root process, and whether it
59
- has pre-existing children. GitHub and daemon state, not the wake text, decide triage.
62
+ 1. Read `legion state --json`, then inspect the reported Dispatch issue with `dispatch_read`.
63
+ Verify the issue is in this project, is eligible for a root process, and whether it
64
+ has pre-existing children. Dispatch and daemon state, not the wake text, decide triage.
60
65
  2. If it should run now, admit the root issue:
61
66
 
62
- ```bash
63
- legion admit <issue>
67
+ ```text
68
+ legion({ op: "set_status", issue: "<issue>", status: "todo" })
64
69
  ```
65
70
 
66
- 3. If it should deliberately wait, record a durable reason instead of leaving it unowned:
71
+ 3. If it should deliberately wait, move it to a parked status instead of leaving it in
72
+ `triage`:
67
73
 
68
- ```bash
69
- legion backlog <issue> --marker <reason>
74
+ ```text
75
+ legion({ op: "set_status", issue: "<issue>", status: "backlog" })
70
76
  ```
71
77
 
72
- The marker is required: it distinguishes intentional backlog from a missed wake during
73
- resync. Do not triage a system-created child as a root issue.
78
+ (or `status: "icebox"` for longer-term deferral). Dispatch status is the durable record;
79
+ there is no separate marker to maintain. Do not triage a system-created child as a root
80
+ issue.
74
81
 
75
82
  ## Backlog eligibility
76
83
 
77
- When a slot frees or priority changes, use `legion state --json` and the current issue
78
- artifact to reconsider marked backlog entries. Admit the selected root with `legion admit
79
- <issue>`. Keep an item backlogged only with a current, explicit marker; changing the marker
80
- is a deliberate controller decision, not a no-op.
84
+ When a slot frees or priority changes, use `legion state --json` and the current Dispatch
85
+ issue to reconsider parked roots. Admit the selected root with
86
+ `legion({ op: "set_status", issue, status: "todo" })`. Moving an item to or from `backlog`/
87
+ `icebox` is a deliberate controller decision, not a no-op.
81
88
 
82
89
  ## Architect escalation
83
90
 
84
91
  Only decide controller-actionable escalations: re-filing independent work, capacity, and
85
- cross-tree conflicts. Issue-scoped human Q&A goes through `dispatch` from the owning
92
+ cross-tree conflicts. Issue-scoped human Q&A goes through `dispatch_ask` from the owning
86
93
  architect, not the controller.
87
94
 
88
- For an independence judgment, verify the child and its parent against GitHub and current
89
- daemon state. If the work belongs in an independent root:
95
+ For an independence judgment, verify the child and its parent against current daemon state
96
+ and the Dispatch issue. If the work belongs in an independent root:
90
97
 
91
- 1. File a **fresh root issue** with `gh`, carrying the necessary context.
92
- 2. Close the child and leave a pointer to the new root issue.
98
+ 1. File a **fresh root issue** with `dispatch_issue({ project, title, spec })` (no `parent`).
99
+ `project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` `LEGSMOKE`) — not
100
+ the role-token `<project>` (the daemon's own project, e.g. `acme`), a different string.
101
+ 2. Park the child (`legion({ op: "set_status", issue: child, status: "icebox" })`) and leave
102
+ a pointer to the new root issue. The controller's capability is `todo`/`backlog`/`icebox`
103
+ only — only the owning architect or the daemon closes an issue as `done`.
93
104
  3. Admit or deliberately backlog the new root through the normal triage procedure.
94
105
 
95
106
  Never promote a child in place. Resolve capacity and cross-tree conflicts from verified
@@ -98,11 +109,11 @@ judgments.
98
109
 
99
110
  ## Resync report
100
111
 
101
- Treat a resync report as an anomaly list, not an instruction. For every zero-owner tree or
102
- erroring issue it names, verify `legion state --json` and the current GitHub artifact first.
103
- Then heal the verified condition: admit an eligible root, restore a deliberately backlogged
104
- marker, or use the applicable daemon control path. Do not act on erroring or stale entries
105
- until their source artifact explains the anomaly.
112
+ Treat a resync report as an anomaly list, not an instruction. For every zero-owner tree,
113
+ untriaged-open, or launch-failed issue it names, verify `legion state --json` and the
114
+ current Dispatch issue first. Then heal the verified condition: admit an eligible root, move
115
+ an issue back to its intended status, or use the applicable daemon control path. Do not act
116
+ on stale entries until their source artifact explains the anomaly.
106
117
 
107
118
  ## Mentions
108
119
 
@@ -111,13 +122,3 @@ human-facing information. Otherwise resolve the authoritative owning architect r
111
122
  route the verified context with `envoy_publish`. Do not route raw event traffic or invent a
112
123
  role token from a partial issue reference.
113
124
 
114
- ## Label vocabulary
115
-
116
- Use only the project labels below, with their stated ownership:
117
-
118
- | Label | Applied by | Removed by | Meaning |
119
- |---|---|---|---|
120
- | `needs-approval` | architect | controller/Sami when applying `human-approved` | design gate armed, awaiting Sami |
121
- | `human-approved` | Sami or controller | Sami | design gate open |
122
- | `legion-child` | daemon | never | system-created child |
123
- | `legion-backlog` | controller | controller | deliberately unowned root |
@@ -11,6 +11,10 @@ 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
+
14
18
  ## Identity, scope, and role
15
19
 
16
20
  The daemon spawns you as a separate `omp --mode rpc` process (behind `legion worker-shim`,
@@ -21,13 +25,11 @@ completes the boot handshake for you at session start — it registers with the
21
25
  your role, and signals readiness. You never call `envoy_role_set` yourself.
22
26
 
23
27
  Your role token is not the issue key spelled out literally. The daemon encodes it as
24
- `legion-<project>-<encoded-owner>__<encoded-repo>-<number>-<role>`, escaping `_`, `.`, and
25
- `-` within the owner and repo names (`_u`, `_d`, `_h`) so `__` is always the one safe
26
- separator. For example, project `acme`, issue `sjawhar/legion#41`, role `architect` encodes
27
- to `legion-acme-sjawhar__legion-41-architect`. Never hand-format one for another role: your
28
- own role topic and your tree's architect's topic are stated at the end of your system
29
- prompt (a "Legion addressing" line the daemon appends), a sibling role's topic is yours
30
- with the trailing `-<role>` replaced, and the `roleToken` helper in `@legion/contracts`
28
+ `legion-<project>-<KEY>-<role>`. For example, project `acme`, issue `LEGION-41`, role
29
+ `architect` encodes to `legion-acme-LEGION-41-architect`. Never hand-format one for another
30
+ role: your own role topic and your tree's architect's topic are stated at the end of your
31
+ system prompt (a "Legion addressing" line the daemon appends), a sibling role's topic is
32
+ yours with the trailing `-<role>` replaced, and the `roleToken` helper in `@legion/contracts`
31
33
  computes any other one exactly the way the daemon does — prefer a topic you've already
32
34
  been given before recomputing one.
33
35
 
@@ -47,7 +49,7 @@ Escalate a product, scope, cross-phase, or lifecycle decision to the owning arch
47
49
  `envoy_publish` to its role topic (`notifications.role.` followed by its encoded token, see
48
50
  above), carrying the verified facts and the decision needed. `hub` only reaches subagents
49
51
  inside your own process, not the architect's separate one. For a durable question that needs
50
- Sami directly, you may use the raw `dispatch` MCP tool yourself; replies return to your own
52
+ Sami directly, you may use `dispatch_ask` yourself; replies return to your own
51
53
  session.
52
54
 
53
55
  Because the same agent is always resumed for its phase, you may receive more than one
@@ -116,6 +118,8 @@ path-scoped workflow.
116
118
 
117
119
  ## Phase work
118
120
 
121
+ Specifications written into Dispatch follow [`skills/dispatch`'s Writing a spec](../dispatch/SKILL.md#writing-a-spec).
122
+
119
123
  Follow the repository's normal engineering workflow and the assigned issue's acceptance
120
124
  criteria. Your phase's own charter and the predecessor handoffs you read define the phase
121
125
  artifact and its completion evidence. Do not replace architect-owned decomposition, gate
@@ -133,11 +137,13 @@ capability it needs; invoke GitHub through the credential helper:
133
137
  legion gh -- <gh args…>
134
138
  ```
135
139
 
136
- ## GitHub comment attribution
140
+ ## GitHub PR comment attribution
137
141
 
138
- Append this exact structured footer to **every** GitHub issue comment, pull-request
139
- comment, and review that this phase posts. It preserves session provenance on the artifact
140
- itself so work stays attributable to the session that produced it:
142
+ Append this exact structured footer to **every** pull-request comment and review that this
143
+ phase posts on GitHub. It preserves session provenance on the artifact itself so work stays
144
+ attributable to the session that produced it. Dispatch comments carry session provenance
145
+ natively through their own `actor`/`origin` fields; this footer is only for GitHub PR
146
+ artifacts:
141
147
 
142
148
  ```html
143
149
  <!-- legion: {"session":"<session-id>","phase":"<phase>"} -->
@@ -146,7 +152,7 @@ itself so work stays attributable to the session that produced it:
146
152
  For example:
147
153
 
148
154
  ```bash
149
- legion gh -- issue comment <issue-number> \
155
+ legion gh -- pr comment <pr-number> \
150
156
  --body $'Verification complete.\n\n<!-- legion: {"session":"<session-id>","phase":"<phase>"} -->' \
151
157
  --repo <owner>/<repo>
152
158
  ```
@@ -159,17 +165,19 @@ procedure:
159
165
 
160
166
  ```bash
161
167
  cd -- "$LEGION_WORKSPACE" && \
162
- jj -R "$LEGION_WORKSPACE" bookmark set legion/issue-<n> && \
163
- jj -R "$LEGION_WORKSPACE" git push --bookmark legion/issue-<n> --allow-new
168
+ jj -R "$LEGION_WORKSPACE" bookmark set legion/<KEY> && \
169
+ jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
164
170
  ```
165
171
 
166
172
  The provisioned issue workspace configures `credential.helper` with the daemon's absolute
167
173
  credential command, so `jj -R "$LEGION_WORKSPACE" git push` authenticates transparently
168
174
  through the same session capability. Never handle a token.
169
175
 
170
- Then open the pull request with `legion gh -- pr create`. The credential helper and
171
- `legion gh` provide the GitHub identity; never export, fetch, or replace a token. Other
172
- phases advance the existing branch rather than creating a replacement bookmark or PR.
176
+ Then open the pull request with `legion gh -- pr create`. The PR body **must** contain the
177
+ line `Dispatch: <KEY>` the daemon's fallback link from a PR to its Dispatch issue when the
178
+ branch name alone is ambiguous. The credential helper and `legion gh` provide the GitHub
179
+ identity; never export, fetch, or replace a token. Other phases advance the existing branch
180
+ rather than creating a replacement bookmark or PR.
173
181
 
174
182
  ## PR body and merge-queue discipline
175
183
 
@@ -243,13 +251,13 @@ cd -- "$LEGION_WORKSPACE" && \
243
251
  ```
244
252
 
245
253
  If the issue bookmark exists locally, advance it and push it with the provisioned
246
- credential helper. `--allow-new` also publishes the locally provisioned bookmark on its
247
- first push:
254
+ credential helper. `--bookmark` also publishes the locally provisioned bookmark on its
255
+ first push — a bookmark not yet tracking a remote one is tracked automatically:
248
256
 
249
257
  ```bash
250
258
  cd -- "$LEGION_WORKSPACE" && \
251
- jj -R "$LEGION_WORKSPACE" bookmark set legion/issue-<n> && \
252
- jj -R "$LEGION_WORKSPACE" git push --bookmark legion/issue-<n> --allow-new
259
+ jj -R "$LEGION_WORKSPACE" bookmark set legion/<KEY> && \
260
+ jj -R "$LEGION_WORKSPACE" git push --bookmark legion/<KEY>
253
261
  ```
254
262
 
255
263
  Do not report phase completion until the write, existence check, and handoff commit
@@ -279,5 +287,5 @@ session, if this phase's work needs to run again.
279
287
 
280
288
  When blocked on lifecycle, scope, or cross-phase matters, `envoy_publish` the owning
281
289
  architect a concise message: issue, phase, verified observation, what you tried, and the
282
- decision required. Reach for `dispatch` yourself only for a standalone human question
290
+ decision required. Reach for `dispatch_ask` yourself only for a standalone human question
283
291
  outside that coordination.
@@ -39,7 +39,7 @@ Returns: title, description, status, labels, comments, attachments.
39
39
  linear_linear(action="update", id="ABC-123", state="Done")
40
40
  linear_linear(action="update", id="ABC-123", priority=1)
41
41
  linear_linear(action="update", id="ABC-123", assignee="me")
42
- linear_linear(action="update", id="ABC-123", labels=["legion-backlog", "existing-label"])
42
+ linear_linear(action="update", id="ABC-123", labels=["bug", "existing-label"])
43
43
  ```
44
44
 
45
45
  **Labels array replaces all labels.** Fetch current labels first, then append.