@sjawhar/pi-legion-envoy 0.28.0 → 0.30.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
@@ -121,8 +121,8 @@ The shared contract supplies the model-facing schemas and descriptions. The
121
121
  feedback, documents, artifacts, and status reads.
122
122
 
123
123
  `dispatch_artifact` accepts exactly one upload source: a local `path`, or inline `content`.
124
- For example, an architect can post its primary specification with
125
- `{ issue, name: "spec.md", content: "# Design", primary: true }`.
124
+ For example, an architect can post a specification directly with
125
+ `{ issue, name: "spec.md", content: "# Design" }`.
126
126
 
127
127
  Lifecycle and scope decisions between Legion roles go through `envoy_publish` to the owning
128
128
  architect's role topic; Dispatch is for durable questions to the human and the shared
package/dist/envoy.js CHANGED
@@ -29700,6 +29700,16 @@ function dispatchToolSchema(spec, z, opts) {
29700
29700
  return spec.validation === undefined ? z.object(shape, opts) : z.refineObject(shape, spec.validation.check, spec.validation.message, opts);
29701
29701
  }
29702
29702
  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).";
29703
+ var SPEC_SECTIONS = [
29704
+ "Decisions needed",
29705
+ "Acceptance",
29706
+ "Requirements",
29707
+ "Design",
29708
+ "Errors",
29709
+ "Testing",
29710
+ "Rejected"
29711
+ ];
29712
+ 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.";
29703
29713
  var ASK_URGENCIES = ["low", "med", "high", "blocking"];
29704
29714
  var DOC_EDIT_OPS = ["replace", "delete", "insert"];
29705
29715
  var dispatchToolSpecs = [
@@ -29711,7 +29721,7 @@ var dispatchToolSpecs = [
29711
29721
  title: z.string().describe("Concise issue title."),
29712
29722
  parent: z.string().describe("Optional parent issue.").optional(),
29713
29723
  external: z.string().describe("Optional external issue reference.").optional(),
29714
- spec: z.string().describe("Optional initial primary-document markdown.").optional()
29724
+ spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional()
29715
29725
  })
29716
29726
  },
29717
29727
  {
@@ -29777,7 +29787,7 @@ var dispatchToolSpecs = [
29777
29787
  },
29778
29788
  {
29779
29789
  name: "dispatch_doc_edit",
29780
- 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}`,
29790
+ 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}`,
29781
29791
  arguments: (z) => ({
29782
29792
  issue: z.string().describe(ISSUE_REFERENCE),
29783
29793
  artifact: z.string().describe("Artifact slug or id for the document."),
@@ -29811,7 +29821,6 @@ var dispatchToolSpecs = [
29811
29821
  name: z.string().describe("Artifact filename shown in Dispatch."),
29812
29822
  path: z.string().describe("Local path to the file to upload.").optional(),
29813
29823
  content: z.string().describe("Inline text to store as a Markdown document.").optional(),
29814
- primary: z.boolean().describe("Make this document the issue primary artifact.").optional(),
29815
29824
  summary: z.string().describe("Optional version summary.").optional()
29816
29825
  }),
29817
29826
  validation: {
@@ -29988,11 +29997,17 @@ var legionRole = _enum2(LEGION_ROLES);
29988
29997
  var requiredUnknown = unknown().refine((value) => value !== undefined, {
29989
29998
  message: "Required"
29990
29999
  });
29991
- var ARCHITECT_MUTABLE_LABELS = ["needs-approval"];
29992
- var architectMutableLabel = _enum2(ARCHITECT_MUTABLE_LABELS);
29993
- function isArchitectMutableLabel(value) {
29994
- return ARCHITECT_MUTABLE_LABELS.some((label) => label === value);
29995
- }
30000
+ var LIFECYCLE_STATUSES = [
30001
+ "triage",
30002
+ "icebox",
30003
+ "backlog",
30004
+ "todo",
30005
+ "in_progress",
30006
+ "testing",
30007
+ "needs_review",
30008
+ "retro",
30009
+ "done"
30010
+ ];
29996
30011
  var architectCapability = strictObject({
29997
30012
  tree: nonEmptyString,
29998
30013
  sessionId: nonEmptyString,
@@ -30045,33 +30060,10 @@ var LegionDaemonApi = {
30045
30060
  request: architectCapability.extend({ generation: number2().int() }),
30046
30061
  response: object({})
30047
30062
  },
30048
- IssueCreate: {
30049
- request: architectCapability.extend({
30050
- title: nonEmptyString,
30051
- body: nonEmptyString,
30052
- labels: array(architectMutableLabel).optional()
30053
- }),
30054
- response: object({ issue: nonEmptyString, url: nonEmptyString })
30055
- },
30056
30063
  WaveRelease: {
30057
- request: architectCapability.extend({ children: array(nonEmptyString).optional() }),
30064
+ request: architectCapability.extend({ issues: array(nonEmptyString).optional() }),
30058
30065
  response: object({ released: array(nonEmptyString) })
30059
30066
  },
30060
- Comment: {
30061
- request: architectCapability.extend({ issue: nonEmptyString, body: nonEmptyString }),
30062
- response: object({ commentId: number2().int(), url: nonEmptyString })
30063
- },
30064
- PostBody: {
30065
- request: architectCapability.extend({ issue: nonEmptyString, body: nonEmptyString }),
30066
- response: object({})
30067
- },
30068
- Labels: {
30069
- request: architectCapability.extend({
30070
- issue: nonEmptyString,
30071
- add: array(architectMutableLabel).optional()
30072
- }),
30073
- response: object({ labels: array(nonEmptyString) })
30074
- },
30075
30067
  Escalate: {
30076
30068
  request: architectCapability.extend({
30077
30069
  kind: _enum2(["re-file", "capacity", "cross-tree"]),
@@ -30079,10 +30071,6 @@ var LegionDaemonApi = {
30079
30071
  }),
30080
30072
  response: object({})
30081
30073
  },
30082
- IssueClose: {
30083
- request: architectCapability.extend({ issue: nonEmptyString, comment: string2().optional() }),
30084
- response: object({})
30085
- },
30086
30074
  ProvisioningCredential: {
30087
30075
  request: architectCapability.extend({ issue: nonEmptyString }),
30088
30076
  response: object({ token: nonEmptyString })
@@ -30145,16 +30133,16 @@ var LegionDaemonApi = {
30145
30133
  secret: nonEmptyString
30146
30134
  })
30147
30135
  },
30148
- GatesApprove: {
30149
- request: controllerIssue,
30136
+ IssueStatus: {
30137
+ request: controllerIssue.extend({
30138
+ status: _enum2(LIFECYCLE_STATUSES),
30139
+ tree: nonEmptyString.optional(),
30140
+ sessionId: nonEmptyString.optional()
30141
+ }),
30150
30142
  response: object({})
30151
30143
  },
30152
- Admission: {
30153
- request: controllerIssue,
30154
- response: object({ result: _enum2(["spawned", "queued"]) })
30155
- },
30156
- Backlog: {
30157
- request: controllerIssue.extend({ marker: nonEmptyString }),
30144
+ GatesRegister: {
30145
+ request: architectCapability.extend({ issue: nonEmptyString, askId: nonEmptyString }),
30158
30146
  response: object({})
30159
30147
  },
30160
30148
  Grant: {
@@ -31218,8 +31206,6 @@ class DispatchClient {
31218
31206
  return this.#json("POST", artifactPath, input);
31219
31207
  const form = new FormData;
31220
31208
  form.set("name", input.name);
31221
- if (input.primary !== undefined)
31222
- form.set("primary", String(input.primary));
31223
31209
  if (input.summary !== undefined)
31224
31210
  form.set("summary", input.summary);
31225
31211
  if (input.actor !== undefined)
@@ -31738,20 +31724,17 @@ Open anchored asks/comments: ${marks.join(", ")}`,
31738
31724
  };
31739
31725
  }
31740
31726
  case "dispatch_artifact": {
31741
- const primary = optionalBoolean(args, "primary");
31742
31727
  const summary = optionalString(args, "summary");
31743
31728
  const name = stringArg(args, "name");
31744
31729
  const content = optionalString(args, "content");
31745
31730
  const result = await client.artifact(issue(), content === undefined ? {
31746
31731
  name,
31747
31732
  file: Bun.file(resolvePath(input.cwd, stringArg(args, "path"))),
31748
- ...primary === undefined ? {} : { primary },
31749
31733
  ...summary === undefined ? {} : { summary },
31750
31734
  actor
31751
31735
  } : {
31752
31736
  name,
31753
31737
  content,
31754
- ...primary === undefined ? {} : { primary },
31755
31738
  ...summary === undefined ? {} : { summary },
31756
31739
  actor
31757
31740
  });
package/dist/legion.js CHANGED
@@ -29698,6 +29698,16 @@ function dispatchToolSchema(spec, z, opts) {
29698
29698
  return spec.validation === undefined ? z.object(shape, opts) : z.refineObject(shape, spec.validation.check, spec.validation.message, opts);
29699
29699
  }
29700
29700
  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).";
29701
+ var SPEC_SECTIONS = [
29702
+ "Decisions needed",
29703
+ "Acceptance",
29704
+ "Requirements",
29705
+ "Design",
29706
+ "Errors",
29707
+ "Testing",
29708
+ "Rejected"
29709
+ ];
29710
+ 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.";
29701
29711
  var ASK_URGENCIES = ["low", "med", "high", "blocking"];
29702
29712
  var DOC_EDIT_OPS = ["replace", "delete", "insert"];
29703
29713
  var dispatchToolSpecs = [
@@ -29709,7 +29719,7 @@ var dispatchToolSpecs = [
29709
29719
  title: z.string().describe("Concise issue title."),
29710
29720
  parent: z.string().describe("Optional parent issue.").optional(),
29711
29721
  external: z.string().describe("Optional external issue reference.").optional(),
29712
- spec: z.string().describe("Optional initial primary-document markdown.").optional()
29722
+ spec: z.string().describe(`Optional initial primary-document markdown. ${SPEC_WRITING_GUIDANCE}`).optional()
29713
29723
  })
29714
29724
  },
29715
29725
  {
@@ -29775,7 +29785,7 @@ var dispatchToolSpecs = [
29775
29785
  },
29776
29786
  {
29777
29787
  name: "dispatch_doc_edit",
29778
- 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}`,
29788
+ 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}`,
29779
29789
  arguments: (z) => ({
29780
29790
  issue: z.string().describe(ISSUE_REFERENCE),
29781
29791
  artifact: z.string().describe("Artifact slug or id for the document."),
@@ -29809,7 +29819,6 @@ var dispatchToolSpecs = [
29809
29819
  name: z.string().describe("Artifact filename shown in Dispatch."),
29810
29820
  path: z.string().describe("Local path to the file to upload.").optional(),
29811
29821
  content: z.string().describe("Inline text to store as a Markdown document.").optional(),
29812
- primary: z.boolean().describe("Make this document the issue primary artifact.").optional(),
29813
29822
  summary: z.string().describe("Optional version summary.").optional()
29814
29823
  }),
29815
29824
  validation: {
@@ -29986,11 +29995,17 @@ var legionRole = _enum2(LEGION_ROLES);
29986
29995
  var requiredUnknown = unknown().refine((value) => value !== undefined, {
29987
29996
  message: "Required"
29988
29997
  });
29989
- var ARCHITECT_MUTABLE_LABELS = ["needs-approval"];
29990
- var architectMutableLabel = _enum2(ARCHITECT_MUTABLE_LABELS);
29991
- function isArchitectMutableLabel(value) {
29992
- return ARCHITECT_MUTABLE_LABELS.some((label) => label === value);
29993
- }
29998
+ var LIFECYCLE_STATUSES = [
29999
+ "triage",
30000
+ "icebox",
30001
+ "backlog",
30002
+ "todo",
30003
+ "in_progress",
30004
+ "testing",
30005
+ "needs_review",
30006
+ "retro",
30007
+ "done"
30008
+ ];
29994
30009
  var architectCapability = strictObject({
29995
30010
  tree: nonEmptyString,
29996
30011
  sessionId: nonEmptyString,
@@ -30043,33 +30058,10 @@ var LegionDaemonApi = {
30043
30058
  request: architectCapability.extend({ generation: number2().int() }),
30044
30059
  response: object({})
30045
30060
  },
30046
- IssueCreate: {
30047
- request: architectCapability.extend({
30048
- title: nonEmptyString,
30049
- body: nonEmptyString,
30050
- labels: array(architectMutableLabel).optional()
30051
- }),
30052
- response: object({ issue: nonEmptyString, url: nonEmptyString })
30053
- },
30054
30061
  WaveRelease: {
30055
- request: architectCapability.extend({ children: array(nonEmptyString).optional() }),
30062
+ request: architectCapability.extend({ issues: array(nonEmptyString).optional() }),
30056
30063
  response: object({ released: array(nonEmptyString) })
30057
30064
  },
30058
- Comment: {
30059
- request: architectCapability.extend({ issue: nonEmptyString, body: nonEmptyString }),
30060
- response: object({ commentId: number2().int(), url: nonEmptyString })
30061
- },
30062
- PostBody: {
30063
- request: architectCapability.extend({ issue: nonEmptyString, body: nonEmptyString }),
30064
- response: object({})
30065
- },
30066
- Labels: {
30067
- request: architectCapability.extend({
30068
- issue: nonEmptyString,
30069
- add: array(architectMutableLabel).optional()
30070
- }),
30071
- response: object({ labels: array(nonEmptyString) })
30072
- },
30073
30065
  Escalate: {
30074
30066
  request: architectCapability.extend({
30075
30067
  kind: _enum2(["re-file", "capacity", "cross-tree"]),
@@ -30077,10 +30069,6 @@ var LegionDaemonApi = {
30077
30069
  }),
30078
30070
  response: object({})
30079
30071
  },
30080
- IssueClose: {
30081
- request: architectCapability.extend({ issue: nonEmptyString, comment: string2().optional() }),
30082
- response: object({})
30083
- },
30084
30072
  ProvisioningCredential: {
30085
30073
  request: architectCapability.extend({ issue: nonEmptyString }),
30086
30074
  response: object({ token: nonEmptyString })
@@ -30143,16 +30131,16 @@ var LegionDaemonApi = {
30143
30131
  secret: nonEmptyString
30144
30132
  })
30145
30133
  },
30146
- GatesApprove: {
30147
- request: controllerIssue,
30134
+ IssueStatus: {
30135
+ request: controllerIssue.extend({
30136
+ status: _enum2(LIFECYCLE_STATUSES),
30137
+ tree: nonEmptyString.optional(),
30138
+ sessionId: nonEmptyString.optional()
30139
+ }),
30148
30140
  response: object({})
30149
30141
  },
30150
- Admission: {
30151
- request: controllerIssue,
30152
- response: object({ result: _enum2(["spawned", "queued"]) })
30153
- },
30154
- Backlog: {
30155
- request: controllerIssue.extend({ marker: nonEmptyString }),
30142
+ GatesRegister: {
30143
+ request: architectCapability.extend({ issue: nonEmptyString, askId: nonEmptyString }),
30156
30144
  response: object({})
30157
30145
  },
30158
30146
  Grant: {
@@ -30399,18 +30387,12 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery) {
30399
30387
  workerReady: (input) => noContent("/legion/v1/worker/ready", input, LegionDaemonApi.WorkerReady.response),
30400
30388
  spawnWorker: (input) => post("/legion/v1/worker/spawn", input, LegionDaemonApi.SpawnWorker.response),
30401
30389
  mergeGate: (input) => post("/legion/v1/merge-gate", input, LegionDaemonApi.MergeGate.response),
30402
- issueCreate: (input) => post("/legion/v1/issues", input, LegionDaemonApi.IssueCreate.response),
30403
- waveRelease: (input) => post("/legion/v1/waves/release", input, LegionDaemonApi.WaveRelease.response),
30404
- comment: (input) => post("/legion/v1/issues/comment", input, LegionDaemonApi.Comment.response),
30390
+ releaseWave: (input) => post("/legion/v1/waves/release", input, LegionDaemonApi.WaveRelease.response),
30405
30391
  provisioningCredential: (input) => post("/legion/v1/provisioning-credential", input, LegionDaemonApi.ProvisioningCredential.response),
30406
- postBody: (input) => noContent("/legion/v1/issues/body", input, LegionDaemonApi.PostBody.response),
30407
- labels: (input) => post("/legion/v1/issues/labels", input, LegionDaemonApi.Labels.response),
30408
30392
  escalate: (input) => noContent("/legion/v1/escalate", input, LegionDaemonApi.Escalate.response),
30409
- issueClose: (input) => noContent("/legion/v1/issues/close", input, LegionDaemonApi.IssueClose.response),
30393
+ issueStatus: (input) => noContent("/legion/v1/issues/status", input, LegionDaemonApi.IssueStatus.response),
30394
+ gatesRegister: (input) => noContent("/legion/v1/gates/register", input, LegionDaemonApi.GatesRegister.response),
30410
30395
  workerSession: (input) => post("/legion/v1/worker-session", input, LegionDaemonApi.WorkerSession.response),
30411
- gatesApprove: (input) => noContent("/legion/v1/gates/approve", input, LegionDaemonApi.GatesApprove.response),
30412
- admission: (input) => post("/legion/v1/admission", input, LegionDaemonApi.Admission.response),
30413
- backlog: (input) => noContent("/legion/v1/backlog", input, LegionDaemonApi.Backlog.response),
30414
30396
  processExit: (input) => noContent("/legion/v1/process/exit", input, LegionDaemonApi.ProcessExit.response),
30415
30397
  grant: (input) => post("/legion/v1/grants", input, LegionDaemonApi.Grant.response),
30416
30398
  githubToken: (input) => post("/legion/v1/gh-token", input, LegionDaemonApi.GitHubToken.response)
@@ -30504,15 +30486,25 @@ function toolFailure(error) {
30504
30486
 
30505
30487
  // src/legion/tools.ts
30506
30488
  var jsonSuccess = (details) => toolSuccess(JSON.stringify(details), details);
30489
+ var LIFECYCLE_STATUSES2 = [
30490
+ "triage",
30491
+ "icebox",
30492
+ "backlog",
30493
+ "todo",
30494
+ "in_progress",
30495
+ "testing",
30496
+ "needs_review",
30497
+ "retro",
30498
+ "done"
30499
+ ];
30500
+ function isLifecycleStatus(value) {
30501
+ return LIFECYCLE_STATUSES2.includes(value);
30502
+ }
30507
30503
  var LEGION_OP_FIELDS = {
30508
- issue_create: ["title", "body", "labels"],
30509
- wave_release: ["children"],
30510
- comment: ["issue", "body"],
30511
- post_spec: ["issue", "body"],
30512
- label_add: ["issue", "label"],
30504
+ set_status: ["issue", "status"],
30505
+ register_gate: ["issue", "askId"],
30506
+ release_wave: ["issues"],
30513
30507
  escalate: ["kind", "context"],
30514
- request_refile: ["issue", "rationale"],
30515
- issue_close: ["issue", "comment"],
30516
30508
  merge_gate: ["pr"],
30517
30509
  spawn_worker: ["issue", "role", "task"]
30518
30510
  };
@@ -30520,27 +30512,20 @@ function legionToolSchema(pi) {
30520
30512
  const z = pi.zod;
30521
30513
  return z.object({
30522
30514
  op: z.enum([
30523
- "issue_create",
30524
- "wave_release",
30525
- "comment",
30526
- "post_spec",
30527
- "label_add",
30515
+ "set_status",
30516
+ "register_gate",
30517
+ "release_wave",
30528
30518
  "escalate",
30529
- "request_refile",
30530
- "issue_close",
30531
30519
  "merge_gate",
30532
30520
  "spawn_worker"
30533
30521
  ]),
30534
- title: z.string().optional(),
30535
- body: z.string().optional(),
30536
- labels: z.array(z.enum(ARCHITECT_MUTABLE_LABELS)).optional(),
30537
- children: z.array(z.string()).optional(),
30538
30522
  issue: z.string().optional(),
30539
- label: z.enum(ARCHITECT_MUTABLE_LABELS).optional(),
30523
+ status: z.enum(LIFECYCLE_STATUSES2).optional(),
30524
+ askId: z.string().optional(),
30540
30525
  kind: z.enum(["re-file", "capacity", "cross-tree"]).optional(),
30541
30526
  context: z.unknown().optional(),
30527
+ issues: z.array(z.string()).optional(),
30542
30528
  rationale: z.string().optional(),
30543
- comment: z.string().optional(),
30544
30529
  pr: z.number().optional(),
30545
30530
  role: z.enum(LEGION_ROLES).optional(),
30546
30531
  task: z.string().optional()
@@ -30589,60 +30574,39 @@ function createLegionTool(deps) {
30589
30574
  sessionId,
30590
30575
  secret: architect.secret
30591
30576
  }));
30592
- case "issue_create": {
30593
- const labels = parameters.labels;
30594
- if (labels !== undefined && (!Array.isArray(labels) || !labels.every((label) => typeof label === "string" && isArchitectMutableLabel(label)))) {
30595
- throw new Error("issue_create labels must use architect-mutable Legion labels");
30596
- }
30597
- return jsonSuccess(await daemon.issueCreate({
30598
- tree: architect.tree,
30599
- sessionId,
30600
- secret: architect.secret,
30601
- title: stringInput("title"),
30602
- body: stringInput("body"),
30603
- labels: labels ?? []
30604
- }));
30605
- }
30606
- case "wave_release": {
30607
- const children = parameters.children;
30608
- if (!Array.isArray(children) || !children.every((child) => typeof child === "string")) {
30609
- throw new Error("wave_release requires children");
30577
+ case "set_status": {
30578
+ const status = parameters.status;
30579
+ if (typeof status !== "string" || !isLifecycleStatus(status)) {
30580
+ throw new Error("set_status requires a valid Legion issue status");
30610
30581
  }
30611
- return jsonSuccess(await daemon.waveRelease({
30612
- tree: architect.tree,
30613
- children,
30614
- sessionId,
30615
- secret: architect.secret
30616
- }));
30617
- }
30618
- case "comment":
30619
- return jsonSuccess(await daemon.comment({
30582
+ await daemon.issueStatus({
30620
30583
  tree: architect.tree,
30621
30584
  sessionId,
30622
30585
  secret: architect.secret,
30623
30586
  issue: stringInput("issue"),
30624
- body: stringInput("body")
30625
- }));
30626
- case "post_spec":
30627
- await daemon.postBody({
30587
+ status
30588
+ });
30589
+ return jsonSuccess({});
30590
+ }
30591
+ case "register_gate":
30592
+ await daemon.gatesRegister({
30628
30593
  tree: architect.tree,
30629
30594
  sessionId,
30630
30595
  secret: architect.secret,
30631
30596
  issue: stringInput("issue"),
30632
- body: stringInput("body")
30597
+ askId: stringInput("askId")
30633
30598
  });
30634
30599
  return jsonSuccess({});
30635
- case "label_add": {
30636
- const label = stringInput("label");
30637
- if (!isArchitectMutableLabel(label)) {
30638
- throw new Error("label changes must use architect-mutable Legion labels");
30600
+ case "release_wave": {
30601
+ const issues = parameters.issues;
30602
+ if (!Array.isArray(issues) || !issues.every((issue) => typeof issue === "string")) {
30603
+ throw new Error("release_wave requires issues");
30639
30604
  }
30640
- return jsonSuccess(await daemon.labels({
30605
+ return jsonSuccess(await daemon.releaseWave({
30641
30606
  tree: architect.tree,
30607
+ issues,
30642
30608
  sessionId,
30643
- secret: architect.secret,
30644
- issue: stringInput("issue"),
30645
- add: [label]
30609
+ secret: architect.secret
30646
30610
  }));
30647
30611
  }
30648
30612
  case "escalate": {
@@ -30661,28 +30625,6 @@ function createLegionTool(deps) {
30661
30625
  });
30662
30626
  return jsonSuccess({});
30663
30627
  }
30664
- case "request_refile":
30665
- await daemon.escalate({
30666
- tree: architect.tree,
30667
- sessionId,
30668
- secret: architect.secret,
30669
- kind: "re-file",
30670
- context: { issue: stringInput("issue"), rationale: stringInput("rationale") }
30671
- });
30672
- return jsonSuccess({});
30673
- case "issue_close": {
30674
- const comment = parameters.comment;
30675
- if (comment !== undefined && typeof comment !== "string")
30676
- throw new Error("issue_close comment must be a string");
30677
- await daemon.issueClose({
30678
- tree: architect.tree,
30679
- sessionId,
30680
- secret: architect.secret,
30681
- issue: stringInput("issue"),
30682
- ...comment === undefined ? {} : { comment }
30683
- });
30684
- return jsonSuccess({});
30685
- }
30686
30628
  case "spawn_worker": {
30687
30629
  const role = parameters.role;
30688
30630
  if (typeof role !== "string" || !LEGION_ROLES.includes(role)) {
@@ -31703,8 +31645,6 @@ class DispatchClient {
31703
31645
  return this.#json("POST", artifactPath, input);
31704
31646
  const form = new FormData;
31705
31647
  form.set("name", input.name);
31706
- if (input.primary !== undefined)
31707
- form.set("primary", String(input.primary));
31708
31648
  if (input.summary !== undefined)
31709
31649
  form.set("summary", input.summary);
31710
31650
  if (input.actor !== undefined)
@@ -32223,20 +32163,17 @@ Open anchored asks/comments: ${marks.join(", ")}`,
32223
32163
  };
32224
32164
  }
32225
32165
  case "dispatch_artifact": {
32226
- const primary = optionalBoolean(args, "primary");
32227
32166
  const summary = optionalString(args, "summary");
32228
32167
  const name = stringArg(args, "name");
32229
32168
  const content = optionalString(args, "content");
32230
32169
  const result = await client.artifact(issue(), content === undefined ? {
32231
32170
  name,
32232
32171
  file: Bun.file(resolvePath(input.cwd, stringArg(args, "path"))),
32233
- ...primary === undefined ? {} : { primary },
32234
32172
  ...summary === undefined ? {} : { summary },
32235
32173
  actor
32236
32174
  } : {
32237
32175
  name,
32238
32176
  content,
32239
- ...primary === undefined ? {} : { primary },
32240
32177
  ...summary === undefined ? {} : { summary },
32241
32178
  actor
32242
32179
  });
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "0.28.0",
3
+ "version": "0.30.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [