@sjawhar/opencode-legion-envoy 6.15.1 → 6.16.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/dist/dispatch.js CHANGED
@@ -14015,7 +14015,7 @@ var dispatchToolSpecs = [
14015
14015
  {
14016
14016
  name: "dispatch_issue",
14017
14017
  example: { project: "DSP", title: "Native workspace" },
14018
- description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless --force is set after reading them. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
14018
+ description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless --force is set after reading them. " + "When passing a spec, before calling, have a fresh read-only reader check it against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
14019
14019
  arguments: (z2) => ({
14020
14020
  project: z2.string().describe("Project key for the new issue."),
14021
14021
  title: z2.string({ max: ISSUE_TITLE_MAX }).describe("Concise issue title, at most 1,000 characters."),
@@ -14281,7 +14281,7 @@ var dispatchToolSpecs = [
14281
14281
  {
14282
14282
  name: "dispatch_request_approval",
14283
14283
  example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
14284
- description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch doc-read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes after it: request approval again once that " + "revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ". A new version of a Legion root spec closes its armed design gate until a human approves " + "it. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
14284
+ description: "Ask a human to approve a document at its current version. Before calling, have a fresh read-only reader check the spec against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch doc-read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes after it: request approval again once that " + "revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ". A new version of a Legion root spec closes its armed design gate until a human approves " + "it. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
14285
14285
  arguments: (z2) => ({
14286
14286
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14287
14287
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -16255,7 +16255,6 @@ var triageAdviceShown = new Set;
16255
16255
  function adviceMemory() {
16256
16256
  return triageAdviceShown;
16257
16257
  }
16258
- var SPEC_CHECK_REMINDER = 'Has a fresh reader checked this spec? Each claim about how a system works today should trace to code read or a command run, and each requirement to the human\'s words or a cited fact. If not, have a fresh read-only subagent check it now and fix what it finds: on Oh My Pi, `task(agent="scout")`, or whatever read-only agent your host bundles; a Legion architect\'s own spec uses the Legion plugin\'s `plan-gap-analyst` agent instead, which only a Legion session has. See the `dispatch-first` skill, "Design in the spec".';
16259
16258
  function renderAdvice(tool, key, advice, opts) {
16260
16259
  if (advice === undefined)
16261
16260
  return [];
@@ -17298,22 +17297,31 @@ async function refuseOpenDecisionBlocks(client, tool, resolved) {
17298
17297
  const asks = new Map(documentAsks.map((ask) => [ask.block_id, ask]));
17299
17298
  const lines = version2.markdown.split(`
17300
17299
  `);
17300
+ const stillOpen = (named, state) => {
17301
+ const next = state === "answered" ? "fold the answer into the text" : "write the decision into the text";
17302
+ return `${named}, ${state} but still open in version ${latest}: ${next} with dispatch doc-edit, which writes a version that carries it`;
17303
+ };
17301
17304
  const open = blocks.flatMap((block) => {
17302
17305
  const ask = asks.get(block.id);
17306
+ const openers = lines.filter((line) => line.includes(`ask{#${block.id} `) || line.includes(`ask{#${block.id}}`));
17307
+ const states = openers.map((line) => /\bstate="(\w+)"/.exec(line)?.[1]);
17303
17308
  const named = ask === undefined ? `block ${block.id}` : `${JSON.stringify(ask.question)} (block ${block.id}, ask ${ask.id})`;
17304
- const states = lines.filter((line) => line.includes(`ask{#${block.id} `) || line.includes(`ask{#${block.id}}`)).map((line) => /\bstate="(\w+)"/.exec(line)?.[1]);
17305
17309
  if (states.length === 0)
17306
17310
  return [`${named}, which version ${latest} does not hold yet`];
17307
17311
  if (!states.includes("open") && states.some((state) => state !== undefined))
17308
17312
  return [];
17309
- if (ask === undefined)
17313
+ if (ask !== undefined)
17314
+ return ask.state === "open" ? [named] : [stillOpen(named, ask.state)];
17315
+ const attribute = (name) => openers.map((line) => new RegExp(`\\b${name}="([^"]+)"`).exec(line)?.[1]).find((value) => value !== undefined);
17316
+ const sourceId = attribute("copied_from");
17317
+ const document = attribute("copied_from_document");
17318
+ if (sourceId === undefined || document === undefined) {
17310
17319
  return [`${named}, whose ask Dispatch has not opened yet`];
17311
- if (ask.state === "open")
17312
- return [named];
17313
- const next = ask.state === "answered" ? "fold the answer into the text" : "write the decision into the text";
17314
- return [
17315
- `${named}, ${ask.state} but still open in version ${latest}: ${next} with dispatch doc-edit, which writes a version that carries it`
17316
- ];
17320
+ }
17321
+ const source = parseDispatchRef(document);
17322
+ const where = source === null ? document : source.owner.kind === "project" ? documentLabel(source.owner.project, source.id) : `${source.owner.issue} ${source.id}`;
17323
+ const ref = source?.owner.kind === "issue" ? dispatchChildRef(dispatchIssueRef(source.owner.issue), "ask", sourceId) : dispatchChildRef(document, "ask", sourceId);
17324
+ return [`${named}, a copy of ask ${ref}, which is open on ${where}: answer it there`];
17317
17325
  });
17318
17326
  if (open.length === 0)
17319
17327
  return;
@@ -17513,7 +17521,6 @@ async function executeDispatchTool(input) {
17513
17521
  return {
17514
17522
  text: [
17515
17523
  `Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
17516
- ...spec === undefined ? [] : [SPEC_CHECK_REMINDER],
17517
17524
  ...adviceLines
17518
17525
  ].join(`
17519
17526
  `),
@@ -18078,8 +18085,7 @@ ${trailer.join(`
18078
18085
  const details = await followedAskDetails(client, result.ask, resolved.artifact);
18079
18086
  const outcome = result.recorded ? `Approval requested for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}).` : `The approval request for ${resolved.artifact.name} (document id ${resolved.artifact.id}) at version ${result.version} (ask ${result.ask.id}) already waits on the human, so this call changed nothing: nothing since it last reached the human (a newer version, a human's reply in its thread, or your progress note) left it waiting on you.`;
18080
18087
  return {
18081
- text: `${outcome} The human's Inbox asks: ${JSON.stringify(result.ask.question)}. The answer arrives as artifact.approved or artifact.changes_requested. An edit before the answer moves this request to the new version and leaves it waiting on you, and an edit after approval makes the approval stale: either way, request again for the new version once the human has agreed to every point in it, which hands this request back or opens a new one.${result.recorded ? `
18082
- ${SPEC_CHECK_REMINDER}` : ""}`,
18088
+ text: `${outcome} The human's Inbox asks: ${JSON.stringify(result.ask.question)}. The answer arrives as artifact.approved or artifact.changes_requested. An edit before the answer moves this request to the new version and leaves it waiting on you, and an edit after approval makes the approval stale: either way, request again for the new version once the human has agreed to every point in it, which hands this request back or opens a new one.`,
18083
18089
  details: { ...details, artifact: resolved.artifact.id, version: result.version }
18084
18090
  };
18085
18091
  }
@@ -13872,7 +13872,7 @@ var dispatchToolSpecs = [
13872
13872
  {
13873
13873
  name: "dispatch_issue",
13874
13874
  example: { project: "DSP", title: "Native workspace" },
13875
- description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless --force is set after reading them. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
13875
+ description: "Create a native Dispatch issue for newly tracked work. Search first with dispatch search; if potentially duplicate issues exist, this returns 409 POSSIBLE_DUPLICATE unless --force is set after reading them. " + "When passing a spec, before calling, have a fresh read-only reader check it against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. " + "A spec holding an ask block whose body breaks its content rule (one or more question paragraphs, then at most one bullet list of options, last) is refused with 400 INVALID_ASK_BLOCK. " + `Do not use it when an existing issue already covers the work; read or update that issue instead. ${ISSUE_REFERENCE}`,
13876
13876
  arguments: (z2) => ({
13877
13877
  project: z2.string().describe("Project key for the new issue."),
13878
13878
  title: z2.string({ max: ISSUE_TITLE_MAX }).describe("Concise issue title, at most 1,000 characters."),
@@ -14138,7 +14138,7 @@ var dispatchToolSpecs = [
14138
14138
  {
14139
14139
  name: "dispatch_request_approval",
14140
14140
  example: { issue: "DSP-1", summary: "A live sync replaces the nightly export." },
14141
- description: "Ask a human to approve a document at its current version. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch doc-read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes after it: request approval again once that " + "revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ". A new version of a Legion root spec closes its armed design gate until a human approves " + "it. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
14141
+ description: "Ask a human to approve a document at its current version. Before calling, have a fresh read-only reader check the spec against the code and the human's words, then resolve every finding; a Legion architect uses `plan-gap-analyst`. Opens an approval ask (Approve / " + "Request changes) in the human's Inbox whose question names the document and version, " + "followed by the summary; the answer pins a review to that version and arrives as " + "artifact.approved or artifact.changes_requested. An open request follows the document: a " + "later version moves it to that version and leaves it waiting on you, as a human's reply in " + "its thread does. Only the first move since the request was opened or handed back sends an " + "event, and never to the session whose version made it; dispatch doc-read shows whom it " + "waits on. Once the revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ", call this again to hand that same Inbox row back. The request carries nothing new. A " + "call while it already waits on the human hands nothing back: the same summary changes " + "nothing, and a different one is refused, since it would rewrite the card the human is " + "reading. Approve and Request changes each close the request, so the next call opens a new " + "one. Call it once per revision, when the revision is complete, never after each edit. An " + "approval goes stale when the document changes after it: request approval again once that " + "revision is complete and " + HUMAN_AGREED_TO_DOCUMENT + ". A new version of a Legion root spec closes its armed design gate until a human approves " + "it. " + "Refused, with nothing sent, while the document holds an open decision block, even when a " + "human asked for approval: the refusal names each block; ask the human to answer or waive " + "it first. " + OWNER_REFERENCE,
14142
14142
  arguments: (z2) => ({
14143
14143
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14144
14144
  project: z2.string().describe("Project key owning the document.").optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "6.15.1",
3
+ "version": "6.16.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/src/server.js",
@@ -372,6 +372,10 @@ says; the human has agreed to every point in the spec; and the current version h
372
372
  approved. A Legion root spec under an armed design gate always goes to approval once all three
373
373
  hold (`skill://legion-architect`).
374
374
 
375
+ Before calling `dispatch request-approval`, have a fresh read-only reader check the current spec
376
+ against the code and the human's words, then resolve every finding. A Legion architect uses its
377
+ `plan-gap-analyst`; another session uses the read-only reader its host supplies.
378
+
375
379
  When all three hold, request it in the pass that finishes the spec: a design waiting with nothing in
376
380
  the human's Inbox waits on nobody. When a human asks for approval while a block is open, do not
377
381
  request it and do not hold it silently: name each open block and ask them to answer it or waive
@@ -148,6 +148,20 @@ artifact by the slug the upload printed or by its filename, and a project docume
148
148
  slug also arrives on `artifact.created` events. Dispatch suffixes a slug two documents would share, so one document's
149
149
  filename can be another's slug (`plan v2` takes `plan-v2`, then a document named `plan-v2` takes `plan-v2-2`): a bare
150
150
  `--artifact` that names both is refused with each one's id, while a `dispatch://` reference's document part is always the slug.
151
+ A document you upload holding a copy of another document's `:::ask` block, under the same `#id` and with the same
152
+ question, options, `multiple` and urgency, on the same issue (or the same project for a project document), opens no
153
+ ask: the copy shows the state and answer of the ask it was copied from, names it and its document in `copied_from` and
154
+ `copied_from_document`, and cannot be answered there. Answer the original: its answer or resolution shows on the copy a
155
+ few seconds later. A copy may hold the original's earlier wording, from before someone reworded it. It stays a copy
156
+ while the original is open, and shows an answer or resolution given while the original still asked that wording; an
157
+ answer or resolution given after the rewording decides a question the copy does not ask, so the copy opens its own ask
158
+ instead. A copy whose text you change opens a new ask. Once the original's block leaves its document, its copies share
159
+ one new ask: the first copy to settle opens it and the others name it.
160
+
161
+ An answered or resolved ask's question is fixed. Editing its block's question, options, `multiple` or urgency in the
162
+ document, or uploading a version that changes them, does not change the ask: a few seconds later the block is written
163
+ back to the question the answer was given on, beside its answer. To ask something new, write a new ask block with its
164
+ own `#id`. An open ask's block is its text: edit it and the ask changes with it.
151
165
 
152
166
  A picture meant to be seen inline in a message, comment or ask is not uploaded with `dispatch artifact`: pass its local
153
167
  path with that command's `--image`, once per picture (PNG, JPEG, GIF or WebP by its bytes, at most 25 MiB each). Each is uploaded to the issue (a
@@ -97,20 +97,36 @@ not request approval and do not wait for `design-approved`; the gate opens at re
97
97
  sub-architect on a child issue has no policy line and never runs the gate: the root approval
98
98
  covers the tree. When the gate is armed, run this exact sequence **before the tree's work starts**:
99
99
 
100
- ```text
101
- dispatch doc-edit --issue <root issue> --ops-json-file - <<'EOF' # extend the primary document in place
102
- ...
103
- EOF
104
- # then settle its decision blocks (below)
105
- dispatch request-approval --issue <root issue> --summary '<what the human is approving>'
106
- # the primary document by default; prints "Approval requested for spec.md (document id <UUID>) at version <N> …"
107
- legion({
108
- op: "register_gate",
109
- issue: "<root issue>",
110
- artifactId: "<UUID>", // the document id from that text, such as 4e0aca36-77b3-43bd-96cf-d58890ae64e4
111
- version: <N>, // the version number the human is asked to approve
112
- })
113
- ```
100
+ 1. Extend the primary document in place:
101
+
102
+ ```bash
103
+ dispatch doc-edit --issue <root issue> --ops-json-file - <<'EOF'
104
+ ...
105
+ EOF
106
+ ```
107
+
108
+ 2. Settle its decision blocks as [Approval of a spec](skill://dispatch/SKILL.md#approval-of-a-spec)
109
+ says.
110
+ 3. Run `task(agent="plan-gap-analyst")` with the current root spec, the code and command output
111
+ it relies on, and the human's words; resolve every finding in the root spec.
112
+ 4. Request approval:
113
+
114
+ ```bash
115
+ dispatch request-approval --issue <root issue> --summary '<what the human is approving>'
116
+ ```
117
+
118
+ The primary document is the default; the command prints `Approval requested for spec.md
119
+ (document id <UUID>) at version <N> …`.
120
+ 5. Register that document version with the daemon:
121
+
122
+ ```ts
123
+ legion({
124
+ op: "register_gate",
125
+ issue: "<root issue>",
126
+ artifactId: "<UUID>", // the document id the request printed
127
+ version: <N>, // the version the human is asked to approve
128
+ })
129
+ ```
114
130
 
115
131
  The decision blocks come first: settle every one as
116
132
  [Approval of a spec](skill://dispatch/SKILL.md#approval-of-a-spec) says before you request approval;
@@ -142,9 +158,10 @@ read covers a human who answers the question between your `dispatch request-appr
142
158
  yourself.
143
159
 
144
160
  Then park. Do not release a wave until a later delivered wake shows `design-approved` on the root;
145
- the daemon starts no phase in the tree before then. On `design-changes-requested`, revise the spec
146
- (a new version of the primary document) as the human's reason asks, request approval again as
147
- above (the answer closed the last request, so this opens a new one), and stay parked.
161
+ the daemon starts no phase in the tree before then. On `design-changes-requested`, (1) revise the
162
+ spec as the human's reason asks, (2) run `task(agent="plan-gap-analyst")` with that new version,
163
+ the code and command output it relies on, and the human's words, then resolve every finding, and
164
+ (3) request approval again and stay parked.
148
165
 
149
166
  **After approval, the root spec changes only when what the tree delivers, or a decision a human
150
167
  settled, changes.** Approval is pinned to the spec version: any new version of the root spec closes
@@ -152,7 +169,7 @@ the gate again with no wake (you made the edit, or the `artifact.version` event
152
169
  you). So edit an approved root spec only when its Summary, its Acceptance, the tree's scope, or a
153
170
  decision a human settled in one of its decision blocks changes. Such a change is a point the human
154
171
  has not agreed to: put the problem behind it to them as its own decision block, with its evidence,
155
- at the end of the section it changes, and request approval again as above once they have answered
172
+ at the end of the section it changes, then repeat steps 2 through 5 once they have answered
156
173
  it. Release no new wave until the next `design-approved` arrives — work already in flight
157
174
  continues. Every merger's `READY` in the tree is refused until a human approves
158
175
  the latest version. A settled decision is the human's. A plan that would overturn one goes back to