@sjawhar/opencode-legion-envoy 1.16.0 → 1.17.1

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.
@@ -13647,6 +13647,16 @@ var MessageEventPayloadSchema = object({
13647
13647
  reply_body: string2().optional(),
13648
13648
  author: object({ kind: string2(), id: string2() }).optional()
13649
13649
  });
13650
+ var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
13651
+ id: string2(),
13652
+ issue_key: string2(),
13653
+ author: object({ kind: string2(), id: string2() }),
13654
+ body: string2(),
13655
+ target: string2(),
13656
+ in_reply_to: string2().nullable(),
13657
+ deliveries: array(unknown()),
13658
+ created_at: string2()
13659
+ });
13650
13660
  var MessageDeliveryEventPayloadSchema = object({
13651
13661
  message_id: string2().optional(),
13652
13662
  attempt: number2().int().positive().optional(),
@@ -13721,7 +13731,9 @@ function snippetText(snippet) {
13721
13731
  // ../contracts/src/dispatch-tools.ts
13722
13732
  function dispatchToolSchema(spec, z, opts) {
13723
13733
  const shape = spec.arguments(z);
13724
- return spec.validation === undefined ? z.object(shape, opts) : z.refineObject(shape, spec.validation.check, spec.validation.message, opts);
13734
+ const strict = opts?.strict ?? spec.strict;
13735
+ const schemaOptions = strict === undefined ? undefined : { strict };
13736
+ return spec.validation === undefined ? z.object(shape, schemaOptions) : z.refineObject(shape, spec.validation.check, spec.validation.message, schemaOptions);
13725
13737
  }
13726
13738
  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).";
13727
13739
  var OWNER_REFERENCE = "Exactly one of issue and project is required. An issue is a native KEY or external owner/repo#n reference; a project is a project key such as CORE and addresses an unlinked project document named by artifact.";
@@ -13945,6 +13957,12 @@ var dispatchToolSpecs = [
13945
13957
  project: z.string().describe("Optional project key to search within.").optional(),
13946
13958
  limit: z.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
13947
13959
  })
13960
+ },
13961
+ {
13962
+ name: "dispatch_open_asks",
13963
+ description: "List this session's active unanswered asks across issues and project documents, including age and whose reply is needed. Call before saying you are waiting for human input.",
13964
+ arguments: () => ({}),
13965
+ strict: true
13948
13966
  }
13949
13967
  ];
13950
13968
  // ../contracts/src/envelope.ts
@@ -14682,6 +14700,12 @@ class DispatchClient {
14682
14700
  async ask(issue, input) {
14683
14701
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "asks"], input);
14684
14702
  }
14703
+ async openAsks(sessionID, since) {
14704
+ return this.#json("GET", ["api", "v1", "asks", "open"], undefined, {
14705
+ author_session: sessionID,
14706
+ ...since === undefined ? {} : { since }
14707
+ });
14708
+ }
14685
14709
  async resolveAsk(id, input) {
14686
14710
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
14687
14711
  }
@@ -14934,7 +14958,8 @@ var issueFreeTools = {
14934
14958
  dispatch_issue: true,
14935
14959
  dispatch_edit_ask: true,
14936
14960
  dispatch_resolve_ask: true,
14937
- dispatch_search: true
14961
+ dispatch_search: true,
14962
+ dispatch_open_asks: true
14938
14963
  };
14939
14964
  function canonicalExternalIssueRef(value) {
14940
14965
  const match = value.trim().match(externalIssueRefPattern);
@@ -15274,6 +15299,45 @@ function askSummary({ ask, replies }) {
15274
15299
  ].join(`
15275
15300
  `);
15276
15301
  }
15302
+ function openAskAge(ageSeconds) {
15303
+ const seconds = Math.max(0, Math.floor(ageSeconds));
15304
+ if (seconds < 60)
15305
+ return `${seconds}s`;
15306
+ const minutes = Math.floor(seconds / 60);
15307
+ if (minutes < 60)
15308
+ return `${minutes}m${seconds % 60 === 0 ? "" : ` ${seconds % 60}s`}`;
15309
+ const hours = Math.floor(minutes / 60);
15310
+ if (hours < 24)
15311
+ return `${hours}h${minutes % 60 === 0 ? "" : ` ${minutes % 60}m`}`;
15312
+ const days = Math.floor(hours / 24);
15313
+ return `${days}d${hours % 24 === 0 ? "" : ` ${hours % 24}h`}`;
15314
+ }
15315
+ function openAskOwner(ask) {
15316
+ return "issue" in ask.owner ? `${ask.owner.issue.key}: ${ask.owner.issue.title}` : `${ask.owner.document.project} / ${ask.owner.document.name}`;
15317
+ }
15318
+ function openAskLine(ask, baseUrl) {
15319
+ const priority = ask.priority === null ? "" : `P${ask.priority} \xB7 `;
15320
+ return `- ${openAskAge(ask.age_seconds)} \xB7 ${priority}${openAskOwner(ask)} \xB7 ${ask.question} \xB7 ${new URL(ask.ref, baseUrl).toString()}`;
15321
+ }
15322
+ function formatOpenAsksSummary(response, baseUrl) {
15323
+ const scope = "active open asks you authored on open issues and project documents";
15324
+ if (response.count === 0) {
15325
+ return ["There are no unanswered asks for this session.", `Scope: ${scope}.`].join(`
15326
+ `);
15327
+ }
15328
+ const waitingOnHuman = response.asks.filter((ask) => ask.waiting_on === "human");
15329
+ const waitingOnAgent = response.asks.filter((ask) => ask.waiting_on === "agent");
15330
+ return [
15331
+ `${response.count} unanswered ${response.count === 1 ? "ask" : "asks"} you authored on active issues and project documents.`,
15332
+ "",
15333
+ `Waiting on human (${waitingOnHuman.length}):`,
15334
+ ...waitingOnHuman.map((ask) => openAskLine(ask, baseUrl)),
15335
+ "",
15336
+ `Waiting on agent (${waitingOnAgent.length}):`,
15337
+ ...waitingOnAgent.map((ask) => openAskLine(ask, baseUrl))
15338
+ ].join(`
15339
+ `);
15340
+ }
15277
15341
  function commentSummary({ comment, replies }) {
15278
15342
  const root = [
15279
15343
  `${comment.id} \xB7 ${comment.author.kind} ${comment.author.id}`,
@@ -15322,6 +15386,15 @@ async function executeDispatchTool(input) {
15322
15386
  if (!input.config.enabled || !configUrl || !configToken) {
15323
15387
  throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
15324
15388
  }
15389
+ if (input.tool === "dispatch_open_asks") {
15390
+ const sessionId = input.sessionId?.trim();
15391
+ if (!sessionId)
15392
+ throw new Error("host session id is required for dispatch_open_asks");
15393
+ toolSchema(input.tool).parse(input.args);
15394
+ const client = new DispatchClient(configUrl, configToken, input.fetchImpl, input.signal);
15395
+ const response = await client.openAsks(sessionId);
15396
+ return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
15397
+ }
15325
15398
  const env = input.env ?? process.env;
15326
15399
  const exec = input.exec ?? defaultExec;
15327
15400
  const ownerArguments = await resolveOwnerArguments(input.tool, input.args, input.cwd, env, exec);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/opencode-legion-envoy",
3
- "version": "1.16.0",
3
+ "version": "1.17.1",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -50,7 +50,7 @@ rest. Use these headings in this order.
50
50
  | Section | Required content | Form |
51
51
  | --- | --- | --- |
52
52
  | **Summary** | The problem, what changes for whom, and how we will know it worked — in plain words. | Three sentences at most. |
53
- | **Decisions needed** | Only decisions that need human authority, taste, or risk appetite. Each is one plain question, two or three options with what each costs, and your recommendation with its reason — understandable without opening anything else. Every item is an anchored `dispatch_ask`; an answered item moves into Requirements with its provenance. If there is nothing to decide, write `None: this records what was agreed.` and do not ask for a review. | One decision per line. |
53
+ | **Decisions needed** | Only decisions that need human authority, taste, or risk appetite. Each is one plain question, two or three options with what each costs, and your recommendation with its reason — understandable without opening anything else. Each is a `dispatch_ask`; anchor it only when it concerns a document passage. An answered item moves into Requirements with its provenance. If there is nothing to decide, write `None: this records what was agreed.` and do not ask for a review. | One decision per line. |
54
54
  | **New since we talked** | Every design point the human did not settle in conversation, marked `inferred:` with the reasoning. Empty is fine. | One plain sentence per point. |
55
55
  | **Acceptance** | Each outcome names what a user will observe and the check that proves it (browser scenario, API call, or command). An outcome without a check is not acceptance. | Numbered lines. |
56
56
  | **Requirements** | What must hold, and where each came from: a quoted human sentence, or `inferred:` plus the reasoning. Readers treat inferred requirements as hypotheses. | `requirement \| where it comes from` table, or prose if the reader follows it more easily. |
@@ -132,9 +132,12 @@ containing block while retaining its quote as display text, so rewording the pas
132
132
  attached; a quote spanning top-level blocks, and existing anchors without a block, stay readable
133
133
  against their original document version if their quote disappears.
134
134
 
135
- An ask must be answerable from its own text and its anchor alone. Anchor a question about a document passage with `anchor`; thread one
136
- about a comment with `reply_to`; thread a follow-up on your own ask with `reply_to_ask`; cite anything else with a `dispatch://`
137
- reference (see [References](#references)). Never write "see above", "the message above", or "as attached".
135
+ An ask must be answerable from its own text and its anchor alone. Anchor a question about a document
136
+ passage with `anchor`. Follow up on an ask or comment with `dispatch_comment`; cite anything else
137
+ with a `dispatch://` reference (see [References](#references)). Never write "see above", "the
138
+ message above", or "as attached".
139
+
140
+ Before saying you are waiting for human input, call `dispatch_open_asks`. It lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply.
138
141
 
139
142
  **Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
140
143
  a setting only they can change, a review click, a conflict between two of their own rules - if
@@ -146,13 +146,20 @@ capability it needs; invoke GitHub through the credential helper:
146
146
  legion gh -- <gh args…>
147
147
  ```
148
148
 
149
- Three facts about `gh` in a worker pane. The `gh` on your `PATH` is a shim
150
- (`packages/pi-envoy/src/legion/gh-shim.ts`) that execs `legion gh -- "$@"`, so `gh …` and
151
- `legion gh -- …` are the same call, and each call redeems a fresh token from your session's
152
- grant — identity is supplied per call, never stored. Never run `gh auth login` or
153
- `gh auth setup-git`; there is no login state to create. The shim refuses `pr merge` (and a raw
154
- `gh api …/merge`): no worker role merges a pull request — the merge queue does, under its own
155
- authority.
149
+ Four facts about `gh` in a worker pane. The `gh` on your `PATH` is a shim
150
+ (`<state_dir>/worker-bin/gh`, installed by the daemon at startup — `packages/daemon/src/daemon/worker-bin.ts`)
151
+ that execs `legion gh -- "$@"`, so `gh …` and `legion gh -- …` are the same call, and each call
152
+ redeems a fresh token from your session's grant — identity is supplied per call, never stored.
153
+ Never run `gh auth login` or `gh auth setup-git`; there is no login state to create. The shim
154
+ refuses `pr merge` (and a raw `gh api …/merge`): no worker role merges a pull request — the merge
155
+ queue does, under its own authority. The credential reaches `legion` through the file
156
+ `$LEGION_GRANT_FILE` names, written before each of your bash commands by the extension; never
157
+ `cat`, `echo`, copy, or `export` it — `legion credential`, `legion gh`, `jj git push`, and
158
+ `legion handoff complete` read it themselves. The file is the pane's, not the command's: a `task`
159
+ subagent, an `eval` subprocess, or a background job in your pane reads the grant your last bash
160
+ command minted, so its `legion gh` or `jj git push` succeeds only within 60 seconds of that call
161
+ and 403s afterwards — a timing artifact, not a broken credential; run credentialed commands from
162
+ your own bash calls.
156
163
 
157
164
  ## GitHub PR comment attribution
158
165