@sjawhar/pi-legion-envoy 1.4.1 → 1.4.3
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
|
@@ -29649,6 +29649,9 @@ function date4(params) {
|
|
|
29649
29649
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
29650
29650
|
config(en_default());
|
|
29651
29651
|
// ../contracts/src/dispatch-api.ts
|
|
29652
|
+
function searchOwnerOf(result) {
|
|
29653
|
+
return result.owner ?? { kind: "issue", key: result.issue.key };
|
|
29654
|
+
}
|
|
29652
29655
|
var DispatchEventSchema = object({
|
|
29653
29656
|
issue_key: string2().nullable(),
|
|
29654
29657
|
artifact_id: string2().nullish(),
|
|
@@ -29810,7 +29813,9 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
|
|
|
29810
29813
|
};
|
|
29811
29814
|
}
|
|
29812
29815
|
var SPEC_SECTIONS = [
|
|
29816
|
+
"Summary",
|
|
29813
29817
|
"Decisions needed",
|
|
29818
|
+
"New since we talked",
|
|
29814
29819
|
"Acceptance",
|
|
29815
29820
|
"Requirements",
|
|
29816
29821
|
"Design",
|
|
@@ -29818,7 +29823,7 @@ var SPEC_SECTIONS = [
|
|
|
29818
29823
|
"Testing",
|
|
29819
29824
|
"Rejected"
|
|
29820
29825
|
];
|
|
29821
|
-
var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "
|
|
29826
|
+
var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Write for a reader who has not seen the code: plain sentences, every identifier expanded on " + "first use, no coined shorthand; see skills/dispatch Writing for the human and Writing a spec.";
|
|
29822
29827
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
29823
29828
|
var DOC_EDIT_OPS = ["replace", "delete", "insert"];
|
|
29824
29829
|
var dispatchToolSpecs = [
|
|
@@ -31774,9 +31779,14 @@ function duplicateCandidates(error) {
|
|
|
31774
31779
|
return error.candidates;
|
|
31775
31780
|
}
|
|
31776
31781
|
function searchResultLine(result, baseUrl) {
|
|
31782
|
+
const href = new URL(result.href, baseUrl).toString();
|
|
31783
|
+
const owner = searchOwnerOf(result);
|
|
31784
|
+
if (owner.kind === "document") {
|
|
31785
|
+
const reference = `dispatch://${owner.project}/artifact/${owner.slug}`;
|
|
31786
|
+
return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
|
|
31787
|
+
}
|
|
31777
31788
|
const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
|
|
31778
31789
|
const label = `${result.issue.key} [${result.issue.status}] ${result.issue.title} - ${result.kind}${artifactName}`;
|
|
31779
|
-
const href = new URL(result.href, baseUrl).toString();
|
|
31780
31790
|
return `${label}: ${snippetText(result.snippet)} -> ${href}`;
|
|
31781
31791
|
}
|
|
31782
31792
|
function askUrgency(args) {
|
|
@@ -32730,12 +32740,20 @@ class EnvoyApiError extends Error {
|
|
|
32730
32740
|
function createEnvoyClient(config) {
|
|
32731
32741
|
const baseUrl = normalizeEnvoyUrl(config.baseUrl);
|
|
32732
32742
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
32743
|
+
const apiToken = process.env["ENVOY_TOKEN"];
|
|
32733
32744
|
const request = async (path, init) => {
|
|
32734
32745
|
const url = `${baseUrl}${path}`;
|
|
32735
32746
|
for (let attempt = 0;attempt < 2; attempt += 1) {
|
|
32736
32747
|
let response;
|
|
32737
32748
|
try {
|
|
32738
|
-
|
|
32749
|
+
const headers = new Headers(init.headers);
|
|
32750
|
+
if (apiToken)
|
|
32751
|
+
headers.set("Authorization", `Bearer ${apiToken}`);
|
|
32752
|
+
response = await config.fetch(url, {
|
|
32753
|
+
...init,
|
|
32754
|
+
headers,
|
|
32755
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
32756
|
+
});
|
|
32739
32757
|
} catch (error) {
|
|
32740
32758
|
if (attempt === 0) {
|
|
32741
32759
|
await waitForRetry();
|
package/dist/legion.js
CHANGED
|
@@ -29648,6 +29648,9 @@ function date4(params) {
|
|
|
29648
29648
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
29649
29649
|
config(en_default());
|
|
29650
29650
|
// ../contracts/src/dispatch-api.ts
|
|
29651
|
+
function searchOwnerOf(result) {
|
|
29652
|
+
return result.owner ?? { kind: "issue", key: result.issue.key };
|
|
29653
|
+
}
|
|
29651
29654
|
var DispatchEventSchema = object({
|
|
29652
29655
|
issue_key: string2().nullable(),
|
|
29653
29656
|
artifact_id: string2().nullish(),
|
|
@@ -29809,7 +29812,9 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
|
|
|
29809
29812
|
};
|
|
29810
29813
|
}
|
|
29811
29814
|
var SPEC_SECTIONS = [
|
|
29815
|
+
"Summary",
|
|
29812
29816
|
"Decisions needed",
|
|
29817
|
+
"New since we talked",
|
|
29813
29818
|
"Acceptance",
|
|
29814
29819
|
"Requirements",
|
|
29815
29820
|
"Design",
|
|
@@ -29817,7 +29822,7 @@ var SPEC_SECTIONS = [
|
|
|
29817
29822
|
"Testing",
|
|
29818
29823
|
"Rejected"
|
|
29819
29824
|
];
|
|
29820
|
-
var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "
|
|
29825
|
+
var SPEC_WRITING_GUIDANCE = `When writing a spec, use these sections in order: ${SPEC_SECTIONS.join(", ")}. ` + "Write for a reader who has not seen the code: plain sentences, every identifier expanded on " + "first use, no coined shorthand; see skills/dispatch Writing for the human and Writing a spec.";
|
|
29821
29826
|
var ASK_URGENCIES = ["low", "med", "high", "blocking"];
|
|
29822
29827
|
var DOC_EDIT_OPS = ["replace", "delete", "insert"];
|
|
29823
29828
|
var dispatchToolSpecs = [
|
|
@@ -32200,9 +32205,14 @@ function duplicateCandidates(error) {
|
|
|
32200
32205
|
return error.candidates;
|
|
32201
32206
|
}
|
|
32202
32207
|
function searchResultLine(result, baseUrl) {
|
|
32208
|
+
const href = new URL(result.href, baseUrl).toString();
|
|
32209
|
+
const owner = searchOwnerOf(result);
|
|
32210
|
+
if (owner.kind === "document") {
|
|
32211
|
+
const reference = `dispatch://${owner.project}/artifact/${owner.slug}`;
|
|
32212
|
+
return `${reference} [document] ${owner.name} - ${result.kind}: ${snippetText(result.snippet)} -> ${href}`;
|
|
32213
|
+
}
|
|
32203
32214
|
const artifactName = result.artifact ? ` ${result.artifact.name}` : "";
|
|
32204
32215
|
const label = `${result.issue.key} [${result.issue.status}] ${result.issue.title} - ${result.kind}${artifactName}`;
|
|
32205
|
-
const href = new URL(result.href, baseUrl).toString();
|
|
32206
32216
|
return `${label}: ${snippetText(result.snippet)} -> ${href}`;
|
|
32207
32217
|
}
|
|
32208
32218
|
function askUrgency(args) {
|
|
@@ -33156,12 +33166,20 @@ class EnvoyApiError extends Error {
|
|
|
33156
33166
|
function createEnvoyClient(config) {
|
|
33157
33167
|
const baseUrl = normalizeEnvoyUrl(config.baseUrl);
|
|
33158
33168
|
const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
33169
|
+
const apiToken = process.env["ENVOY_TOKEN"];
|
|
33159
33170
|
const request = async (path, init) => {
|
|
33160
33171
|
const url = `${baseUrl}${path}`;
|
|
33161
33172
|
for (let attempt = 0;attempt < 2; attempt += 1) {
|
|
33162
33173
|
let response;
|
|
33163
33174
|
try {
|
|
33164
|
-
|
|
33175
|
+
const headers = new Headers(init.headers);
|
|
33176
|
+
if (apiToken)
|
|
33177
|
+
headers.set("Authorization", `Bearer ${apiToken}`);
|
|
33178
|
+
response = await config.fetch(url, {
|
|
33179
|
+
...init,
|
|
33180
|
+
headers,
|
|
33181
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
33182
|
+
});
|
|
33165
33183
|
} catch (error) {
|
|
33166
33184
|
if (attempt === 0) {
|
|
33167
33185
|
await waitForRetry();
|
|
@@ -13,31 +13,49 @@ The server enforces high signal: an ask question is at most 800 characters with
|
|
|
13
13
|
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
|
|
14
14
|
longer exist.
|
|
15
15
|
|
|
16
|
+
## Writing for the human
|
|
17
|
+
|
|
18
|
+
Sami, 2026-09-12, on what Legion had been producing: "It's completely incomprehensible. It's just
|
|
19
|
+
compressed jargon nonsense. I have no idea what the fuck it's saying." Every spec, ask, comment,
|
|
20
|
+
message, and PR body is read by a person who has not read the code, does not share this session's
|
|
21
|
+
vocabulary, and is often on a phone. Write for that person.
|
|
22
|
+
|
|
23
|
+
- Plain English, full sentences, one idea per sentence. Never repo shorthand or nouns you coined:
|
|
24
|
+
not "fix 8c", "READY-target", "PR B", "spec@v3", "the pair", "the packet" — say what the thing is.
|
|
25
|
+
- Expand every identifier the first time it appears: an issue key gets its title, a PR number its
|
|
26
|
+
title, a file what it is for, a session id who it is. Link a URL rather than pasting a bare id.
|
|
27
|
+
- Frame a request as current state → desired state → proposed change, with at least two options,
|
|
28
|
+
what each costs, and your recommendation with its reason.
|
|
29
|
+
- Before posting, test it: could Sami, reading only this text on his phone, know what he is being
|
|
30
|
+
told or asked? If not, rewrite it. Length is not the problem; density is.
|
|
31
|
+
|
|
16
32
|
## Writing a spec
|
|
17
33
|
|
|
18
|
-
A spec
|
|
19
|
-
these
|
|
34
|
+
A spec has two readers: the human who decides reads the top; the implementer who builds reads the
|
|
35
|
+
rest. Use these headings in this order.
|
|
20
36
|
|
|
21
37
|
| Section | Required content | Form |
|
|
22
38
|
| --- | --- | --- |
|
|
23
|
-
| **
|
|
24
|
-
| **
|
|
25
|
-
| **
|
|
26
|
-
| **
|
|
27
|
-
| **
|
|
28
|
-
| **
|
|
29
|
-
| **
|
|
39
|
+
| **Summary** | The problem, what changes for whom, and how we will know it worked — in plain words. | Three sentences at most. |
|
|
40
|
+
| **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. |
|
|
41
|
+
| **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. |
|
|
42
|
+
| **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. |
|
|
43
|
+
| **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. |
|
|
44
|
+
| **Design** | The files, components, routes, and data flow that change. | Prose or tables; a diagram only for real structure. |
|
|
45
|
+
| **Errors** | The behaviour for every error condition. Never a silent fallback. | `condition \| behaviour` table. |
|
|
46
|
+
| **Testing** | Which proof exercises each acceptance line. | One line per acceptance item. |
|
|
47
|
+
| **Rejected** | Each alternative considered and why it was rejected, so it is not proposed again. | One alternative per line. |
|
|
30
48
|
|
|
31
49
|
### Rules
|
|
32
50
|
|
|
33
|
-
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
- Do not use TBD, TODO, or placeholders; an open item is a Decision needed.
|
|
51
|
+
- The spec is the issue's one primary document. Extend it in place — a new version that keeps the
|
|
52
|
+
human's own text — never a second "spec" artifact beside it.
|
|
53
|
+
- No hedging ("might", "could consider"). No TBD, TODO, or placeholders: an open item is a
|
|
54
|
+
Decision needed.
|
|
38
55
|
- Keep each section to one screen; work that exceeds one screen per section is two specs.
|
|
39
|
-
- Update the spec
|
|
40
|
-
- Before sending it: no sections conflict,
|
|
56
|
+
- Update the spec as decisions land: the spec is the record, comments are the discussion.
|
|
57
|
+
- Before sending it: no sections conflict, every requirement has exactly one reading, and the
|
|
58
|
+
Summary and Decisions pass the phone test above.
|
|
41
59
|
|
|
42
60
|
## Your owner
|
|
43
61
|
|
|
@@ -60,9 +78,11 @@ Before you create an issue or start a design document, search:
|
|
|
60
78
|
```ts
|
|
61
79
|
dispatch_search({ query, project?, limit? })
|
|
62
80
|
```
|
|
63
|
-
It returns every issue, document, comment, ask, and message that contains the words
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
It returns every issue, document, comment, ask, and message that contains the words. Issue-owned hit lines
|
|
82
|
+
start with the issue key; standalone project-document hit lines start with
|
|
83
|
+
`dispatch://PROJECT/artifact/<slug>`, followed by the absolute link. Cite the hit you build on
|
|
84
|
+
(`dispatch://KEY` or the document reference), or state "no prior issue" in the spec. Websearch syntax applies:
|
|
85
|
+
`"merge queue"`, `-daemon`, `OR`.
|
|
66
86
|
|
|
67
87
|
`dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns the candidates (`POSSIBLE_DUPLICATE`).
|
|
68
88
|
Read them; reference the existing issue, or repeat the call with `force: true` when it is genuinely new work.
|
|
@@ -83,9 +103,16 @@ dispatch_ask({
|
|
|
83
103
|
})
|
|
84
104
|
```
|
|
85
105
|
It returns `details` `{ issue, topic, ask }` for an issue or `{ project, artifact, document, topic, ask }` for a project document.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
|
|
107
|
+
An ask is read on a phone by someone who has not read the code. Open with one or two plain
|
|
108
|
+
sentences: what needs deciding and why it matters now. Each option is a button with a label and
|
|
109
|
+
one sentence saying what happens if it is chosen; never enumerate choices in prose. Put the
|
|
110
|
+
recommendation and its reason last, in `question`. Never put file paths, line numbers, sequence
|
|
111
|
+
numbers, document versions, or role tokens in the question; if the human needs that detail, anchor
|
|
112
|
+
the ask to the document passage instead. Apply the phone test from "Writing for the human" before
|
|
113
|
+
posting. Anchor a document question with `anchor: { artifact, quote, occurrence? }`; `occurrence`
|
|
114
|
+
is zero-based and selects a repeated quote, and an anchor whose quote later disappears becomes
|
|
115
|
+
orphaned but stays readable against its original document version.
|
|
89
116
|
|
|
90
117
|
An ask must be answerable from its own text and its anchor alone. Anchor a question about a document passage with `anchor`; thread one
|
|
91
118
|
about a comment with `reply_to`; thread a follow-up on your own ask with `reply_to_ask`; cite anything else with a `dispatch://`
|
|
@@ -223,13 +250,14 @@ dispatch_artifact({ issue?, project?, name, path, summary? })
|
|
|
223
250
|
Or, when the text is already in the call, post a Markdown document directly:
|
|
224
251
|
|
|
225
252
|
```ts
|
|
226
|
-
dispatch_artifact({ issue?, project?, name: "
|
|
253
|
+
dispatch_artifact({ issue?, project?, name: "load-test-results.md", content: "# Load test\n..." })
|
|
227
254
|
```
|
|
228
255
|
|
|
229
256
|
Exactly one of `issue` and `project` is required. A project upload creates an unlinked project document; it must not include `artifact`.
|
|
230
257
|
Exactly one of `path` and `content` is required. It returns issue or project-document owner details plus `artifact`, `version`, and its
|
|
231
|
-
write `topic`. Uploading the same `name` creates its next version
|
|
232
|
-
|
|
258
|
+
write `topic`. Uploading the same `name` creates its next version — so uploading `spec.md` **replaces the issue's own specification**
|
|
259
|
+
with your text. Never do that: the spec is edited in place with `dispatch_doc_edit` (see [The Spec](#the-spec)). Address an existing
|
|
260
|
+
artifact by the slug shown in the upload result or by its filename; the slug also arrives on `artifact.created` events.
|
|
233
261
|
|
|
234
262
|
## Messages
|
|
235
263
|
|
|
@@ -75,21 +75,33 @@ Wave releases, child closures, and your own status are visible from the issue tr
|
|
|
75
75
|
handoffs; do not narrate them into the spec or a `dispatch_message`. A blocker only Sami can
|
|
76
76
|
clear is a `dispatch_ask`.
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
The issue's primary document **is** the root specification. Extend it in place — a new version
|
|
79
|
+
that keeps the human's own text and adds Summary, Decisions needed, New since we talked, the
|
|
80
|
+
adoption/decomposition and waves, acceptance criteria, and the integration test — never a second
|
|
81
|
+
"spec" artifact beside it (`dispatch_artifact` with the primary document's name replaces the
|
|
82
|
+
human's document; do not do that). Both readers described in
|
|
83
|
+
[Writing for the human](../dispatch/SKILL.md#writing-for-the-human) must be able to follow it.
|
|
84
|
+
When the config-armed root design gate applies, run this exact sequence **before any
|
|
85
|
+
Legion-role spawn**, including a sub-architect:
|
|
82
86
|
|
|
83
87
|
```text
|
|
84
|
-
|
|
88
|
+
dispatch_doc_edit({ issue: "<root issue>", ... }) // extend the primary document in place
|
|
85
89
|
askId = dispatch_ask({
|
|
86
90
|
issue: "<root issue>",
|
|
87
|
-
question: "<
|
|
88
|
-
options: [
|
|
91
|
+
question: "<what is true today, in one sentence> <what will be true when this lands, in one sentence> <how: one issue or N child issues, and what the first step is> I recommend Approve because <one reason>.",
|
|
92
|
+
options: [
|
|
93
|
+
{ label: "Approve", description: "Work starts as described; the first worker is spawned now." },
|
|
94
|
+
{ label: "Hold", description: "Nothing starts; reply on the issue with what should change first." },
|
|
95
|
+
]
|
|
89
96
|
})
|
|
90
97
|
legion({ op: "register_gate", issue: "<root issue>", askId })
|
|
91
98
|
```
|
|
92
99
|
|
|
100
|
+
Both options are required: a question with only `Approve` is not a decision. The whole ask is
|
|
101
|
+
read on a phone by someone who has not read the code: no file paths, line numbers, document
|
|
102
|
+
versions, or role tokens in it. Sami, 2026-09-12, on a gate ask that broke this rule: "I have no
|
|
103
|
+
idea what the fuck you're talking about."
|
|
104
|
+
|
|
93
105
|
Then park. Do not release a wave or spawn a Legion role until a later delivered wake
|
|
94
106
|
shows `design-approved` on the root. On a deployment whose design gate is off
|
|
95
107
|
(`gates.design: off` in its `legion.yaml`), the daemon satisfies the gate as you register it
|
|
@@ -44,6 +44,10 @@ into a state holder: daemon state and the Dispatch project remain authoritative.
|
|
|
44
44
|
relevant Dispatch issue. A stale or duplicate wake may cost a read, never a wrong action.
|
|
45
45
|
- **Controller state is disposable.** Do not reconstruct or preserve local controller
|
|
46
46
|
bookkeeping between turns.
|
|
47
|
+
- **Write for a human.** Every `dispatch_comment`, `dispatch_message`, and `dispatch_ask` you
|
|
48
|
+
post follows the dispatch skill's "Writing for the human" rules: plain sentences, every
|
|
49
|
+
identifier expanded on first use, no coined shorthand. A triage note that reads like a log
|
|
50
|
+
line is not a triage note.
|
|
47
51
|
|
|
48
52
|
## Wake routing table
|
|
49
53
|
|