@sjawhar/pi-legion-envoy 5.11.1 → 5.13.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 +44 -18
- package/dist/legion.js +1 -1
- package/dist/skills/dispatch/SKILL.md +10 -4
- package/dist/skills/envoy/SKILL.md +1 -1
- package/dist/skills/legion-controller/SKILL.md +23 -9
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -35188,6 +35188,27 @@ function onEnvoyRoleRegained(listener) {
|
|
|
35188
35188
|
legionRoleClaimBridge().regained = listener;
|
|
35189
35189
|
}
|
|
35190
35190
|
|
|
35191
|
+
// src/side-turn.ts
|
|
35192
|
+
function btwPrompt(question) {
|
|
35193
|
+
return [
|
|
35194
|
+
"<btw>",
|
|
35195
|
+
"Ephemeral side question for current interactive session.",
|
|
35196
|
+
"Answer briefly, directly; use conversation context already provided.",
|
|
35197
|
+
"NEVER use tools.",
|
|
35198
|
+
"NEVER ask follow-up questions.",
|
|
35199
|
+
"Question:",
|
|
35200
|
+
question,
|
|
35201
|
+
"</btw>"
|
|
35202
|
+
].join(`
|
|
35203
|
+
`);
|
|
35204
|
+
}
|
|
35205
|
+
function sideTurn(pi, context) {
|
|
35206
|
+
const runEphemeralTurn = context?.runEphemeralTurn;
|
|
35207
|
+
if (runEphemeralTurn === undefined)
|
|
35208
|
+
return pi.askEphemeral;
|
|
35209
|
+
return ({ prompt, signal }) => runEphemeralTurn({ promptText: btwPrompt(prompt), signal });
|
|
35210
|
+
}
|
|
35211
|
+
|
|
35191
35212
|
// src/subagent-session.ts
|
|
35192
35213
|
import fs from "fs";
|
|
35193
35214
|
import path3 from "path";
|
|
@@ -35433,7 +35454,6 @@ function envoyExtension(pi) {
|
|
|
35433
35454
|
availabilityWarningSessionIDs.add(sessionID2);
|
|
35434
35455
|
context.ui.notify(`envoy: Dispatch open-ask check unavailable (${messageFor(error48)}); the stop-time ask reminder is off until it recovers`, "warning");
|
|
35435
35456
|
};
|
|
35436
|
-
const askEphemeral = pi.askEphemeral;
|
|
35437
35457
|
const overriddenTimeout = Number(process.env.ENVOY_SELF_CHECK_TIMEOUT_MS);
|
|
35438
35458
|
const selfCheckTimeoutMs = Number.isFinite(overriddenTimeout) && overriddenTimeout > 0 ? overriddenTimeout : ASK_SELF_CHECK_TIMEOUT_MS;
|
|
35439
35459
|
const selfCheckWarningSessionIDs = new Set;
|
|
@@ -35542,13 +35562,18 @@ function envoyExtension(pi) {
|
|
|
35542
35562
|
} else if (rendered.malformedDelivery === true) {
|
|
35543
35563
|
console.warn("[envoy] dropping malformed Dispatch targeted delivery without a reply address");
|
|
35544
35564
|
} else if (rendered.delivery?.mode === "btw") {
|
|
35545
|
-
|
|
35565
|
+
const answer = sideTurn(pi, activeSessionContext);
|
|
35566
|
+
if (shuttingDown) {
|
|
35567
|
+
await postDispatchReply(rendered.delivery, {
|
|
35568
|
+
error: "This OMP session is shutting down"
|
|
35569
|
+
});
|
|
35570
|
+
} else if (answer === undefined) {
|
|
35546
35571
|
await postDispatchReply(rendered.delivery, {
|
|
35547
35572
|
error: "This OMP host does not support BTW delivery"
|
|
35548
35573
|
});
|
|
35549
35574
|
} else {
|
|
35550
35575
|
try {
|
|
35551
|
-
const reply2 = await
|
|
35576
|
+
const reply2 = await answer({ prompt: rendered.delivery.body });
|
|
35552
35577
|
await postDispatchReply(rendered.delivery, { body: reply2.replyText });
|
|
35553
35578
|
} catch (error48) {
|
|
35554
35579
|
await postDispatchReply(rendered.delivery, { error: messageFor(error48) });
|
|
@@ -35733,7 +35758,7 @@ function envoyExtension(pi) {
|
|
|
35733
35758
|
],
|
|
35734
35759
|
port: 0,
|
|
35735
35760
|
title: activeSessionContext?.sessionManager.getSessionName?.() ?? "",
|
|
35736
|
-
capabilities:
|
|
35761
|
+
capabilities: sideTurn(pi, activeSessionContext) === undefined ? CAPABILITIES_WITHOUT_BTW : DELIVERY_CAPABILITIES,
|
|
35737
35762
|
driving: false,
|
|
35738
35763
|
selfSubscribed: true
|
|
35739
35764
|
});
|
|
@@ -36127,7 +36152,7 @@ function envoyExtension(pi) {
|
|
|
36127
36152
|
registerEnvoyWhoamiCommand(pi, replyAddress);
|
|
36128
36153
|
pi.on("before_agent_start", async (event, context) => {
|
|
36129
36154
|
const id = context.sessionManager.getSessionId();
|
|
36130
|
-
if (id === "" || event.prompt.trim() === "" || !context.hasUI ||
|
|
36155
|
+
if (id === "" || event.prompt.trim() === "" || !context.hasUI || sideTurn(pi, context) === undefined || legionManaged(id)) {
|
|
36131
36156
|
return;
|
|
36132
36157
|
}
|
|
36133
36158
|
if (await isSubagent(context))
|
|
@@ -36158,39 +36183,40 @@ function envoyExtension(pi) {
|
|
|
36158
36183
|
pi.on("agent_end", async (event, context) => {
|
|
36159
36184
|
const id = context.sessionManager.getSessionId();
|
|
36160
36185
|
const lastReply = event.messages?.findLast((message) => message.role === "assistant");
|
|
36161
|
-
|
|
36186
|
+
const ask = sideTurn(pi, context);
|
|
36187
|
+
if (event.willContinue === true || lastReply?.stopReason !== "stop" || !context.hasUI || id === "" || ask === undefined || !checkOwed(id)) {
|
|
36162
36188
|
return;
|
|
36163
36189
|
}
|
|
36190
|
+
const settle = { context, ask, seenRun: runSeq, generation: awarenessGeneration };
|
|
36164
36191
|
if (askCheckInFlight) {
|
|
36165
|
-
settledDuringCheck =
|
|
36192
|
+
settledDuringCheck = settle;
|
|
36166
36193
|
return;
|
|
36167
36194
|
}
|
|
36168
36195
|
askCheckInFlight = true;
|
|
36196
|
+
context.setTimeout(() => checkFromSettle(id, settle), 0);
|
|
36197
|
+
});
|
|
36198
|
+
async function checkFromSettle(id, first) {
|
|
36169
36199
|
try {
|
|
36170
|
-
let settle =
|
|
36200
|
+
let settle = first;
|
|
36171
36201
|
while (settle !== undefined) {
|
|
36172
36202
|
await checkAtSettle(id, settle);
|
|
36173
36203
|
settle = takeSettledDuringCheck();
|
|
36174
|
-
if (settle?.context.sessionManager.getSessionId() !== id || settle.seenRun !== runSeq || !checkOwed(id)) {
|
|
36175
|
-
settle = undefined;
|
|
36176
|
-
}
|
|
36177
36204
|
}
|
|
36178
36205
|
} finally {
|
|
36179
36206
|
askCheckInFlight = false;
|
|
36180
36207
|
settledDuringCheck = undefined;
|
|
36181
36208
|
}
|
|
36182
|
-
}
|
|
36209
|
+
}
|
|
36183
36210
|
function checkOwed(id) {
|
|
36184
|
-
return !(shuttingDown || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period === 0 || !askAwareness.check_due || askAwareness.checks >= ASK_CHECKS_PER_PERIOD || askAwareness.baseline_as_of === null
|
|
36211
|
+
return !(shuttingDown || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period === 0 || !askAwareness.check_due || askAwareness.checks >= ASK_CHECKS_PER_PERIOD || askAwareness.baseline_as_of === null);
|
|
36185
36212
|
}
|
|
36186
36213
|
async function checkAtSettle(id, settle) {
|
|
36187
|
-
const { context, seenRun } = settle;
|
|
36214
|
+
const { context, ask, seenRun, generation } = settle;
|
|
36188
36215
|
const period = askAwareness.period;
|
|
36189
|
-
const generation = awarenessGeneration;
|
|
36190
36216
|
const baseline = askAwareness.baseline_as_of;
|
|
36191
|
-
if (baseline === null || askEphemeral === undefined)
|
|
36192
|
-
return;
|
|
36193
36217
|
const stale = () => shuttingDown || generation !== awarenessGeneration || context.sessionManager.getSessionId() !== id || legionManaged(id) || askAwareness.session_id !== id || askAwareness.period !== period || !askAwareness.check_due;
|
|
36218
|
+
if (baseline === null || !checkOwed(id) || stale() || runSeq !== seenRun)
|
|
36219
|
+
return;
|
|
36194
36220
|
let open;
|
|
36195
36221
|
try {
|
|
36196
36222
|
open = await queryOpenAsks(id, baseline);
|
|
@@ -36207,7 +36233,7 @@ function envoyExtension(pi) {
|
|
|
36207
36233
|
askCheckAbort = abort;
|
|
36208
36234
|
let answered;
|
|
36209
36235
|
try {
|
|
36210
|
-
answered =
|
|
36236
|
+
answered = ask({
|
|
36211
36237
|
prompt: ASK_SELF_CHECK_PROMPT(open.snapshot.asks),
|
|
36212
36238
|
signal: abort.signal
|
|
36213
36239
|
}).then((reply) => reply.replyText, (error48) => {
|
package/dist/legion.js
CHANGED
|
@@ -16192,7 +16192,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
16192
16192
|
// package.json
|
|
16193
16193
|
var package_default = {
|
|
16194
16194
|
name: "@sjawhar/pi-legion-envoy",
|
|
16195
|
-
version: "5.
|
|
16195
|
+
version: "5.13.0",
|
|
16196
16196
|
type: "module",
|
|
16197
16197
|
omp: {
|
|
16198
16198
|
extensions: [
|
|
@@ -106,7 +106,10 @@ A spec has two readers: the human who decides reads the **Summary** and **New si
|
|
|
106
106
|
between two lanes or a halt condition, a question for the platform PO (see
|
|
107
107
|
[Before you ask](#before-you-ask) under Asking).
|
|
108
108
|
- Keep each section to one screen; work that exceeds one screen per section is two specs.
|
|
109
|
-
- Update the spec as decisions land: the spec is the record, comments are the discussion.
|
|
109
|
+
- Update the spec as decisions land: the spec is the record, comments are the discussion. It
|
|
110
|
+
records decisions and requirements, never progress: no status, timestamps, "Update HH:MMZ"
|
|
111
|
+
section, PR list, or handoff notes. Progress is not a Dispatch object at all; it lives in your
|
|
112
|
+
transcript and your pull request (see [Messages](#messages)).
|
|
110
113
|
- Before sending it: no sections conflict, every requirement has exactly one reading, and the
|
|
111
114
|
Summary and every ask block pass the phone test above.
|
|
112
115
|
|
|
@@ -274,6 +277,10 @@ start with the issue key; standalone project-document hit lines start with
|
|
|
274
277
|
|
|
275
278
|
`dispatch_issue` refuses a title that near-duplicates an issue in the same project and returns the candidates (`POSSIBLE_DUPLICATE`).
|
|
276
279
|
Read them; reference the existing issue, or repeat the call with `force: true` when it is genuinely new work.
|
|
280
|
+
The check compares title words only (shared stemmed terms), never meaning: "four tests that fail a
|
|
281
|
+
merge" pairs with "four CI gates that cannot fail a merge". So when you force past a candidate, give
|
|
282
|
+
the new issue a title that names what differs where you can, and open its spec's Summary with the
|
|
283
|
+
distinction from the named issue, citing it (`dispatch://KEY`), for whoever reads the next pairing.
|
|
277
284
|
|
|
278
285
|
## Reading a project's backlog
|
|
279
286
|
|
|
@@ -548,9 +555,8 @@ the document's approval state; `stale` means it was approved and then edited - r
|
|
|
548
555
|
## The Spec
|
|
549
556
|
|
|
550
557
|
The spec holds requirements, design, acceptance, decisions, and rejected alternatives, structured per [Writing a spec](#writing-a-spec).
|
|
551
|
-
It changes only when a decision or requirement changes, and every version that records one is named with `summary`.
|
|
552
|
-
|
|
553
|
-
Dispatch object at all: it lives in your transcript and your pull request (see [Messages](#messages)).
|
|
558
|
+
It changes only when a decision or requirement changes, and every version that records one is named with `summary`. What it
|
|
559
|
+
never carries is in [Rules](#rules) under Writing a spec.
|
|
554
560
|
|
|
555
561
|
Read the current document before changing it:
|
|
556
562
|
|
|
@@ -140,7 +140,7 @@ Each session row from `envoy_sessions` carries `capabilities`, the targeted-deli
|
|
|
140
140
|
session's host honours: `aside` (a message queued beside the model's work), `btw` (an ephemeral
|
|
141
141
|
question the host answers without disturbing the current turn), and `steer` (an interjection at
|
|
142
142
|
the next tool boundary). An OMP session advertises `aside`, `btw`, and `steer` (`aside` and
|
|
143
|
-
`steer` on a host
|
|
143
|
+
`steer` on a host that cannot run a side turn); a Claude Code session advertises `aside` only, because
|
|
144
144
|
its channel notifications queue for the next turn. Target a session only with a mode it
|
|
145
145
|
advertises.
|
|
146
146
|
|
|
@@ -97,7 +97,8 @@ arrives as a wake. At every start, before anything else:
|
|
|
97
97
|
`issues.<KEY>.architect.state` is `failed` and whose `issues.<KEY>.phase` is not `done`, exactly
|
|
98
98
|
as the matching wake below. A parked tree (root phase `done`: it lingers or is closed) needs
|
|
99
99
|
nothing from you: a failed architect ignores the park and reads `failed` until the tree closes.
|
|
100
|
-
2. List the project's triage issues
|
|
100
|
+
2. List the project's triage issues handed to Legion with
|
|
101
|
+
`dispatch_issues({project, status: "triage", label: "legion", limit: 250})`.
|
|
101
102
|
When its first line ends `(showing N of M)`, it is one page: say in your summary how many rows
|
|
102
103
|
it left unread. The rows show no parent, so open each row with `dispatch_read`: one whose
|
|
103
104
|
`Links:` name a `child_of` issue is a child, which its parent's architect owns, so leave it,
|
|
@@ -105,12 +106,24 @@ arrives as a wake. At every start, before anything else:
|
|
|
105
106
|
of this issue, not its parent). Of the rest, triage each that `legion state --json` does not
|
|
106
107
|
record under `issues` as a new issue. A root recorded there and now in `triage` is work the
|
|
107
108
|
daemon holds that a human pulled back: never re-admit it yourself; name it in your summary to
|
|
108
|
-
the human ("<KEY> was pulled back to triage; what do you want?").
|
|
109
|
-
way you learn of an unrecorded root moved back into triage, or of a child detached to a root
|
|
110
|
-
while it is in triage, since the daemon wakes you only on a root's creation.
|
|
109
|
+
the human ("<KEY> was pulled back to triage; what do you want?").
|
|
111
110
|
|
|
112
111
|
The issue record and Dispatch are the truth; the topic is the wake.
|
|
113
112
|
|
|
113
|
+
### Issues handed to Legion (Go daemon)
|
|
114
|
+
|
|
115
|
+
The Go daemon works only the issues handed to it with the Dispatch label `legion` (in any case),
|
|
116
|
+
since its project may be shared with humans and other agents. It never admits a root in `todo`
|
|
117
|
+
without the label, and wakes you for a root in `triage` only while the root carries it and is
|
|
118
|
+
unrecorded: on its creation with the label, and on each change to it after that while it stays
|
|
119
|
+
in triage, the change that adds the label included (the dashboard creates an issue without
|
|
120
|
+
labels, so a human adds it from the issue header). Leave an issue without the label alone:
|
|
121
|
+
handing work to Legion is its owner's decision, so never triage, park, label, or comment on it.
|
|
122
|
+
A child needs no label: it runs under its tree's architect once its root is admitted.
|
|
123
|
+
`legion status <KEY> todo` admits a root only while it carries the label, so a root you file for
|
|
124
|
+
Legion to run carries it (`labels: ["legion"]` in `dispatch_issue`). Taking the label off a
|
|
125
|
+
waiting root drops it from the waiting line; taking it off an admitted tree does not stop it.
|
|
126
|
+
|
|
114
127
|
## Deployment instructions
|
|
115
128
|
|
|
116
129
|
Deployment instructions, when present, are the operator's standing rules for this repository —
|
|
@@ -137,7 +150,7 @@ quoted here.
|
|
|
137
150
|
|
|
138
151
|
| Wake | Content | Controller action |
|
|
139
152
|
|---|---|---|
|
|
140
|
-
| New issue created in the Dispatch project (`issue.created`, status `triage`; under the TypeScript daemon resync heals misses, under the Go daemon the boot step above does). From the Go daemon: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for
|
|
153
|
+
| New issue created in the Dispatch project (`issue.created`, status `triage`; under the TypeScript daemon resync heals misses, under the Go daemon the boot step above does). From the Go daemon: `triage on <KEY>` (payload `{kind: "triage"}`) on the controller topic, for an unrecorded root carrying the `legion` label only ("Issues handed to Legion" above) | issue key + triage context (incl. pre-existing children) | Triage: `legion status <KEY> todo` to admit, or set `backlog`/`icebox` to park |
|
|
141
154
|
| Backlog eligibility | slot freed / priority change | Reconsider parked items and move the eligible root to `todo` |
|
|
142
155
|
| Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; issue-scoped human Q&A goes through `dispatch_ask` from the owning architect, not here |
|
|
143
156
|
| Resync report | artifact-driven anomaly list (zero-owner trees, untriaged-open, launch-failed, admission-drift) | Verify against fresh state, then heal |
|
|
@@ -172,9 +185,9 @@ quoted here.
|
|
|
172
185
|
legion status <issue> backlog
|
|
173
186
|
```
|
|
174
187
|
|
|
175
|
-
(or `icebox` for longer-term deferral). Dispatch status is the durable record;
|
|
176
|
-
|
|
177
|
-
issue.
|
|
188
|
+
(or `icebox` for longer-term deferral). Dispatch status is the durable record; there is no
|
|
189
|
+
other marker to maintain, beyond the Go daemon's `legion` label, which parking leaves in
|
|
190
|
+
place. Do not triage a system-created child as a root issue.
|
|
178
191
|
4. When you post a triage note (a `dispatch_comment` on the issue saying what you decided and
|
|
179
192
|
why), name who will be asked: `Assigned to <login>, who will get this tree's questions`,
|
|
180
193
|
or, when the `Assignee:` line says `unassigned`, `Unassigned — nobody's Inbox shows this
|
|
@@ -197,7 +210,8 @@ architect, not the controller.
|
|
|
197
210
|
For an independence judgment, verify the child and its parent against current daemon state
|
|
198
211
|
and the Dispatch issue. If the work belongs in an independent root:
|
|
199
212
|
|
|
200
|
-
1. File a **fresh root issue** with `dispatch_issue({ project, title, spec })` (no `parent
|
|
213
|
+
1. File a **fresh root issue** with `dispatch_issue({ project, title, spec })` (no `parent`;
|
|
214
|
+
under the Go daemon add `labels: ["legion"]`, without which it is never admitted).
|
|
201
215
|
`project` is the issue key's prefix before `-<n>` (e.g. `LEGSMOKE-3` → `LEGSMOKE`) — not
|
|
202
216
|
the role-token `<project>` (the daemon's own project, e.g. `acme`), a different string.
|
|
203
217
|
2. Park the child (`legion status <child> icebox`) and leave
|