@sjawhar/pi-legion-envoy 1.32.0 → 1.34.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 +54 -10
- package/dist/legion.js +126 -22
- package/dist/skills/dispatch/SKILL.md +27 -4
- package/dist/skills/envoy/SKILL.md +10 -0
- package/dist/skills/legion-architect/SKILL.md +1 -1
- package/package.json +2 -2
package/dist/envoy.js
CHANGED
|
@@ -29649,6 +29649,7 @@ function date4(params) {
|
|
|
29649
29649
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
29650
29650
|
config(en_default());
|
|
29651
29651
|
// ../contracts/src/dispatch-api.ts
|
|
29652
|
+
var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
|
|
29652
29653
|
function searchOwnerOf(result) {
|
|
29653
29654
|
return result.owner ?? { kind: "issue", key: result.issue.key };
|
|
29654
29655
|
}
|
|
@@ -29731,6 +29732,8 @@ var CommentEventPayloadSchema = object({
|
|
|
29731
29732
|
ask_id: string2().nullish(),
|
|
29732
29733
|
ask_question: string2().optional(),
|
|
29733
29734
|
ask_state: _enum2(["open", "answered", "resolved"]).optional(),
|
|
29735
|
+
ask_waiting_on: _enum2(["human", "agent"]).optional(),
|
|
29736
|
+
turn: _enum2(["human", "agent"]).nullish(),
|
|
29734
29737
|
anchor: object({ block_id: string2().nullable().optional(), quote: string2().optional() }).nullish(),
|
|
29735
29738
|
suggestion: object({ replace_with: string2().optional() }).nullish(),
|
|
29736
29739
|
author: object({ kind: string2(), id: string2() }).optional(),
|
|
@@ -29757,7 +29760,7 @@ var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
|
29757
29760
|
var MessageDeliveryEventPayloadSchema = object({
|
|
29758
29761
|
message_id: string2().optional(),
|
|
29759
29762
|
attempt: number2().int().positive().optional(),
|
|
29760
|
-
delivery: _enum2(
|
|
29763
|
+
delivery: _enum2(DELIVERY_CAPABILITIES).optional(),
|
|
29761
29764
|
session_id: string2().optional(),
|
|
29762
29765
|
target: string2().optional(),
|
|
29763
29766
|
title: string2().optional(),
|
|
@@ -29848,6 +29851,16 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
|
|
|
29848
29851
|
message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
|
|
29849
29852
|
};
|
|
29850
29853
|
}
|
|
29854
|
+
var commentValidation = (() => {
|
|
29855
|
+
const owner = documentOwnerValidation(true);
|
|
29856
|
+
return {
|
|
29857
|
+
check: (value) => {
|
|
29858
|
+
const input = value;
|
|
29859
|
+
return owner.check(value) && (input.turn === undefined || typeof input.reply_to_ask === "string");
|
|
29860
|
+
},
|
|
29861
|
+
message: `${owner.message} turn requires reply_to_ask.`
|
|
29862
|
+
};
|
|
29863
|
+
})();
|
|
29851
29864
|
var SPEC_SECTIONS = [
|
|
29852
29865
|
"Summary",
|
|
29853
29866
|
"Decisions needed",
|
|
@@ -29943,9 +29956,10 @@ var dispatchToolSpecs = [
|
|
|
29943
29956
|
occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
|
|
29944
29957
|
body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
|
|
29945
29958
|
reply_to: z.string().describe("Full id of a comment to reply to; replying to any comment in a thread continues that " + "thread (an ask's clarification thread included).").optional(),
|
|
29946
|
-
reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional()
|
|
29959
|
+
reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
|
|
29960
|
+
turn: z.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional()
|
|
29947
29961
|
}),
|
|
29948
|
-
validation:
|
|
29962
|
+
validation: commentValidation
|
|
29949
29963
|
},
|
|
29950
29964
|
{
|
|
29951
29965
|
name: "dispatch_suggest",
|
|
@@ -30308,6 +30322,19 @@ var stateRole = strictObject({
|
|
|
30308
30322
|
launchFailures: number2().int().nonnegative().optional(),
|
|
30309
30323
|
locator: stateLocator.optional()
|
|
30310
30324
|
});
|
|
30325
|
+
var stateQueuedWorkerIdentity = {
|
|
30326
|
+
roleToken: nonEmptyString,
|
|
30327
|
+
issue: nonEmptyString,
|
|
30328
|
+
role: nonEmptyString
|
|
30329
|
+
};
|
|
30330
|
+
var stateQueuedWorker = union([
|
|
30331
|
+
strictObject(stateQueuedWorkerIdentity),
|
|
30332
|
+
strictObject({
|
|
30333
|
+
...stateQueuedWorkerIdentity,
|
|
30334
|
+
kind: _enum2(["assignment", "catchup"]),
|
|
30335
|
+
queuedAt: nonEmptyString
|
|
30336
|
+
})
|
|
30337
|
+
]);
|
|
30311
30338
|
var LegionDaemonApi = {
|
|
30312
30339
|
State: {
|
|
30313
30340
|
response: strictObject({
|
|
@@ -30324,7 +30351,8 @@ var LegionDaemonApi = {
|
|
|
30324
30351
|
controllerLocator: stateLocator.optional(),
|
|
30325
30352
|
roles: record(string2(), stateRole),
|
|
30326
30353
|
controllerPendingNotices: number2().int().nonnegative(),
|
|
30327
|
-
pendingStatusWrites: array(nonEmptyString)
|
|
30354
|
+
pendingStatusWrites: array(nonEmptyString),
|
|
30355
|
+
workerAdmission: strictObject({ queue: array(stateQueuedWorker) })
|
|
30328
30356
|
})
|
|
30329
30357
|
},
|
|
30330
30358
|
ControllerReady: {
|
|
@@ -30411,7 +30439,8 @@ var LegionDaemonApi = {
|
|
|
30411
30439
|
request: architectCapability.extend({
|
|
30412
30440
|
issue: nonEmptyString,
|
|
30413
30441
|
role: legionRole,
|
|
30414
|
-
task: nonEmptyString
|
|
30442
|
+
task: nonEmptyString,
|
|
30443
|
+
requestId: uuid2()
|
|
30415
30444
|
}),
|
|
30416
30445
|
response: object({
|
|
30417
30446
|
status: _enum2(["spawned", "resumed", "queued"]),
|
|
@@ -31088,7 +31117,7 @@ var TolerantInboundEnvelopeSchema = object({
|
|
|
31088
31117
|
}).passthrough();
|
|
31089
31118
|
var DispatchDeliveryRequestSchema = object({
|
|
31090
31119
|
attempt: number2().int().positive(),
|
|
31091
|
-
mode: _enum2(
|
|
31120
|
+
mode: _enum2(DELIVERY_CAPABILITIES)
|
|
31092
31121
|
});
|
|
31093
31122
|
var DispatchTargetedFrameSchema = object({
|
|
31094
31123
|
event: DispatchEventSchema,
|
|
@@ -32580,21 +32609,34 @@ async function executeDispatchTool(input) {
|
|
|
32580
32609
|
if (replyTo !== undefined && replyToAsk !== undefined) {
|
|
32581
32610
|
throw new Error("reply_to and reply_to_ask cannot both be set");
|
|
32582
32611
|
}
|
|
32612
|
+
const requestedTurn = optionalString(args, "turn");
|
|
32613
|
+
if (requestedTurn !== undefined && replyToAsk === undefined) {
|
|
32614
|
+
throw new Error("turn requires reply_to_ask");
|
|
32615
|
+
}
|
|
32616
|
+
if (requestedTurn !== undefined && requestedTurn !== "agent" && requestedTurn !== "human") {
|
|
32617
|
+
throw new Error("turn must be agent or human");
|
|
32618
|
+
}
|
|
32583
32619
|
const commentInput = {
|
|
32584
32620
|
body: stringArg(args, "body"),
|
|
32585
32621
|
...anchored === undefined ? {} : { anchor: anchored },
|
|
32586
32622
|
...replyTo === undefined ? {} : { reply_to: replyTo },
|
|
32587
32623
|
...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
|
|
32624
|
+
...requestedTurn === undefined ? {} : { turn: requestedTurn },
|
|
32588
32625
|
actor
|
|
32589
32626
|
};
|
|
32590
32627
|
const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
|
|
32628
|
+
const askState = comment.turn === null ? "" : ` (ask now waiting on ${comment.turn})`;
|
|
32591
32629
|
return {
|
|
32592
|
-
text: `Posted comment ${comment.id}`,
|
|
32630
|
+
text: `Posted comment ${comment.id}${askState}`,
|
|
32593
32631
|
details: resolved === undefined ? {
|
|
32594
32632
|
issue: comment.issue_key,
|
|
32595
32633
|
topic: dispatchIssueSubject(issue(), ">"),
|
|
32596
|
-
comment: comment.id
|
|
32597
|
-
|
|
32634
|
+
comment: comment.id,
|
|
32635
|
+
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
32636
|
+
} : writeResultDetails(resolved, {
|
|
32637
|
+
comment: comment.id,
|
|
32638
|
+
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
32639
|
+
})
|
|
32598
32640
|
};
|
|
32599
32641
|
}
|
|
32600
32642
|
case "dispatch_suggest": {
|
|
@@ -33275,6 +33317,8 @@ async function copySessionID(copyToClipboard, sessionID) {
|
|
|
33275
33317
|
// extensions/envoy.ts
|
|
33276
33318
|
var codec2 = import_nats.StringCodec();
|
|
33277
33319
|
var NATS_RETRY_INTERVAL_MS = 15000;
|
|
33320
|
+
var ALL_CAPABILITIES = DELIVERY_CAPABILITIES;
|
|
33321
|
+
var CAPABILITIES_WITHOUT_BTW = DELIVERY_CAPABILITIES.filter((capability) => capability !== "btw");
|
|
33278
33322
|
var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
33279
33323
|
var OPEN_ASKS_TIMEOUT_MS = 3000;
|
|
33280
33324
|
function isRoleClaimEntry(entry) {
|
|
@@ -33533,7 +33577,7 @@ function envoyExtension(pi) {
|
|
|
33533
33577
|
topics: [...new Set([agentSubject(sessionID), ...subscriptions.keys()])],
|
|
33534
33578
|
port: 0,
|
|
33535
33579
|
title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
|
|
33536
|
-
capabilities: typeof pi.askEphemeral === "function" ?
|
|
33580
|
+
capabilities: typeof pi.askEphemeral === "function" ? ALL_CAPABILITIES : CAPABILITIES_WITHOUT_BTW,
|
|
33537
33581
|
driving: false,
|
|
33538
33582
|
selfSubscribed: true
|
|
33539
33583
|
});
|
package/dist/legion.js
CHANGED
|
@@ -16112,7 +16112,7 @@ var require_mod4 = __commonJS(function(exports) {
|
|
|
16112
16112
|
});
|
|
16113
16113
|
|
|
16114
16114
|
// extensions/legion.ts
|
|
16115
|
-
import { randomUUID as
|
|
16115
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
16116
16116
|
import fs from "fs";
|
|
16117
16117
|
import path4 from "path";
|
|
16118
16118
|
|
|
@@ -29649,6 +29649,7 @@ function date4(params) {
|
|
|
29649
29649
|
// ../../node_modules/.bun/zod@4.3.6/node_modules/zod/v4/classic/external.js
|
|
29650
29650
|
config(en_default());
|
|
29651
29651
|
// ../contracts/src/dispatch-api.ts
|
|
29652
|
+
var DELIVERY_CAPABILITIES = ["aside", "btw", "steer"];
|
|
29652
29653
|
function searchOwnerOf(result) {
|
|
29653
29654
|
return result.owner ?? { kind: "issue", key: result.issue.key };
|
|
29654
29655
|
}
|
|
@@ -29731,6 +29732,8 @@ var CommentEventPayloadSchema = object({
|
|
|
29731
29732
|
ask_id: string2().nullish(),
|
|
29732
29733
|
ask_question: string2().optional(),
|
|
29733
29734
|
ask_state: _enum2(["open", "answered", "resolved"]).optional(),
|
|
29735
|
+
ask_waiting_on: _enum2(["human", "agent"]).optional(),
|
|
29736
|
+
turn: _enum2(["human", "agent"]).nullish(),
|
|
29734
29737
|
anchor: object({ block_id: string2().nullable().optional(), quote: string2().optional() }).nullish(),
|
|
29735
29738
|
suggestion: object({ replace_with: string2().optional() }).nullish(),
|
|
29736
29739
|
author: object({ kind: string2(), id: string2() }).optional(),
|
|
@@ -29757,7 +29760,7 @@ var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
|
29757
29760
|
var MessageDeliveryEventPayloadSchema = object({
|
|
29758
29761
|
message_id: string2().optional(),
|
|
29759
29762
|
attempt: number2().int().positive().optional(),
|
|
29760
|
-
delivery: _enum2(
|
|
29763
|
+
delivery: _enum2(DELIVERY_CAPABILITIES).optional(),
|
|
29761
29764
|
session_id: string2().optional(),
|
|
29762
29765
|
target: string2().optional(),
|
|
29763
29766
|
title: string2().optional(),
|
|
@@ -29848,6 +29851,16 @@ function documentOwnerValidation(requireArtifact, alwaysRequireArtifact = false)
|
|
|
29848
29851
|
message: alwaysRequireArtifact ? "Exactly one of issue and project is required; artifact or ref must name the document." : "Exactly one of issue and project is required; with project, artifact names the document."
|
|
29849
29852
|
};
|
|
29850
29853
|
}
|
|
29854
|
+
var commentValidation = (() => {
|
|
29855
|
+
const owner = documentOwnerValidation(true);
|
|
29856
|
+
return {
|
|
29857
|
+
check: (value) => {
|
|
29858
|
+
const input = value;
|
|
29859
|
+
return owner.check(value) && (input.turn === undefined || typeof input.reply_to_ask === "string");
|
|
29860
|
+
},
|
|
29861
|
+
message: `${owner.message} turn requires reply_to_ask.`
|
|
29862
|
+
};
|
|
29863
|
+
})();
|
|
29851
29864
|
var SPEC_SECTIONS = [
|
|
29852
29865
|
"Summary",
|
|
29853
29866
|
"Decisions needed",
|
|
@@ -29943,9 +29956,10 @@ var dispatchToolSpecs = [
|
|
|
29943
29956
|
occurrence: z.number({ int: true, min: 0 }).describe("Optional zero-based occurrence of quote.").optional(),
|
|
29944
29957
|
body: z.string({ max: 2000 }).describe("Review comment, at most 2,000 characters."),
|
|
29945
29958
|
reply_to: z.string().describe("Full id of a comment to reply to; replying to any comment in a thread continues that " + "thread (an ask's clarification thread included).").optional(),
|
|
29946
|
-
reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional()
|
|
29959
|
+
reply_to_ask: z.string().describe("Optional ask id to reply to, threading this comment under that question. Mutually " + "exclusive with reply_to.").optional(),
|
|
29960
|
+
turn: z.enum(["agent", "human"]).describe("Only with reply_to_ask: who holds the turn after this reply. agent: a progress note - " + "you keep the turn and the ask stays 'Waiting on agents' for the human; human (default): " + "you need the human to act - the ask returns to 'Waiting on you'.").optional()
|
|
29947
29961
|
}),
|
|
29948
|
-
validation:
|
|
29962
|
+
validation: commentValidation
|
|
29949
29963
|
},
|
|
29950
29964
|
{
|
|
29951
29965
|
name: "dispatch_suggest",
|
|
@@ -30308,6 +30322,19 @@ var stateRole = strictObject({
|
|
|
30308
30322
|
launchFailures: number2().int().nonnegative().optional(),
|
|
30309
30323
|
locator: stateLocator.optional()
|
|
30310
30324
|
});
|
|
30325
|
+
var stateQueuedWorkerIdentity = {
|
|
30326
|
+
roleToken: nonEmptyString,
|
|
30327
|
+
issue: nonEmptyString,
|
|
30328
|
+
role: nonEmptyString
|
|
30329
|
+
};
|
|
30330
|
+
var stateQueuedWorker = union([
|
|
30331
|
+
strictObject(stateQueuedWorkerIdentity),
|
|
30332
|
+
strictObject({
|
|
30333
|
+
...stateQueuedWorkerIdentity,
|
|
30334
|
+
kind: _enum2(["assignment", "catchup"]),
|
|
30335
|
+
queuedAt: nonEmptyString
|
|
30336
|
+
})
|
|
30337
|
+
]);
|
|
30311
30338
|
var LegionDaemonApi = {
|
|
30312
30339
|
State: {
|
|
30313
30340
|
response: strictObject({
|
|
@@ -30324,7 +30351,8 @@ var LegionDaemonApi = {
|
|
|
30324
30351
|
controllerLocator: stateLocator.optional(),
|
|
30325
30352
|
roles: record(string2(), stateRole),
|
|
30326
30353
|
controllerPendingNotices: number2().int().nonnegative(),
|
|
30327
|
-
pendingStatusWrites: array(nonEmptyString)
|
|
30354
|
+
pendingStatusWrites: array(nonEmptyString),
|
|
30355
|
+
workerAdmission: strictObject({ queue: array(stateQueuedWorker) })
|
|
30328
30356
|
})
|
|
30329
30357
|
},
|
|
30330
30358
|
ControllerReady: {
|
|
@@ -30411,7 +30439,8 @@ var LegionDaemonApi = {
|
|
|
30411
30439
|
request: architectCapability.extend({
|
|
30412
30440
|
issue: nonEmptyString,
|
|
30413
30441
|
role: legionRole,
|
|
30414
|
-
task: nonEmptyString
|
|
30442
|
+
task: nonEmptyString,
|
|
30443
|
+
requestId: uuid2()
|
|
30415
30444
|
}),
|
|
30416
30445
|
response: object({
|
|
30417
30446
|
status: _enum2(["spawned", "resumed", "queued"]),
|
|
@@ -30637,6 +30666,14 @@ function parseControlDirective(raw) {
|
|
|
30637
30666
|
}
|
|
30638
30667
|
|
|
30639
30668
|
// src/legion/daemon-client.ts
|
|
30669
|
+
function defaultSleep(ms) {
|
|
30670
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
30671
|
+
setTimeout(resolve, ms);
|
|
30672
|
+
return promise;
|
|
30673
|
+
}
|
|
30674
|
+
var SPAWN_WORKER_RETRY_DELAYS_MS = [2000, 5000];
|
|
30675
|
+
var SPAWN_WORKER_ATTEMPTS = SPAWN_WORKER_RETRY_DELAYS_MS.length + 1;
|
|
30676
|
+
|
|
30640
30677
|
class LegionDaemonApiError extends Error {
|
|
30641
30678
|
method;
|
|
30642
30679
|
path;
|
|
@@ -30650,6 +30687,30 @@ class LegionDaemonApiError extends Error {
|
|
|
30650
30687
|
this.responseBody = responseBody;
|
|
30651
30688
|
}
|
|
30652
30689
|
}
|
|
30690
|
+
|
|
30691
|
+
class LegionDaemonTransportError extends Error {
|
|
30692
|
+
path;
|
|
30693
|
+
requestId;
|
|
30694
|
+
attempts;
|
|
30695
|
+
constructor(path, requestId, attempts, cause) {
|
|
30696
|
+
super(`POST ${path} got no response in ${attempts} attempts (request ${requestId}): ${messageFor(cause)}. The daemon may have received the request; read legion state (workerAdmission.queue and the role's claim) before sending it again.`, { cause });
|
|
30697
|
+
this.path = path;
|
|
30698
|
+
this.requestId = requestId;
|
|
30699
|
+
this.attempts = attempts;
|
|
30700
|
+
this.name = "LegionDaemonTransportError";
|
|
30701
|
+
}
|
|
30702
|
+
}
|
|
30703
|
+
|
|
30704
|
+
class LegionDaemonResponseReadError extends Error {
|
|
30705
|
+
path;
|
|
30706
|
+
requestId;
|
|
30707
|
+
constructor(path, requestId, cause) {
|
|
30708
|
+
super(`POST ${path} response body could not be read (request ${requestId}): ${messageFor(cause)}. The daemon may have received the request; read legion state (workerAdmission.queue and the role's claim) before sending it again.`, { cause });
|
|
30709
|
+
this.path = path;
|
|
30710
|
+
this.requestId = requestId;
|
|
30711
|
+
this.name = "LegionDaemonResponseReadError";
|
|
30712
|
+
}
|
|
30713
|
+
}
|
|
30653
30714
|
function isInvalidSessionSecret(error) {
|
|
30654
30715
|
if (!(error instanceof LegionDaemonApiError) || error.status !== 403)
|
|
30655
30716
|
return false;
|
|
@@ -30668,19 +30729,42 @@ function sessionCapability(body) {
|
|
|
30668
30729
|
return { sessionId: candidate.sessionId, secret: candidate.secret };
|
|
30669
30730
|
}
|
|
30670
30731
|
var WORKER_SESSION_PATH = "/legion/v1/worker-session";
|
|
30671
|
-
function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery) {
|
|
30732
|
+
function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery, sleep = defaultSleep) {
|
|
30672
30733
|
const endpoint = baseUrl.replace(/\/+$/, "");
|
|
30673
|
-
const postOnce = async (path, body, schema) => {
|
|
30674
|
-
const response = await
|
|
30734
|
+
const postOnce = async (path, body, schema, fetchImpl = fetchFn, onResponseReadError) => {
|
|
30735
|
+
const response = await fetchImpl(`${endpoint}${path}`, {
|
|
30675
30736
|
method: "POST",
|
|
30676
30737
|
headers: { "Content-Type": "application/json" },
|
|
30677
30738
|
body: JSON.stringify(body)
|
|
30678
30739
|
});
|
|
30679
|
-
|
|
30740
|
+
let responseBody;
|
|
30741
|
+
try {
|
|
30742
|
+
responseBody = await response.text();
|
|
30743
|
+
} catch (error) {
|
|
30744
|
+
throw onResponseReadError?.(error) ?? error;
|
|
30745
|
+
}
|
|
30680
30746
|
if (!response.ok)
|
|
30681
30747
|
throw new LegionDaemonApiError("POST", path, response.status, responseBody);
|
|
30682
30748
|
return schema.parse(JSON.parse(responseBody));
|
|
30683
30749
|
};
|
|
30750
|
+
const transportRetryingFetch = (path, requestId) => {
|
|
30751
|
+
let transportFailures = 0;
|
|
30752
|
+
return async (input, init) => {
|
|
30753
|
+
for (;; ) {
|
|
30754
|
+
try {
|
|
30755
|
+
return await fetchFn(input, init);
|
|
30756
|
+
} catch (error) {
|
|
30757
|
+
transportFailures += 1;
|
|
30758
|
+
const delay = SPAWN_WORKER_RETRY_DELAYS_MS[transportFailures - 1];
|
|
30759
|
+
if (delay === undefined) {
|
|
30760
|
+
throw new LegionDaemonTransportError(path, requestId, transportFailures, error);
|
|
30761
|
+
}
|
|
30762
|
+
console.error(`[legion] POST ${path} attempt ${transportFailures}/${SPAWN_WORKER_ATTEMPTS} got no response (${messageFor(error)}); retrying request ${requestId} in ${delay} ms`);
|
|
30763
|
+
await sleep(delay);
|
|
30764
|
+
}
|
|
30765
|
+
}
|
|
30766
|
+
};
|
|
30767
|
+
};
|
|
30684
30768
|
const recoveries = new Map;
|
|
30685
30769
|
const secretAfterRefusal = async (active, sessionId, refusedSecret) => {
|
|
30686
30770
|
let record = recoveries.get(sessionId);
|
|
@@ -30705,16 +30789,16 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery) {
|
|
|
30705
30789
|
}
|
|
30706
30790
|
return (await record.inFlight).secret;
|
|
30707
30791
|
};
|
|
30708
|
-
const post = async (path, body, schema) => {
|
|
30792
|
+
const post = async (path, body, schema, fetchImpl = fetchFn, onResponseReadError) => {
|
|
30709
30793
|
try {
|
|
30710
|
-
return await postOnce(path, body, schema);
|
|
30794
|
+
return await postOnce(path, body, schema, fetchImpl, onResponseReadError);
|
|
30711
30795
|
} catch (error) {
|
|
30712
30796
|
const capability = sessionCapability(body);
|
|
30713
30797
|
if (path === WORKER_SESSION_PATH || recovery === undefined || capability === undefined || !isInvalidSessionSecret(error)) {
|
|
30714
30798
|
throw error;
|
|
30715
30799
|
}
|
|
30716
30800
|
const secret = await secretAfterRefusal(recovery, capability.sessionId, capability.secret);
|
|
30717
|
-
return await postOnce(path, { ...body, secret }, schema);
|
|
30801
|
+
return await postOnce(path, { ...body, secret }, schema, fetchImpl, onResponseReadError);
|
|
30718
30802
|
}
|
|
30719
30803
|
};
|
|
30720
30804
|
const get = async (path, schema) => {
|
|
@@ -30734,7 +30818,7 @@ function createLegionDaemonClient(baseUrl, fetchFn = fetch, recovery) {
|
|
|
30734
30818
|
processReady: (input) => noContent("/legion/v1/process/ready", input, LegionDaemonApi.ProcessReady.response),
|
|
30735
30819
|
workerStarted: (input) => post("/legion/v1/worker/started", input, LegionDaemonApi.WorkerStarted.response),
|
|
30736
30820
|
workerReady: (input) => noContent("/legion/v1/worker/ready", input, LegionDaemonApi.WorkerReady.response),
|
|
30737
|
-
spawnWorker: (input) => post("/legion/v1/worker/spawn", input, LegionDaemonApi.SpawnWorker.response),
|
|
30821
|
+
spawnWorker: (input) => post("/legion/v1/worker/spawn", input, LegionDaemonApi.SpawnWorker.response, transportRetryingFetch("/legion/v1/worker/spawn", input.requestId), (error) => new LegionDaemonResponseReadError("/legion/v1/worker/spawn", input.requestId, error)),
|
|
30738
30822
|
releaseWave: (input) => post("/legion/v1/waves/release", input, LegionDaemonApi.WaveRelease.response),
|
|
30739
30823
|
provisioningCredential: (input) => post("/legion/v1/provisioning-credential", input, LegionDaemonApi.ProvisioningCredential.response),
|
|
30740
30824
|
escalate: (input) => noContent("/legion/v1/escalate", input, LegionDaemonApi.Escalate.response),
|
|
@@ -30813,6 +30897,9 @@ commit_trailers = '"Omp-Session: ${id}"'
|
|
|
30813
30897
|
}
|
|
30814
30898
|
}
|
|
30815
30899
|
|
|
30900
|
+
// src/legion/tools.ts
|
|
30901
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
30902
|
+
|
|
30816
30903
|
// src/tool-result.ts
|
|
30817
30904
|
function toolSuccess(text, details = {}) {
|
|
30818
30905
|
return { content: [{ type: "text", text }], details };
|
|
@@ -30868,7 +30955,7 @@ function createLegionTool(deps) {
|
|
|
30868
30955
|
return {
|
|
30869
30956
|
name: "legion",
|
|
30870
30957
|
label: "legion",
|
|
30871
|
-
description: "Perform a Legion lifecycle write through the Legion daemon. " + "register_gate records the root spec document a human must approve: `artifactId` is the " + "document id (a UUID) and `version` the version number, both copied from the `artifact` and " + "`version` fields of dispatch_request_approval's result \u2014 never the slug or file name you " + "passed to that tool. " + `spawn_worker's response "status" means: "spawned" \u2014 a fresh pane just opened and is ` + 'running now; "resumed" \u2014 an existing worker was prompted directly over its live socket ' + "and its turn started, or (if its boot has not confirmed yet) its task was recorded to " + 'deliver once that boot completes; "queued" \u2014 the task was recorded and this role will ' + "start on its own: either the running-worker cap is full, or the live worker acknowledged " + "the task without starting a turn and the daemon is retrying it. Never re-spawn a role " + 'after "resumed" or "queued" \u2014 wait for the worker-started notification instead.',
|
|
30958
|
+
description: "Perform a Legion lifecycle write through the Legion daemon. " + "register_gate records the root spec document a human must approve: `artifactId` is the " + "document id (a UUID) and `version` the version number, both copied from the `artifact` and " + "`version` fields of dispatch_request_approval's result \u2014 never the slug or file name you " + "passed to that tool. " + `spawn_worker's response "status" means: "spawned" \u2014 a fresh pane just opened and is ` + 'running now; "resumed" \u2014 an existing worker was prompted directly over its live socket ' + "and its turn started, or (if its boot has not confirmed yet) its task was recorded to " + 'deliver once that boot completes; "queued" \u2014 the task was recorded and this role will ' + "start on its own: either the running-worker cap is full, or the live worker acknowledged " + "the task without starting a turn and the daemon is retrying it. Never re-spawn a role " + 'after "resumed" or "queued" \u2014 wait for the worker-started notification instead. A call ' + 'that fails with "got no response in 3 attempts" was retried by the plugin with one ' + "request id; the daemon may still have received it \u2014 read legion state " + "(workerAdmission.queue, and the role in roles) before sending it again. A spawn_worker " + "identical to the task already queued for the role changes nothing and is not announced again.",
|
|
30872
30959
|
defaultInactive: true,
|
|
30873
30960
|
parameters: legionToolSchema(pi),
|
|
30874
30961
|
execute: async (_id, parameters, _signal, _onUpdate, context) => {
|
|
@@ -30958,13 +31045,15 @@ function createLegionTool(deps) {
|
|
|
30958
31045
|
if (typeof role !== "string" || !LEGION_ROLES.includes(role)) {
|
|
30959
31046
|
throw new Error("spawn_worker requires a valid Legion role");
|
|
30960
31047
|
}
|
|
31048
|
+
const requestId = randomUUID2();
|
|
30961
31049
|
return jsonSuccess(await daemon.spawnWorker({
|
|
30962
31050
|
tree: architect.tree,
|
|
30963
31051
|
sessionId,
|
|
30964
31052
|
secret: architect.secret,
|
|
30965
31053
|
issue: stringInput("issue"),
|
|
30966
31054
|
role,
|
|
30967
|
-
task: stringInput("task")
|
|
31055
|
+
task: stringInput("task"),
|
|
31056
|
+
requestId
|
|
30968
31057
|
}));
|
|
30969
31058
|
}
|
|
30970
31059
|
default:
|
|
@@ -31541,7 +31630,7 @@ var TolerantInboundEnvelopeSchema = object({
|
|
|
31541
31630
|
}).passthrough();
|
|
31542
31631
|
var DispatchDeliveryRequestSchema = object({
|
|
31543
31632
|
attempt: number2().int().positive(),
|
|
31544
|
-
mode: _enum2(
|
|
31633
|
+
mode: _enum2(DELIVERY_CAPABILITIES)
|
|
31545
31634
|
});
|
|
31546
31635
|
var DispatchTargetedFrameSchema = object({
|
|
31547
31636
|
event: DispatchEventSchema,
|
|
@@ -33011,21 +33100,34 @@ async function executeDispatchTool(input) {
|
|
|
33011
33100
|
if (replyTo !== undefined && replyToAsk !== undefined) {
|
|
33012
33101
|
throw new Error("reply_to and reply_to_ask cannot both be set");
|
|
33013
33102
|
}
|
|
33103
|
+
const requestedTurn = optionalString(args, "turn");
|
|
33104
|
+
if (requestedTurn !== undefined && replyToAsk === undefined) {
|
|
33105
|
+
throw new Error("turn requires reply_to_ask");
|
|
33106
|
+
}
|
|
33107
|
+
if (requestedTurn !== undefined && requestedTurn !== "agent" && requestedTurn !== "human") {
|
|
33108
|
+
throw new Error("turn must be agent or human");
|
|
33109
|
+
}
|
|
33014
33110
|
const commentInput = {
|
|
33015
33111
|
body: stringArg(args, "body"),
|
|
33016
33112
|
...anchored === undefined ? {} : { anchor: anchored },
|
|
33017
33113
|
...replyTo === undefined ? {} : { reply_to: replyTo },
|
|
33018
33114
|
...replyToAsk === undefined ? {} : { ask_id: replyToAsk },
|
|
33115
|
+
...requestedTurn === undefined ? {} : { turn: requestedTurn },
|
|
33019
33116
|
actor
|
|
33020
33117
|
};
|
|
33021
33118
|
const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
|
|
33119
|
+
const askState = comment.turn === null ? "" : ` (ask now waiting on ${comment.turn})`;
|
|
33022
33120
|
return {
|
|
33023
|
-
text: `Posted comment ${comment.id}`,
|
|
33121
|
+
text: `Posted comment ${comment.id}${askState}`,
|
|
33024
33122
|
details: resolved === undefined ? {
|
|
33025
33123
|
issue: comment.issue_key,
|
|
33026
33124
|
topic: dispatchIssueSubject(issue(), ">"),
|
|
33027
|
-
comment: comment.id
|
|
33028
|
-
|
|
33125
|
+
comment: comment.id,
|
|
33126
|
+
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
33127
|
+
} : writeResultDetails(resolved, {
|
|
33128
|
+
comment: comment.id,
|
|
33129
|
+
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
33130
|
+
})
|
|
33029
33131
|
};
|
|
33030
33132
|
}
|
|
33031
33133
|
case "dispatch_suggest": {
|
|
@@ -33698,6 +33800,8 @@ async function copySessionID(copyToClipboard, sessionID) {
|
|
|
33698
33800
|
// extensions/envoy.ts
|
|
33699
33801
|
var codec2 = import_nats.StringCodec();
|
|
33700
33802
|
var NATS_RETRY_INTERVAL_MS = 15000;
|
|
33803
|
+
var ALL_CAPABILITIES = DELIVERY_CAPABILITIES;
|
|
33804
|
+
var CAPABILITIES_WITHOUT_BTW = DELIVERY_CAPABILITIES.filter((capability) => capability !== "btw");
|
|
33701
33805
|
var ROLE_CLAIM_ENTRY = "envoy-role-claim";
|
|
33702
33806
|
var OPEN_ASKS_TIMEOUT_MS = 3000;
|
|
33703
33807
|
function isRoleClaimEntry(entry) {
|
|
@@ -33956,7 +34060,7 @@ function envoyExtension(pi) {
|
|
|
33956
34060
|
topics: [...new Set([agentSubject(sessionID), ...subscriptions.keys()])],
|
|
33957
34061
|
port: 0,
|
|
33958
34062
|
title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
|
|
33959
|
-
capabilities: typeof pi.askEphemeral === "function" ?
|
|
34063
|
+
capabilities: typeof pi.askEphemeral === "function" ? ALL_CAPABILITIES : CAPABILITIES_WITHOUT_BTW,
|
|
33960
34064
|
driving: false,
|
|
33961
34065
|
selfSubscribed: true
|
|
33962
34066
|
});
|
|
@@ -34707,7 +34811,7 @@ function isToolDeviceInvocation(toolCall) {
|
|
|
34707
34811
|
return toolCall.toolName === "write" && typeof toolCall.input.path === "string" && toolCall.input.path.startsWith("xd://");
|
|
34708
34812
|
}
|
|
34709
34813
|
function legionExtension(pi) {
|
|
34710
|
-
const instance =
|
|
34814
|
+
const instance = randomUUID3().slice(0, 8);
|
|
34711
34815
|
logger2.debug("extension instance loaded", { extension: import.meta.url, instance });
|
|
34712
34816
|
globalThis[LEGION_LOADED_MARKER] = import.meta.url;
|
|
34713
34817
|
const defaults = envoyDefaultsFromEnvironment(process.env);
|
|
@@ -42,6 +42,16 @@ agent's `envoy.json` to set the GitHub OAuth callback origin: the value must
|
|
|
42
42
|
be the exact URL humans type in their browser, and the GitHub App callback is
|
|
43
43
|
`<DISPATCH_SERVER_URL>/auth/callback`.
|
|
44
44
|
|
|
45
|
+
### Finding a route
|
|
46
|
+
|
|
47
|
+
The tools cover the everyday surface. For anything else, ask the server: `GET /api/v1` (no
|
|
48
|
+
credential) returns every route as `{method, path, auth, description}` sorted by path — `auth`
|
|
49
|
+
is `public`, `any` (a human or a bearer), `human` (a bearer gets `403 HUMAN_ONLY`), or `bearer`.
|
|
50
|
+
A path Dispatch does not serve under `/api` or `/v1` answers
|
|
51
|
+
`404 {"code":"NOT_FOUND","error":"no route for GET /v1/issues","hint":"GET /api/v1 lists every
|
|
52
|
+
route"}`; when you see that, you typed the path wrong — read the index rather than guessing. Every
|
|
53
|
+
`/api/v1` error body carries a `code`; branch on the code, never on the text.
|
|
54
|
+
|
|
45
55
|
## Writing a spec
|
|
46
56
|
|
|
47
57
|
A spec has two readers: the human who decides reads the top; the implementer who builds reads the
|
|
@@ -194,6 +204,14 @@ an answer: the human did not understand the question or needs more before choosi
|
|
|
194
204
|
in the same thread with `dispatch_comment({ reply_to_ask })`, or reword the question itself with `dispatch_edit_ask` when the wording
|
|
195
205
|
was the problem; either puts the ask back in front of them. Do not open a second ask.
|
|
196
206
|
|
|
207
|
+
Every reply to an open ask says whose turn it is next, and the Inbox files the ask by that, not by who spoke last. Your plain reply
|
|
208
|
+
(`turn` omitted, or `turn: "human"`) hands the turn to the human: the ask returns to their `Waiting on you`. When you are not done
|
|
209
|
+
yet — "dispatched two auditors, back with results", "checking the release branch, back shortly", any working-on-it note — reply with
|
|
210
|
+
`turn: "agent"`: the note lands in the thread, the ask stays under `Waiting on agents`, and the human is not told to act. Use
|
|
211
|
+
`turn: "agent"` for every progress note and `turn: "human"` (the default) only when you need them. A human's reply always hands the
|
|
212
|
+
turn to you. The result names the state (`ask now waiting on agent` / `human`), the delivered `comment.created` carries it as
|
|
213
|
+
`ask_waiting_on`, and every ask read carries it as `waiting_on`.
|
|
214
|
+
|
|
197
215
|
## Approval of a spec
|
|
198
216
|
|
|
199
217
|
Approval is a property of a document, not a question you phrase: a human approves a specific version, the way a pull-request
|
|
@@ -300,7 +318,7 @@ an ask block without a question or with a blank option is rejected with `INVALID
|
|
|
300
318
|
Add feedback with:
|
|
301
319
|
|
|
302
320
|
```ts
|
|
303
|
-
dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask? })
|
|
321
|
+
dispatch_comment({ issue?, project?, artifact?, ref?, quote?, occurrence?, body, reply_to?, reply_to_ask?, turn? })
|
|
304
322
|
```
|
|
305
323
|
|
|
306
324
|
It returns issue or project-document owner details plus `comment` and, for writes, `topic`.
|
|
@@ -309,8 +327,10 @@ It returns issue or project-document owner details plus `comment` and, for write
|
|
|
309
327
|
for display. Omit both for a floating issue comment. A reply (`reply_to`/`reply_to_ask`) takes no
|
|
310
328
|
`quote`; it belongs to its parent's anchor. Reply to any comment in a thread; the server keeps
|
|
311
329
|
threads flat. A reply to a resolved thread reopens it. Use `reply_to_ask` to reply directly under a
|
|
312
|
-
question asked with `dispatch_ask
|
|
313
|
-
|
|
330
|
+
question asked with `dispatch_ask`; `turn` (only with `reply_to_ask`) says who holds the turn after
|
|
331
|
+
the reply — `agent` for a progress note that keeps the ask waiting on you, `human` (the default) when
|
|
332
|
+
the human needs to act; see [Asking](#asking). Comments are edited only by their author from the
|
|
333
|
+
dashboard. A delivered `comment.created` event carries the comment `id`; reply to it with
|
|
314
334
|
`dispatch_comment({ reply_to: <id> })`.
|
|
315
335
|
|
|
316
336
|
Propose an exact replacement instead of describing it:
|
|
@@ -395,7 +415,10 @@ dispatch_message({
|
|
|
395
415
|
delivery can post its answer automatically; use this call when the frame asks the primary agent to
|
|
396
416
|
reply. `dispatch_message` itself never carries `target` or `delivery`: agent-to-agent traffic goes
|
|
397
417
|
through Envoy or the hub. A bearer that targets over HTTP names its own session in `actor`
|
|
398
|
-
(`{kind: "session", id}`), and the card shows that session as the author.
|
|
418
|
+
(`{kind: "session", id}`), and the card shows that session as the author. `GET /api/v1/agents`
|
|
419
|
+
(any authenticated caller) lists live sessions with their capabilities (`aside`, `btw`, `steer`);
|
|
420
|
+
target only a session that advertises the mode you want. Sending to a session with no issue
|
|
421
|
+
(`POST /api/v1/agents/{session_id}/messages`) stays human-only.
|
|
399
422
|
|
|
400
423
|
## What comes back
|
|
401
424
|
|
|
@@ -90,6 +90,16 @@ envoy_send(
|
|
|
90
90
|
)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
### Delivery capabilities
|
|
94
|
+
|
|
95
|
+
Each session row from `envoy_sessions` carries `capabilities`, the targeted-delivery modes that
|
|
96
|
+
session's host honours: `aside` (a message queued beside the model's work), `btw` (an ephemeral
|
|
97
|
+
question the host answers without disturbing the current turn), and `steer` (an interjection at
|
|
98
|
+
the next tool boundary). An OMP session advertises `aside`, `btw`, and `steer` (`aside` and
|
|
99
|
+
`steer` on a host without `askEphemeral`); a Claude Code session advertises `aside` only, because
|
|
100
|
+
its channel notifications queue for the next turn. Target a session only with a mode it
|
|
101
|
+
advertises.
|
|
102
|
+
|
|
93
103
|
## Waiting for CI or a merge
|
|
94
104
|
|
|
95
105
|
Subscribe to `notifications.github.example-org.example-repo.pr.42.>` and end the turn. The single
|
|
@@ -301,7 +301,7 @@ active phase worker.
|
|
|
301
301
|
| `design-approved` | Payload `{type:"design-approved"}`. A human approved the root spec document at its current version; the gate is open. Proceed to section 2. |
|
|
302
302
|
| `design-changes-requested` | Payload `{type:"design-changes-requested", version, reason, author?}`. A human asked for changes to the root spec at `version`, for `reason`. Revise the spec, call `dispatch_request_approval` again, and stay parked; the gate is closed. |
|
|
303
303
|
| `phase-complete` | Payload `{type:"phase-complete", issue, role, summary}`. May arrive live or via `catchup-overseer`'s `phaseCompletions`. Read the committed handoff for that phase, then spawn the next phase's owner, or `spawn_worker` on the same role again to resume it with corrections if the handoff shows unresolved gaps. A `reviewer` completion whose GitHub review is `CHANGES_REQUESTED` (the daemon returns the issue's Dispatch status to `in_progress` for this, on the reviewer's completion and again when you spawn the corrective implementer unless the daemon already knows the issue is `in_progress`) means `spawn_worker` the **implementer** again with the review findings — thread URLs and blocking items — as its task, then route back through tester and reviewer in order; never `spawn_worker` the reviewer directly off this wake and never proceed to retro on this verdict. A reviewer completion with an `APPROVED` review proceeds to retro (step 5). A `reviewer` completion after a conflict-forced rebase whose review body names an unchanged fingerprint is a confirmation, not a round: if retro already completed, `spawn_worker` the merger; otherwise resume the step you were on. An `implementer` completion that follows the merge is its production report: read the record on the pull request and the issue, then run step 7 — the issue is already at `retro`, the daemon writes no status for this completion, and you set `done` yourself. A `tester` completion whose handoff carries `implementerProof.verdict: "rejected"`, or a failure naming the production-like proof, goes back to the **implementer** with that finding — never forward to the reviewer, and never by supplying the proof from another role. A worker that reports no surface reaches the changed path gets a child issue in this tree (infrastructure, tooling, or a skill) and a resume once it lands; that report is never a reason to advance the phase. |
|
|
304
|
-
| `worker-queued` | Payload `{type:"worker-queued", issue, role}`. This role's task is queued for promotion — either the deployment's worker cap is full, or the live worker acknowledged the task without starting a turn and the daemon is retrying it (counted; the worker is replaced after three such failures, still with the same task). Do not respawn or retry — wait for `worker-started`. |
|
|
304
|
+
| `worker-queued` | Payload `{type:"worker-queued", issue, role}`. This role's task is queued for promotion — either the deployment's worker cap is full, or the live worker acknowledged the task without starting a turn and the daemon is retrying it (counted; the worker is replaced after three such failures, still with the same task). Do not respawn or retry — wait for `worker-started`. `legion state` shows the queue (`workerAdmission.queue`: role token, issue, role, kind, and the time the task was first queued — never the task text); read it before re-sending. A `spawn_worker` identical to the queued task changes nothing and is not announced again. Different text replaces the queued task silently in the same FIFO slot and retains its original queue time. A `spawn_worker` that fails with "got no response in 3 attempts" was already retried by the plugin under one request id and may still have reached the daemon: read the queue and the role's claim in `legion state` before sending it again. |
|
|
305
305
|
| `worker-started` | Payload `{type:"worker-started", issue, role}`. A previously queued role has been promoted and is now running. Treat it exactly as a normal spawn: resume tracking that role's live session. |
|
|
306
306
|
| `pr-ready` | Verify the live PR head, green status, and review state. Continue the review/retro/merger order only for that current head. |
|
|
307
307
|
| `pr-review` | Payload `{type:"pr-review", state, author, body}`. Delivered to whichever role is currently active for the issue, falling back to you when no worker phase is active. Follows the same verdict rule as a reviewer's `phase-complete`: `state: "changes_requested"` sends the implementer back in with the review findings, then tester, then reviewer — never the reviewer again and never retro; that `spawn_worker` returns the issue to `in_progress` on its own (the daemon writes it for a corrective implementer whenever the PR's latest recorded review is changes requested, a human's after approval included), so you set nothing by hand; `state: "approved"` proceeds toward retro (step 5) once the step 6 integration/merge-gate conditions are met. `state: "approved"` on a rebased head whose body names an unchanged fingerprint is that confirmation: proceed to retro if it has not run, otherwise to the merger — never to a second retro or test round. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sjawhar/pi-legion-envoy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.34.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"omp": {
|
|
6
6
|
"extensions": [
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
]
|
|
13
13
|
},
|
|
14
14
|
"legion": {
|
|
15
|
-
"daemonApiVersion":
|
|
15
|
+
"daemonApiVersion": 4
|
|
16
16
|
},
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|