@sjawhar/opencode-legion-envoy 1.16.0 → 1.17.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.
@@ -13721,7 +13721,9 @@ function snippetText(snippet) {
13721
13721
  // ../contracts/src/dispatch-tools.ts
13722
13722
  function dispatchToolSchema(spec, z, opts) {
13723
13723
  const shape = spec.arguments(z);
13724
- return spec.validation === undefined ? z.object(shape, opts) : z.refineObject(shape, spec.validation.check, spec.validation.message, opts);
13724
+ const strict = opts?.strict ?? spec.strict;
13725
+ const schemaOptions = strict === undefined ? undefined : { strict };
13726
+ return spec.validation === undefined ? z.object(shape, schemaOptions) : z.refineObject(shape, spec.validation.check, spec.validation.message, schemaOptions);
13725
13727
  }
13726
13728
  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
13729
  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 +13947,12 @@ var dispatchToolSpecs = [
13945
13947
  project: z.string().describe("Optional project key to search within.").optional(),
13946
13948
  limit: z.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
13947
13949
  })
13950
+ },
13951
+ {
13952
+ name: "dispatch_open_asks",
13953
+ 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.",
13954
+ arguments: () => ({}),
13955
+ strict: true
13948
13956
  }
13949
13957
  ];
13950
13958
  // ../contracts/src/envelope.ts
@@ -14682,6 +14690,12 @@ class DispatchClient {
14682
14690
  async ask(issue, input) {
14683
14691
  return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue), "asks"], input);
14684
14692
  }
14693
+ async openAsks(sessionID, since) {
14694
+ return this.#json("GET", ["api", "v1", "asks", "open"], undefined, {
14695
+ author_session: sessionID,
14696
+ ...since === undefined ? {} : { since }
14697
+ });
14698
+ }
14685
14699
  async resolveAsk(id, input) {
14686
14700
  return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
14687
14701
  }
@@ -14934,7 +14948,8 @@ var issueFreeTools = {
14934
14948
  dispatch_issue: true,
14935
14949
  dispatch_edit_ask: true,
14936
14950
  dispatch_resolve_ask: true,
14937
- dispatch_search: true
14951
+ dispatch_search: true,
14952
+ dispatch_open_asks: true
14938
14953
  };
14939
14954
  function canonicalExternalIssueRef(value) {
14940
14955
  const match = value.trim().match(externalIssueRefPattern);
@@ -15274,6 +15289,45 @@ function askSummary({ ask, replies }) {
15274
15289
  ].join(`
15275
15290
  `);
15276
15291
  }
15292
+ function openAskAge(ageSeconds) {
15293
+ const seconds = Math.max(0, Math.floor(ageSeconds));
15294
+ if (seconds < 60)
15295
+ return `${seconds}s`;
15296
+ const minutes = Math.floor(seconds / 60);
15297
+ if (minutes < 60)
15298
+ return `${minutes}m${seconds % 60 === 0 ? "" : ` ${seconds % 60}s`}`;
15299
+ const hours = Math.floor(minutes / 60);
15300
+ if (hours < 24)
15301
+ return `${hours}h${minutes % 60 === 0 ? "" : ` ${minutes % 60}m`}`;
15302
+ const days = Math.floor(hours / 24);
15303
+ return `${days}d${hours % 24 === 0 ? "" : ` ${hours % 24}h`}`;
15304
+ }
15305
+ function openAskOwner(ask) {
15306
+ return "issue" in ask.owner ? `${ask.owner.issue.key}: ${ask.owner.issue.title}` : `${ask.owner.document.project} / ${ask.owner.document.name}`;
15307
+ }
15308
+ function openAskLine(ask, baseUrl) {
15309
+ const priority = ask.priority === null ? "" : `P${ask.priority} \xB7 `;
15310
+ return `- ${openAskAge(ask.age_seconds)} \xB7 ${priority}${openAskOwner(ask)} \xB7 ${ask.question} \xB7 ${new URL(ask.ref, baseUrl).toString()}`;
15311
+ }
15312
+ function formatOpenAsksSummary(response, baseUrl) {
15313
+ const scope = "active open asks you authored on open issues and project documents";
15314
+ if (response.count === 0) {
15315
+ return ["There are no unanswered asks for this session.", `Scope: ${scope}.`].join(`
15316
+ `);
15317
+ }
15318
+ const waitingOnHuman = response.asks.filter((ask) => ask.waiting_on === "human");
15319
+ const waitingOnAgent = response.asks.filter((ask) => ask.waiting_on === "agent");
15320
+ return [
15321
+ `${response.count} unanswered ${response.count === 1 ? "ask" : "asks"} you authored on active issues and project documents.`,
15322
+ "",
15323
+ `Waiting on human (${waitingOnHuman.length}):`,
15324
+ ...waitingOnHuman.map((ask) => openAskLine(ask, baseUrl)),
15325
+ "",
15326
+ `Waiting on agent (${waitingOnAgent.length}):`,
15327
+ ...waitingOnAgent.map((ask) => openAskLine(ask, baseUrl))
15328
+ ].join(`
15329
+ `);
15330
+ }
15277
15331
  function commentSummary({ comment, replies }) {
15278
15332
  const root = [
15279
15333
  `${comment.id} \xB7 ${comment.author.kind} ${comment.author.id}`,
@@ -15322,6 +15376,15 @@ async function executeDispatchTool(input) {
15322
15376
  if (!input.config.enabled || !configUrl || !configToken) {
15323
15377
  throw new Error("Dispatch is disabled; resolve both DISPATCH_URL and DISPATCH_TOKEN");
15324
15378
  }
15379
+ if (input.tool === "dispatch_open_asks") {
15380
+ const sessionId = input.sessionId?.trim();
15381
+ if (!sessionId)
15382
+ throw new Error("host session id is required for dispatch_open_asks");
15383
+ toolSchema(input.tool).parse(input.args);
15384
+ const client = new DispatchClient(configUrl, configToken, input.fetchImpl, input.signal);
15385
+ const response = await client.openAsks(sessionId);
15386
+ return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
15387
+ }
15325
15388
  const env = input.env ?? process.env;
15326
15389
  const exec = input.exec ?? defaultExec;
15327
15390
  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.0",
4
4
  "type": "module",
5
5
  "main": "dist/src/server.js",
6
6
  "exports": {
@@ -136,6 +136,8 @@ An ask must be answerable from its own text and its anchor alone. Anchor a quest
136
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
137
  reference (see [References](#references)). Never write "see above", "the message above", or "as attached".
138
138
 
139
+ 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.
140
+
139
141
  **Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
140
142
  a setting only they can change, a review click, a conflict between two of their own rules - if
141
143
  your work waits on it, open a `dispatch_ask` with `kind: "action"` the moment you know, the
@@ -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