@sjawhar/pi-legion-envoy 0.41.0 → 0.41.2

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/dist/envoy.js CHANGED
@@ -29663,14 +29663,16 @@ var IssueEventPayloadSchema = object({
29663
29663
  route: string2().nullish()
29664
29664
  });
29665
29665
  var ArtifactCreatedEventPayloadSchema = object({
29666
- artifact: object({ name: string2().optional() }).optional()
29666
+ artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
29667
29667
  });
29668
29668
  var ArtifactVersionEventPayloadSchema = object({
29669
+ artifact_id: string2().optional(),
29669
29670
  name: string2().optional(),
29670
29671
  version: object({ number: number2().optional(), summary: string2().nullish() }).optional(),
29671
29672
  diff: string2().optional()
29672
29673
  });
29673
29674
  var askEventPayloadFields = {
29675
+ id: string2().optional(),
29674
29676
  opened_event_id: number2().int().positive(),
29675
29677
  question: string2().optional(),
29676
29678
  options: array(object({ label: string2().optional() })).nullish(),
@@ -29700,15 +29702,22 @@ var AskEditedEventPayloadSchema = object({
29700
29702
  edited_by: object({ kind: string2(), id: string2() }).passthrough()
29701
29703
  });
29702
29704
  var CommentEventPayloadSchema = object({
29705
+ id: string2().optional(),
29703
29706
  artifact_name: string2().optional(),
29704
29707
  body: string2().optional(),
29705
29708
  reply_to: string2().nullish(),
29706
29709
  ask_id: string2().nullish(),
29707
29710
  ask_question: string2().optional(),
29708
29711
  anchor: object({ quote: string2().optional() }).nullish(),
29709
- suggestion: object({ replace_with: string2().optional() }).nullish()
29712
+ suggestion: object({ replace_with: string2().optional() }).nullish(),
29713
+ author: object({ kind: string2(), id: string2() }).optional(),
29714
+ created_at: string2().optional()
29715
+ });
29716
+ var MessageEventPayloadSchema = object({
29717
+ id: string2().optional(),
29718
+ body: string2().optional(),
29719
+ author: object({ kind: string2(), id: string2() }).optional()
29710
29720
  });
29711
- var MessageEventPayloadSchema = object({ body: string2().optional() });
29712
29721
  var ChildStatusEventPayloadSchema = object({
29713
29722
  child_key: string2().optional(),
29714
29723
  from: string2().optional(),
@@ -29809,7 +29818,7 @@ var dispatchToolSpecs = [
29809
29818
  },
29810
29819
  {
29811
29820
  name: "dispatch_ask",
29812
- description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + `use dispatch_message instead. Question is at most 800 characters and has at most 8 options. ${OWNER_REFERENCE}`,
29821
+ description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". Question is at most 800 ` + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29813
29822
  arguments: (z) => ({
29814
29823
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29815
29824
  project: z.string().describe("Project key owning the document.").optional(),
@@ -29890,7 +29899,7 @@ var dispatchToolSpecs = [
29890
29899
  },
29891
29900
  {
29892
29901
  name: "dispatch_message",
29893
- description: "Post a plain issue update. Do not use it for a decision or line-specific review; use dispatch_ask " + `or dispatch_comment instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29902
+ description: "Post a note to the issue's Conversation for humans: a status they should see now, or a reply to a human's message. " + "Not a progress ledger (the issue's progress.md artifact), a decision (dispatch_ask), or document feedback " + `(dispatch_comment). Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29894
29903
  arguments: (z) => ({
29895
29904
  issue: z.string().describe(ISSUE_REFERENCE),
29896
29905
  body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters.")
@@ -29898,7 +29907,7 @@ var dispatchToolSpecs = [
29898
29907
  },
29899
29908
  {
29900
29909
  name: "dispatch_doc_edit",
29901
- description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29910
+ description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + "dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. The spec (or any document) holds requirements, " + `design, and decisions \u2014 record progress in the issue's progress.md artifact instead. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29902
29911
  arguments: (z) => ({
29903
29912
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29904
29913
  project: z.string().describe("Project key owning the document.").optional(),
@@ -30988,6 +30997,8 @@ var DISPATCH_PAYLOAD_SCHEMAS = {
30988
30997
  "ask.resolved": AskEventPayloadSchema,
30989
30998
  "comment.created": CommentEventPayloadSchema,
30990
30999
  "comment.resolved": CommentEventPayloadSchema,
31000
+ "comment.reopened": CommentEventPayloadSchema,
31001
+ "comment.edited": CommentEventPayloadSchema,
30991
31002
  "suggestion.accepted": CommentEventPayloadSchema,
30992
31003
  "suggestion.rejected": CommentEventPayloadSchema,
30993
31004
  "message.created": MessageEventPayloadSchema,
@@ -31757,7 +31768,7 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
31757
31768
  return { args, ref: null, owner: null };
31758
31769
  const refArgument = args.ref;
31759
31770
  const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
31760
- throw new Error("ref must be a valid dispatch:// reference");
31771
+ throw new Error("ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/artifact/<slug>, or dispatch://PROJECT/artifact/<slug>");
31761
31772
  })() : null;
31762
31773
  const issueArgument = args.issue;
31763
31774
  const projectArgument = args.project;
@@ -31829,13 +31840,27 @@ async function resolveArtifact(client, owner, artifactReference) {
31829
31840
  if (artifactReference === undefined) {
31830
31841
  throw new Error("artifact is required for a project document");
31831
31842
  }
31832
- return {
31833
- owner,
31834
- artifact: await client.getProjectArtifact(owner.project, artifactReference)
31835
- };
31843
+ try {
31844
+ return {
31845
+ owner,
31846
+ artifact: await client.getProjectArtifact(owner.project, artifactReference)
31847
+ };
31848
+ } catch (error) {
31849
+ if (!(error instanceof DispatchServiceError) || error.status !== 404)
31850
+ throw error;
31851
+ const artifacts = await client.listProjectArtifacts(owner.project, true);
31852
+ const matches = artifacts.filter((candidate) => candidate.name === artifactReference);
31853
+ if (matches.length > 1) {
31854
+ throw new Error(`artifact name ${artifactReference} is ambiguous in project ${owner.project}; ` + `${matches.length} documents share it \u2014 use its slug instead`);
31855
+ }
31856
+ const artifact = matches[0];
31857
+ if (!artifact)
31858
+ throw error;
31859
+ return { owner, artifact };
31860
+ }
31836
31861
  }
31837
31862
  const issue = await client.getIssue(owner.issue);
31838
- const artifact = artifactReference === undefined || artifactReference === "spec" ? issue.artifacts.find((candidate) => candidate.primary || candidate.id === issue.primary_artifact_id) : issue.artifacts.find((candidate) => candidate.id === artifactReference || candidate.slug === artifactReference);
31863
+ const artifact = artifactReference === undefined || artifactReference === "spec" ? issue.artifacts.find((candidate) => candidate.primary || candidate.id === issue.primary_artifact_id) : issue.artifacts.find((candidate) => candidate.id === artifactReference || candidate.slug === artifactReference || candidate.name === artifactReference);
31839
31864
  if (!artifact) {
31840
31865
  throw new Error(`artifact ${artifactReference ?? "spec"} was not found on issue ${issue.key}`);
31841
31866
  }
@@ -32201,8 +32226,9 @@ Open anchored asks/comments: ${marks.join(", ")}`,
32201
32226
  };
32202
32227
  const artifactOwner = documentOwner();
32203
32228
  const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
32229
+ const artifactRef = artifactOwner.kind === "project" ? `dispatch://${artifactOwner.project}/artifact/${result.artifact.slug}` : `dispatch://${issue()}/artifact/${result.artifact.slug}`;
32204
32230
  return {
32205
- text: `Uploaded ${result.artifact.name} as version ${result.version.number}`,
32231
+ text: `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef})`,
32206
32232
  details: artifactOwner.kind === "project" ? {
32207
32233
  ...documentResultDetails(result.artifact),
32208
32234
  version: result.version.number
package/dist/legion.js CHANGED
@@ -29662,14 +29662,16 @@ var IssueEventPayloadSchema = object({
29662
29662
  route: string2().nullish()
29663
29663
  });
29664
29664
  var ArtifactCreatedEventPayloadSchema = object({
29665
- artifact: object({ name: string2().optional() }).optional()
29665
+ artifact: object({ id: string2().optional(), slug: string2().optional(), name: string2().optional() }).optional()
29666
29666
  });
29667
29667
  var ArtifactVersionEventPayloadSchema = object({
29668
+ artifact_id: string2().optional(),
29668
29669
  name: string2().optional(),
29669
29670
  version: object({ number: number2().optional(), summary: string2().nullish() }).optional(),
29670
29671
  diff: string2().optional()
29671
29672
  });
29672
29673
  var askEventPayloadFields = {
29674
+ id: string2().optional(),
29673
29675
  opened_event_id: number2().int().positive(),
29674
29676
  question: string2().optional(),
29675
29677
  options: array(object({ label: string2().optional() })).nullish(),
@@ -29699,15 +29701,22 @@ var AskEditedEventPayloadSchema = object({
29699
29701
  edited_by: object({ kind: string2(), id: string2() }).passthrough()
29700
29702
  });
29701
29703
  var CommentEventPayloadSchema = object({
29704
+ id: string2().optional(),
29702
29705
  artifact_name: string2().optional(),
29703
29706
  body: string2().optional(),
29704
29707
  reply_to: string2().nullish(),
29705
29708
  ask_id: string2().nullish(),
29706
29709
  ask_question: string2().optional(),
29707
29710
  anchor: object({ quote: string2().optional() }).nullish(),
29708
- suggestion: object({ replace_with: string2().optional() }).nullish()
29711
+ suggestion: object({ replace_with: string2().optional() }).nullish(),
29712
+ author: object({ kind: string2(), id: string2() }).optional(),
29713
+ created_at: string2().optional()
29714
+ });
29715
+ var MessageEventPayloadSchema = object({
29716
+ id: string2().optional(),
29717
+ body: string2().optional(),
29718
+ author: object({ kind: string2(), id: string2() }).optional()
29709
29719
  });
29710
- var MessageEventPayloadSchema = object({ body: string2().optional() });
29711
29720
  var ChildStatusEventPayloadSchema = object({
29712
29721
  child_key: string2().optional(),
29713
29722
  from: string2().optional(),
@@ -29808,7 +29817,7 @@ var dispatchToolSpecs = [
29808
29817
  },
29809
29818
  {
29810
29819
  name: "dispatch_ask",
29811
- description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + `use dispatch_message instead. Question is at most 800 characters and has at most 8 options. ${OWNER_REFERENCE}`,
29820
+ description: "Open a durable, answerable decision on an issue or project document. Do not use it for a status update or discussion; " + "use dispatch_message instead. Anchor a document question, thread reply_to/reply_to_ask, or cite a dispatch:// " + `reference \u2014 it must be answerable from its own text and anchor alone, never "see above". Question is at most 800 ` + `characters and has at most 8 options. ${OWNER_REFERENCE}`,
29812
29821
  arguments: (z) => ({
29813
29822
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29814
29823
  project: z.string().describe("Project key owning the document.").optional(),
@@ -29889,7 +29898,7 @@ var dispatchToolSpecs = [
29889
29898
  },
29890
29899
  {
29891
29900
  name: "dispatch_message",
29892
- description: "Post a plain issue update. Do not use it for a decision or line-specific review; use dispatch_ask " + `or dispatch_comment instead. Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29901
+ description: "Post a note to the issue's Conversation for humans: a status they should see now, or a reply to a human's message. " + "Not a progress ledger (the issue's progress.md artifact), a decision (dispatch_ask), or document feedback " + `(dispatch_comment). Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
29893
29902
  arguments: (z) => ({
29894
29903
  issue: z.string().describe(ISSUE_REFERENCE),
29895
29904
  body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters.")
@@ -29897,7 +29906,7 @@ var dispatchToolSpecs = [
29897
29906
  },
29898
29907
  {
29899
29908
  name: "dispatch_doc_edit",
29900
- description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + `dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29909
+ description: "Apply deterministic text edits to an issue or project document. Do not use it for review feedback or for reading; use " + "dispatch_comment, dispatch_suggest, or dispatch_doc_read instead. The spec (or any document) holds requirements, " + `design, and decisions \u2014 record progress in the issue's progress.md artifact instead. ${OWNER_REFERENCE} ${SPEC_WRITING_GUIDANCE}`,
29901
29910
  arguments: (z) => ({
29902
29911
  issue: z.string().describe(ISSUE_REFERENCE).optional(),
29903
29912
  project: z.string().describe("Project key owning the document.").optional(),
@@ -31435,6 +31444,8 @@ var DISPATCH_PAYLOAD_SCHEMAS = {
31435
31444
  "ask.resolved": AskEventPayloadSchema,
31436
31445
  "comment.created": CommentEventPayloadSchema,
31437
31446
  "comment.resolved": CommentEventPayloadSchema,
31447
+ "comment.reopened": CommentEventPayloadSchema,
31448
+ "comment.edited": CommentEventPayloadSchema,
31438
31449
  "suggestion.accepted": CommentEventPayloadSchema,
31439
31450
  "suggestion.rejected": CommentEventPayloadSchema,
31440
31451
  "message.created": MessageEventPayloadSchema,
@@ -32197,7 +32208,7 @@ async function resolveOwnerArguments(tool, args, cwd, env, exec) {
32197
32208
  return { args, ref: null, owner: null };
32198
32209
  const refArgument = args.ref;
32199
32210
  const ref = typeof refArgument === "string" ? parseDispatchRef(refArgument) ?? (() => {
32200
- throw new Error("ref must be a valid dispatch:// reference");
32211
+ throw new Error("ref must be a valid dispatch:// reference such as dispatch://KEY-1, " + "dispatch://KEY-1/ask/<uuid>, dispatch://KEY-1/comment/<uuid>, " + "dispatch://KEY-1/artifact/<slug>, or dispatch://PROJECT/artifact/<slug>");
32201
32212
  })() : null;
32202
32213
  const issueArgument = args.issue;
32203
32214
  const projectArgument = args.project;
@@ -32269,13 +32280,27 @@ async function resolveArtifact(client, owner, artifactReference) {
32269
32280
  if (artifactReference === undefined) {
32270
32281
  throw new Error("artifact is required for a project document");
32271
32282
  }
32272
- return {
32273
- owner,
32274
- artifact: await client.getProjectArtifact(owner.project, artifactReference)
32275
- };
32283
+ try {
32284
+ return {
32285
+ owner,
32286
+ artifact: await client.getProjectArtifact(owner.project, artifactReference)
32287
+ };
32288
+ } catch (error) {
32289
+ if (!(error instanceof DispatchServiceError) || error.status !== 404)
32290
+ throw error;
32291
+ const artifacts = await client.listProjectArtifacts(owner.project, true);
32292
+ const matches = artifacts.filter((candidate) => candidate.name === artifactReference);
32293
+ if (matches.length > 1) {
32294
+ throw new Error(`artifact name ${artifactReference} is ambiguous in project ${owner.project}; ` + `${matches.length} documents share it \u2014 use its slug instead`);
32295
+ }
32296
+ const artifact = matches[0];
32297
+ if (!artifact)
32298
+ throw error;
32299
+ return { owner, artifact };
32300
+ }
32276
32301
  }
32277
32302
  const issue = await client.getIssue(owner.issue);
32278
- const artifact = artifactReference === undefined || artifactReference === "spec" ? issue.artifacts.find((candidate) => candidate.primary || candidate.id === issue.primary_artifact_id) : issue.artifacts.find((candidate) => candidate.id === artifactReference || candidate.slug === artifactReference);
32303
+ const artifact = artifactReference === undefined || artifactReference === "spec" ? issue.artifacts.find((candidate) => candidate.primary || candidate.id === issue.primary_artifact_id) : issue.artifacts.find((candidate) => candidate.id === artifactReference || candidate.slug === artifactReference || candidate.name === artifactReference);
32279
32304
  if (!artifact) {
32280
32305
  throw new Error(`artifact ${artifactReference ?? "spec"} was not found on issue ${issue.key}`);
32281
32306
  }
@@ -32641,8 +32666,9 @@ Open anchored asks/comments: ${marks.join(", ")}`,
32641
32666
  };
32642
32667
  const artifactOwner = documentOwner();
32643
32668
  const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
32669
+ const artifactRef = artifactOwner.kind === "project" ? `dispatch://${artifactOwner.project}/artifact/${result.artifact.slug}` : `dispatch://${issue()}/artifact/${result.artifact.slug}`;
32644
32670
  return {
32645
- text: `Uploaded ${result.artifact.name} as version ${result.version.number}`,
32671
+ text: `Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef})`,
32646
32672
  details: artifactOwner.kind === "project" ? {
32647
32673
  ...documentResultDetails(result.artifact),
32648
32674
  version: result.version.number
@@ -5,17 +5,17 @@ description: "Use when asking Sami a question, updating the spec, commenting on
5
5
 
6
6
  # Dispatch
7
7
 
8
- Dispatch is your issue's living spec, asks, comments, and artifacts. The transcript is your
9
- scratch pad. Anything meant for a human goes through a `dispatch_*` tool.
8
+ Dispatch is your issue's or project document's living spec, asks, comments, and artifacts. The transcript is your scratch pad. Anything
9
+ meant for a human goes through a `dispatch_*` tool.
10
10
 
11
- The server enforces high signal: an ask question is at most 800 characters with at most eight
12
- options; comment and message bodies are at most 2,000 characters; an artifact is at most 25 MiB.
13
- It refuses over-limit input; it never truncates it. GitHub threads and markers no longer exist.
11
+ The server enforces high signal: an ask question is at most 800 characters with at most eight options; comment and message bodies are at
12
+ most 2,000 characters; an artifact is at most 25 MiB. It refuses over-limit input; it never truncates it. GitHub threads and markers no
13
+ longer exist.
14
14
 
15
15
  ## Writing a spec
16
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.
17
+ A spec is a decision record for the human who decides and the implementer who builds, not a transcript of your thinking. Use exactly
18
+ these document headings in this order.
19
19
 
20
20
  | Section | Required content | Form |
21
21
  | --- | --- | --- |
@@ -35,32 +35,23 @@ transcript of your thinking. Use exactly these document headings in this order.
35
35
  - Do not hedge with “might” or “could consider.”
36
36
  - Do not use TBD, TODO, or placeholders; an open item is a Decision needed.
37
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.
38
+ - Update the spec in place as decisions land: the spec is the record, comments are the discussion.
39
+ - Before sending it: no sections conflict, and every requirement has exactly one reading.
46
40
 
47
41
  ## Your owner
48
42
 
49
- Every session works on an issue or project document. Legion pre-fills `issue` from
50
- `LEGION_ISSUE`: use a native issue key such as `LEGION-3`, an external `owner/repo#n`
51
- reference, or a bare positive number (resolved against the cwd repository). Otherwise pass
52
- exactly one owner to every owner-scoped tool: `issue` for an issue, or `project` and
53
- `artifact` for an unlinked project document. A project key such as `CORE` with artifact
54
- `design-notes` identifies `dispatch://CORE/artifact/design-notes`. On first use, an external
55
- issue reference creates its native issue in the project configured for that repository in
56
- Dispatch Settings, then falls back to `DISPATCH_DEFAULT_PROJECT`.
43
+ Every session works on an issue or project document. Legion pre-fills `issue` from `LEGION_ISSUE`: use a native issue key such as
44
+ `LEGION-3`, an external `owner/repo#n` reference, or a bare positive number (resolved against the cwd repository). Otherwise pass
45
+ exactly one owner to every owner-scoped tool: `issue` for an issue, or `project` and `artifact` for an unlinked project document (see
46
+ [References](#references) for the resulting ref shape). On first use, an external issue reference creates its native issue in the
47
+ project configured for that repository in Dispatch Settings, then falls back to `DISPATCH_DEFAULT_PROJECT`.
57
48
 
58
49
  Architects create newly tracked child work with:
59
50
  ```ts
60
51
  dispatch_issue({ project, title, parent?, external?, spec?, force? })
61
52
  ```
62
- It returns `details` `{ issue, topic }`. Use `dispatch_issue` only to create an issue; never use
63
- it to park a question. When `spec` is supplied, follow [Writing a spec](#writing-a-spec).
53
+ It returns `details` `{ issue, topic }`. Use `dispatch_issue` only to create an issue; never use it to park a question. When `spec` is
54
+ supplied, follow [Writing a spec](#writing-a-spec).
64
55
 
65
56
  ## Search first
66
57
 
@@ -68,13 +59,12 @@ Before you create an issue or start a design document, search:
68
59
  ```ts
69
60
  dispatch_search({ query, project?, limit? })
70
61
  ```
71
- It returns every issue, document, comment, ask, and message that contains the words, with the
72
- issue key and a link. Cite the hit you build on (`dispatch://KEY` or the document reference), or
73
- state "no prior issue" in the spec. Websearch syntax applies: `"merge queue"`, `-daemon`, `OR`.
62
+ It returns every issue, document, comment, ask, and message that contains the words, with the issue key and a link. Cite the hit you
63
+ build on (`dispatch://KEY` or the document reference), or state "no prior issue" in the spec. Websearch syntax applies: `"merge queue"`,
64
+ `-daemon`, `OR`.
74
65
 
75
- `dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns
76
- the candidates (`POSSIBLE_DUPLICATE`). Read them; reference the existing issue, or repeat the
77
- call with `force: true` when it is genuinely new work.
66
+ `dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns the candidates (`POSSIBLE_DUPLICATE`).
67
+ Read them; reference the existing issue, or repeat the call with `force: true` when it is genuinely new work.
78
68
 
79
69
  ## Asking
80
70
 
@@ -91,14 +81,14 @@ dispatch_ask({
91
81
  anchor?: { artifact, quote, occurrence? },
92
82
  })
93
83
  ```
94
- It returns `details` `{ issue, topic, ask }` for an issue or `{ project, artifact, document,
95
- topic, ask }` for a project document. Options are buttons: never enumerate choices in
96
- prose. Put the recommendation in `question`, and put each selectable choice in `options`.
97
- Anchor a document question with `anchor: { artifact, quote, occurrence? }`; `occurrence` is
98
- zero-based and selects a repeated quote. The server writes the resulting mark. The HTTP API also
99
- accepts `{ artifact, mark_id }` from a browser that has already written its mark; Dispatch tools
100
- use the quote form. An anchor whose quote disappears becomes orphaned but remains readable against
101
- its original document version.
84
+ It returns `details` `{ issue, topic, ask }` for an issue or `{ project, artifact, document, topic, ask }` for a project document.
85
+ Options are buttons: never enumerate choices in prose. Put the recommendation in `question`, and put each selectable choice in
86
+ `options`. Anchor a document question with `anchor: { artifact, quote, occurrence? }`; `occurrence` is zero-based and selects a repeated
87
+ quote, and an anchor whose quote later disappears becomes orphaned but stays readable against its original document version.
88
+
89
+ An ask must be answerable from its own text and its anchor alone. Anchor a question about a document passage with `anchor`; thread one
90
+ about a comment with `reply_to`; thread a follow-up on your own ask with `reply_to_ask`; cite anything else with a `dispatch://`
91
+ reference (see [References](#references)). Never write "see above", "the message above", or "as attached".
102
92
 
103
93
  Correct or refine an open ask in place instead of opening a second question:
104
94
  ```ts
@@ -110,12 +100,11 @@ dispatch_edit_ask({
110
100
  urgency?,
111
101
  })
112
102
  ```
113
- At least one field besides `ask` is required. Use this only while the same decision remains
114
- open: it keeps the prior text in the event log. An answered or resolved ask cannot be edited.
115
- If the decision is moot or superseded, retract the old ask and open a new one.
103
+ At least one field besides `ask` is required. Use this only while the same decision remains open: it keeps the prior text in the event
104
+ log. An answered or resolved ask cannot be edited. If the decision is moot or superseded, retract the old ask and open a new one.
116
105
 
117
- An ask stays open until a human answers, unless its question no longer needs that answer. Retract a
118
- moot or superseded question, or self-resolve one after finding the answer:
106
+ An ask stays open until a human answers, unless its question no longer needs that answer. Retract a moot or superseded question, or
107
+ self-resolve one after finding the answer:
119
108
  ```ts
120
109
  dispatch_resolve_ask({
121
110
  ask,
@@ -123,31 +112,31 @@ dispatch_resolve_ask({
123
112
  reason: "A newer ask supersedes this question.",
124
113
  })
125
114
  ```
126
- Use `retracted` when the question is obsolete and `resolved` when you found the answer. Include the
127
- reason because the question remains in its Conversation card and reply thread. Resolution is not an
128
- answer: it never records a human decision, and an answered ask cannot be resolved.
115
+ Use `retracted` when the question is obsolete and `resolved` when you found the answer. Include the reason because the question remains
116
+ in its Conversation card and reply thread. Resolution is not an answer: it never records a human decision, and an answered ask cannot be
117
+ resolved. A human may reply to an open or answered ask; so may you, e.g. after finding the answer — use `reply_to_ask` on
118
+ `dispatch_comment` (mutually exclusive with `reply_to`).
129
119
 
130
- An ask is a thread, not a dead end: a human can reply to it before or after answering, and you
131
- (the asker) can reply too — e.g. acknowledging a clarifying question, or following up after the
132
- answer. Use `reply_to_ask` on `dispatch_comment` to reply under your own ask; it is mutually
133
- exclusive with `reply_to`.
120
+ ## The Spec
134
121
 
135
- ## The spec is where narrative goes
136
- Write and update the issue specification according to [Writing a spec](#writing-a-spec).
122
+ The spec holds requirements, design, acceptance, decisions, and rejected alternatives, structured per [Writing a spec](#writing-a-spec).
123
+ It changes only when a decision or requirement changes, and every version that records one is named with `summary`. Never write
124
+ progress, status, timestamps, an "Update HH:MMZ" section, a PR list, or handoff notes into the spec — that belongs in
125
+ [Progress](#progress) instead.
137
126
 
138
127
  Read the current document before changing it:
139
128
 
140
129
  ```ts
141
130
  dispatch_doc_read({ issue?, project?, artifact?, version?, ref? })
142
131
  ```
143
- It returns live or versioned markdown with open marks. `issue` with an omitted `artifact`
144
- reads the issue specification; a project needs `artifact`; and a
145
- `dispatch://PROJECT/artifact/<slug>` ref supplies both. Then write narrative with:
132
+ It returns live or versioned markdown with open marks. `issue` with an omitted `artifact` reads the issue specification; a project needs
133
+ `artifact`; and a `dispatch://PROJECT/artifact/<slug>` ref supplies both. Then write with:
134
+
146
135
  ```ts
147
136
  dispatch_doc_edit({ issue?, project?, artifact, ops, summary? })
148
137
  ```
149
- It returns issue or project-document owner details plus `applied`, optional `version`, and its
150
- write `topic`. `ops` is an array of this exact `EditOp` shape:
138
+ It returns issue or project-document owner details plus `applied`, optional `version`, and its write `topic`. `ops` is an array of this
139
+ exact `EditOp` shape:
151
140
 
152
141
  ```ts
153
142
  type EditOp = {
@@ -161,16 +150,40 @@ type EditOp = {
161
150
  };
162
151
  ```
163
152
 
164
- Target `replace` and `delete` by the document's plain text: inline-code and link text match
165
- without Markdown syntax, and a table-cell anchor is its cell text. `replace` requires `find` and
166
- `with`; `delete` requires `find`; `insert` requires `markdown` and exactly one of `after` or
167
- `before`. An insert anchor is a quote, `"start"`, `"end"`, or `"heading:Title"`. Every insert
168
- creates a sibling block before or after the quote or heading's enclosing document block; `"start"`
169
- and `"end"` select the document edges. At a table-cell quote, pipe-table body-row fragments extend
170
- that table before or after the matched row; omit table header and delimiter rows, and do not exceed
171
- the table width. Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated
172
- target. When a decision lands, pass `summary` to name the resulting version. Never paste progress
173
- into a message.
153
+ Target `replace` and `delete` by the document's plain text: inline-code and link text match without Markdown syntax, and a table-cell
154
+ anchor is its cell text. Quote code-block contents without their Markdown fences. A quote must stay within one textblock; split changes
155
+ that span separate blocks into separate operations.
156
+
157
+ `replace` requires `find` and `with`; `delete` requires `find`; `insert` requires `markdown` and exactly one of `after` or `before`. An
158
+ insert anchor is a quote, `"start"`, `"end"`, or `"heading:Title"`. Ordinary inserts create a sibling block before or after the quote or
159
+ heading's enclosing document block; `"start"` and `"end"` select the document edges. At a table-cell quote, a body-row fragment (no
160
+ header or delimiter rows) extends that table before or after the matched row instead; short rows are padded, wider rows are rejected,
161
+ and deleting a cell's quoted text removes only that text.
162
+
163
+ Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated target; re-read a missing or ambiguous target before
164
+ retrying. Pass `summary` to name the version when recording a decision.
165
+
166
+ ## Progress
167
+
168
+ Every issue you work has one progress artifact, `progress.md` — for humans reading later and for your own successor after compaction. It
169
+ is never a wake signal.
170
+
171
+ Create it once, on first use:
172
+ ```ts
173
+ dispatch_artifact({ issue, name: "progress.md", content: "### 2026-09-11 15:00Z - Started\n..." })
174
+ ```
175
+ The server slugs `progress.md` to `progress-md`; address every later edit with that slug. Append — never edit or remove an earlier
176
+ entry:
177
+ ```ts
178
+ dispatch_doc_edit({
179
+ issue,
180
+ artifact: "progress-md",
181
+ ops: [{ op: "insert", after: "end", markdown: "### 2026-09-11 16:10Z - Blocked\n..." }],
182
+ })
183
+ ```
184
+ Newest entry last. Each entry is `### <UTC time> - <headline>` followed by 1-5 lines: what changed (cite `dispatch://` refs or PR
185
+ links), what is blocked and on whom, and what is next. `.legion/<phase>.json` is the durable machine handoff between phases;
186
+ `progress.md` is the human-readable narrative for the same work — keep both, never conflate one for the other.
174
187
 
175
188
  ## Comments and suggestions
176
189
 
@@ -180,12 +193,11 @@ Add feedback with:
180
193
  dispatch_comment({ issue?, project?, artifact?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
181
194
  ```
182
195
 
183
- It returns issue or project-document owner details plus `comment` and, for writes, `topic`.
184
- `quote` requires `artifact`; omit both for a floating issue comment. A reply
185
- (`reply_to`/`reply_to_ask`) takes no `quote`; it belongs to its parent's anchor. Use `reply_to`
186
- to continue a comment thread at its root; a reply to a resolved thread reopens it. Use
187
- `reply_to_ask` to reply directly under a question asked with `dispatch_ask`. The two are
188
- mutually exclusive. Comments are edited only by their author from the dashboard.
196
+ It returns issue or project-document owner details plus `comment` and, for writes, `topic`. `quote` requires `artifact`; omit both for a
197
+ floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no `quote`; it belongs to its parent's anchor. Use `reply_to` to
198
+ continue a comment thread at its root; a reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly under a question
199
+ asked with `dispatch_ask`. Comments are edited only by their author from the dashboard. A delivered `comment.created` event carries the
200
+ comment `id`; reply to it with `dispatch_comment({ reply_to: <id> })`.
189
201
 
190
202
  Propose an exact replacement instead of describing it:
191
203
 
@@ -193,15 +205,9 @@ Propose an exact replacement instead of describing it:
193
205
  dispatch_suggest({ issue?, project?, artifact, quote, replace_with, body?, occurrence? })
194
206
  ```
195
207
 
196
- It returns issue or project-document owner details plus `comment` and its write `topic`. A
197
- human accepts or rejects a suggestion. On
198
- `TARGET_AMBIGUOUS`, add zero-based `occurrence`. On `TARGET_NOT_FOUND`, re-read the document
199
- before retrying. `INVALID_ANCHOR` requires exactly one nonempty anchor `quote` or `mark_id`;
200
- `ANCHOR_MISSING` means a browser mark was not observed in the live tree, and
201
- `ANCHOR_ORPHANED` means its marked text no longer exists. `INVALID_MARKDOWN` and `DOC_SCHEMA`
202
- reject Markdown or a live tree outside the Proof schema. `INVALID_OP` names a malformed edit;
203
- `CAP_EXCEEDED` never truncates; `ISSUE_CLOSED` rejects a write. `ACTOR_KIND` and `ROUTE_INVALID`
204
- reject an invalid actor or route.
208
+ It returns issue or project-document owner details plus `comment` and its write `topic`. A human accepts or rejects a suggestion.
209
+ Errors: `TARGET_AMBIGUOUS` (add `occurrence`), `TARGET_NOT_FOUND` (re-read first), `INVALID_ANCHOR`/`ANCHOR_MISSING`/`ANCHOR_ORPHANED`
210
+ (bad, unwritten, or stale quote), `INVALID_MARKDOWN`/`DOC_SCHEMA` (malformed content), `CAP_EXCEEDED`, `ISSUE_CLOSED`.
205
211
 
206
212
  ## Artifacts
207
213
 
@@ -217,39 +223,37 @@ Or, when the text is already in the call, post a Markdown document directly:
217
223
  dispatch_artifact({ issue?, project?, name: "spec.md", content: "# Design\n..." })
218
224
  ```
219
225
 
220
- Exactly one of `issue` and `project` is required. A project upload creates an unlinked project
221
- document; it must not include `artifact`. Exactly one of `path` and `content` is required. The
222
- inline form sends JSON with `Content-Type: application/json`. It returns issue or
223
- project-document owner details plus `artifact`, `version`, and its write `topic`. Uploading the
224
- same `name` creates its next version. Use `content` when the text is already in the call.
226
+ Exactly one of `issue` and `project` is required. A project upload creates an unlinked project document; it must not include `artifact`.
227
+ Exactly one of `path` and `content` is required. It returns issue or project-document owner details plus `artifact`, `version`, and its
228
+ write `topic`. Uploading the same `name` creates its next version. Address an existing artifact by the slug shown in the upload result
229
+ or by its filename; the slug also arrives on `artifact.created` events.
225
230
 
226
231
  ## Messages
227
232
 
228
- Use the escape valve only for a note that fits nowhere else:
233
+ Post a note to the issue's Conversation for humans: a short status they should see now (a deploy landed, a blocker appeared), or a reply
234
+ to a human's message.
229
235
 
230
236
  ```ts
231
237
  dispatch_message({ issue, body })
232
238
  ```
233
239
 
234
- It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 characters. Your
235
- message does not wake anyone. Do not use it for status, a decision, or document feedback.
240
+ It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 characters. It is not a progress ledger (`progress.md`), a
241
+ decision (`dispatch_ask`), or document feedback (`dispatch_comment`). Your message does not wake anyone unless the issue is routed.
236
242
 
237
243
  ## What comes back
238
244
 
239
- A write result's `details.topic` subscribes the host to its owner. Issue writes use
240
- `notifications.dispatch.issue.<KEY>.>`; project-document writes use
241
- `notifications.dispatch.document.<PROJECT>.<SLUG>.>`. The owner topic carries every Dispatch
242
- event; `notify` only controls agent wake and routed delivery. After a restart, catch up with:
245
+ A write result's `details.topic` subscribes the host to its owner. Issue writes use `notifications.dispatch.issue.<KEY>.>`;
246
+ project-document writes use `notifications.dispatch.document.<PROJECT>.<SLUG>.>`. The owner topic carries every Dispatch event; `notify`
247
+ only controls agent wake and routed delivery. After a restart, catch up with:
243
248
 
244
249
  ```ts
245
250
  dispatch_read({ issue?, project?, artifact?, ref? })
246
251
  ```
247
252
 
248
- With an issue ref, it returns the issue summary, open asks, references, and recent events with
249
- `details` `{ issue }`. With a project document owner or ref, it returns a document summary with
250
- `details` `{ project, document }`. With an ask ref, it returns that ask's question, options,
251
- state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted
252
- reply chain. Reads do not subscribe; use `dispatch_doc_read` for document contents.
253
+ With an issue ref, it returns the issue summary, open asks, references, and recent events with `details` `{ issue }`. With a project
254
+ document owner or ref, it returns a document summary with `details` `{ project, document }`. With an ask ref, it returns that ask's
255
+ question, options, state, answer, and its reply thread. With a comment ref, it returns that comment and its quoted reply chain. Reads do
256
+ not subscribe; use `dispatch_doc_read` for document contents.
253
257
 
254
258
  ## References
255
259
 
@@ -266,6 +270,8 @@ dispatch://PROJECT/artifact/<slug>/ask/<id>
266
270
  dispatch://PROJECT/artifact/<slug>/comment/<id>
267
271
  ```
268
272
 
273
+ A bare UUID or `KEY#seq` is not a reference; the `dispatch://` form is what Dispatch links and records.
274
+
269
275
  ## Before / after
270
276
 
271
277
  Before — a wall of text hides the decision and makes the choices unclickable:
@@ -283,48 +289,37 @@ After — anchor the decision and make each option a button:
283
289
  dispatch_ask({
284
290
  issue: "LEGION-815",
285
291
  question:
286
- "Choose the release gate. Recommendation: ship after release-note review because the tested deployment is otherwise ready.",
292
+ "Choose the release gate. Recommendation: ship after release-note review, since the tested deployment is otherwise ready.",
287
293
  options: [
288
- {
289
- label: "Review notes, then ship",
290
- description: "Keeps the tested release intact and publishes reviewed instructions.",
291
- },
292
- {
293
- label: "Ship now",
294
- description: "Meets the demo deadline; release notes follow separately.",
295
- },
296
- {
297
- label: "Remove dashboard changes",
298
- description: "Narrows the release but requires another deployment test.",
299
- },
294
+ { label: "Review notes, then ship", description: "Keeps the release intact and reviewed." },
295
+ { label: "Ship now", description: "Meets the demo deadline; release notes follow later." },
300
296
  ],
301
297
  urgency: "high",
302
- anchor: {
303
- artifact: "spec",
304
- quote: "Release requires reviewed operator instructions before deployment.",
305
- },
298
+ anchor: { artifact: "spec", quote: "Release requires reviewed operator instructions before deployment." },
306
299
  })
307
300
  ```
308
301
 
309
- Before — a status message loses the durable outcome:
302
+ Before — progress typed once into a message, gone once compaction drops it from context:
310
303
 
311
- ```text
312
- Done, PR merged.
304
+ ```ts
305
+ dispatch_message({ issue: "LEGION-815", body: "Merged the release PR, moving to docs next." })
313
306
  ```
314
307
 
315
- After — record the result in the spec and name its version:
308
+ After — append it to the progress artifact, where the record survives:
316
309
 
317
310
  ```ts
318
311
  dispatch_doc_edit({
319
312
  issue: "LEGION-815",
320
- artifact: "spec",
313
+ artifact: "progress-md",
321
314
  ops: [
322
315
  {
323
- op: "replace",
324
- find: "## Delivery\n\nRelease pending.",
325
- with: "## Delivery\n\nRelease merged and ready for deployment.",
316
+ op: "insert",
317
+ after: "end",
318
+ markdown:
319
+ "### 2026-09-11 15:40Z - Release PR merged\n" +
320
+ "- dispatch://LEGION-815/artifact/spec stays unchanged; this is progress, not a decision.\n" +
321
+ "- Next: docs review.",
326
322
  },
327
323
  ],
328
- summary: "Recorded merged release",
329
324
  })
330
325
  ```
@@ -71,6 +71,9 @@ exercise a criterion end to end, building that path is a child issue of this tre
71
71
  inert until released.
72
72
 
73
73
  Specifications written into Dispatch follow [`skills/dispatch`'s Writing a spec](../dispatch/SKILL.md#writing-a-spec).
74
+ Record ongoing status — wave releases, child closures, blockers — in the issue's `progress.md`
75
+ artifact (see [`skills/dispatch`'s Progress](../dispatch/SKILL.md#progress)), never in the spec
76
+ or a `dispatch_message`.
74
77
 
75
78
  Write one root specification containing the accepted scope, adoption/decomposition,
76
79
  waves, acceptance criteria, and integration test. When the config-armed root design gate
@@ -97,7 +97,9 @@ committed predecessor handoffs in lifecycle order from `$LEGION_WORKSPACE/.legio
97
97
  Read only files that precede the assigned phase. There is no handoff schema (rejected
98
98
  design — no schema validation runs anywhere in this pipeline): write the phase-specific
99
99
  fields the next phase and the architect need, consistent with what predecessor phases
100
- already wrote. The durable copy lives in `$LEGION_WORKSPACE/.legion/<phase>.json`. If a
100
+ already wrote. The durable copy lives in `$LEGION_WORKSPACE/.legion/<phase>.json` the machine
101
+ handoff between phases, not a human-readable status; post that to the issue's `progress.md`
102
+ artifact instead (see [`skills/dispatch`'s Progress](../dispatch/SKILL.md#progress)). If a
101
103
  committed handoff conflicts with memory or a prior transcript, the committed file wins: it
102
104
  is the copy that survived.
103
105
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "0.41.0",
3
+ "version": "0.41.2",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [