@sjawhar/pi-legion-envoy 7.2.0 → 7.2.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
@@ -30349,7 +30349,7 @@ var dispatchToolSpecs = [
30349
30349
  {
30350
30350
  name: "dispatch_doc_read",
30351
30351
  example: { issue: "DSP-1" },
30352
- description: "Read a live document or a named document version. Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + OWNER_REFERENCE,
30352
+ description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + "A file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
30353
30353
  arguments: (z2) => ({
30354
30354
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
30355
30355
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -32356,6 +32356,20 @@ class DispatchClient {
32356
32356
  async docRead(id, version2) {
32357
32357
  return version2 === undefined ? this.#json("GET", ["api", "v1", "artifacts", id, "text"]) : this.#json("GET", ["api", "v1", "artifacts", id, "versions", String(version2)]);
32358
32358
  }
32359
+ async fileVersion(id, version2) {
32360
+ const url2 = this.#url(["api", "v1", "artifacts", id, "versions", String(version2)]);
32361
+ const response = await this.fetchImpl(url2, {
32362
+ method: "GET",
32363
+ headers: { Accept: "*/*", Authorization: `Bearer ${this.token}` },
32364
+ signal: this.#signal
32365
+ });
32366
+ if (!response.ok)
32367
+ return this.#response("GET", url2, response);
32368
+ return {
32369
+ mime: response.headers.get("Content-Type") ?? "application/octet-stream",
32370
+ bytes: new Uint8Array(await response.arrayBuffer())
32371
+ };
32372
+ }
32359
32373
  async artifactBlocks(id) {
32360
32374
  return this.#json("GET", ["api", "v1", "artifacts", id, "blocks"]);
32361
32375
  }
@@ -33618,6 +33632,30 @@ async function blockAsks(client, resolved, state) {
33618
33632
  const asks = await (resolved.issue === undefined ? client.getArtifactAsks(resolved.artifact.id, state) : client.listIssueAsks(resolved.issue.key, state));
33619
33633
  return asks.filter((ask) => typeof ask.block_id === "string" && ask.block_artifact?.id === resolved.artifact.id);
33620
33634
  }
33635
+ async function readUploadedFile(client, resolved, requested) {
33636
+ const { artifact } = resolved;
33637
+ const latest = Math.max(0, ...artifact.versions.map((version2) => version2.number));
33638
+ const number4 = requested ?? latest;
33639
+ const file2 = await client.fileVersion(artifact.id, number4);
33640
+ const of = number4 === latest ? "" : ` of ${latest}`;
33641
+ const size = `${file2.bytes.length.toLocaleString("en-US")} bytes`;
33642
+ const details = resolved.owner.kind === "project" ? { project: artifact.project, document: documentLabel(artifact.project, artifact.slug) } : { issue: resolved.issue?.key };
33643
+ let text;
33644
+ try {
33645
+ text = new TextDecoder("utf-8", { fatal: true }).decode(file2.bytes);
33646
+ } catch {
33647
+ return {
33648
+ text: `${artifact.name} is an uploaded ${file2.mime} file (version ${number4}${of}, ${size}) that is not ` + `UTF-8 text, so dispatch_doc_read cannot show it. GET /api/v1/artifacts/${artifact.id}/versions/${number4} serves its bytes.`,
33649
+ details
33650
+ };
33651
+ }
33652
+ return {
33653
+ text: `File ${artifact.name}: ${file2.mime}, version ${number4}${of}, ${size}.
33654
+
33655
+ ${text}`,
33656
+ details
33657
+ };
33658
+ }
33621
33659
  async function refuseOpenDecisionBlocks(client, tool, resolved) {
33622
33660
  const artifact = resolved.artifact;
33623
33661
  const latest = artifact.approval?.latest_version;
@@ -34315,6 +34353,9 @@ ${followsAsk(askOwner)}`,
34315
34353
  const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
34316
34354
  const resolved = await resolveDocument(documentOwner(), artifactReference);
34317
34355
  const version2 = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
34356
+ if (resolved.artifact.kind === "file" || resolved.artifact.kind === "image") {
34357
+ return readUploadedFile(client, resolved, version2);
34358
+ }
34318
34359
  const documentPromise = client.docRead(resolved.artifact.id, version2);
34319
34360
  const marksPromise = openArtifactMarks(client, resolved);
34320
34361
  const marksResultPromise = marksPromise.then((value) => ({ status: "fulfilled", value }), (reason) => ({ status: "rejected", reason }));
package/dist/legion.js CHANGED
@@ -14436,7 +14436,7 @@ var dispatchToolSpecs = [
14436
14436
  {
14437
14437
  name: "dispatch_doc_read",
14438
14438
  example: { issue: "DSP-1" },
14439
- description: "Read a live document or a named document version. Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + OWNER_REFERENCE,
14439
+ description: "Read a live document or a named document version, or the text of an uploaded file at its latest or named version. " + "Do not use it for issue status, asks, or events; " + "use dispatch_read instead. Supply ref, issue, or project plus artifact; issue plus an omitted artifact reads the primary document. " + "A live read returns its document token for an optional dispatch_doc_edit precondition; use /blocks for per-block tokens. " + "A file that is not UTF-8 text is described, with the route that serves its bytes. " + OWNER_REFERENCE,
14440
14440
  arguments: (z2) => ({
14441
14441
  issue: z2.string().describe(ISSUE_REFERENCE).optional(),
14442
14442
  project: z2.string().describe("Project key owning the document.").optional(),
@@ -15118,6 +15118,20 @@ class DispatchClient {
15118
15118
  async docRead(id, version2) {
15119
15119
  return version2 === undefined ? this.#json("GET", ["api", "v1", "artifacts", id, "text"]) : this.#json("GET", ["api", "v1", "artifacts", id, "versions", String(version2)]);
15120
15120
  }
15121
+ async fileVersion(id, version2) {
15122
+ const url2 = this.#url(["api", "v1", "artifacts", id, "versions", String(version2)]);
15123
+ const response = await this.fetchImpl(url2, {
15124
+ method: "GET",
15125
+ headers: { Accept: "*/*", Authorization: `Bearer ${this.token}` },
15126
+ signal: this.#signal
15127
+ });
15128
+ if (!response.ok)
15129
+ return this.#response("GET", url2, response);
15130
+ return {
15131
+ mime: response.headers.get("Content-Type") ?? "application/octet-stream",
15132
+ bytes: new Uint8Array(await response.arrayBuffer())
15133
+ };
15134
+ }
15121
15135
  async artifactBlocks(id) {
15122
15136
  return this.#json("GET", ["api", "v1", "artifacts", id, "blocks"]);
15123
15137
  }
@@ -16403,6 +16417,30 @@ async function blockAsks(client, resolved, state) {
16403
16417
  const asks = await (resolved.issue === undefined ? client.getArtifactAsks(resolved.artifact.id, state) : client.listIssueAsks(resolved.issue.key, state));
16404
16418
  return asks.filter((ask) => typeof ask.block_id === "string" && ask.block_artifact?.id === resolved.artifact.id);
16405
16419
  }
16420
+ async function readUploadedFile(client, resolved, requested) {
16421
+ const { artifact } = resolved;
16422
+ const latest = Math.max(0, ...artifact.versions.map((version2) => version2.number));
16423
+ const number4 = requested ?? latest;
16424
+ const file2 = await client.fileVersion(artifact.id, number4);
16425
+ const of = number4 === latest ? "" : ` of ${latest}`;
16426
+ const size = `${file2.bytes.length.toLocaleString("en-US")} bytes`;
16427
+ const details = resolved.owner.kind === "project" ? { project: artifact.project, document: documentLabel(artifact.project, artifact.slug) } : { issue: resolved.issue?.key };
16428
+ let text;
16429
+ try {
16430
+ text = new TextDecoder("utf-8", { fatal: true }).decode(file2.bytes);
16431
+ } catch {
16432
+ return {
16433
+ text: `${artifact.name} is an uploaded ${file2.mime} file (version ${number4}${of}, ${size}) that is not ` + `UTF-8 text, so dispatch_doc_read cannot show it. GET /api/v1/artifacts/${artifact.id}/versions/${number4} serves its bytes.`,
16434
+ details
16435
+ };
16436
+ }
16437
+ return {
16438
+ text: `File ${artifact.name}: ${file2.mime}, version ${number4}${of}, ${size}.
16439
+
16440
+ ${text}`,
16441
+ details
16442
+ };
16443
+ }
16406
16444
  async function refuseOpenDecisionBlocks(client, tool, resolved) {
16407
16445
  const artifact = resolved.artifact;
16408
16446
  const latest = artifact.approval?.latest_version;
@@ -17100,6 +17138,9 @@ ${followsAsk(askOwner)}`,
17100
17138
  const artifactReference = optionalString(args, "artifact") ?? (ownerArguments.ref?.kind === "spec" || ownerArguments.ref?.kind === "artifact" ? ownerArguments.ref.id : undefined);
17101
17139
  const resolved = await resolveDocument(documentOwner(), artifactReference);
17102
17140
  const version2 = optionalNumber(args, "version") ?? ownerArguments.ref?.version;
17141
+ if (resolved.artifact.kind === "file" || resolved.artifact.kind === "image") {
17142
+ return readUploadedFile(client, resolved, version2);
17143
+ }
17103
17144
  const documentPromise = client.docRead(resolved.artifact.id, version2);
17104
17145
  const marksPromise = openArtifactMarks(client, resolved);
17105
17146
  const marksResultPromise = marksPromise.then((value) => ({ status: "fulfilled", value }), (reason) => ({ status: "rejected", reason }));
@@ -17418,7 +17459,7 @@ function endInjectedUserTurns(sessionID) {
17418
17459
  // package.json
17419
17460
  var package_default = {
17420
17461
  name: "@sjawhar/pi-legion-envoy",
17421
- version: "7.2.0",
17462
+ version: "7.2.2",
17422
17463
  license: "Apache-2.0",
17423
17464
  type: "module",
17424
17465
  omp: {
@@ -418,10 +418,10 @@ Read the current document before changing it:
418
418
  ```ts
419
419
  dispatch_doc_read({ issue?, project?, artifact?, version?, ref? })
420
420
  ```
421
- It returns live or versioned markdown with open marks. A live read ends with a document token; `issue` with an
422
- omitted `artifact` reads the issue specification; a project needs `artifact`; and a
423
- `dispatch://PROJECT/artifact/<document-ref>` ref supplies both, where `document-ref` is the slug (an id or a
424
- filename resolves when no document has that slug).
421
+ It returns live or versioned markdown with open marks. A live read ends with a document token; `issue` with an omitted
422
+ `artifact` reads the issue specification; a project needs `artifact`; and a `dispatch://PROJECT/artifact/<document-ref>`
423
+ ref supplies both, where `document-ref` is the slug (an id or a filename resolves when no document has that slug). A file
424
+ `dispatch_artifact` uploaded reads its text at the latest or named version, or a description when it is not UTF-8 text.
425
425
 
426
426
  Editing one is [Editing a document](skill://dispatch/references/document-edits.md): the shape of `dispatch_doc_edit`,
427
427
  how to quote the text you mean, one `replace` per paragraph, preconditions against a stale edit, and
@@ -304,8 +304,8 @@ line), the full definition of a proof, what the tester verifies, and the simplif
304
304
 
305
305
  - **Review threads** are disposed of one by one, never in bulk, and only an `Accepted:` from the
306
306
  thread's opener (or, on a bot's thread, from the Legion reviewer) closes one. The implementer
307
- runs `legion threads resolve` before every push that answers a review, and the merger before
308
- READY: `skill://legion-worker/references/review-threads.md`.
307
+ runs `legion threads resolve` after every push that answers a review, before its completion,
308
+ and the merger before READY: `skill://legion-worker/references/review-threads.md`.
309
309
  - **No deferrals.** A finding that changes
310
310
  behaviour, hides an error, or breaks a gate is fixed in this pull request; naming, duplication,
311
311
  or wording cleanup is batched into the one `Fast-follow:` line instead of iterating per push.
@@ -39,7 +39,7 @@ confirmation or a new round, as the fingerprint decides (*The reviewer*, below,
39
39
  nothing restarts. Different: a new round — thermo again, one review.
40
40
  - Answer every thread you opened, and every thread a bot opened that is none of Legion's role
41
41
  Apps, as `skill://legion-worker/references/review-threads.md` says; the same reference says
42
- when every thread is settled enough to approve.
42
+ when every thread is settled enough to approve, and resolving one never gates your approval.
43
43
 
44
44
  A reviewer's phase ends with its completion, not with its review. A round that writes a handoff
45
45
  takes this order: write, commit and push the handoff; submit the review of the head that push
@@ -29,7 +29,7 @@ later phase keeps it current rather than replacing it:
29
29
  **Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
30
30
  - Thread <id>: fixed in <commit-sha> — <one line>.
31
31
  - Thread <id>: not a defect — <reason>.
32
- `legion threads resolve --pr <n> --repo <owner>/<repo>` at <head-sha>:
32
+ `legion threads resolve --pr <n> --repo <owner>/<repo>`, run after the push that made <head-sha>:
33
33
  resolved <thread URL> — its opener's acceptance
34
34
  resolved <thread URL> — the Legion reviewer's acceptance of a bot's thread
35
35
  left open <thread URL> — newest reply by <login> is not an acceptance
@@ -1,8 +1,9 @@
1
1
  # Review threads
2
2
 
3
3
  Part of `skill://legion-worker`. Read it when you reply to, accept, or resolve a review thread,
4
- or run `legion threads resolve`: the implementer before every push that answers a review, the
5
- reviewer on every re-review, the merger before READY. Every path it cites is in sjawhar/legion.
4
+ or run `legion threads resolve`: the implementer after every push that answers a review, the
5
+ merger before READY, and the reviewer, who answers threads on every re-review and runs nothing.
6
+ Every path it cites is in sjawhar/legion.
6
7
 
7
8
  - **Threads are dispositioned individually, never resolved in bulk.** Every open review
8
9
  thread gets its own line naming the fixing commit or the reason it isn't a defect. The
@@ -19,16 +20,19 @@ reviewer on every re-review, the merger before READY. Every path it cites is in
19
20
  When neither is set, add `--gh` to that command, which applies the fallback's rule below through
20
21
  your own `gh`; where no `legion` command is installed, use `gh api graphql` with the session's
21
22
  GitHub credential and the fallback below.
22
- In a Legion pane, the **implementer** runs the command before every push that answers a review
23
- (the corrective push and the final `.legion/` deletion push) and pastes its output into the
24
- `Threads` section. The command resolves each unresolved thread whose newest submitted comment is
25
- the opener's own `Accepted:` reply. On a thread a bot account opened that is none of Legion's
26
- role Apps (the daemon names them, keyed by App role), the Legion reviewer's `Accepted:` also
27
- closes it. GitHub cannot tell a CI bot, which never accepts, from a person whose `gh` is routed
28
- to an App, so the reviewer adjudicates such a finding, and it may accept one an App-routed person
29
- raised. The subject of a finding never closes it: the implementer's `Fixed in <commit>: …` or
30
- `Declined: …` answers a thread and closes none. A thread either Legion App opened, a reviewer's
31
- finding included, still needs its opener's `Accepted:`. It makes one `resolveReviewThread` per
23
+ In a Legion pane, the **implementer** runs the command after every push that answers a review
24
+ (the corrective push, and the final `.legion/` deletion push where the daemon has one) and
25
+ before its `handoff_complete`, and pastes its output, stamped with the head it just pushed, into
26
+ the `Threads` section. The output is then recorded against the head the reviewer will read, and
27
+ nothing reads thread state before the implementer's completion. The command resolves each
28
+ unresolved thread whose newest submitted comment is the opener's own `Accepted:` reply. On a
29
+ thread a bot account opened that is none of Legion's role Apps (the daemon names them, keyed by
30
+ App role), the Legion reviewer's `Accepted:` also closes it. GitHub cannot tell a CI bot, which
31
+ never accepts, from a person whose `gh` is routed to an App, so the reviewer adjudicates such a
32
+ finding, and it may accept one an App-routed person raised. The subject of a finding never
33
+ closes it: the implementer's `Fixed in <commit>: …` or `Declined: …` answers a thread and closes
34
+ none. A thread either Legion App opened, a reviewer's finding included, still needs its opener's
35
+ `Accepted:`. It makes one `resolveReviewThread` per
32
36
  thread, prints `resolved <url> — <whose acceptance>` (its opener's, or the Legion reviewer's on a
33
37
  bot's thread, so the ledger shows which) or `left open <url> — newest reply by <login> is …`
34
38
  naming why, and exits 1 naming the thread's URL and GitHub's message when GitHub refuses one.
@@ -63,10 +67,18 @@ reviewer on every re-review, the merger before READY. Every path it cites is in
63
67
  Re-read `reviewThreads` and confirm that thread's `isResolved` is true. In either route, report
64
68
  a refused resolution to the architect, which opens an ask for a human to resolve the thread by
65
69
  hand — never skip it silently. The merger runs the command once more before publishing READY
66
- and does not publish while any `left open` line remains.
70
+ and does not publish while any `left open` line remains. That run is where every accepted
71
+ thread's resolution is guaranteed, since the merge queue's gate counts the unresolved threads at
72
+ the head. Acceptances posted after the implementer's last run are resolved here.
67
73
 
68
74
  - **The reviewer, on a re-review.** When you re-review after a corrective push, answer every
69
- thread you opened in one of the three forms above — `Accepted:` is the only reply the
70
- implementer's `legion threads resolve` acts on — and approve only once every thread you opened
71
- carries your `Accepted:` reply and the implementer's run has resolved it (verify
72
- `isResolved: true` with `gh api graphql`, never from the PR body).
75
+ thread you opened, and every thread a bot opened that is none of Legion's role Apps, in one of
76
+ the three forms above — `Accepted:` is the only reply `legion threads resolve` acts on. A bot's
77
+ finding you cannot accept becomes your own: leave it `Still open:` and request changes.
78
+ Approve once each of those threads has your own `Accepted:` as its newest submitted comment,
79
+ whether or not GitHub shows the thread resolved yet, and every other unresolved thread its
80
+ opener's (read the newest comments with `gh api graphql`, never from the PR body). Another
81
+ opener's thread that a person resolved with GitHub's button, with no `Accepted:`, gates nothing:
82
+ neither `legion threads resolve` nor the merge queue's gate counts a resolved thread. Resolution
83
+ is the pull request author's App's, so your approval never waits on it. The merger resolves
84
+ accepted threads that remain open before publishing READY.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "7.2.0",
3
+ "version": "7.2.2",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {