@sjawhar/pi-legion-envoy 5.16.7 → 5.16.9
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 +68 -10
- package/dist/legion.js +73 -13
- package/dist/skills/dispatch/SKILL.md +14 -6
- package/dist/skills/legion-worker/SKILL.md +58 -22
- package/package.json +2 -2
package/dist/envoy.js
CHANGED
|
@@ -30028,6 +30028,16 @@ var messageValidation = {
|
|
|
30028
30028
|
},
|
|
30029
30029
|
message: "issue is required unless in_reply_to names a message delivered to this session, which is the one message with no issue."
|
|
30030
30030
|
};
|
|
30031
|
+
var readOwner = documentOwnerValidation(true);
|
|
30032
|
+
var readValidation = {
|
|
30033
|
+
check: (value) => {
|
|
30034
|
+
const input = value;
|
|
30035
|
+
if (typeof input.message !== "string")
|
|
30036
|
+
return readOwner.check(value);
|
|
30037
|
+
return input.issue === undefined && input.project === undefined && input.artifact === undefined && input.ref === undefined;
|
|
30038
|
+
},
|
|
30039
|
+
message: `${readOwner.message} message stands alone: it names the conversation, so name no issue, project, artifact, or ref with it.`
|
|
30040
|
+
};
|
|
30031
30041
|
var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
|
|
30032
30042
|
function componentsArgument(z2) {
|
|
30033
30043
|
return z2.object({
|
|
@@ -30255,7 +30265,7 @@ var dispatchToolSpecs = [
|
|
|
30255
30265
|
{
|
|
30256
30266
|
name: "dispatch_message",
|
|
30257
30267
|
example: { issue: "DSP-1", body: "Implementation started." },
|
|
30258
|
-
description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation
|
|
30268
|
+
description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch_read({message}) reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
|
|
30259
30269
|
arguments: (z2) => ({
|
|
30260
30270
|
issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
|
|
30261
30271
|
body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
|
|
@@ -30349,14 +30359,15 @@ var dispatchToolSpecs = [
|
|
|
30349
30359
|
{
|
|
30350
30360
|
name: "dispatch_read",
|
|
30351
30361
|
example: { issue: "DSP-1" },
|
|
30352
|
-
description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain. Do not use it for document
|
|
30362
|
+
description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch_doc_read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + OWNER_REFERENCE,
|
|
30353
30363
|
arguments: (z2) => ({
|
|
30354
30364
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
30355
30365
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
30356
30366
|
artifact: z2.string().describe("Project document artifact id, slug, or filename.").optional(),
|
|
30357
|
-
ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional()
|
|
30367
|
+
ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
|
|
30368
|
+
message: z2.string().describe("A message id (uuid): reads the conversation it belongs to, the root message and every " + "reply. Name nothing else with it.").optional()
|
|
30358
30369
|
}),
|
|
30359
|
-
validation:
|
|
30370
|
+
validation: readValidation
|
|
30360
30371
|
},
|
|
30361
30372
|
{
|
|
30362
30373
|
name: "dispatch_search",
|
|
@@ -30600,6 +30611,7 @@ function controllerToken(project) {
|
|
|
30600
30611
|
|
|
30601
30612
|
// ../contracts/src/legion-daemon-api.ts
|
|
30602
30613
|
var nonEmptyString = exports_external.string().min(1);
|
|
30614
|
+
var appLogin = exports_external.string().regex(/^[^[\]]+\[bot\]$/);
|
|
30603
30615
|
var legionRole = exports_external.enum(LEGION_ROLES);
|
|
30604
30616
|
var requiredUnknown = exports_external.unknown().refine((value) => value !== undefined, {
|
|
30605
30617
|
message: "Required"
|
|
@@ -30853,7 +30865,11 @@ var LegionDaemonApi = {
|
|
|
30853
30865
|
},
|
|
30854
30866
|
GitHubToken: {
|
|
30855
30867
|
request: exports_external.strictObject({ grantId: nonEmptyString }),
|
|
30856
|
-
response: exports_external.object({
|
|
30868
|
+
response: exports_external.object({
|
|
30869
|
+
token: nonEmptyString,
|
|
30870
|
+
appLogin: exports_external.string().endsWith("[bot]"),
|
|
30871
|
+
legionAppLogins: exports_external.object({ implement: appLogin, review: appLogin }).optional()
|
|
30872
|
+
})
|
|
30857
30873
|
},
|
|
30858
30874
|
GitCredential: {
|
|
30859
30875
|
request: exports_external.strictObject({ grantId: nonEmptyString })
|
|
@@ -32438,8 +32454,8 @@ class DispatchClient {
|
|
|
32438
32454
|
async message(issue2, input) {
|
|
32439
32455
|
return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "messages"], input);
|
|
32440
32456
|
}
|
|
32441
|
-
async messageReply(id, input) {
|
|
32442
|
-
return this.#json("POST", ["api", "v1", "messages", id, "reply"], input);
|
|
32457
|
+
async messageReply(id, input, options = {}) {
|
|
32458
|
+
return this.#json("POST", ["api", "v1", "messages", id, "reply"], input, options.followUp === true ? { follow_up: "true" } : undefined);
|
|
32443
32459
|
}
|
|
32444
32460
|
async getMessage(issue2, id) {
|
|
32445
32461
|
return this.#json("GET", [
|
|
@@ -32451,6 +32467,9 @@ class DispatchClient {
|
|
|
32451
32467
|
id
|
|
32452
32468
|
]);
|
|
32453
32469
|
}
|
|
32470
|
+
async getMessageThread(id, session) {
|
|
32471
|
+
return this.#json("GET", ["api", "v1", "messages", id], undefined, { session });
|
|
32472
|
+
}
|
|
32454
32473
|
async artifact(issue2, input) {
|
|
32455
32474
|
const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"];
|
|
32456
32475
|
if ("content" in input)
|
|
@@ -33102,6 +33121,13 @@ function argumentProblems(tool, args) {
|
|
|
33102
33121
|
}
|
|
33103
33122
|
break;
|
|
33104
33123
|
}
|
|
33124
|
+
case "dispatch_read": {
|
|
33125
|
+
const message = optionalString(args, "message");
|
|
33126
|
+
if (message !== undefined && messageIdOf(message) === undefined) {
|
|
33127
|
+
problems.push("message must be a full message id (uuid) or a dispatch://KEY/message/<id> reference");
|
|
33128
|
+
}
|
|
33129
|
+
break;
|
|
33130
|
+
}
|
|
33105
33131
|
}
|
|
33106
33132
|
return problems;
|
|
33107
33133
|
}
|
|
@@ -33239,6 +33265,9 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
|
|
|
33239
33265
|
if (tool === "dispatch_message" && typeof replyTarget === "string" && !replyTarget.startsWith("dispatch://")) {
|
|
33240
33266
|
return { args, ref, owner: null };
|
|
33241
33267
|
}
|
|
33268
|
+
if (tool === "dispatch_read" && typeof args.message === "string") {
|
|
33269
|
+
return { args, ref, owner: null };
|
|
33270
|
+
}
|
|
33242
33271
|
const legionIssue = env.LEGION_ISSUE;
|
|
33243
33272
|
if (!legionIssue) {
|
|
33244
33273
|
problems.push(ownerRequiredProblem);
|
|
@@ -34161,16 +34190,30 @@ ${followsAsk(askOwner)}`,
|
|
|
34161
34190
|
const inReplyTo = messageInReplyTo(args);
|
|
34162
34191
|
const body = stringArg(args, "body");
|
|
34163
34192
|
if (owner === null && inReplyTo !== undefined) {
|
|
34164
|
-
const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor });
|
|
34193
|
+
const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor }, { followUp: true });
|
|
34194
|
+
if (reply.duplicate === true && reply.body === body) {
|
|
34195
|
+
return {
|
|
34196
|
+
text: `Dispatch already has this exact text in the conversation (message ${reply.id}); ` + "nothing new was posted. Send different text if you have more to say.",
|
|
34197
|
+
details: { message: reply.id, in_reply_to: inReplyTo, posted: false, duplicate: true }
|
|
34198
|
+
};
|
|
34199
|
+
}
|
|
34165
34200
|
if (reply.body !== body) {
|
|
34166
34201
|
return {
|
|
34167
34202
|
text: `Message ${inReplyTo} was already answered by message ${reply.id}; Dispatch kept ` + "that reply and posted nothing. Wait for their next message rather than answering " + "this one again.",
|
|
34168
34203
|
details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
|
|
34169
34204
|
};
|
|
34170
34205
|
}
|
|
34206
|
+
const readBack = `dispatch_read({message: "${inReplyTo}"}) reads the conversation back.`;
|
|
34207
|
+
const parent = reply.in_reply_to ?? undefined;
|
|
34208
|
+
const follows = parent === inReplyTo ? undefined : parent;
|
|
34171
34209
|
return {
|
|
34172
|
-
text: `Replied to message ${inReplyTo} with message ${reply.id}`,
|
|
34173
|
-
details: {
|
|
34210
|
+
text: follows === undefined ? `Replied to message ${inReplyTo} with message ${reply.id}. ${readBack}` : `Replied to message ${inReplyTo} with message ${reply.id}, a follow-up threaded ` + `under your reply ${follows}. ${readBack}`,
|
|
34211
|
+
details: {
|
|
34212
|
+
message: reply.id,
|
|
34213
|
+
in_reply_to: inReplyTo,
|
|
34214
|
+
posted: true,
|
|
34215
|
+
...follows === undefined ? {} : { follows }
|
|
34216
|
+
}
|
|
34174
34217
|
};
|
|
34175
34218
|
}
|
|
34176
34219
|
const issueKey = issue2();
|
|
@@ -34342,6 +34385,21 @@ ${trailer.join(`
|
|
|
34342
34385
|
};
|
|
34343
34386
|
}
|
|
34344
34387
|
case "dispatch_read": {
|
|
34388
|
+
const message = optionalString(args, "message");
|
|
34389
|
+
if (message !== undefined) {
|
|
34390
|
+
const sessionId = input.sessionId?.trim();
|
|
34391
|
+
if (!sessionId)
|
|
34392
|
+
throw new Error("host session id is required for dispatch_read({message})");
|
|
34393
|
+
const thread = await client.getMessageThread(messageIdOf(message), sessionId);
|
|
34394
|
+
const issueKey2 = thread.message.issue_key;
|
|
34395
|
+
return {
|
|
34396
|
+
text: messageSummary(thread, issueKey2 === null ? [] : await graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))),
|
|
34397
|
+
details: {
|
|
34398
|
+
message: thread.message.id,
|
|
34399
|
+
...issueKey2 === null ? {} : { issue: issueKey2 }
|
|
34400
|
+
}
|
|
34401
|
+
};
|
|
34402
|
+
}
|
|
34345
34403
|
if (ownerArguments.ref?.kind === "ask") {
|
|
34346
34404
|
const ref = ownerArguments.ref;
|
|
34347
34405
|
const id = await resolveIdPrefix(input.tool, "ask", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveDocument(ref.owner, ref.artifact)).artifact.id, "all"));
|
package/dist/legion.js
CHANGED
|
@@ -30197,6 +30197,16 @@ var messageValidation = {
|
|
|
30197
30197
|
},
|
|
30198
30198
|
message: "issue is required unless in_reply_to names a message delivered to this session, which is the one message with no issue."
|
|
30199
30199
|
};
|
|
30200
|
+
var readOwner = documentOwnerValidation(true);
|
|
30201
|
+
var readValidation = {
|
|
30202
|
+
check: (value) => {
|
|
30203
|
+
const input = value;
|
|
30204
|
+
if (typeof input.message !== "string")
|
|
30205
|
+
return readOwner.check(value);
|
|
30206
|
+
return input.issue === undefined && input.project === undefined && input.artifact === undefined && input.ref === undefined;
|
|
30207
|
+
},
|
|
30208
|
+
message: `${readOwner.message} message stands alone: it names the conversation, so name no issue, project, artifact, or ref with it.`
|
|
30209
|
+
};
|
|
30200
30210
|
var ISSUE_COMPONENTS_MODES = ["inherit", "explicit", "none"];
|
|
30201
30211
|
function componentsArgument(z2) {
|
|
30202
30212
|
return z2.object({
|
|
@@ -30424,7 +30434,7 @@ var dispatchToolSpecs = [
|
|
|
30424
30434
|
{
|
|
30425
30435
|
name: "dispatch_message",
|
|
30426
30436
|
example: { issue: "DSP-1", body: "Implementation started." },
|
|
30427
|
-
description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation
|
|
30437
|
+
description: "Post a note humans must read now: a reply to a human's message or a deliverable that landed. A blocker only a human can " + "clear is an ask (dispatch_ask), so it lands in their inbox. Never progress or status updates - Dispatch is a high-signal " + "record, not a log. Not a decision (dispatch_ask) or document feedback (dispatch_comment). To answer a human's direct message to this session - " + "one sent from the Agents page, which names no issue - pass that message's bare id as in_reply_to and no issue; " + "the reply lands in that conversation. Another call with the same in_reply_to and new text posts a follow-up, " + "threaded under this session's first reply; the same text again posts nothing. dispatch_read({message}) reads " + "that conversation back. Every other message names its issue. " + `Body is at most 2,000 characters. ${ISSUE_REFERENCE}`,
|
|
30428
30438
|
arguments: (z2) => ({
|
|
30429
30439
|
issue: z2.string().describe(`${ISSUE_REFERENCE} Omit it only when in_reply_to answers a human's direct message to this session.`).optional(),
|
|
30430
30440
|
body: z2.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
|
|
@@ -30518,14 +30528,15 @@ var dispatchToolSpecs = [
|
|
|
30518
30528
|
{
|
|
30519
30529
|
name: "dispatch_read",
|
|
30520
30530
|
example: { issue: "DSP-1" },
|
|
30521
|
-
description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain. Do not use it for document
|
|
30531
|
+
description: "Read an issue or project-document summary, targeted ask, or targeted comment reply chain, or the conversation " + "a message belongs to. Do not use it for document contents; use dispatch_doc_read instead. Supply ref, issue, " + "or project plus artifact; or message alone, which reads a human's direct message to this session and every " + "reply to it (they belong to no issue). " + "Every read ends with `Referenced by:` (what cites or hangs off this node, each with its dispatch:// address, " + "an excerpt, and when) and `Links:` (what it cites), so tracing provenance is one call. " + OWNER_REFERENCE,
|
|
30522
30532
|
arguments: (z2) => ({
|
|
30523
30533
|
issue: z2.string().describe(ISSUE_REFERENCE).optional(),
|
|
30524
30534
|
project: z2.string().describe("Project key owning the document.").optional(),
|
|
30525
30535
|
artifact: z2.string().describe("Project document artifact id, slug, or filename.").optional(),
|
|
30526
|
-
ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional()
|
|
30536
|
+
ref: z2.string().describe("Optional dispatch:// issue or document reference.").optional(),
|
|
30537
|
+
message: z2.string().describe("A message id (uuid): reads the conversation it belongs to, the root message and every " + "reply. Name nothing else with it.").optional()
|
|
30527
30538
|
}),
|
|
30528
|
-
validation:
|
|
30539
|
+
validation: readValidation
|
|
30529
30540
|
},
|
|
30530
30541
|
{
|
|
30531
30542
|
name: "dispatch_search",
|
|
@@ -30769,6 +30780,7 @@ function controllerToken(project) {
|
|
|
30769
30780
|
|
|
30770
30781
|
// ../contracts/src/legion-daemon-api.ts
|
|
30771
30782
|
var nonEmptyString = exports_external.string().min(1);
|
|
30783
|
+
var appLogin = exports_external.string().regex(/^[^[\]]+\[bot\]$/);
|
|
30772
30784
|
var legionRole = exports_external.enum(LEGION_ROLES);
|
|
30773
30785
|
var requiredUnknown = exports_external.unknown().refine((value) => value !== undefined, {
|
|
30774
30786
|
message: "Required"
|
|
@@ -31022,7 +31034,11 @@ var LegionDaemonApi = {
|
|
|
31022
31034
|
},
|
|
31023
31035
|
GitHubToken: {
|
|
31024
31036
|
request: exports_external.strictObject({ grantId: nonEmptyString }),
|
|
31025
|
-
response: exports_external.object({
|
|
31037
|
+
response: exports_external.object({
|
|
31038
|
+
token: nonEmptyString,
|
|
31039
|
+
appLogin: exports_external.string().endsWith("[bot]"),
|
|
31040
|
+
legionAppLogins: exports_external.object({ implement: appLogin, review: appLogin }).optional()
|
|
31041
|
+
})
|
|
31026
31042
|
},
|
|
31027
31043
|
GitCredential: {
|
|
31028
31044
|
request: exports_external.strictObject({ grantId: nonEmptyString })
|
|
@@ -31354,8 +31370,8 @@ class DispatchClient {
|
|
|
31354
31370
|
async message(issue2, input) {
|
|
31355
31371
|
return this.#json("POST", ["api", "v1", "issues", await this.#resolveIssue(issue2), "messages"], input);
|
|
31356
31372
|
}
|
|
31357
|
-
async messageReply(id, input) {
|
|
31358
|
-
return this.#json("POST", ["api", "v1", "messages", id, "reply"], input);
|
|
31373
|
+
async messageReply(id, input, options = {}) {
|
|
31374
|
+
return this.#json("POST", ["api", "v1", "messages", id, "reply"], input, options.followUp === true ? { follow_up: "true" } : undefined);
|
|
31359
31375
|
}
|
|
31360
31376
|
async getMessage(issue2, id) {
|
|
31361
31377
|
return this.#json("GET", [
|
|
@@ -31367,6 +31383,9 @@ class DispatchClient {
|
|
|
31367
31383
|
id
|
|
31368
31384
|
]);
|
|
31369
31385
|
}
|
|
31386
|
+
async getMessageThread(id, session) {
|
|
31387
|
+
return this.#json("GET", ["api", "v1", "messages", id], undefined, { session });
|
|
31388
|
+
}
|
|
31370
31389
|
async artifact(issue2, input) {
|
|
31371
31390
|
const artifactPath = ["api", "v1", "issues", await this.#resolveIssue(issue2), "artifacts"];
|
|
31372
31391
|
if ("content" in input)
|
|
@@ -32041,6 +32060,13 @@ function argumentProblems(tool, args) {
|
|
|
32041
32060
|
}
|
|
32042
32061
|
break;
|
|
32043
32062
|
}
|
|
32063
|
+
case "dispatch_read": {
|
|
32064
|
+
const message = optionalString(args, "message");
|
|
32065
|
+
if (message !== undefined && messageIdOf(message) === undefined) {
|
|
32066
|
+
problems.push("message must be a full message id (uuid) or a dispatch://KEY/message/<id> reference");
|
|
32067
|
+
}
|
|
32068
|
+
break;
|
|
32069
|
+
}
|
|
32044
32070
|
}
|
|
32045
32071
|
return problems;
|
|
32046
32072
|
}
|
|
@@ -32178,6 +32204,9 @@ async function resolveOwnerArguments(tool, input, cwd, env, exec, serverUrl, pro
|
|
|
32178
32204
|
if (tool === "dispatch_message" && typeof replyTarget === "string" && !replyTarget.startsWith("dispatch://")) {
|
|
32179
32205
|
return { args, ref, owner: null };
|
|
32180
32206
|
}
|
|
32207
|
+
if (tool === "dispatch_read" && typeof args.message === "string") {
|
|
32208
|
+
return { args, ref, owner: null };
|
|
32209
|
+
}
|
|
32181
32210
|
const legionIssue = env.LEGION_ISSUE;
|
|
32182
32211
|
if (!legionIssue) {
|
|
32183
32212
|
problems.push(ownerRequiredProblem);
|
|
@@ -33100,16 +33129,30 @@ ${followsAsk(askOwner)}`,
|
|
|
33100
33129
|
const inReplyTo = messageInReplyTo(args);
|
|
33101
33130
|
const body = stringArg(args, "body");
|
|
33102
33131
|
if (owner === null && inReplyTo !== undefined) {
|
|
33103
|
-
const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor });
|
|
33132
|
+
const reply = await client.messageReply(inReplyTo, { body, attempt: 1, actor }, { followUp: true });
|
|
33133
|
+
if (reply.duplicate === true && reply.body === body) {
|
|
33134
|
+
return {
|
|
33135
|
+
text: `Dispatch already has this exact text in the conversation (message ${reply.id}); ` + "nothing new was posted. Send different text if you have more to say.",
|
|
33136
|
+
details: { message: reply.id, in_reply_to: inReplyTo, posted: false, duplicate: true }
|
|
33137
|
+
};
|
|
33138
|
+
}
|
|
33104
33139
|
if (reply.body !== body) {
|
|
33105
33140
|
return {
|
|
33106
33141
|
text: `Message ${inReplyTo} was already answered by message ${reply.id}; Dispatch kept ` + "that reply and posted nothing. Wait for their next message rather than answering " + "this one again.",
|
|
33107
33142
|
details: { message: reply.id, in_reply_to: inReplyTo, posted: false }
|
|
33108
33143
|
};
|
|
33109
33144
|
}
|
|
33145
|
+
const readBack = `dispatch_read({message: "${inReplyTo}"}) reads the conversation back.`;
|
|
33146
|
+
const parent = reply.in_reply_to ?? undefined;
|
|
33147
|
+
const follows = parent === inReplyTo ? undefined : parent;
|
|
33110
33148
|
return {
|
|
33111
|
-
text: `Replied to message ${inReplyTo} with message ${reply.id}`,
|
|
33112
|
-
details: {
|
|
33149
|
+
text: follows === undefined ? `Replied to message ${inReplyTo} with message ${reply.id}. ${readBack}` : `Replied to message ${inReplyTo} with message ${reply.id}, a follow-up threaded ` + `under your reply ${follows}. ${readBack}`,
|
|
33150
|
+
details: {
|
|
33151
|
+
message: reply.id,
|
|
33152
|
+
in_reply_to: inReplyTo,
|
|
33153
|
+
posted: true,
|
|
33154
|
+
...follows === undefined ? {} : { follows }
|
|
33155
|
+
}
|
|
33113
33156
|
};
|
|
33114
33157
|
}
|
|
33115
33158
|
const issueKey = issue2();
|
|
@@ -33281,6 +33324,21 @@ ${trailer.join(`
|
|
|
33281
33324
|
};
|
|
33282
33325
|
}
|
|
33283
33326
|
case "dispatch_read": {
|
|
33327
|
+
const message = optionalString(args, "message");
|
|
33328
|
+
if (message !== undefined) {
|
|
33329
|
+
const sessionId = input.sessionId?.trim();
|
|
33330
|
+
if (!sessionId)
|
|
33331
|
+
throw new Error("host session id is required for dispatch_read({message})");
|
|
33332
|
+
const thread = await client.getMessageThread(messageIdOf(message), sessionId);
|
|
33333
|
+
const issueKey2 = thread.message.issue_key;
|
|
33334
|
+
return {
|
|
33335
|
+
text: messageSummary(thread, issueKey2 === null ? [] : await graphSections(client, dispatchChildRef(dispatchIssueRef(issueKey2), "message", thread.message.id))),
|
|
33336
|
+
details: {
|
|
33337
|
+
message: thread.message.id,
|
|
33338
|
+
...issueKey2 === null ? {} : { issue: issueKey2 }
|
|
33339
|
+
}
|
|
33340
|
+
};
|
|
33341
|
+
}
|
|
33284
33342
|
if (ownerArguments.ref?.kind === "ask") {
|
|
33285
33343
|
const ref = ownerArguments.ref;
|
|
33286
33344
|
const id = await resolveIdPrefix(input.tool, "ask", ref.id, refOwnerName(ref), async () => ref.owner.kind === "issue" ? client.listIssueAsks(ref.owner.issue) : client.getArtifactAsks((await resolveDocument(ref.owner, ref.artifact)).artifact.id, "all"));
|
|
@@ -33415,7 +33473,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
33415
33473
|
// package.json
|
|
33416
33474
|
var package_default = {
|
|
33417
33475
|
name: "@sjawhar/pi-legion-envoy",
|
|
33418
|
-
version: "5.16.
|
|
33476
|
+
version: "5.16.9",
|
|
33419
33477
|
type: "module",
|
|
33420
33478
|
omp: {
|
|
33421
33479
|
extensions: [
|
|
@@ -33428,7 +33486,7 @@ var package_default = {
|
|
|
33428
33486
|
},
|
|
33429
33487
|
legion: {
|
|
33430
33488
|
daemonApiVersion: 9,
|
|
33431
|
-
goDaemonApiVersion:
|
|
33489
|
+
goDaemonApiVersion: 11
|
|
33432
33490
|
},
|
|
33433
33491
|
repository: {
|
|
33434
33492
|
type: "git",
|
|
@@ -33990,9 +34048,11 @@ var LegionGoGrantResponse = exports_external.strictObject({
|
|
|
33990
34048
|
grantId: nonEmptyString2,
|
|
33991
34049
|
expiresAt: timestamp
|
|
33992
34050
|
});
|
|
34051
|
+
var appLogin2 = exports_external.string().regex(/^[^[\]]+\[bot\]$/);
|
|
33993
34052
|
var LegionGoGitHubTokenResponse = exports_external.strictObject({
|
|
33994
34053
|
token: nonEmptyString2,
|
|
33995
|
-
appLogin: exports_external.string().endsWith("[bot]")
|
|
34054
|
+
appLogin: exports_external.string().endsWith("[bot]"),
|
|
34055
|
+
legionAppLogins: exports_external.strictObject({ implement: appLogin2, review: appLogin2 }).optional()
|
|
33996
34056
|
});
|
|
33997
34057
|
var LegionGoGitCredentialResponse = exports_external.strictObject({
|
|
33998
34058
|
username: exports_external.literal("x-access-token"),
|
|
@@ -894,15 +894,23 @@ dispatch_message({
|
|
|
894
894
|
|
|
895
895
|
Leave `issue` out — there is no issue to post into, and naming one would file your answer on
|
|
896
896
|
unrelated work. Dispatch threads the reply under their message in the same conversation, and the
|
|
897
|
-
human sees it
|
|
897
|
+
human sees it in your conversation on the Agents page, where it shows as an unread reply until
|
|
898
|
+
they read it. Every other message still names its issue, so keep the `issue`
|
|
898
899
|
the frame gave you whenever it gave you one; a `dispatch://KEY/message/<id>` reference names the
|
|
899
900
|
issue its message lives on, so that form is a reply on that issue, not a direct message.
|
|
900
901
|
|
|
901
|
-
|
|
902
|
-
Dispatch
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
902
|
+
Have more to say after you answered? Call it again with the same `in_reply_to` and the new text:
|
|
903
|
+
Dispatch threads that follow-up under your first reply, and the tool result names the reply it
|
|
904
|
+
follows. The same text again posts nothing, so a retry is safe. Your host may already have
|
|
905
|
+
answered a **BTW** automatically before you got here; a second call is then your follow-up to
|
|
906
|
+
that answer, so read the result before writing again.
|
|
907
|
+
|
|
908
|
+
Read the whole conversation back — their message and every reply, yours included — with the
|
|
909
|
+
message id alone; it has no issue:
|
|
910
|
+
|
|
911
|
+
```ts
|
|
912
|
+
dispatch_read({ message: "<the direct message's id, or any reply's>" })
|
|
913
|
+
```
|
|
906
914
|
|
|
907
915
|
## Following
|
|
908
916
|
|
|
@@ -188,11 +188,17 @@ reviewer, or an architect (in Legion's own deployment, `legion-implementer[bot]`
|
|
|
188
188
|
check
|
|
189
189
|
`jj -R "$LEGION_WORKSPACE" log -r 'main@origin..@' -T 'author.email() ++ " | " ++ committer.email() ++ " " ++ description.first_line() ++ "\n"'`
|
|
190
190
|
shows your role's App in both columns **on every commit you made** — not on the whole list:
|
|
191
|
-
earlier phases' commits are legitimately authored by their own role's App
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
191
|
+
earlier phases' commits are legitimately authored by their own role's App. Their *committer* is
|
|
192
|
+
a different matter, and no longer noise to accept. Resolving a conflict rewrites nothing now —
|
|
193
|
+
it is the forward merge above — so it changes no committer at all, and the one rewrite still
|
|
194
|
+
open to you (*Rewriting pushed commits*, below) resets the committer only of commits on your own
|
|
195
|
+
chain that descend from the commit you named, after its guard cleared. Another role's commit
|
|
196
|
+
carrying you as committer, which you did not rewrite that way, is evidence that something
|
|
197
|
+
rewrote commits it should not have — the observable symptom of LEGION-118. Stop and send the
|
|
198
|
+
architect that log; do not accept it as a side effect. A wrong identity on your own commit, the
|
|
199
|
+
other App or none, is a pane-environment problem to report to the architect, not something to
|
|
200
|
+
pin (`docs/solutions/legion/shared-main-repo-hazards-for-concurrent-issue-workspaces.md`,
|
|
201
|
+
Hazard 1).
|
|
196
202
|
Your session receives the credential capability it needs; invoke GitHub through the
|
|
197
203
|
credential helper:
|
|
198
204
|
|
|
@@ -285,9 +291,11 @@ later phase keeps it current rather than replacing it:
|
|
|
285
291
|
- Thread <id>: fixed in <commit-sha> — <one line>.
|
|
286
292
|
- Thread <id>: not a defect — <reason>.
|
|
287
293
|
`legion threads resolve --pr <n> --repo <owner>/<repo>` at <head-sha>:
|
|
288
|
-
resolved <thread URL>
|
|
294
|
+
resolved <thread URL> — its opener's acceptance
|
|
295
|
+
resolved <thread URL> — the Legion reviewer's acceptance of a bot's thread
|
|
289
296
|
left open <thread URL> — newest reply by <login> is not an acceptance
|
|
290
297
|
left open <thread URL> — newest reply by <login> is an unsubmitted draft in a pending review
|
|
298
|
+
left open <thread URL> — newest reply by <login> is not its opener's or the Legion reviewer's acceptance
|
|
291
299
|
|
|
292
300
|
**Thermo:** `ce-simplify-code` once at <head-sha>: <0 applied | applied → new head <sha>>; thermonuclear pair at the final head <sha>:
|
|
293
301
|
<verdict>. (omitted entirely on a docs-only PR — there is no code for either pass, so neither runs)
|
|
@@ -329,7 +337,8 @@ this proof.
|
|
|
329
337
|
|
|
330
338
|
- **Threads are dispositioned individually, never resolved in bulk.** Every open review
|
|
331
339
|
thread gets its own line naming the fixing commit or the reason it isn't a defect. The
|
|
332
|
-
reviewer answers each thread it opened
|
|
340
|
+
reviewer answers each thread it opened, and each thread a bot opened that is none of Legion's
|
|
341
|
+
role Apps, with exactly one of `Accepted: fixed in <commit> — <one line>`,
|
|
333
342
|
`Accepted: not a defect — <reason>`, or `Still open: <what remains>`; nothing else is an
|
|
334
343
|
acceptance, and nobody replies after an `Accepted:` (any later reply that is not itself an
|
|
335
344
|
`Accepted:` — the opener's own follow-up included — leaves the thread open, because resolution
|
|
@@ -341,9 +350,16 @@ this proof.
|
|
|
341
350
|
In a Legion pane, the **implementer** runs the command before every push that answers a review
|
|
342
351
|
(the corrective push and the final `.legion/` deletion push) and pastes its output into the
|
|
343
352
|
`Threads` section. The command resolves each unresolved thread whose newest submitted comment is
|
|
344
|
-
the opener's own `Accepted:` reply
|
|
345
|
-
|
|
346
|
-
|
|
353
|
+
the opener's own `Accepted:` reply. On a thread a bot account opened that is none of Legion's
|
|
354
|
+
role Apps (the daemon names them, keyed by App role), the Legion reviewer's `Accepted:` also
|
|
355
|
+
closes it. GitHub cannot tell a CI bot, which never accepts, from a person whose `gh` is routed
|
|
356
|
+
to an App, so the reviewer adjudicates such a finding, and it may accept one an App-routed person
|
|
357
|
+
raised. The subject of a finding never closes it: the implementer's `Fixed in <commit>: …` or
|
|
358
|
+
`Declined: …` answers a thread and closes none. A thread either Legion App opened, a reviewer's
|
|
359
|
+
finding included, still needs its opener's `Accepted:`. It makes one `resolveReviewThread` per
|
|
360
|
+
thread, prints `resolved <url> — <whose acceptance>` (its opener's, or the Legion reviewer's on a
|
|
361
|
+
bot's thread, so the ledger shows which) or `left open <url> — newest reply by <login> is …`
|
|
362
|
+
naming why, and exits 1 naming the thread's URL and GitHub's message when GitHub refuses one.
|
|
347
363
|
|
|
348
364
|
Without a grant, page through `reviewThreads`, skip `isResolved: true`, and compare the opener
|
|
349
365
|
with the newest comment. Query shape, inside `repository { pullRequest { … } }`:
|
|
@@ -353,15 +369,18 @@ this proof.
|
|
|
353
369
|
pageInfo { hasNextPage endCursor }
|
|
354
370
|
nodes {
|
|
355
371
|
id isResolved
|
|
356
|
-
opener: comments(first: 1) { nodes { author { login } } }
|
|
357
|
-
newest: comments(last: 1) { nodes { author { login } body state } }
|
|
372
|
+
opener: comments(first: 1) { nodes { author { __typename login } } }
|
|
373
|
+
newest: comments(last: 1) { nodes { author { __typename login } body state } }
|
|
358
374
|
}
|
|
359
375
|
}
|
|
360
376
|
```
|
|
361
377
|
|
|
362
|
-
Resolve only when the newest comment is submitted, its `author
|
|
363
|
-
and
|
|
364
|
-
|
|
378
|
+
Resolve only when the newest comment is submitted, its `author` is the opener's account (the same
|
|
379
|
+
`__typename` and `login`: a login alone is a string anyone may register), and its `body`, after
|
|
380
|
+
removing leading spaces, tabs, CR, and LF, begins `Accepted:`. Without a
|
|
381
|
+
grant nothing names Legion's own App logins, so this route closes a bot's thread only on its
|
|
382
|
+
opener's `Accepted:`: leave one the Legion reviewer accepted for the implementer's or merger's
|
|
383
|
+
run in a pane, or report it. For each thread to resolve:
|
|
365
384
|
|
|
366
385
|
```graphql
|
|
367
386
|
mutation($threadId: ID!) {
|
|
@@ -377,14 +396,17 @@ this proof.
|
|
|
377
396
|
changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
|
|
378
397
|
Findings about naming, duplication, or wording are batched into the single `Fast-follow`
|
|
379
398
|
line instead of iterating per push.
|
|
380
|
-
- **
|
|
399
|
+
- **Reintegrate the base only on a real conflict, except after a base retarget — and with a
|
|
400
|
+
merge, never `jj rebase`.** Sami, 2026-09-11, verbatim:
|
|
381
401
|
"Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
|
|
382
|
-
The implementer
|
|
383
|
-
because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never
|
|
402
|
+
The implementer merges the base into the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
|
|
403
|
+
because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never reintegrate the base to
|
|
384
404
|
pick up `main` or refresh CI. A single failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
|
|
385
405
|
pushing a new commit. A conflict-forced rebase that leaves the branch's diff unchanged is a
|
|
386
|
-
confirmation, not a new round (see *The unchanged-diff check* below)
|
|
387
|
-
|
|
406
|
+
confirmation, not a new round (see *The unchanged-diff check* below); that name is the event's,
|
|
407
|
+
kept by the rules below and the learnings that cite it, and the operation it names is always
|
|
408
|
+
the merge here. Before merging, record
|
|
409
|
+
the fingerprint at the current tip; after pushing the merged branch, record it at the new
|
|
388
410
|
tip; post one PR comment (Legion footer):
|
|
389
411
|
`rebase <old-tip-sha> → <new-tip-sha>; fingerprint <before> → <after>; unchanged|changed`.
|
|
390
412
|
Every issue workspace is a `jj workspace` of the same shared repository and operation log, and
|
|
@@ -633,12 +655,19 @@ remote branch sideways onto your commit and drops theirs (jj 0.45.1:
|
|
|
633
655
|
push is refused by jj itself (`unexpectedly moved on the remote`).
|
|
634
656
|
|
|
635
657
|
**Rewriting pushed commits** — a `jj squash --into` a commit already on GitHub, or any other
|
|
636
|
-
rewrite of a commit you already pushed —
|
|
637
|
-
|
|
658
|
+
rewrite of a commit you already pushed — is the LEGION-118 hazard in a second shape: jj rebases
|
|
659
|
+
every descendant of any commit it rewrites, and in the one shared repository a descendant can be
|
|
660
|
+
another tree's branch stacked on your pushed commit, which then moves, with its bookmark, onto a
|
|
661
|
+
rewritten copy. So look for a descendant outside your own chain first, and record the pushed tip
|
|
662
|
+
— which the rewrite leaves outside `::@-` — after a fetch and while your chain still descends
|
|
663
|
+
from it:
|
|
638
664
|
|
|
639
665
|
```bash
|
|
640
666
|
cd -- "$LEGION_WORKSPACE" && \
|
|
641
667
|
jj -R "$LEGION_WORKSPACE" git fetch && \
|
|
668
|
+
foreign=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
|
|
669
|
+
-r 'descendants(<the commit you are about to rewrite>) ~ ::@') && \
|
|
670
|
+
{ [ -z "$foreign" ] || { echo "not mine, and descends from the commit to rewrite: $foreign" >&2; false; }; } && \
|
|
642
671
|
behind=$(jj -R "$LEGION_WORKSPACE" log --no-graph -T 'commit_id.short() ++ "\n"' \
|
|
643
672
|
-r 'remote_bookmarks(exact:"legion/<KEY>", exact:"origin") ~ ::@-') && \
|
|
644
673
|
{ [ -z "$behind" ] || { echo "legion/<KEY>@origin is at $behind, which @- does not descend from" >&2; false; }; } && \
|
|
@@ -647,6 +676,13 @@ cd -- "$LEGION_WORKSPACE" && \
|
|
|
647
676
|
>"${TMPDIR:-/tmp}/legion-<KEY>-$LEGION_ROLE-rewritten-tip"
|
|
648
677
|
```
|
|
649
678
|
|
|
679
|
+
`descendants(<commit>) ~ ::@` is everything built on the commit you are about to rewrite that is
|
|
680
|
+
not on your own chain. Non-empty means the rewrite would move work that is not yours: do not
|
|
681
|
+
rewrite it. Put the change in a new commit on top instead, and report the listed commits to the
|
|
682
|
+
architect. On a two-workspace rig of this shape a `jj squash --into` a pushed commit reported
|
|
683
|
+
`Rebased 13 descendant commits` and moved a second issue's twelve commits and its bookmark; the
|
|
684
|
+
check above listed those thirteen and refused before anything moved.
|
|
685
|
+
|
|
650
686
|
Then rewrite, resolve, and push with the procedure above. It lets the remote branch sit on the
|
|
651
687
|
tip you recorded, which the rewrite replaced, and on nothing else: when another role pushed after
|
|
652
688
|
you recorded it, the push is refused. The push deletes the file.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/pi-legion-envoy",
|
|
3
|
-
"version": "5.16.
|
|
3
|
+
"version": "5.16.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"omp": {
|
|
6
6
|
"extensions": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"legion": {
|
|
15
15
|
"daemonApiVersion": 9,
|
|
16
|
-
"goDaemonApiVersion":
|
|
16
|
+
"goDaemonApiVersion": 11
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"type": "git",
|