@sjawhar/pi-legion-envoy 1.8.1 → 1.10.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/dist/envoy.js +160 -15
- package/dist/legion.js +160 -15
- package/dist/skills/AGENTS.md +6 -6
- package/dist/skills/dispatch/SKILL.md +32 -0
- package/dist/skills/legion-architect/SKILL.md +0 -5
- package/dist/skills/legion-controller/SKILL.md +0 -4
- package/dist/skills/legion-oracle/SKILL.md +14 -9
- package/dist/skills/legion-worker/SKILL.md +50 -15
- package/package.json +1 -1
- package/dist/skills/github/SKILL.md +0 -208
- package/dist/skills/linear/SKILL.md +0 -76
package/dist/envoy.js
CHANGED
|
@@ -29730,10 +29730,20 @@ var CommentEventPayloadSchema = object({
|
|
|
29730
29730
|
var MessageEventPayloadSchema = object({
|
|
29731
29731
|
id: string2().optional(),
|
|
29732
29732
|
body: string2().optional(),
|
|
29733
|
-
|
|
29733
|
+
target: string2().nullish(),
|
|
29734
|
+
in_reply_to: string2().nullish(),
|
|
29734
29735
|
reply_body: string2().optional(),
|
|
29735
29736
|
author: object({ kind: string2(), id: string2() }).optional()
|
|
29736
29737
|
});
|
|
29738
|
+
var MessageDeliveryEventPayloadSchema = object({
|
|
29739
|
+
message_id: string2().optional(),
|
|
29740
|
+
attempt: number2().int().positive().optional(),
|
|
29741
|
+
delivery: _enum2(["btw", "aside", "steer"]).optional(),
|
|
29742
|
+
session_id: string2().optional(),
|
|
29743
|
+
title: string2().optional(),
|
|
29744
|
+
state: _enum2(["sent", "failed"]).optional(),
|
|
29745
|
+
error: string2().optional()
|
|
29746
|
+
});
|
|
29737
29747
|
var ChildStatusEventPayloadSchema = object({
|
|
29738
29748
|
child_key: string2().optional(),
|
|
29739
29749
|
from: string2().optional(),
|
|
@@ -29932,7 +29942,7 @@ var dispatchToolSpecs = [
|
|
|
29932
29942
|
arguments: (z) => ({
|
|
29933
29943
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
29934
29944
|
body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
|
|
29935
|
-
|
|
29945
|
+
in_reply_to: z.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation.").optional()
|
|
29936
29946
|
})
|
|
29937
29947
|
},
|
|
29938
29948
|
{
|
|
@@ -31008,6 +31018,31 @@ var TolerantInboundEnvelopeSchema = object({
|
|
|
31008
31018
|
source: InboundEnvelopeSchema.shape.source.optional().catch(undefined),
|
|
31009
31019
|
sender: object(tolerantShape(InboundSenderSchema.shape)).optional().catch(undefined)
|
|
31010
31020
|
}).passthrough();
|
|
31021
|
+
var DispatchDeliveryRequestSchema = object({
|
|
31022
|
+
attempt: number2().int().positive(),
|
|
31023
|
+
mode: _enum2(["btw", "aside", "steer"])
|
|
31024
|
+
});
|
|
31025
|
+
var DispatchTargetedMessagePayloadSchema = object({
|
|
31026
|
+
id: string2(),
|
|
31027
|
+
issue_key: string2(),
|
|
31028
|
+
author: object({ kind: string2(), id: string2() }),
|
|
31029
|
+
body: string2(),
|
|
31030
|
+
target: string2(),
|
|
31031
|
+
in_reply_to: string2().nullable(),
|
|
31032
|
+
deliveries: array(unknown()),
|
|
31033
|
+
created_at: string2()
|
|
31034
|
+
});
|
|
31035
|
+
var DispatchTargetedFrameSchema = object({
|
|
31036
|
+
event: DispatchEventSchema,
|
|
31037
|
+
delivery: DispatchDeliveryRequestSchema
|
|
31038
|
+
}).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
|
|
31039
|
+
var RecoverableDispatchDeliveryFailureSchema = object({
|
|
31040
|
+
event: object({
|
|
31041
|
+
issue_key: string2(),
|
|
31042
|
+
payload: object({ id: string2(), body: string2().optional() }).passthrough()
|
|
31043
|
+
}).passthrough(),
|
|
31044
|
+
delivery: DispatchDeliveryRequestSchema
|
|
31045
|
+
});
|
|
31011
31046
|
function senderLabel(envelope) {
|
|
31012
31047
|
const sender = envelope.source_session ?? envelope.source ?? "unknown";
|
|
31013
31048
|
return envelope.sender?.title === undefined ? sender : `${sender} (${envelope.sender.title})`;
|
|
@@ -31034,6 +31069,8 @@ var DISPATCH_PAYLOAD_SCHEMAS = {
|
|
|
31034
31069
|
"suggestion.accepted": CommentEventPayloadSchema,
|
|
31035
31070
|
"suggestion.rejected": CommentEventPayloadSchema,
|
|
31036
31071
|
"message.created": MessageEventPayloadSchema,
|
|
31072
|
+
"message.delivery": MessageDeliveryEventPayloadSchema,
|
|
31073
|
+
"message.answered": MessageEventPayloadSchema,
|
|
31037
31074
|
"child.status": ChildStatusEventPayloadSchema
|
|
31038
31075
|
};
|
|
31039
31076
|
function dispatchOwner(event, topic) {
|
|
@@ -31066,6 +31103,9 @@ function dispatchMessageReplyPreview(event) {
|
|
|
31066
31103
|
const parsed = MessageEventPayloadSchema.safeParse(event.payload);
|
|
31067
31104
|
return parsed.success && parsed.data.reply_body !== undefined && parsed.data.reply_body !== "" ? parsed.data.reply_body : undefined;
|
|
31068
31105
|
}
|
|
31106
|
+
function isObject2(value) {
|
|
31107
|
+
return typeof value === "object" && value !== null;
|
|
31108
|
+
}
|
|
31069
31109
|
function parseDispatchFrame(rawPayload) {
|
|
31070
31110
|
let value;
|
|
31071
31111
|
try {
|
|
@@ -31073,11 +31113,41 @@ function parseDispatchFrame(rawPayload) {
|
|
|
31073
31113
|
} catch {
|
|
31074
31114
|
return { raw: rawPayload };
|
|
31075
31115
|
}
|
|
31116
|
+
const targeted = DispatchTargetedFrameSchema.safeParse(value);
|
|
31117
|
+
if (targeted.success) {
|
|
31118
|
+
const wireFrame = value;
|
|
31119
|
+
return {
|
|
31120
|
+
event: targeted.data.event,
|
|
31121
|
+
notify: wireFrame.event.notify,
|
|
31122
|
+
delivery: targeted.data.delivery
|
|
31123
|
+
};
|
|
31124
|
+
}
|
|
31125
|
+
if (isObject2(value) && "delivery" in value) {
|
|
31126
|
+
const recoverable = RecoverableDispatchDeliveryFailureSchema.safeParse(value);
|
|
31127
|
+
if (recoverable.success) {
|
|
31128
|
+
return {
|
|
31129
|
+
raw: value,
|
|
31130
|
+
rejectedDelivery: {
|
|
31131
|
+
attempt: recoverable.data.delivery.attempt,
|
|
31132
|
+
mode: recoverable.data.delivery.mode,
|
|
31133
|
+
messageID: recoverable.data.event.payload.id,
|
|
31134
|
+
issueKey: recoverable.data.event.issue_key,
|
|
31135
|
+
body: recoverable.data.event.payload.body ?? ""
|
|
31136
|
+
},
|
|
31137
|
+
malformedDelivery: true
|
|
31138
|
+
};
|
|
31139
|
+
}
|
|
31140
|
+
return { raw: value, malformedDelivery: true };
|
|
31141
|
+
}
|
|
31076
31142
|
const parsed = DispatchEventSchema.safeParse(value);
|
|
31077
31143
|
if (!parsed.success)
|
|
31078
31144
|
return { raw: value };
|
|
31079
31145
|
const wireEvent = value;
|
|
31080
|
-
return {
|
|
31146
|
+
return {
|
|
31147
|
+
event: parsed.data,
|
|
31148
|
+
notify: wireEvent.notify,
|
|
31149
|
+
delivery: undefined
|
|
31150
|
+
};
|
|
31081
31151
|
}
|
|
31082
31152
|
function inboundTimestamp(milliseconds) {
|
|
31083
31153
|
if (milliseconds === undefined)
|
|
@@ -31115,6 +31185,10 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31115
31185
|
}
|
|
31116
31186
|
let dispatchEvent;
|
|
31117
31187
|
let dispatchIssue;
|
|
31188
|
+
let dispatchReply;
|
|
31189
|
+
let delivery;
|
|
31190
|
+
let rejectedDelivery;
|
|
31191
|
+
let malformedDelivery = false;
|
|
31118
31192
|
let askQuestion;
|
|
31119
31193
|
let messageReplyPreview;
|
|
31120
31194
|
const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
|
|
@@ -31144,6 +31218,20 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31144
31218
|
}
|
|
31145
31219
|
askQuestion = dispatchAskQuestion(frame.event);
|
|
31146
31220
|
messageReplyPreview = dispatchMessageReplyPreview(frame.event);
|
|
31221
|
+
if (frame.event.type === "message.created" && frame.event.issue_key !== null) {
|
|
31222
|
+
const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
|
|
31223
|
+
const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
|
|
31224
|
+
if (message.success && requested.success && message.data.id !== undefined) {
|
|
31225
|
+
delivery = {
|
|
31226
|
+
attempt: requested.data.attempt,
|
|
31227
|
+
mode: requested.data.mode,
|
|
31228
|
+
messageID: message.data.id,
|
|
31229
|
+
issueKey: frame.event.issue_key,
|
|
31230
|
+
body: message.data.body ?? envelope.payload_summary ?? ""
|
|
31231
|
+
};
|
|
31232
|
+
dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
|
|
31233
|
+
}
|
|
31234
|
+
}
|
|
31147
31235
|
dispatchEvent = {
|
|
31148
31236
|
owner: dispatchOwner(frame.event, subject ?? envelope.topic),
|
|
31149
31237
|
...frame.event.issue_key === null ? {
|
|
@@ -31158,6 +31246,8 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31158
31246
|
};
|
|
31159
31247
|
} else {
|
|
31160
31248
|
dispatchEvent = frame.raw;
|
|
31249
|
+
rejectedDelivery = frame.rejectedDelivery;
|
|
31250
|
+
malformedDelivery = frame.malformedDelivery === true;
|
|
31161
31251
|
}
|
|
31162
31252
|
}
|
|
31163
31253
|
}
|
|
@@ -31182,7 +31272,7 @@ ${envelope.payload ?? ""}`;
|
|
|
31182
31272
|
}
|
|
31183
31273
|
}
|
|
31184
31274
|
const role = envelope.sender?.roles?.[0];
|
|
31185
|
-
const reply = replyWith(envelope);
|
|
31275
|
+
const reply = dispatchReply ?? replyWith(envelope);
|
|
31186
31276
|
const sourceIssue = parseIssues.includes("source") || KNOWN_SOURCES[envelope.source] !== undefined ? undefined : `source=${envelope.source}`;
|
|
31187
31277
|
const unrecognised = [
|
|
31188
31278
|
...parseIssues,
|
|
@@ -31212,7 +31302,14 @@ ${envelope.payload ?? ""}`;
|
|
|
31212
31302
|
},
|
|
31213
31303
|
...unrecognised === undefined ? {} : { unrecognised }
|
|
31214
31304
|
};
|
|
31215
|
-
return {
|
|
31305
|
+
return {
|
|
31306
|
+
skip: false,
|
|
31307
|
+
content: encode3({ envoy: rendered }),
|
|
31308
|
+
envelope,
|
|
31309
|
+
...delivery === undefined ? {} : { delivery },
|
|
31310
|
+
...rejectedDelivery === undefined ? {} : { rejectedDelivery },
|
|
31311
|
+
...malformedDelivery ? { malformedDelivery: true } : {}
|
|
31312
|
+
};
|
|
31216
31313
|
}
|
|
31217
31314
|
|
|
31218
31315
|
// ../envoy-client/src/dispatch-config.ts
|
|
@@ -31876,13 +31973,13 @@ function askId(args) {
|
|
|
31876
31973
|
}
|
|
31877
31974
|
return reference.id;
|
|
31878
31975
|
}
|
|
31879
|
-
function
|
|
31880
|
-
const
|
|
31881
|
-
if (
|
|
31882
|
-
return
|
|
31883
|
-
const reference = parseDispatchRef(
|
|
31976
|
+
function messageInReplyTo(args) {
|
|
31977
|
+
const inReplyTo = optionalString(args, "in_reply_to");
|
|
31978
|
+
if (inReplyTo === undefined || !inReplyTo.startsWith("dispatch://"))
|
|
31979
|
+
return inReplyTo;
|
|
31980
|
+
const reference = parseDispatchRef(inReplyTo);
|
|
31884
31981
|
if (reference?.kind !== "message") {
|
|
31885
|
-
throw new Error("
|
|
31982
|
+
throw new Error("in_reply_to must be a bare message id or a dispatch://.../message/<id> reference");
|
|
31886
31983
|
}
|
|
31887
31984
|
return reference.id;
|
|
31888
31985
|
}
|
|
@@ -32330,10 +32427,10 @@ async function executeDispatchTool(input) {
|
|
|
32330
32427
|
};
|
|
32331
32428
|
}
|
|
32332
32429
|
case "dispatch_message": {
|
|
32333
|
-
const
|
|
32430
|
+
const inReplyTo = messageInReplyTo(args);
|
|
32334
32431
|
const message = await client.message(issue(), {
|
|
32335
32432
|
body: stringArg(args, "body"),
|
|
32336
|
-
...
|
|
32433
|
+
...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
|
|
32337
32434
|
actor
|
|
32338
32435
|
});
|
|
32339
32436
|
const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
|
|
@@ -32708,6 +32805,7 @@ var SessionWireSchema = object({
|
|
|
32708
32805
|
title: string2(),
|
|
32709
32806
|
topics: array(string2()),
|
|
32710
32807
|
roles: array(string2()).optional(),
|
|
32808
|
+
capabilities: array(string2()).optional(),
|
|
32711
32809
|
self_subscribed: boolean2(),
|
|
32712
32810
|
last_seen: number2().int().optional()
|
|
32713
32811
|
});
|
|
@@ -32816,6 +32914,7 @@ function createEnvoyClient(config) {
|
|
|
32816
32914
|
topics: expandSubscriptionTopics(input.topics),
|
|
32817
32915
|
port: input.port,
|
|
32818
32916
|
title: input.title,
|
|
32917
|
+
...input.capabilities === undefined ? {} : { capabilities: input.capabilities },
|
|
32819
32918
|
driving: input.driving,
|
|
32820
32919
|
...input.selfSubscribed === undefined ? {} : { self_subscribed: input.selfSubscribed }
|
|
32821
32920
|
}))),
|
|
@@ -33050,6 +33149,26 @@ function envoyExtension(pi) {
|
|
|
33050
33149
|
});
|
|
33051
33150
|
return connection;
|
|
33052
33151
|
};
|
|
33152
|
+
const postDispatchReply = async (delivery, result) => {
|
|
33153
|
+
if (!dispatchConfig.enabled || dispatchConfig.url === null || dispatchConfig.token === null) {
|
|
33154
|
+
throw new Error("Dispatch reply endpoint is not configured");
|
|
33155
|
+
}
|
|
33156
|
+
const response = await fetch(`${dispatchConfig.url}/api/v1/messages/${delivery.messageID}/reply`, {
|
|
33157
|
+
method: "POST",
|
|
33158
|
+
headers: {
|
|
33159
|
+
Authorization: `Bearer ${dispatchConfig.token}`,
|
|
33160
|
+
"Content-Type": "application/json"
|
|
33161
|
+
},
|
|
33162
|
+
body: JSON.stringify({
|
|
33163
|
+
actor: { kind: "session", id: sessionID },
|
|
33164
|
+
attempt: delivery.attempt,
|
|
33165
|
+
...result
|
|
33166
|
+
})
|
|
33167
|
+
});
|
|
33168
|
+
if (!response.ok) {
|
|
33169
|
+
throw new Error(`Dispatch reply failed: ${response.status} ${await response.text()}`);
|
|
33170
|
+
}
|
|
33171
|
+
};
|
|
33053
33172
|
const deliver = async (subject, raw, reply) => {
|
|
33054
33173
|
const rendered = renderInbound(raw, sessionID, subject);
|
|
33055
33174
|
for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
|
|
@@ -33069,9 +33188,34 @@ function envoyExtension(pi) {
|
|
|
33069
33188
|
inbox.pop();
|
|
33070
33189
|
}
|
|
33071
33190
|
try {
|
|
33072
|
-
|
|
33191
|
+
if (rendered.rejectedDelivery !== undefined) {
|
|
33192
|
+
console.warn(`[envoy] rejecting malformed Dispatch targeted delivery ${rendered.rejectedDelivery.messageID}`);
|
|
33193
|
+
await postDispatchReply(rendered.rejectedDelivery, {
|
|
33194
|
+
error: "Invalid Dispatch targeted delivery frame"
|
|
33195
|
+
});
|
|
33196
|
+
} else if (rendered.malformedDelivery === true) {
|
|
33197
|
+
console.warn("[envoy] dropping malformed Dispatch targeted delivery without a reply address");
|
|
33198
|
+
} else if (rendered.delivery?.mode === "btw") {
|
|
33199
|
+
if (pi.askEphemeral === undefined) {
|
|
33200
|
+
await postDispatchReply(rendered.delivery, {
|
|
33201
|
+
error: "This OMP host does not support BTW delivery"
|
|
33202
|
+
});
|
|
33203
|
+
} else {
|
|
33204
|
+
try {
|
|
33205
|
+
const reply = await pi.askEphemeral({ prompt: rendered.delivery.body });
|
|
33206
|
+
await postDispatchReply(rendered.delivery, { body: reply.replyText });
|
|
33207
|
+
} catch (error) {
|
|
33208
|
+
await postDispatchReply(rendered.delivery, { error: messageFor(error) });
|
|
33209
|
+
}
|
|
33210
|
+
}
|
|
33211
|
+
} else {
|
|
33212
|
+
pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, {
|
|
33213
|
+
deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
|
|
33214
|
+
triggerTurn: true
|
|
33215
|
+
});
|
|
33216
|
+
}
|
|
33073
33217
|
} catch (error) {
|
|
33074
|
-
console.warn(`[envoy] failed to
|
|
33218
|
+
console.warn(`[envoy] failed to deliver envelope ${envelope?.event_id ?? "unknown"}`, error);
|
|
33075
33219
|
throw error;
|
|
33076
33220
|
}
|
|
33077
33221
|
if (dedupeKey !== undefined) {
|
|
@@ -33150,6 +33294,7 @@ function envoyExtension(pi) {
|
|
|
33150
33294
|
topics: [...new Set([agentSubject(sessionID), ...subscriptions.keys()])],
|
|
33151
33295
|
port: 0,
|
|
33152
33296
|
title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
|
|
33297
|
+
capabilities: typeof pi.askEphemeral === "function" ? ["aside", "btw"] : ["aside"],
|
|
33153
33298
|
driving: false,
|
|
33154
33299
|
selfSubscribed: true
|
|
33155
33300
|
});
|
package/dist/legion.js
CHANGED
|
@@ -29729,10 +29729,20 @@ var CommentEventPayloadSchema = object({
|
|
|
29729
29729
|
var MessageEventPayloadSchema = object({
|
|
29730
29730
|
id: string2().optional(),
|
|
29731
29731
|
body: string2().optional(),
|
|
29732
|
-
|
|
29732
|
+
target: string2().nullish(),
|
|
29733
|
+
in_reply_to: string2().nullish(),
|
|
29733
29734
|
reply_body: string2().optional(),
|
|
29734
29735
|
author: object({ kind: string2(), id: string2() }).optional()
|
|
29735
29736
|
});
|
|
29737
|
+
var MessageDeliveryEventPayloadSchema = object({
|
|
29738
|
+
message_id: string2().optional(),
|
|
29739
|
+
attempt: number2().int().positive().optional(),
|
|
29740
|
+
delivery: _enum2(["btw", "aside", "steer"]).optional(),
|
|
29741
|
+
session_id: string2().optional(),
|
|
29742
|
+
title: string2().optional(),
|
|
29743
|
+
state: _enum2(["sent", "failed"]).optional(),
|
|
29744
|
+
error: string2().optional()
|
|
29745
|
+
});
|
|
29736
29746
|
var ChildStatusEventPayloadSchema = object({
|
|
29737
29747
|
child_key: string2().optional(),
|
|
29738
29748
|
from: string2().optional(),
|
|
@@ -29931,7 +29941,7 @@ var dispatchToolSpecs = [
|
|
|
29931
29941
|
arguments: (z) => ({
|
|
29932
29942
|
issue: z.string().describe(ISSUE_REFERENCE),
|
|
29933
29943
|
body: z.string({ max: 2000 }).describe("Update text, at most 2,000 characters."),
|
|
29934
|
-
|
|
29944
|
+
in_reply_to: z.string().describe("Optional message id or dispatch://KEY/message/<id> reference to reply to, threading " + "this message under it so the reply stays with the original in the Conversation.").optional()
|
|
29935
29945
|
})
|
|
29936
29946
|
},
|
|
29937
29947
|
{
|
|
@@ -31456,6 +31466,31 @@ var TolerantInboundEnvelopeSchema = object({
|
|
|
31456
31466
|
source: InboundEnvelopeSchema.shape.source.optional().catch(undefined),
|
|
31457
31467
|
sender: object(tolerantShape(InboundSenderSchema.shape)).optional().catch(undefined)
|
|
31458
31468
|
}).passthrough();
|
|
31469
|
+
var DispatchDeliveryRequestSchema = object({
|
|
31470
|
+
attempt: number2().int().positive(),
|
|
31471
|
+
mode: _enum2(["btw", "aside", "steer"])
|
|
31472
|
+
});
|
|
31473
|
+
var DispatchTargetedMessagePayloadSchema = object({
|
|
31474
|
+
id: string2(),
|
|
31475
|
+
issue_key: string2(),
|
|
31476
|
+
author: object({ kind: string2(), id: string2() }),
|
|
31477
|
+
body: string2(),
|
|
31478
|
+
target: string2(),
|
|
31479
|
+
in_reply_to: string2().nullable(),
|
|
31480
|
+
deliveries: array(unknown()),
|
|
31481
|
+
created_at: string2()
|
|
31482
|
+
});
|
|
31483
|
+
var DispatchTargetedFrameSchema = object({
|
|
31484
|
+
event: DispatchEventSchema,
|
|
31485
|
+
delivery: DispatchDeliveryRequestSchema
|
|
31486
|
+
}).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
|
|
31487
|
+
var RecoverableDispatchDeliveryFailureSchema = object({
|
|
31488
|
+
event: object({
|
|
31489
|
+
issue_key: string2(),
|
|
31490
|
+
payload: object({ id: string2(), body: string2().optional() }).passthrough()
|
|
31491
|
+
}).passthrough(),
|
|
31492
|
+
delivery: DispatchDeliveryRequestSchema
|
|
31493
|
+
});
|
|
31459
31494
|
function senderLabel(envelope) {
|
|
31460
31495
|
const sender = envelope.source_session ?? envelope.source ?? "unknown";
|
|
31461
31496
|
return envelope.sender?.title === undefined ? sender : `${sender} (${envelope.sender.title})`;
|
|
@@ -31482,6 +31517,8 @@ var DISPATCH_PAYLOAD_SCHEMAS = {
|
|
|
31482
31517
|
"suggestion.accepted": CommentEventPayloadSchema,
|
|
31483
31518
|
"suggestion.rejected": CommentEventPayloadSchema,
|
|
31484
31519
|
"message.created": MessageEventPayloadSchema,
|
|
31520
|
+
"message.delivery": MessageDeliveryEventPayloadSchema,
|
|
31521
|
+
"message.answered": MessageEventPayloadSchema,
|
|
31485
31522
|
"child.status": ChildStatusEventPayloadSchema
|
|
31486
31523
|
};
|
|
31487
31524
|
function dispatchOwner(event, topic) {
|
|
@@ -31514,6 +31551,9 @@ function dispatchMessageReplyPreview(event) {
|
|
|
31514
31551
|
const parsed = MessageEventPayloadSchema.safeParse(event.payload);
|
|
31515
31552
|
return parsed.success && parsed.data.reply_body !== undefined && parsed.data.reply_body !== "" ? parsed.data.reply_body : undefined;
|
|
31516
31553
|
}
|
|
31554
|
+
function isObject2(value) {
|
|
31555
|
+
return typeof value === "object" && value !== null;
|
|
31556
|
+
}
|
|
31517
31557
|
function parseDispatchFrame(rawPayload) {
|
|
31518
31558
|
let value;
|
|
31519
31559
|
try {
|
|
@@ -31521,11 +31561,41 @@ function parseDispatchFrame(rawPayload) {
|
|
|
31521
31561
|
} catch {
|
|
31522
31562
|
return { raw: rawPayload };
|
|
31523
31563
|
}
|
|
31564
|
+
const targeted = DispatchTargetedFrameSchema.safeParse(value);
|
|
31565
|
+
if (targeted.success) {
|
|
31566
|
+
const wireFrame = value;
|
|
31567
|
+
return {
|
|
31568
|
+
event: targeted.data.event,
|
|
31569
|
+
notify: wireFrame.event.notify,
|
|
31570
|
+
delivery: targeted.data.delivery
|
|
31571
|
+
};
|
|
31572
|
+
}
|
|
31573
|
+
if (isObject2(value) && "delivery" in value) {
|
|
31574
|
+
const recoverable = RecoverableDispatchDeliveryFailureSchema.safeParse(value);
|
|
31575
|
+
if (recoverable.success) {
|
|
31576
|
+
return {
|
|
31577
|
+
raw: value,
|
|
31578
|
+
rejectedDelivery: {
|
|
31579
|
+
attempt: recoverable.data.delivery.attempt,
|
|
31580
|
+
mode: recoverable.data.delivery.mode,
|
|
31581
|
+
messageID: recoverable.data.event.payload.id,
|
|
31582
|
+
issueKey: recoverable.data.event.issue_key,
|
|
31583
|
+
body: recoverable.data.event.payload.body ?? ""
|
|
31584
|
+
},
|
|
31585
|
+
malformedDelivery: true
|
|
31586
|
+
};
|
|
31587
|
+
}
|
|
31588
|
+
return { raw: value, malformedDelivery: true };
|
|
31589
|
+
}
|
|
31524
31590
|
const parsed = DispatchEventSchema.safeParse(value);
|
|
31525
31591
|
if (!parsed.success)
|
|
31526
31592
|
return { raw: value };
|
|
31527
31593
|
const wireEvent = value;
|
|
31528
|
-
return {
|
|
31594
|
+
return {
|
|
31595
|
+
event: parsed.data,
|
|
31596
|
+
notify: wireEvent.notify,
|
|
31597
|
+
delivery: undefined
|
|
31598
|
+
};
|
|
31529
31599
|
}
|
|
31530
31600
|
function inboundTimestamp(milliseconds) {
|
|
31531
31601
|
if (milliseconds === undefined)
|
|
@@ -31563,6 +31633,10 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31563
31633
|
}
|
|
31564
31634
|
let dispatchEvent;
|
|
31565
31635
|
let dispatchIssue;
|
|
31636
|
+
let dispatchReply;
|
|
31637
|
+
let delivery;
|
|
31638
|
+
let rejectedDelivery;
|
|
31639
|
+
let malformedDelivery = false;
|
|
31566
31640
|
let askQuestion;
|
|
31567
31641
|
let messageReplyPreview;
|
|
31568
31642
|
const dispatchRendered = envelope.source === "dispatch" && envelope.payload !== undefined;
|
|
@@ -31592,6 +31666,20 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31592
31666
|
}
|
|
31593
31667
|
askQuestion = dispatchAskQuestion(frame.event);
|
|
31594
31668
|
messageReplyPreview = dispatchMessageReplyPreview(frame.event);
|
|
31669
|
+
if (frame.event.type === "message.created" && frame.event.issue_key !== null) {
|
|
31670
|
+
const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
|
|
31671
|
+
const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
|
|
31672
|
+
if (message.success && requested.success && message.data.id !== undefined) {
|
|
31673
|
+
delivery = {
|
|
31674
|
+
attempt: requested.data.attempt,
|
|
31675
|
+
mode: requested.data.mode,
|
|
31676
|
+
messageID: message.data.id,
|
|
31677
|
+
issueKey: frame.event.issue_key,
|
|
31678
|
+
body: message.data.body ?? envelope.payload_summary ?? ""
|
|
31679
|
+
};
|
|
31680
|
+
dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
|
|
31681
|
+
}
|
|
31682
|
+
}
|
|
31595
31683
|
dispatchEvent = {
|
|
31596
31684
|
owner: dispatchOwner(frame.event, subject ?? envelope.topic),
|
|
31597
31685
|
...frame.event.issue_key === null ? {
|
|
@@ -31606,6 +31694,8 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31606
31694
|
};
|
|
31607
31695
|
} else {
|
|
31608
31696
|
dispatchEvent = frame.raw;
|
|
31697
|
+
rejectedDelivery = frame.rejectedDelivery;
|
|
31698
|
+
malformedDelivery = frame.malformedDelivery === true;
|
|
31609
31699
|
}
|
|
31610
31700
|
}
|
|
31611
31701
|
}
|
|
@@ -31630,7 +31720,7 @@ ${envelope.payload ?? ""}`;
|
|
|
31630
31720
|
}
|
|
31631
31721
|
}
|
|
31632
31722
|
const role = envelope.sender?.roles?.[0];
|
|
31633
|
-
const reply = replyWith(envelope);
|
|
31723
|
+
const reply = dispatchReply ?? replyWith(envelope);
|
|
31634
31724
|
const sourceIssue = parseIssues.includes("source") || KNOWN_SOURCES[envelope.source] !== undefined ? undefined : `source=${envelope.source}`;
|
|
31635
31725
|
const unrecognised = [
|
|
31636
31726
|
...parseIssues,
|
|
@@ -31660,7 +31750,14 @@ ${envelope.payload ?? ""}`;
|
|
|
31660
31750
|
},
|
|
31661
31751
|
...unrecognised === undefined ? {} : { unrecognised }
|
|
31662
31752
|
};
|
|
31663
|
-
return {
|
|
31753
|
+
return {
|
|
31754
|
+
skip: false,
|
|
31755
|
+
content: encode3({ envoy: rendered }),
|
|
31756
|
+
envelope,
|
|
31757
|
+
...delivery === undefined ? {} : { delivery },
|
|
31758
|
+
...rejectedDelivery === undefined ? {} : { rejectedDelivery },
|
|
31759
|
+
...malformedDelivery ? { malformedDelivery: true } : {}
|
|
31760
|
+
};
|
|
31664
31761
|
}
|
|
31665
31762
|
|
|
31666
31763
|
// ../envoy-client/src/dispatch-config.ts
|
|
@@ -32302,13 +32399,13 @@ function askId(args) {
|
|
|
32302
32399
|
}
|
|
32303
32400
|
return reference.id;
|
|
32304
32401
|
}
|
|
32305
|
-
function
|
|
32306
|
-
const
|
|
32307
|
-
if (
|
|
32308
|
-
return
|
|
32309
|
-
const reference = parseDispatchRef(
|
|
32402
|
+
function messageInReplyTo(args) {
|
|
32403
|
+
const inReplyTo = optionalString(args, "in_reply_to");
|
|
32404
|
+
if (inReplyTo === undefined || !inReplyTo.startsWith("dispatch://"))
|
|
32405
|
+
return inReplyTo;
|
|
32406
|
+
const reference = parseDispatchRef(inReplyTo);
|
|
32310
32407
|
if (reference?.kind !== "message") {
|
|
32311
|
-
throw new Error("
|
|
32408
|
+
throw new Error("in_reply_to must be a bare message id or a dispatch://.../message/<id> reference");
|
|
32312
32409
|
}
|
|
32313
32410
|
return reference.id;
|
|
32314
32411
|
}
|
|
@@ -32756,10 +32853,10 @@ async function executeDispatchTool(input) {
|
|
|
32756
32853
|
};
|
|
32757
32854
|
}
|
|
32758
32855
|
case "dispatch_message": {
|
|
32759
|
-
const
|
|
32856
|
+
const inReplyTo = messageInReplyTo(args);
|
|
32760
32857
|
const message = await client.message(issue(), {
|
|
32761
32858
|
body: stringArg(args, "body"),
|
|
32762
|
-
...
|
|
32859
|
+
...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
|
|
32763
32860
|
actor
|
|
32764
32861
|
});
|
|
32765
32862
|
const messageRef = `dispatch://${message.issue_key}/message/${message.id}`;
|
|
@@ -33134,6 +33231,7 @@ var SessionWireSchema = object({
|
|
|
33134
33231
|
title: string2(),
|
|
33135
33232
|
topics: array(string2()),
|
|
33136
33233
|
roles: array(string2()).optional(),
|
|
33234
|
+
capabilities: array(string2()).optional(),
|
|
33137
33235
|
self_subscribed: boolean2(),
|
|
33138
33236
|
last_seen: number2().int().optional()
|
|
33139
33237
|
});
|
|
@@ -33242,6 +33340,7 @@ function createEnvoyClient(config) {
|
|
|
33242
33340
|
topics: expandSubscriptionTopics(input.topics),
|
|
33243
33341
|
port: input.port,
|
|
33244
33342
|
title: input.title,
|
|
33343
|
+
...input.capabilities === undefined ? {} : { capabilities: input.capabilities },
|
|
33245
33344
|
driving: input.driving,
|
|
33246
33345
|
...input.selfSubscribed === undefined ? {} : { self_subscribed: input.selfSubscribed }
|
|
33247
33346
|
}))),
|
|
@@ -33468,6 +33567,26 @@ function envoyExtension(pi) {
|
|
|
33468
33567
|
});
|
|
33469
33568
|
return connection;
|
|
33470
33569
|
};
|
|
33570
|
+
const postDispatchReply = async (delivery, result) => {
|
|
33571
|
+
if (!dispatchConfig.enabled || dispatchConfig.url === null || dispatchConfig.token === null) {
|
|
33572
|
+
throw new Error("Dispatch reply endpoint is not configured");
|
|
33573
|
+
}
|
|
33574
|
+
const response = await fetch(`${dispatchConfig.url}/api/v1/messages/${delivery.messageID}/reply`, {
|
|
33575
|
+
method: "POST",
|
|
33576
|
+
headers: {
|
|
33577
|
+
Authorization: `Bearer ${dispatchConfig.token}`,
|
|
33578
|
+
"Content-Type": "application/json"
|
|
33579
|
+
},
|
|
33580
|
+
body: JSON.stringify({
|
|
33581
|
+
actor: { kind: "session", id: sessionID },
|
|
33582
|
+
attempt: delivery.attempt,
|
|
33583
|
+
...result
|
|
33584
|
+
})
|
|
33585
|
+
});
|
|
33586
|
+
if (!response.ok) {
|
|
33587
|
+
throw new Error(`Dispatch reply failed: ${response.status} ${await response.text()}`);
|
|
33588
|
+
}
|
|
33589
|
+
};
|
|
33471
33590
|
const deliver = async (subject, raw, reply) => {
|
|
33472
33591
|
const rendered = renderInbound(raw, sessionID, subject);
|
|
33473
33592
|
for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
|
|
@@ -33487,9 +33606,34 @@ function envoyExtension(pi) {
|
|
|
33487
33606
|
inbox.pop();
|
|
33488
33607
|
}
|
|
33489
33608
|
try {
|
|
33490
|
-
|
|
33609
|
+
if (rendered.rejectedDelivery !== undefined) {
|
|
33610
|
+
console.warn(`[envoy] rejecting malformed Dispatch targeted delivery ${rendered.rejectedDelivery.messageID}`);
|
|
33611
|
+
await postDispatchReply(rendered.rejectedDelivery, {
|
|
33612
|
+
error: "Invalid Dispatch targeted delivery frame"
|
|
33613
|
+
});
|
|
33614
|
+
} else if (rendered.malformedDelivery === true) {
|
|
33615
|
+
console.warn("[envoy] dropping malformed Dispatch targeted delivery without a reply address");
|
|
33616
|
+
} else if (rendered.delivery?.mode === "btw") {
|
|
33617
|
+
if (pi.askEphemeral === undefined) {
|
|
33618
|
+
await postDispatchReply(rendered.delivery, {
|
|
33619
|
+
error: "This OMP host does not support BTW delivery"
|
|
33620
|
+
});
|
|
33621
|
+
} else {
|
|
33622
|
+
try {
|
|
33623
|
+
const reply = await pi.askEphemeral({ prompt: rendered.delivery.body });
|
|
33624
|
+
await postDispatchReply(rendered.delivery, { body: reply.replyText });
|
|
33625
|
+
} catch (error) {
|
|
33626
|
+
await postDispatchReply(rendered.delivery, { error: messageFor(error) });
|
|
33627
|
+
}
|
|
33628
|
+
}
|
|
33629
|
+
} else {
|
|
33630
|
+
pi.sendMessage({ customType: "envoy-message", content: rendered.content, display: true }, {
|
|
33631
|
+
deliverAs: rendered.delivery?.mode === "aside" ? "aside" : "steer",
|
|
33632
|
+
triggerTurn: true
|
|
33633
|
+
});
|
|
33634
|
+
}
|
|
33491
33635
|
} catch (error) {
|
|
33492
|
-
console.warn(`[envoy] failed to
|
|
33636
|
+
console.warn(`[envoy] failed to deliver envelope ${envelope?.event_id ?? "unknown"}`, error);
|
|
33493
33637
|
throw error;
|
|
33494
33638
|
}
|
|
33495
33639
|
if (dedupeKey !== undefined) {
|
|
@@ -33568,6 +33712,7 @@ function envoyExtension(pi) {
|
|
|
33568
33712
|
topics: [...new Set([agentSubject(sessionID), ...subscriptions.keys()])],
|
|
33569
33713
|
port: 0,
|
|
33570
33714
|
title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
|
|
33715
|
+
capabilities: typeof pi.askEphemeral === "function" ? ["aside", "btw"] : ["aside"],
|
|
33571
33716
|
driving: false,
|
|
33572
33717
|
selfSubscribed: true
|
|
33573
33718
|
});
|
package/dist/skills/AGENTS.md
CHANGED
|
@@ -8,9 +8,8 @@ event intake, process lifecycle, credentials, and role delivery.
|
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
skills/
|
|
11
|
-
├── dispatch/ #
|
|
12
|
-
├──
|
|
13
|
-
├── linear/ # Linear adapter instructions
|
|
11
|
+
├── dispatch/ # Writing specs, asks, comments, and artifacts on native Dispatch
|
|
12
|
+
├── envoy/ # Envoy subscriptions, agent-to-agent messages, and topic formats
|
|
14
13
|
├── legion-architect/ # Tree ownership, decomposition, gates, and scheduling
|
|
15
14
|
├── legion-controller/ # Derived-verdict control-plane operation
|
|
16
15
|
├── legion-oracle/ # Repository-grounded research
|
|
@@ -27,9 +26,10 @@ returns that schema to the architect. It writes the same phase-specific payload
|
|
|
27
26
|
The committed predecessor handoff wins after revival or re-creation.
|
|
28
27
|
|
|
29
28
|
Workers do not run a controller loop or mutate lifecycle labels. Workers coordinate
|
|
30
|
-
lifecycle, scope, and cross-phase decisions with the owning architect
|
|
31
|
-
the verified observation and decision needed
|
|
32
|
-
|
|
29
|
+
lifecycle, scope, and cross-phase decisions with the owning architect by `envoy_publish` to its
|
|
30
|
+
role topic, sending the verified observation and decision needed (`hub` reaches only subagents
|
|
31
|
+
inside the worker's own process). A worker may call the native `dispatch_*` tools directly for a
|
|
32
|
+
durable human question; replies come back to the worker's own session.
|
|
33
33
|
|
|
34
34
|
## Durable artifacts
|
|
35
35
|
|
|
@@ -222,6 +222,20 @@ and deleting a cell's quoted text removes only that text.
|
|
|
222
222
|
Use `replace` for inline continuation. Use zero-based `occurrence` for a repeated target; re-read a missing or ambiguous target before
|
|
223
223
|
retrying. Pass `summary` to name the version when recording a decision.
|
|
224
224
|
|
|
225
|
+
## Typed blocks
|
|
226
|
+
|
|
227
|
+
The server declares typed document blocks at `GET /api/v1/schema/blocks`. Write one only with the
|
|
228
|
+
container-directive form `:::name{#block-id key="value"}` on its own line, ordinary block children,
|
|
229
|
+
and a closing `:::` at the same nesting. An unclosed typed block at document level is rejected. For
|
|
230
|
+
a new typed block, omit `#block-id`; Dispatch mints it. When editing an existing typed block, retain
|
|
231
|
+
its id and every rendered attribute.
|
|
232
|
+
|
|
233
|
+
Use only the type names, content rule, attributes, and enum values returned by the schema. Values are
|
|
234
|
+
quoted: `:::callout{kind="warning" title="Risk"}`. Do not write Pandoc-style `::: {.callout}`, leaf
|
|
235
|
+
`::name` directives, or text `:name` directives; those strings are literal when quoted inside a code
|
|
236
|
+
block. Do not set attributes the schema marks `server: true`; the server ignores them and reasserts
|
|
237
|
+
its authoritative value at settlement.
|
|
238
|
+
|
|
225
239
|
## Comments and suggestions
|
|
226
240
|
|
|
227
241
|
Add feedback with:
|
|
@@ -284,6 +298,24 @@ dispatch_message({ issue, body })
|
|
|
284
298
|
It returns `details` `{ issue, topic, message }`. `body` is capped at 2,000 characters. A message is not a decision
|
|
285
299
|
(`dispatch_ask`) or document feedback (`dispatch_comment`), and it does not wake anyone unless the issue is routed.
|
|
286
300
|
|
|
301
|
+
### Targeted agent messages
|
|
302
|
+
|
|
303
|
+
A human can target the issue message at a live Envoy session or role as **BTW**, **Aside**, or
|
|
304
|
+
**Steer**. The incoming Dispatch frame names the issue and includes a `reply_with` instruction;
|
|
305
|
+
reply on the same open issue with the existing tool, never a new targeted send:
|
|
306
|
+
|
|
307
|
+
```ts
|
|
308
|
+
dispatch_message({
|
|
309
|
+
issue: "CORE-1",
|
|
310
|
+
in_reply_to: "<targeted-message-id>",
|
|
311
|
+
body: "The requested answer.",
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`in_reply_to` correlates the answer under the human's message in its Conversation card. A BTW
|
|
316
|
+
delivery can post its answer automatically; use this call when the frame asks the primary agent to
|
|
317
|
+
reply. Agent bearers cannot set `target` or `delivery`.
|
|
318
|
+
|
|
287
319
|
## What comes back
|
|
288
320
|
|
|
289
321
|
A write result's `details.topic` subscribes the host to its owner, and the first such subscription on an issue or document also tells
|
|
@@ -10,11 +10,6 @@ with human-created children; either way you own its complete outcome. Work from
|
|
|
10
10
|
wakes and current artifacts. Do not perform code work yourself and do not rely on a
|
|
11
11
|
separate coordinator to finish necessary work.
|
|
12
12
|
|
|
13
|
-
This skill documents the target Dispatch-native contract. The `legion` tool's
|
|
14
|
-
`register_gate`/`release_wave`/`set_status` ops, the Dispatch key format, and the
|
|
15
|
-
`dispatch_*` tool family land with PR B (#TBD); until that PR merges, this skill's contract
|
|
16
|
-
is not yet runnable on `main`.
|
|
17
|
-
|
|
18
13
|
## Tool and ownership boundaries
|
|
19
14
|
|
|
20
15
|
- Use the `legion` tool for lifecycle writes. Its issue key is the Dispatch key
|
|
@@ -9,10 +9,6 @@ The controller is the one persistent, wake-driven session for a Legion project.
|
|
|
9
9
|
triage, escalation, and human-interaction judgments; it never does phase-worker work or
|
|
10
10
|
routes raw events into an architect.
|
|
11
11
|
|
|
12
|
-
This skill documents the target Dispatch-native contract. The `legion` tool's `set_status`
|
|
13
|
-
op, `dispatch_read`/`dispatch_issue`, and the Dispatch key format land with PR B (#TBD);
|
|
14
|
-
until that PR merges, this skill's contract is not yet runnable on `main`.
|
|
15
|
-
|
|
16
12
|
## Start and claim the controller role
|
|
17
13
|
|
|
18
14
|
The Legion extension claims `legion-<project>-controller` and registers controller readiness
|
|
@@ -36,14 +36,18 @@ digraph oracle_decision {
|
|
|
36
36
|
|
|
37
37
|
## Research Strategy
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
If the deployment instructions name a librarian role, ask it first (publish to its
|
|
40
|
+
`notifications.role.<name>` topic with `expects_reply: required`). Then run steps 1-2 (parallel
|
|
41
|
+
OK), and 3-4 if needed, with tools a Legion pane actually has: `read`, `grep`, `web_search`, and
|
|
42
|
+
`task(agent="scout")` (fast read-only codebase search) or `task(agent="oracle")` (deeper
|
|
43
|
+
read-only analysis when the answer needs judgment across many files). Do not name any other agent.
|
|
40
44
|
|
|
41
45
|
| Step | Tool | Query |
|
|
42
46
|
|------|------|-------|
|
|
43
|
-
| 1. Institutional learnings | `
|
|
44
|
-
| 2. Codebase patterns | `
|
|
45
|
-
| 3. Framework docs |
|
|
46
|
-
| 4. External practices | `
|
|
47
|
+
| 1. Institutional learnings | `grep` then `read` over `docs/solutions/` (front-matter `tags`, then the body) | [question]'s keywords |
|
|
48
|
+
| 2. Codebase patterns | `task(agent="scout")`; `task(agent="oracle")` when judgment across many files is needed | Find how [module] handles [topic] |
|
|
49
|
+
| 3. Framework docs | `read` the library's documentation URL | [library] [topic] |
|
|
50
|
+
| 4. External practices | `web_search`, then `read` the primary source | Current best practices for [topic] |
|
|
47
51
|
|
|
48
52
|
## Output
|
|
49
53
|
|
|
@@ -54,10 +58,11 @@ Run steps 1-2 first (parallel OK), then 3-4 if needed:
|
|
|
54
58
|
## Example
|
|
55
59
|
|
|
56
60
|
```
|
|
57
|
-
|
|
61
|
+
Question: How should I paginate a GraphQL connection?
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
grep pagination docs/solutions/ → no matches
|
|
64
|
+
task(agent="scout") → packages/daemon/src/state/fetch.ts loops on
|
|
65
|
+
pageInfo.hasNextPage / endCursor (contextsPage)
|
|
61
66
|
|
|
62
|
-
Answer:
|
|
67
|
+
Answer: cursor-based, per the `while (page.hasNextPage)` loop in packages/daemon/src/state/fetch.ts
|
|
63
68
|
```
|
|
@@ -11,10 +11,6 @@ phase gets its own long-lived process against the same jj workspace, run in turn
|
|
|
11
11
|
the phase assigned to you, report its completion to the architect, and leave the durable
|
|
12
12
|
copy the next phase can trust.
|
|
13
13
|
|
|
14
|
-
This skill documents the target Dispatch-native contract. `LEGION_ISSUE` as a Dispatch key,
|
|
15
|
-
the `dispatch_ask` tool, and the `Dispatch: <KEY>` PR-body linkage land with PR B (#TBD);
|
|
16
|
-
until that PR merges, this skill's contract is not yet runnable on `main`.
|
|
17
|
-
|
|
18
14
|
## Identity, scope, and role
|
|
19
15
|
|
|
20
16
|
The daemon spawns you as a separate `omp --mode rpc` process (behind `legion worker-shim`,
|
|
@@ -95,12 +91,15 @@ committed predecessor handoffs in lifecycle order from `$LEGION_WORKSPACE/.legio
|
|
|
95
91
|
4. `test.json`
|
|
96
92
|
5. `review.json`
|
|
97
93
|
|
|
98
|
-
Read only files that precede the assigned phase.
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
Read only files that precede the assigned phase. Every handoff is validated when it is read:
|
|
95
|
+
`validatePhaseHandoff` (`packages/contracts/src/handoff-schema.ts`) checks the file, and the
|
|
96
|
+
ledger (`packages/daemon/src/handoff/ledger.ts`) treats a file that fails validation as missing.
|
|
97
|
+
Undeclared fields pass validation untouched and reach the next worker; a declared field of the
|
|
98
|
+
wrong type fails the whole file, so `legion handoff read` returns null for that phase.
|
|
99
|
+
Write the phase-specific fields the next phase and the architect need, consistent with what
|
|
100
|
+
predecessor phases already wrote. The durable copy lives in
|
|
101
|
+
`$LEGION_WORKSPACE/.legion/<phase>.json`. If a committed handoff conflicts with memory or a prior
|
|
102
|
+
transcript, the committed file wins: it is the copy that survived.
|
|
104
103
|
|
|
105
104
|
## jj Safety Rules
|
|
106
105
|
|
|
@@ -138,6 +137,14 @@ capability it needs; invoke GitHub through the credential helper:
|
|
|
138
137
|
legion gh -- <gh args…>
|
|
139
138
|
```
|
|
140
139
|
|
|
140
|
+
Three facts about `gh` in a worker pane. The `gh` on your `PATH` is a shim
|
|
141
|
+
(`packages/pi-envoy/src/legion/gh-shim.ts`) that execs `legion gh -- "$@"`, so `gh …` and
|
|
142
|
+
`legion gh -- …` are the same call, and each call redeems a fresh token from your session's
|
|
143
|
+
grant — identity is supplied per call, never stored. Never run `gh auth login` or
|
|
144
|
+
`gh auth setup-git`; there is no login state to create. The shim refuses `pr merge` (and a raw
|
|
145
|
+
`gh api …/merge`): no worker role merges a pull request — the merge queue does, under its own
|
|
146
|
+
authority.
|
|
147
|
+
|
|
141
148
|
## GitHub PR comment attribution
|
|
142
149
|
|
|
143
150
|
Append this exact structured footer to **every** pull-request comment and review that this
|
|
@@ -158,6 +165,15 @@ legion gh -- pr comment <pr-number> \
|
|
|
158
165
|
--repo <owner>/<repo>
|
|
159
166
|
```
|
|
160
167
|
|
|
168
|
+
## Planner artifact
|
|
169
|
+
|
|
170
|
+
The plan lives in `.legion/plan.json` and the Dispatch issue document; never commit a plan or spec file to the repository.
|
|
171
|
+
No `docs/plans/*`, `docs/superpowers/plans/*`, or spec markdown goes into the pull request: plan
|
|
172
|
+
and spec content goes into the issue, never into a PR (the root `AGENTS.md`'s `docs/plans/` row
|
|
173
|
+
is human-authored design history, not a Legion artifact). A skill step that says "save the plan
|
|
174
|
+
to a file" is satisfied by the handoff write in the completion gate below; the planner's only
|
|
175
|
+
commit is `plan: record handoff`.
|
|
176
|
+
|
|
161
177
|
## Implementer push and pull request
|
|
162
178
|
|
|
163
179
|
Only the implementer creates the issue bookmark, pushes it, and opens the pull request.
|
|
@@ -212,17 +228,36 @@ Negative control: <deliberately broken input> → <refusal or failure observed>.
|
|
|
212
228
|
changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
|
|
213
229
|
Findings about naming, duplication, or wording are batched into the single `Fast-follow`
|
|
214
230
|
line instead of iterating per push.
|
|
231
|
+
- **Rebase only on a real conflict.** Sami, 2026-09-11, verbatim:
|
|
232
|
+
"Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
|
|
233
|
+
The implementer rebases the issue branch only when GitHub reports it `CONFLICTING` or the
|
|
234
|
+
controller asks because of a conflict — never to pick up `main` or to refresh CI. A single
|
|
235
|
+
failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
|
|
236
|
+
pushing a new commit.
|
|
237
|
+
- **No deferrals.** Sami, 2026-09-11, verbatim: "My rule is no deferrals." The `Fast-follow:`
|
|
238
|
+
field names naming, duplication, or wording cleanup only; anything that changes behaviour,
|
|
239
|
+
hides an error, or breaks a gate lands in this PR.
|
|
215
240
|
- The tester fills in the `E2E` section: the real surface a user reaches the criterion
|
|
216
241
|
through, the exact command or run id, what was observed, the head SHA, and one negative
|
|
217
242
|
control — a deliberately broken input and the refusal or failure it produced. A unit or
|
|
218
|
-
integration test is a regression lock, never proof of a criterion.
|
|
243
|
+
integration test is a regression lock, never proof of a criterion. Environment or
|
|
244
|
+
secret-scrub evidence (e.g. "`LEGION_*`/`DISPATCH_*`/`ENVOY_*` unset") is recorded once, in
|
|
245
|
+
`.legion/test.json`, and only when the issue's acceptance criteria call for it — never
|
|
246
|
+
re-pasted into the PR body each round.
|
|
219
247
|
- The reviewer verifies the `CI`, `Threads`, and `E2E` facts against GitHub directly —
|
|
220
248
|
never from a handoff — then runs `task(agent="thermonuclear-deep-review")` and
|
|
221
249
|
`task(agent="thermonuclear-code-quality")` once at that head and records the verdict.
|
|
222
|
-
Skip the `Thermo` line entirely on a docs-only PR.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
250
|
+
Skip the `Thermo` line entirely on a docs-only PR. Submit **one review per round** —
|
|
251
|
+
`REQUEST_CHANGES` when any correctness finding stands, otherwise `COMMENT` while the head
|
|
252
|
+
still carries `.legion/`; `APPROVE` only for the head that differs from the reviewed one by
|
|
253
|
+
the `.legion/` deletion alone, named by SHA — carrying every inline comment in that single
|
|
254
|
+
call: `legion gh -- api --method POST repos/{owner}/{repo}/pulls/{number}/reviews --input body.json`
|
|
255
|
+
with `commit_id`, `event` (`REQUEST_CHANGES`, `COMMENT`, or `APPROVE`), `body` (with the
|
|
256
|
+
Legion footer), and a `comments[]` array of `{path, line, side, body}`, one entry per
|
|
257
|
+
finding — never one `pr review` call per finding (each submission fires a `pr-review` wake).
|
|
258
|
+
Then return the issue to the architect; when clean, have the architect send the implementer
|
|
259
|
+
back to push the `.legion/` deletion (the review App cannot push), then review **that** head
|
|
260
|
+
and approve it by name.
|
|
226
261
|
- Once a base is frozen for others to stack on, never rewrite it — fixes land as new
|
|
227
262
|
commits on top, and the `Chain` line records what is frozen.
|
|
228
263
|
- The merger confirms the approved head still equals the current head, then publishes
|
package/package.json
CHANGED
|
@@ -1,208 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: github
|
|
3
|
-
description: Manage GitHub issues via Projects V2. Use when LEGION_ISSUE_BACKEND=github.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# GitHub (gh CLI)
|
|
7
|
-
|
|
8
|
-
Direct CLI operations via `gh`. No embedded MCP — all commands are shell invocations.
|
|
9
|
-
|
|
10
|
-
## Setup
|
|
11
|
-
|
|
12
|
-
Requires `gh` CLI installed and authenticated:
|
|
13
|
-
```bash
|
|
14
|
-
gh auth login
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
**Inside a Legion worker workspace**, skip `gh auth login`/`gh auth setup-git`: `gh` on `PATH`
|
|
18
|
-
is a shim that execs `legion gh -- <args>` (`packages/pi-envoy/src/legion/gh-shim.ts`), which
|
|
19
|
-
redeems a fresh token from the daemon's grant on every invocation and hands it straight to the
|
|
20
|
-
real `gh` binary — it is already authenticated per call, independent of any local login state.
|
|
21
|
-
|
|
22
|
-
## Operations
|
|
23
|
-
|
|
24
|
-
### Search/List Issues (via project)
|
|
25
|
-
|
|
26
|
-
List all items in a GitHub Project V2:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
gh project item-list $PROJECT_NUM --owner $OWNER --format json
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
**Parameters:**
|
|
33
|
-
- `$PROJECT_NUM`: Project number (from `LEGION_ID` format: `owner/project-number`)
|
|
34
|
-
- `$OWNER`: Repository owner
|
|
35
|
-
- `--format json`: Returns structured data for parsing
|
|
36
|
-
|
|
37
|
-
**Example:**
|
|
38
|
-
```bash
|
|
39
|
-
gh project item-list 42 --owner acme --format json | jq '.items[] | {id, title, status}'
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### Get Issue Details
|
|
43
|
-
|
|
44
|
-
Fetch full issue metadata:
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
gh issue view $ISSUE_NUMBER --json title,body,labels,comments,state -R $OWNER/$REPO
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
**Parameters:**
|
|
51
|
-
- `$ISSUE_NUMBER`: Issue number (e.g., `123`)
|
|
52
|
-
- `-R $OWNER/$REPO`: Repository (required for multi-repo support)
|
|
53
|
-
- `--json`: Fields to return (title, body, labels, comments, state, etc.)
|
|
54
|
-
|
|
55
|
-
**Example:**
|
|
56
|
-
```bash
|
|
57
|
-
gh issue view 123 --json title,body,labels,state -R acme/backend
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Update Status (Projects V2 — GraphQL)
|
|
61
|
-
|
|
62
|
-
Update issue status in a GitHub Project V2. Requires field and option IDs from project schema:
|
|
63
|
-
|
|
64
|
-
```bash
|
|
65
|
-
gh api graphql -f query='mutation {
|
|
66
|
-
updateProjectV2ItemFieldValue(input: {
|
|
67
|
-
projectId: "$PROJECT_ID"
|
|
68
|
-
itemId: "$ITEM_ID"
|
|
69
|
-
fieldId: "$STATUS_FIELD_ID"
|
|
70
|
-
value: { singleSelectOptionId: "$OPTION_ID" }
|
|
71
|
-
}) { projectV2Item { id } }
|
|
72
|
-
}'
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
**Parameters:**
|
|
76
|
-
- `$PROJECT_ID`: GraphQL ID of the project (not the number)
|
|
77
|
-
- `$ITEM_ID`: GraphQL ID of the issue in the project
|
|
78
|
-
- `$STATUS_FIELD_ID`: GraphQL ID of the Status field
|
|
79
|
-
- `$OPTION_ID`: GraphQL ID of the status option (e.g., "In Progress", "Done")
|
|
80
|
-
|
|
81
|
-
**Note:** Field and option IDs must be resolved from the project schema. The controller caches these after first query.
|
|
82
|
-
|
|
83
|
-
**Resolve IDs (one-time):**
|
|
84
|
-
```bash
|
|
85
|
-
gh api graphql -f query='query {
|
|
86
|
-
repository(owner: "$OWNER", name: "$REPO") {
|
|
87
|
-
projectV2(number: $PROJECT_NUM) {
|
|
88
|
-
fields(first: 20) {
|
|
89
|
-
nodes {
|
|
90
|
-
... on ProjectV2SingleSelectField {
|
|
91
|
-
id
|
|
92
|
-
name
|
|
93
|
-
options { id name }
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}'
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
### Add Label
|
|
103
|
-
|
|
104
|
-
Add a label to an issue (additive — does not remove existing labels):
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
gh issue edit $ISSUE_NUMBER --add-label "bug" -R $OWNER/$REPO
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**Parameters:**
|
|
111
|
-
- `$ISSUE_NUMBER`: Issue number
|
|
112
|
-
- `--add-label`: Label to add (can be used multiple times)
|
|
113
|
-
- `-R $OWNER/$REPO`: Repository
|
|
114
|
-
|
|
115
|
-
**Example:**
|
|
116
|
-
```bash
|
|
117
|
-
gh issue edit 123 --add-label "bug" -R acme/backend
|
|
118
|
-
```
|
|
119
|
-
|
|
120
|
-
### Remove Label
|
|
121
|
-
|
|
122
|
-
Remove a label from an issue:
|
|
123
|
-
|
|
124
|
-
```bash
|
|
125
|
-
gh issue edit $ISSUE_NUMBER --remove-label "wontfix" -R $OWNER/$REPO
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
**Parameters:**
|
|
129
|
-
- `$ISSUE_NUMBER`: Issue number
|
|
130
|
-
- `--remove-label`: Label to remove (can be used multiple times)
|
|
131
|
-
- `-R $OWNER/$REPO`: Repository
|
|
132
|
-
|
|
133
|
-
**Example:**
|
|
134
|
-
```bash
|
|
135
|
-
gh issue edit 123 --remove-label "wontfix" -R acme/backend
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
### Comment on Issue
|
|
139
|
-
|
|
140
|
-
Add a comment to an issue:
|
|
141
|
-
|
|
142
|
-
```bash
|
|
143
|
-
gh issue comment $ISSUE_NUMBER --body "Fixed in commit abc123" -R $OWNER/$REPO
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
**Parameters:**
|
|
147
|
-
- `$ISSUE_NUMBER`: Issue number
|
|
148
|
-
- `--body`: Comment text (supports Markdown)
|
|
149
|
-
- `-R $OWNER/$REPO`: Repository
|
|
150
|
-
|
|
151
|
-
**Example:**
|
|
152
|
-
```bash
|
|
153
|
-
gh issue comment 123 --body "Implemented in PR #456" -R acme/backend
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### Create Issue
|
|
157
|
-
|
|
158
|
-
Create a new issue:
|
|
159
|
-
|
|
160
|
-
```bash
|
|
161
|
-
gh issue create --title "Bug: Login fails" --body "Details" -R $OWNER/$REPO
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
**Parameters:**
|
|
165
|
-
- `--title`: Issue title (required)
|
|
166
|
-
- `--body`: Issue description (optional, supports Markdown)
|
|
167
|
-
- `-R $OWNER/$REPO`: Repository
|
|
168
|
-
|
|
169
|
-
**Example:**
|
|
170
|
-
```bash
|
|
171
|
-
gh issue create --title "Feature: Add dark mode" --body "User request from #789" -R acme/backend
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
## Key Differences from Linear
|
|
175
|
-
|
|
176
|
-
| Aspect | Linear | GitHub |
|
|
177
|
-
|--------|--------|--------|
|
|
178
|
-
| **Labels** | Replace all (read-modify-write) | Additive (`--add-label`, `--remove-label`) |
|
|
179
|
-
| **Status** | Direct field update | Projects V2 GraphQL mutation |
|
|
180
|
-
| **PR Association** | Attachment field | Native (issue ↔ PR link) |
|
|
181
|
-
| **API** | MCP tool dispatch | Direct `gh` CLI |
|
|
182
|
-
| **Multi-repo** | Single team | `-R owner/repo` per command |
|
|
183
|
-
|
|
184
|
-
## Important Notes
|
|
185
|
-
|
|
186
|
-
- **Always specify `-R $OWNER/$REPO`** for multi-repo project support
|
|
187
|
-
- **Labels are additive**: Use `--add-label` and `--remove-label` separately (unlike Linear which replaces all)
|
|
188
|
-
- **Status updates require Projects V2 GraphQL** — not just issue labels
|
|
189
|
-
- **PR association is automatic** — GitHub links issues and PRs natively
|
|
190
|
-
- **`$OWNER` and `$REPO` come from `LEGION_ID`** (format: `owner/project-number`)
|
|
191
|
-
- **Field/option IDs must be cached** by the controller after first resolution
|
|
192
|
-
|
|
193
|
-
## Error Handling
|
|
194
|
-
|
|
195
|
-
Common errors and solutions:
|
|
196
|
-
|
|
197
|
-
| Error | Cause | Solution |
|
|
198
|
-
|-------|-------|----------|
|
|
199
|
-
| `Could not resolve to a Repository` | Wrong `-R` format | Use `-R owner/repo` (not `owner-repo`) |
|
|
200
|
-
| `Could not resolve to an Issue` | Issue doesn't exist | Verify issue number is correct |
|
|
201
|
-
| `GraphQL error: Field not found` | Wrong field ID | Re-resolve field IDs from project schema |
|
|
202
|
-
| `Not authenticated` | `gh` not logged in | Run `gh auth login` |
|
|
203
|
-
|
|
204
|
-
## Reference
|
|
205
|
-
|
|
206
|
-
- **Project number**: Visible in GitHub UI (e.g., `https://github.com/orgs/acme/projects/42` → `42`)
|
|
207
|
-
- **Issue number**: Visible in URL (e.g., `https://github.com/acme/backend/issues/123` → `123`)
|
|
208
|
-
- **GraphQL IDs**: Base64-encoded, returned by GraphQL queries (not human-readable)
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: linear
|
|
3
|
-
description: Manage Linear issues. Use when working with tasks, tickets, bugs, or Linear.
|
|
4
|
-
mcp:
|
|
5
|
-
linear:
|
|
6
|
-
command: npx
|
|
7
|
-
args: ["-y", "github:obra/streamlinear"]
|
|
8
|
-
env:
|
|
9
|
-
LINEAR_API_TOKEN: ${LINEAR_API_TOKEN}
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
# Linear (Stream Linear)
|
|
13
|
-
|
|
14
|
-
Single-tool MCP with action dispatch. All operations go through `linear_linear`.
|
|
15
|
-
|
|
16
|
-
## Actions
|
|
17
|
-
|
|
18
|
-
### Search Issues
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
linear_linear(action="search") # Your active issues
|
|
22
|
-
linear_linear(action="search", query="auth bug") # Text search
|
|
23
|
-
linear_linear(action="search", query={state: "In Progress"}) # Filter
|
|
24
|
-
linear_linear(action="search", query={team: "ENG", assignee: "me"})
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
### Get Issue Details
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
linear_linear(action="get", id="ABC-123") # By short ID
|
|
31
|
-
linear_linear(action="get", id="https://linear.app/...") # By URL
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Returns: title, description, status, labels, comments, attachments.
|
|
35
|
-
|
|
36
|
-
### Update Issue
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
linear_linear(action="update", id="ABC-123", state="Done")
|
|
40
|
-
linear_linear(action="update", id="ABC-123", priority=1)
|
|
41
|
-
linear_linear(action="update", id="ABC-123", assignee="me")
|
|
42
|
-
linear_linear(action="update", id="ABC-123", labels=["bug", "existing-label"])
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
**Labels array replaces all labels.** Fetch current labels first, then append.
|
|
46
|
-
|
|
47
|
-
### Comment on Issue
|
|
48
|
-
|
|
49
|
-
```
|
|
50
|
-
linear_linear(action="comment", id="ABC-123", body="Fixed in commit abc123")
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### Create Issue
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
linear_linear(action="create", title="Bug: Login fails", team="ENG")
|
|
57
|
-
linear_linear(action="create", title="Bug", team="ENG", body="Details", priority=2)
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Raw GraphQL
|
|
61
|
-
|
|
62
|
-
```
|
|
63
|
-
linear_linear(action="graphql", graphql="query { viewer { name } }")
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### Help
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
linear_linear(action="help")
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## Reference
|
|
73
|
-
|
|
74
|
-
- Priority: 0=None, 1=Urgent, 2=High, 3=Medium, 4=Low
|
|
75
|
-
- State matching is fuzzy: "done" → "Done", "in prog" → "In Progress"
|
|
76
|
-
- IDs accept: `ABC-123`, Linear URLs, or UUIDs
|