@sjawhar/pi-legion-envoy 1.27.2 → 1.28.1
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 +19 -11
- package/dist/legion.js +19 -11
- package/dist/skills/envoy/SKILL.md +10 -5
- package/dist/skills/legion-retro/SKILL.md +1 -1
- package/dist/skills/legion-worker/SKILL.md +10 -8
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -29746,7 +29746,7 @@ var MessageEventPayloadSchema = object({
|
|
|
29746
29746
|
});
|
|
29747
29747
|
var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
29748
29748
|
id: string2(),
|
|
29749
|
-
issue_key: string2(),
|
|
29749
|
+
issue_key: string2().nullable(),
|
|
29750
29750
|
author: object({ kind: string2(), id: string2() }),
|
|
29751
29751
|
body: string2(),
|
|
29752
29752
|
target: string2(),
|
|
@@ -29759,6 +29759,7 @@ var MessageDeliveryEventPayloadSchema = object({
|
|
|
29759
29759
|
attempt: number2().int().positive().optional(),
|
|
29760
29760
|
delivery: _enum2(["btw", "aside", "steer"]).optional(),
|
|
29761
29761
|
session_id: string2().optional(),
|
|
29762
|
+
target: string2().optional(),
|
|
29762
29763
|
title: string2().optional(),
|
|
29763
29764
|
state: _enum2(["sent", "failed"]).optional(),
|
|
29764
29765
|
error: string2().optional()
|
|
@@ -31095,7 +31096,7 @@ var DispatchTargetedFrameSchema = object({
|
|
|
31095
31096
|
}).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
|
|
31096
31097
|
var RecoverableDispatchDeliveryFailureSchema = object({
|
|
31097
31098
|
event: object({
|
|
31098
|
-
issue_key: string2(),
|
|
31099
|
+
issue_key: string2().nullable(),
|
|
31099
31100
|
payload: object({ id: string2(), body: string2().optional() }).passthrough()
|
|
31100
31101
|
}).passthrough(),
|
|
31101
31102
|
delivery: DispatchDeliveryRequestSchema
|
|
@@ -31298,7 +31299,7 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31298
31299
|
askQuestion = dispatchAskQuestion(frame.event);
|
|
31299
31300
|
messageReplyPreview = dispatchMessageReplyPreview(frame.event);
|
|
31300
31301
|
dispatchReply = dispatchCommentReplyWith(frame.event, subject ?? envelope.topic);
|
|
31301
|
-
if (frame.event.type === "message.created"
|
|
31302
|
+
if (frame.event.type === "message.created") {
|
|
31302
31303
|
const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
|
|
31303
31304
|
const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
|
|
31304
31305
|
if (message.success && requested.success && message.data.id !== undefined) {
|
|
@@ -31309,7 +31310,9 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31309
31310
|
issueKey: frame.event.issue_key,
|
|
31310
31311
|
body: message.data.body ?? envelope.payload_summary ?? ""
|
|
31311
31312
|
};
|
|
31312
|
-
|
|
31313
|
+
if (frame.event.issue_key !== null) {
|
|
31314
|
+
dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
|
|
31315
|
+
}
|
|
31313
31316
|
}
|
|
31314
31317
|
}
|
|
31315
31318
|
dispatchEvent = {
|
|
@@ -32614,17 +32617,18 @@ async function executeDispatchTool(input) {
|
|
|
32614
32617
|
}
|
|
32615
32618
|
case "dispatch_message": {
|
|
32616
32619
|
const inReplyTo = messageInReplyTo(args);
|
|
32617
|
-
const
|
|
32620
|
+
const issueKey = issue();
|
|
32621
|
+
const message = await client.message(issueKey, {
|
|
32618
32622
|
body: stringArg(args, "body"),
|
|
32619
32623
|
...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
|
|
32620
32624
|
actor
|
|
32621
32625
|
});
|
|
32622
|
-
const messageRef = `dispatch://${
|
|
32626
|
+
const messageRef = `dispatch://${issueKey}/message/${message.id}`;
|
|
32623
32627
|
return {
|
|
32624
32628
|
text: `Posted message ${message.id} (${messageRef})`,
|
|
32625
32629
|
details: {
|
|
32626
|
-
issue:
|
|
32627
|
-
topic: dispatchIssueSubject(
|
|
32630
|
+
issue: issueKey,
|
|
32631
|
+
topic: dispatchIssueSubject(issueKey, ">"),
|
|
32628
32632
|
message: message.id
|
|
32629
32633
|
}
|
|
32630
32634
|
};
|
|
@@ -33396,6 +33400,13 @@ function envoyExtension(pi) {
|
|
|
33396
33400
|
};
|
|
33397
33401
|
const deliver = async (subject, raw, reply) => {
|
|
33398
33402
|
const rendered = renderInbound(raw, sessionID, subject);
|
|
33403
|
+
if (reply !== "" && subject === agentSubject(sessionID)) {
|
|
33404
|
+
try {
|
|
33405
|
+
(await ensureConnection()).publish(reply);
|
|
33406
|
+
} catch (error) {
|
|
33407
|
+
console.warn(`[envoy] failed to acknowledge envelope ${rendered.envelope?.event_id ?? "unknown"}`, error);
|
|
33408
|
+
}
|
|
33409
|
+
}
|
|
33399
33410
|
for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
|
|
33400
33411
|
closeIntentionally(topic);
|
|
33401
33412
|
const dedupeKey = rendered.envelope?.dedupe_key;
|
|
@@ -33452,9 +33463,6 @@ function envoyExtension(pi) {
|
|
|
33452
33463
|
}
|
|
33453
33464
|
}
|
|
33454
33465
|
}
|
|
33455
|
-
if (reply !== "" && subject === agentSubject(sessionID)) {
|
|
33456
|
-
(await ensureConnection()).publish(reply);
|
|
33457
|
-
}
|
|
33458
33466
|
};
|
|
33459
33467
|
const RESUBSCRIBE_DELAY_MS = Number(process.env.ENVOY_RESUBSCRIBE_DELAY_MS ?? "") || 5000;
|
|
33460
33468
|
let shuttingDown = false;
|
package/dist/legion.js
CHANGED
|
@@ -29746,7 +29746,7 @@ var MessageEventPayloadSchema = object({
|
|
|
29746
29746
|
});
|
|
29747
29747
|
var DispatchTargetedMessagePayloadSchema = MessageEventPayloadSchema.extend({
|
|
29748
29748
|
id: string2(),
|
|
29749
|
-
issue_key: string2(),
|
|
29749
|
+
issue_key: string2().nullable(),
|
|
29750
29750
|
author: object({ kind: string2(), id: string2() }),
|
|
29751
29751
|
body: string2(),
|
|
29752
29752
|
target: string2(),
|
|
@@ -29759,6 +29759,7 @@ var MessageDeliveryEventPayloadSchema = object({
|
|
|
29759
29759
|
attempt: number2().int().positive().optional(),
|
|
29760
29760
|
delivery: _enum2(["btw", "aside", "steer"]).optional(),
|
|
29761
29761
|
session_id: string2().optional(),
|
|
29762
|
+
target: string2().optional(),
|
|
29762
29763
|
title: string2().optional(),
|
|
29763
29764
|
state: _enum2(["sent", "failed"]).optional(),
|
|
29764
29765
|
error: string2().optional()
|
|
@@ -31548,7 +31549,7 @@ var DispatchTargetedFrameSchema = object({
|
|
|
31548
31549
|
}).refine(({ event }) => event.type === "message.created" && DispatchTargetedMessagePayloadSchema.safeParse(event.payload).success, { message: "targeted delivery requires a message.created event" });
|
|
31549
31550
|
var RecoverableDispatchDeliveryFailureSchema = object({
|
|
31550
31551
|
event: object({
|
|
31551
|
-
issue_key: string2(),
|
|
31552
|
+
issue_key: string2().nullable(),
|
|
31552
31553
|
payload: object({ id: string2(), body: string2().optional() }).passthrough()
|
|
31553
31554
|
}).passthrough(),
|
|
31554
31555
|
delivery: DispatchDeliveryRequestSchema
|
|
@@ -31751,7 +31752,7 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31751
31752
|
askQuestion = dispatchAskQuestion(frame.event);
|
|
31752
31753
|
messageReplyPreview = dispatchMessageReplyPreview(frame.event);
|
|
31753
31754
|
dispatchReply = dispatchCommentReplyWith(frame.event, subject ?? envelope.topic);
|
|
31754
|
-
if (frame.event.type === "message.created"
|
|
31755
|
+
if (frame.event.type === "message.created") {
|
|
31755
31756
|
const message = MessageEventPayloadSchema.safeParse(frame.event.payload);
|
|
31756
31757
|
const requested = DispatchDeliveryRequestSchema.safeParse(frame.delivery);
|
|
31757
31758
|
if (message.success && requested.success && message.data.id !== undefined) {
|
|
@@ -31762,7 +31763,9 @@ function renderInbound(raw, sessionID, subject) {
|
|
|
31762
31763
|
issueKey: frame.event.issue_key,
|
|
31763
31764
|
body: message.data.body ?? envelope.payload_summary ?? ""
|
|
31764
31765
|
};
|
|
31765
|
-
|
|
31766
|
+
if (frame.event.issue_key !== null) {
|
|
31767
|
+
dispatchReply = `dispatch_message(issue="${frame.event.issue_key}", in_reply_to="${message.data.id}", body="...")`;
|
|
31768
|
+
}
|
|
31766
31769
|
}
|
|
31767
31770
|
}
|
|
31768
31771
|
dispatchEvent = {
|
|
@@ -33045,17 +33048,18 @@ async function executeDispatchTool(input) {
|
|
|
33045
33048
|
}
|
|
33046
33049
|
case "dispatch_message": {
|
|
33047
33050
|
const inReplyTo = messageInReplyTo(args);
|
|
33048
|
-
const
|
|
33051
|
+
const issueKey = issue();
|
|
33052
|
+
const message = await client.message(issueKey, {
|
|
33049
33053
|
body: stringArg(args, "body"),
|
|
33050
33054
|
...inReplyTo === undefined ? {} : { in_reply_to: inReplyTo },
|
|
33051
33055
|
actor
|
|
33052
33056
|
});
|
|
33053
|
-
const messageRef = `dispatch://${
|
|
33057
|
+
const messageRef = `dispatch://${issueKey}/message/${message.id}`;
|
|
33054
33058
|
return {
|
|
33055
33059
|
text: `Posted message ${message.id} (${messageRef})`,
|
|
33056
33060
|
details: {
|
|
33057
|
-
issue:
|
|
33058
|
-
topic: dispatchIssueSubject(
|
|
33061
|
+
issue: issueKey,
|
|
33062
|
+
topic: dispatchIssueSubject(issueKey, ">"),
|
|
33059
33063
|
message: message.id
|
|
33060
33064
|
}
|
|
33061
33065
|
};
|
|
@@ -33819,6 +33823,13 @@ function envoyExtension(pi) {
|
|
|
33819
33823
|
};
|
|
33820
33824
|
const deliver = async (subject, raw, reply) => {
|
|
33821
33825
|
const rendered = renderInbound(raw, sessionID, subject);
|
|
33826
|
+
if (reply !== "" && subject === agentSubject(sessionID)) {
|
|
33827
|
+
try {
|
|
33828
|
+
(await ensureConnection()).publish(reply);
|
|
33829
|
+
} catch (error) {
|
|
33830
|
+
console.warn(`[envoy] failed to acknowledge envelope ${rendered.envelope?.event_id ?? "unknown"}`, error);
|
|
33831
|
+
}
|
|
33832
|
+
}
|
|
33822
33833
|
for (const topic of subscriptionRemovedTopics(raw, sessionID) ?? [])
|
|
33823
33834
|
closeIntentionally(topic);
|
|
33824
33835
|
const dedupeKey = rendered.envelope?.dedupe_key;
|
|
@@ -33875,9 +33886,6 @@ function envoyExtension(pi) {
|
|
|
33875
33886
|
}
|
|
33876
33887
|
}
|
|
33877
33888
|
}
|
|
33878
|
-
if (reply !== "" && subject === agentSubject(sessionID)) {
|
|
33879
|
-
(await ensureConnection()).publish(reply);
|
|
33880
|
-
}
|
|
33881
33889
|
};
|
|
33882
33890
|
const RESUBSCRIBE_DELAY_MS = Number(process.env.ENVOY_RESUBSCRIBE_DELAY_MS ?? "") || 5000;
|
|
33883
33891
|
let shuttingDown = false;
|
|
@@ -148,11 +148,16 @@ not construct a token from a partial issue reference.
|
|
|
148
148
|
|
|
149
149
|
## Legion exception lane
|
|
150
150
|
|
|
151
|
-
`no_holder` and `
|
|
152
|
-
add a second subscriber or manually retry.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
151
|
+
`no_holder`, `delivery_failed`, and `receipt_timeout` for a Legion role are daemon liveness
|
|
152
|
+
signals, not reasons to add a second subscriber or manually retry. `no_holder`: no live session
|
|
153
|
+
holds the role. `delivery_failed`: the message was not forwarded at all — the holder lookup
|
|
154
|
+
failed, the holder was stale, or the publish errored. `receipt_timeout`: the listener forwarded
|
|
155
|
+
the message to a registered, live holder and no receipt arrived inside the window; its payload
|
|
156
|
+
keeps `original_topic`, `event_id`, `payload`, and the original `dedupe_key`, so the daemon can
|
|
157
|
+
re-send the same message and the receiver's dedupe drops a copy that did arrive. Treat any of
|
|
158
|
+
these wakes as the daemon's responsibility to revive or recreate the backing worker and
|
|
159
|
+
re-deliver; otherwise it resurrects the root process with derived catch-up and the workspace
|
|
160
|
+
handoffs. Raw delivery failures stay out of architect context.
|
|
156
161
|
|
|
157
162
|
## Slack
|
|
158
163
|
|
|
@@ -45,7 +45,7 @@ before step 3. The design gate is not a substitute for review and retro.
|
|
|
45
45
|
|
|
46
46
|
1. Re-read the issue, its acceptance criteria, the PR, test evidence, and review evidence.
|
|
47
47
|
Confirm the PR carries both proofs: the implementer's own `E2E (implementer)` line and the tester's `E2E (tester)` line,
|
|
48
|
-
each naming a production-like surface (
|
|
48
|
+
each naming a production-like surface (the daemon's test harness (`packages/daemon/src/daemon/__tests__/`) and real-process fixtures; a live check at the operator's next daemon restart, recorded on the PR; a sandbox repository, a devN
|
|
49
49
|
stack, staging, or a local stack with real migrations), a command or run id, an observation, a head
|
|
50
50
|
SHA, and a negative control. If either is missing, or links only a unit suite, the retro's first
|
|
51
51
|
durable learning is that gap and the issue goes back — to the implementer for its own proof, to the
|
|
@@ -251,7 +251,7 @@ PR opens, and every later phase keeps it current rather than replacing it:
|
|
|
251
251
|
```
|
|
252
252
|
## Verification
|
|
253
253
|
|
|
254
|
-
**CI:** `Tests` run <run-id> — jobs lint,
|
|
254
|
+
**CI:** `Tests` run <run-id> — jobs lint, typecheck, test all success at <head-sha>; `PR Title` run <run-id> — job pr-title success at <head-sha>.
|
|
255
255
|
|
|
256
256
|
**Threads:** <n> resolved, 0 unresolved. Each disposed individually, never in bulk:
|
|
257
257
|
- Thread <id>: fixed in <commit-sha> — <one line>.
|
|
@@ -276,6 +276,7 @@ Verified the implementer's proof by <re-running its command | driving the same s
|
|
|
276
276
|
**Fast-follow:** <one named cleanup item and where it will land>, or "none".
|
|
277
277
|
|
|
278
278
|
**Chain:** stacked on <base bookmark> frozen at <sha> / not stacked.
|
|
279
|
+
**Retarget:** Retargeting a pull request to a new base does not re-run Tests; after a retarget, rebase onto the new base and push — the new head runs Tests against the new merge result — and cite that run in the PR body.
|
|
279
280
|
```
|
|
280
281
|
|
|
281
282
|
- **Threads are dispositioned individually, never resolved in bulk.** Every open review
|
|
@@ -301,11 +302,11 @@ Verified the implementer's proof by <re-running its command | driving the same s
|
|
|
301
302
|
changes behavior, hides an error, or breaks a gate is fixed here — never deferred.
|
|
302
303
|
Findings about naming, duplication, or wording are batched into the single `Fast-follow`
|
|
303
304
|
line instead of iterating per push.
|
|
304
|
-
- **Rebase only on a real conflict.** Sami, 2026-09-11, verbatim:
|
|
305
|
+
- **Rebase only on a real conflict, except after a base retarget.** Sami, 2026-09-11, verbatim:
|
|
305
306
|
"Please don't do unnecessary rebases (i.e. unless there are merge conflicts). The CI queue is too long and slow."
|
|
306
|
-
The implementer rebases the issue branch only when GitHub reports it `CONFLICTING
|
|
307
|
-
|
|
308
|
-
failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
|
|
307
|
+
The implementer rebases the issue branch only when GitHub reports it `CONFLICTING`, the controller asks
|
|
308
|
+
because of a conflict, or after the pull request is retargeted to a new base. Otherwise, never rebase to
|
|
309
|
+
pick up `main` or refresh CI. A single failed CI job is re-run on its own with `legion gh -- run rerun <run-id> --failed`, never by
|
|
309
310
|
pushing a new commit. A conflict-forced rebase that leaves the branch's diff unchanged is a
|
|
310
311
|
confirmation, not a new round (see *The unchanged-diff check* below). Before rebasing, record
|
|
311
312
|
the fingerprint at the current tip; after pushing the rebased branch, record it at the new
|
|
@@ -317,9 +318,10 @@ Verified the implementer's proof by <re-running its command | driving the same s
|
|
|
317
318
|
field names naming, duplication, or wording cleanup only; anything that changes behaviour,
|
|
318
319
|
hides an error, or breaks a gate lands in this PR.
|
|
319
320
|
- **The implementer proves the change before its phase completes, and writes the `E2E (implementer)` line when the pull request opens.**
|
|
320
|
-
The proof is the changed behaviour exercised on the surface a user reaches it through —
|
|
321
|
-
|
|
322
|
-
|
|
321
|
+
The proof is the changed behaviour exercised on the surface a user reaches it through — the daemon's
|
|
322
|
+
test harness (`packages/daemon/src/daemon/__tests__/`) and real-process fixtures; a live check at the
|
|
323
|
+
operator's next daemon restart, recorded on the PR; a sandbox repository, a real browser, a devN stack,
|
|
324
|
+
or a local stack with real migrations — with the exact command or run id, what was observed, the head SHA,
|
|
323
325
|
one negative control. The same proof goes into `.legion/implement.json` as its required `proof`
|
|
324
326
|
array (`legion handoff write --phase implement` refuses a payload without one and names the
|
|
325
327
|
field), and into the PR body, because the reviewer and the merger verify facts on GitHub and
|