@sjawhar/pi-legion-envoy 1.46.0 → 1.48.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/README.md +3 -3
- package/dist/envoy.js +91 -4
- package/dist/legion.js +24 -2
- package/dist/skills/dispatch/SKILL.md +44 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,10 +85,10 @@ extension files it does not contain.
|
|
|
85
85
|
|
|
86
86
|
## Native Dispatch tools
|
|
87
87
|
|
|
88
|
-
The extension registers
|
|
88
|
+
The extension registers twenty native Dispatch tools: `dispatch_issue`, `dispatch_issue_update`, `dispatch_ask`, `dispatch_edit_ask`,
|
|
89
89
|
`dispatch_resolve_ask`, `dispatch_resolve_comment`, `dispatch_follow`, `dispatch_comment`, `dispatch_suggest`,
|
|
90
90
|
`dispatch_message`, `dispatch_doc_edit`, `dispatch_doc_read`, `dispatch_request_approval`, `dispatch_artifact`,
|
|
91
|
-
`dispatch_read`, `dispatch_search`, `dispatch_open_asks`, and `dispatch_whoami`, when Dispatch configuration resolves both a base URL and bearer token.
|
|
91
|
+
`dispatch_read`, `dispatch_search`, `dispatch_issues`, `dispatch_architecture_sync`, `dispatch_open_asks`, and `dispatch_whoami`, when Dispatch configuration resolves both a base URL and bearer token.
|
|
92
92
|
|
|
93
93
|
Configure the shared `envoy.json` with:
|
|
94
94
|
|
|
@@ -112,7 +112,7 @@ whose trimmed contents are the token — how the Legion daemon delivers it to a
|
|
|
112
112
|
pane) wins over every other token source and never falls back when unreadable.
|
|
113
113
|
Omitting `dispatch.serverUrl` while `dispatch.enabled` is true targets
|
|
114
114
|
`http://localhost:8766`, the Go server's listen address. Invalid configuration,
|
|
115
|
-
an invalid URL, or an empty token leaves the
|
|
115
|
+
an invalid URL, or an empty token leaves the twenty tools unavailable and
|
|
116
116
|
reports the source of the error.
|
|
117
117
|
|
|
118
118
|
Owner-scoped calls use either an issue (a native `KEY` or external `owner/repo#n` reference) or
|
package/dist/envoy.js
CHANGED
|
@@ -30121,10 +30121,32 @@ var dispatchToolSpecs = [
|
|
|
30121
30121
|
limit: z.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
|
|
30122
30122
|
})
|
|
30123
30123
|
},
|
|
30124
|
+
{
|
|
30125
|
+
name: "dispatch_issues",
|
|
30126
|
+
description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, and open-ask count, so you can see backlog shape without " + "opening every issue. Optionally filter by status, parent, label, or how recently it changed. Do " + "not use it to search by keyword or phrase; dispatch_search remains the keyword surface. Rows are " + "capped at limit (default 50, max 250), applied to the response here, not by the server.",
|
|
30127
|
+
arguments: (z) => ({
|
|
30128
|
+
project: z.string().describe("Project key to list issues from."),
|
|
30129
|
+
status: z.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
|
|
30130
|
+
parent: z.string().describe("Optional parent issue key filter.").optional(),
|
|
30131
|
+
label: z.string().describe("Optional label filter.").optional(),
|
|
30132
|
+
updated_since: z.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
|
|
30133
|
+
limit: z.number({ int: true, min: 1, max: 250 }).describe("Maximum rows, 1-250; default 50.").optional()
|
|
30134
|
+
})
|
|
30135
|
+
},
|
|
30136
|
+
{
|
|
30137
|
+
name: "dispatch_architecture_sync",
|
|
30138
|
+
description: "Import a project's architecture model from its configured source repository now, instead of " + "waiting for the server's five-minute schedule. Returns the imported commit and component " + "count, or the recorded error when the model was rejected (the previous model stays up). " + "The source itself is configured by a human in Settings; 404 SOURCE_NOT_FOUND without one.",
|
|
30139
|
+
arguments: (z) => ({
|
|
30140
|
+
project: z.string().describe("Project key whose architecture source to sync, such as CORE.")
|
|
30141
|
+
}),
|
|
30142
|
+
strict: true
|
|
30143
|
+
},
|
|
30124
30144
|
{
|
|
30125
30145
|
name: "dispatch_open_asks",
|
|
30126
|
-
description: "List
|
|
30127
|
-
arguments: () => ({
|
|
30146
|
+
description: "List active unanswered asks, oldest first, with age and whose reply is needed. Omit project to " + "see only this session's own authored asks (call before saying you are waiting for human input); " + "supply project to see every open ask across that project's issues and documents, whoever authored " + "them.",
|
|
30147
|
+
arguments: (z) => ({
|
|
30148
|
+
project: z.string().describe("Project key; when supplied, lists every open ask in the project instead of only this session's own.").optional()
|
|
30149
|
+
}),
|
|
30128
30150
|
strict: true
|
|
30129
30151
|
},
|
|
30130
30152
|
{
|
|
@@ -31919,9 +31941,15 @@ class DispatchClient {
|
|
|
31919
31941
|
...since === undefined ? {} : { since }
|
|
31920
31942
|
});
|
|
31921
31943
|
}
|
|
31944
|
+
async openAsksForProject(project) {
|
|
31945
|
+
return this.#json("GET", ["api", "v1", "asks", "open"], undefined, { project });
|
|
31946
|
+
}
|
|
31922
31947
|
async whoami() {
|
|
31923
31948
|
return this.#json("GET", ["api", "v1", "whoami"]);
|
|
31924
31949
|
}
|
|
31950
|
+
async syncArchitectureSource(project) {
|
|
31951
|
+
return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
|
|
31952
|
+
}
|
|
31925
31953
|
async resolveAsk(id, input) {
|
|
31926
31954
|
return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
|
|
31927
31955
|
}
|
|
@@ -32335,8 +32363,10 @@ var issueFreeTools = {
|
|
|
32335
32363
|
dispatch_resolve_comment: true,
|
|
32336
32364
|
dispatch_follow: true,
|
|
32337
32365
|
dispatch_search: true,
|
|
32366
|
+
dispatch_issues: true,
|
|
32338
32367
|
dispatch_open_asks: true,
|
|
32339
|
-
dispatch_whoami: true
|
|
32368
|
+
dispatch_whoami: true,
|
|
32369
|
+
dispatch_architecture_sync: true
|
|
32340
32370
|
};
|
|
32341
32371
|
function canonicalExternalIssueRef(value) {
|
|
32342
32372
|
const match = value.trim().match(externalIssueRefPattern);
|
|
@@ -32996,10 +33026,15 @@ async function executeDispatchTool(input) {
|
|
|
32996
33026
|
if (problems.length > 0)
|
|
32997
33027
|
throw new ToolInputError(input.tool, problems);
|
|
32998
33028
|
if (input.tool === "dispatch_open_asks") {
|
|
33029
|
+
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
33030
|
+
const project = optionalString(ownerArguments.args, "project");
|
|
33031
|
+
if (project !== undefined) {
|
|
33032
|
+
const response = await client.openAsksForProject(project);
|
|
33033
|
+
return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
|
|
33034
|
+
}
|
|
32999
33035
|
const sessionId = input.sessionId?.trim();
|
|
33000
33036
|
if (!sessionId)
|
|
33001
33037
|
throw new Error("host session id is required for dispatch_open_asks");
|
|
33002
|
-
const client = new DispatchClient(configUrl, configToken, fetchImpl, input.signal);
|
|
33003
33038
|
const response = await client.openAsks(sessionId);
|
|
33004
33039
|
return { text: formatOpenAsksSummary(response, configUrl), details: { ...response } };
|
|
33005
33040
|
}
|
|
@@ -33146,6 +33181,58 @@ async function executeDispatchTool(input) {
|
|
|
33146
33181
|
details: { query, results }
|
|
33147
33182
|
};
|
|
33148
33183
|
}
|
|
33184
|
+
case "dispatch_issues": {
|
|
33185
|
+
const project = stringArg(args, "project");
|
|
33186
|
+
const status = optionalString(args, "status");
|
|
33187
|
+
const parent = optionalString(args, "parent");
|
|
33188
|
+
const label = optionalString(args, "label");
|
|
33189
|
+
const updatedSince = optionalString(args, "updated_since");
|
|
33190
|
+
const limit = Math.min(Math.max(optionalNumber(args, "limit") ?? 50, 1), 250);
|
|
33191
|
+
const issues = await client.listIssues({
|
|
33192
|
+
project,
|
|
33193
|
+
...status === undefined ? {} : { status },
|
|
33194
|
+
...parent === undefined ? {} : { parent },
|
|
33195
|
+
...label === undefined ? {} : { label },
|
|
33196
|
+
...updatedSince === undefined ? {} : { updated_since: updatedSince }
|
|
33197
|
+
});
|
|
33198
|
+
const rows = issues.slice(0, limit).map((row) => ({
|
|
33199
|
+
key: row.key,
|
|
33200
|
+
title: row.title,
|
|
33201
|
+
status: row.status,
|
|
33202
|
+
priority: row.priority,
|
|
33203
|
+
parent: row.parent,
|
|
33204
|
+
labels: row.labels ?? [],
|
|
33205
|
+
open_asks: row.open_asks,
|
|
33206
|
+
updated_at: row.updated_at
|
|
33207
|
+
}));
|
|
33208
|
+
return {
|
|
33209
|
+
text: rows.length === 0 ? `No issues in ${project}.` : [
|
|
33210
|
+
`${rows.length} ${rows.length === 1 ? "issue" : "issues"} in ${project}` + (issues.length > rows.length ? ` (showing ${rows.length} of ${issues.length})` : ""),
|
|
33211
|
+
...rows.map((row) => `${row.key} [${row.status}]${row.priority === null ? "" : ` P${row.priority}`} ${row.title}` + (row.open_asks === 0 ? "" : ` \xB7 ${row.open_asks} open ${row.open_asks === 1 ? "ask" : "asks"}`))
|
|
33212
|
+
].join(`
|
|
33213
|
+
`),
|
|
33214
|
+
details: { issues: rows }
|
|
33215
|
+
};
|
|
33216
|
+
}
|
|
33217
|
+
case "dispatch_architecture_sync": {
|
|
33218
|
+
const project = stringArg(args, "project");
|
|
33219
|
+
const source = await client.syncArchitectureSource(project);
|
|
33220
|
+
const at = source.last_sync_at ?? "unknown time";
|
|
33221
|
+
return {
|
|
33222
|
+
text: source.last_error === null ? `Synced ${project} architecture from ${source.repo}@${source.branch}: commit ${source.last_commit ?? "unknown"} (${at}).` : [
|
|
33223
|
+
`Sync failed for ${project} (${source.repo}@${source.branch}): ${source.last_error}`,
|
|
33224
|
+
source.last_commit === null ? "No model has ever imported for this project." : `The previous model stays up (commit ${source.last_commit}).`
|
|
33225
|
+
].join(`
|
|
33226
|
+
`),
|
|
33227
|
+
details: {
|
|
33228
|
+
project,
|
|
33229
|
+
repo: source.repo,
|
|
33230
|
+
branch: source.branch,
|
|
33231
|
+
commit: source.last_commit,
|
|
33232
|
+
error: source.last_error
|
|
33233
|
+
}
|
|
33234
|
+
};
|
|
33235
|
+
}
|
|
33149
33236
|
case "dispatch_resolve_ask": {
|
|
33150
33237
|
const kind = stringArg(args, "kind");
|
|
33151
33238
|
const ask = await client.resolveAsk(stringArg(args, "ask"), {
|
package/dist/legion.js
CHANGED
|
@@ -29395,10 +29395,32 @@ var dispatchToolSpecs = [
|
|
|
29395
29395
|
limit: z.number({ int: true, min: 1, max: 50 }).describe("Maximum results, 1-50; default 20.").optional()
|
|
29396
29396
|
})
|
|
29397
29397
|
},
|
|
29398
|
+
{
|
|
29399
|
+
name: "dispatch_issues",
|
|
29400
|
+
description: "List a project's issues for a roadmap or backlog pass: every issue in one project, each carrying " + "its status, priority, parent, labels, and open-ask count, so you can see backlog shape without " + "opening every issue. Optionally filter by status, parent, label, or how recently it changed. Do " + "not use it to search by keyword or phrase; dispatch_search remains the keyword surface. Rows are " + "capped at limit (default 50, max 250), applied to the response here, not by the server.",
|
|
29401
|
+
arguments: (z) => ({
|
|
29402
|
+
project: z.string().describe("Project key to list issues from."),
|
|
29403
|
+
status: z.enum(ISSUE_STATUSES).describe("Optional lifecycle status filter.").optional(),
|
|
29404
|
+
parent: z.string().describe("Optional parent issue key filter.").optional(),
|
|
29405
|
+
label: z.string().describe("Optional label filter.").optional(),
|
|
29406
|
+
updated_since: z.string().describe("Optional RFC3339 timestamp; only issues updated at or after it.").optional(),
|
|
29407
|
+
limit: z.number({ int: true, min: 1, max: 250 }).describe("Maximum rows, 1-250; default 50.").optional()
|
|
29408
|
+
})
|
|
29409
|
+
},
|
|
29410
|
+
{
|
|
29411
|
+
name: "dispatch_architecture_sync",
|
|
29412
|
+
description: "Import a project's architecture model from its configured source repository now, instead of " + "waiting for the server's five-minute schedule. Returns the imported commit and component " + "count, or the recorded error when the model was rejected (the previous model stays up). " + "The source itself is configured by a human in Settings; 404 SOURCE_NOT_FOUND without one.",
|
|
29413
|
+
arguments: (z) => ({
|
|
29414
|
+
project: z.string().describe("Project key whose architecture source to sync, such as CORE.")
|
|
29415
|
+
}),
|
|
29416
|
+
strict: true
|
|
29417
|
+
},
|
|
29398
29418
|
{
|
|
29399
29419
|
name: "dispatch_open_asks",
|
|
29400
|
-
description: "List
|
|
29401
|
-
arguments: () => ({
|
|
29420
|
+
description: "List active unanswered asks, oldest first, with age and whose reply is needed. Omit project to " + "see only this session's own authored asks (call before saying you are waiting for human input); " + "supply project to see every open ask across that project's issues and documents, whoever authored " + "them.",
|
|
29421
|
+
arguments: (z) => ({
|
|
29422
|
+
project: z.string().describe("Project key; when supplied, lists every open ask in the project instead of only this session's own.").optional()
|
|
29423
|
+
}),
|
|
29402
29424
|
strict: true
|
|
29403
29425
|
},
|
|
29404
29426
|
{
|
|
@@ -148,6 +148,20 @@ start with the issue key; standalone project-document hit lines start with
|
|
|
148
148
|
`dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns the candidates (`POSSIBLE_DUPLICATE`).
|
|
149
149
|
Read them; reference the existing issue, or repeat the call with `force: true` when it is genuinely new work.
|
|
150
150
|
|
|
151
|
+
## Reading a project's backlog
|
|
152
|
+
|
|
153
|
+
To see the shape of a project rather than find a phrase, list its issues:
|
|
154
|
+
```ts
|
|
155
|
+
dispatch_issues({ project, status?, parent?, label?, updated_since?, limit? })
|
|
156
|
+
```
|
|
157
|
+
Each row carries the issue key, title, status, priority, parent, labels, its open-ask count, and
|
|
158
|
+
when it last changed — a roadmap or backlog pass without opening every issue. Filter with `status`
|
|
159
|
+
(a lifecycle status), `parent` (one issue's children), `label`, or `updated_since` (an RFC3339
|
|
160
|
+
timestamp, for "what moved this week"). `limit` caps the rows at 50 by default and 250 at most.
|
|
161
|
+
|
|
162
|
+
This is not search: it matches no text. Use `dispatch_search` for a keyword or phrase, and
|
|
163
|
+
`dispatch_issues` when you want every issue in a project and its current state.
|
|
164
|
+
|
|
151
165
|
## Asking
|
|
152
166
|
|
|
153
167
|
### Before you ask
|
|
@@ -171,8 +185,8 @@ production import). Every `dispatch_ask` passes three gates first:
|
|
|
171
185
|
would have to define. This is the phone test in [Writing for the human](#writing-for-the-human).
|
|
172
186
|
If you cannot write it that way, you do not understand it well enough to ask.
|
|
173
187
|
|
|
174
|
-
The platform PO audits open asks. One that fails a gate
|
|
175
|
-
record.
|
|
188
|
+
The platform PO audits open asks. One that fails a gate — or that points at another message in
|
|
189
|
+
prose instead of carrying its content (below) — is retracted, with the PO's answer as the record.
|
|
176
190
|
|
|
177
191
|
Open a decision with:
|
|
178
192
|
```ts
|
|
@@ -209,11 +223,38 @@ passage with `anchor`. Follow up on an ask or comment with `dispatch_comment`; c
|
|
|
209
223
|
with a `dispatch://` reference (see [References](#references)). Never write "see above", "the
|
|
210
224
|
message above", or "as attached".
|
|
211
225
|
|
|
226
|
+
**Pointing at another message is a defect, not a shortcut.** Sami, 2026-09-17, verbatim, on an
|
|
227
|
+
ask that read "the settings listed in my comment just above" after a long procedure had been posted
|
|
228
|
+
as a comment: "you just dump information into messages and then add a new ask that references a
|
|
229
|
+
previous message in prose with no link or no context whatsoever and uses compressed shorthand
|
|
230
|
+
jargon." The ask view does not show the issue's comments, so that ask was unanswerable; "Cloud
|
|
231
|
+
Identity licence check / 2SV override / 1-day grace" was shorthand he had never used. The rules
|
|
232
|
+
that follow from it:
|
|
233
|
+
|
|
234
|
+
- An ask that names another message in prose — "my comment above", "the procedure I posted",
|
|
235
|
+
"see the earlier message" — is retracted by the PO as failing the gates. Put the content IN the
|
|
236
|
+
ask. If it does not fit the 800-character budget, the step is too big: split the step, never
|
|
237
|
+
point elsewhere. The only pointers an ask may carry are a `dispatch://` reference or a document
|
|
238
|
+
`anchor`, and they cite — the ask still says in one line what the reader will find there and can
|
|
239
|
+
be answered without following them.
|
|
240
|
+
- Expand every term the reader has not used first. A product name, an internal setting, an
|
|
241
|
+
acronym, a value you coined this session — write what it is in the ask, in his words.
|
|
242
|
+
- A runbook the human must execute is one ask per step, each self-contained: what to do, where,
|
|
243
|
+
what result proves it, `Done` / `Can't` options. Each later step opens only after the previous is
|
|
244
|
+
answered and states that step's verified result in one line ("Step 1 done: the licence shows
|
|
245
|
+
Cloud Identity Free on the admin console.") — never a pointer to the earlier ask.
|
|
246
|
+
|
|
212
247
|
**A decision about an uploaded artifact links it.** If the human must read an artifact to answer,
|
|
213
248
|
the question carries `dispatch://KEY/artifact/<slug>` (or `ref`), never just its filename. Text
|
|
214
249
|
they must read to decide belongs in the spec in the first place — see [Artifacts](#artifacts).
|
|
215
250
|
|
|
216
|
-
|
|
251
|
+
Import a project's architecture model from its configured source repository now (a human configures the source in Settings):
|
|
252
|
+
```ts
|
|
253
|
+
dispatch_architecture_sync({ project: "CORE" })
|
|
254
|
+
```
|
|
255
|
+
It returns the imported commit, or the recorded error when the model was rejected — the previous model stays up. Without a configured source it answers 404 `SOURCE_NOT_FOUND`.
|
|
256
|
+
|
|
257
|
+
Before saying you are waiting for human input, call `dispatch_open_asks`. With no arguments it lists this session's active asks across open issues and project documents, including whether the human or agent owes the next reply. With `dispatch_open_asks({ project })` it lists every open ask in that project — on its issues and on its documents, whoever authored them — which is how you audit what a whole project is waiting on rather than just your own asks.
|
|
217
258
|
|
|
218
259
|
**Anything that needs the human is an ask, or it does not exist.** An approval, a credential,
|
|
219
260
|
a setting only they can change, a review click, a conflict between two of their own rules - if
|