@sjawhar/pi-legion-envoy 1.58.0 → 1.59.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 +64 -3
- package/dist/legion.js +38 -28
- package/dist/skills/dispatch/SKILL.md +6 -2
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -32251,6 +32251,9 @@ class DispatchClient {
|
|
|
32251
32251
|
async syncArchitectureSource(project) {
|
|
32252
32252
|
return this.#json("POST", ["api", "v1", "projects", project, "architecture-source", "sync"]);
|
|
32253
32253
|
}
|
|
32254
|
+
async getArchitectureSource(project) {
|
|
32255
|
+
return this.#json("GET", ["api", "v1", "projects", project, "architecture-source"]);
|
|
32256
|
+
}
|
|
32254
32257
|
async resolveAsk(id, input) {
|
|
32255
32258
|
return this.#json("POST", ["api", "v1", "asks", id, "resolve"], input);
|
|
32256
32259
|
}
|
|
@@ -32746,6 +32749,20 @@ function componentsChange(input, after) {
|
|
|
32746
32749
|
return "components -> inherit";
|
|
32747
32750
|
}
|
|
32748
32751
|
}
|
|
32752
|
+
var architectureComponentsAction = 'Review the `dispatch` skill, "Architecture components", to attach it to the parts it changes or mark it as non-architectural with a reason.';
|
|
32753
|
+
async function architectureGuidance(client, project, components) {
|
|
32754
|
+
if (components.mode === "none" || components.ids.length > 0)
|
|
32755
|
+
return;
|
|
32756
|
+
try {
|
|
32757
|
+
await client.getArchitectureSource(project);
|
|
32758
|
+
return `Project ${project} has an architecture model, but this issue is not linked to any of its current components. ${architectureComponentsAction}`;
|
|
32759
|
+
} catch (error) {
|
|
32760
|
+
if (error instanceof DispatchServiceError && error.status === 404 && error.code === "SOURCE_NOT_FOUND") {
|
|
32761
|
+
return;
|
|
32762
|
+
}
|
|
32763
|
+
return `Could not check whether project ${project} has an architecture model: ${messageFor(error)}. ${architectureComponentsAction}`;
|
|
32764
|
+
}
|
|
32765
|
+
}
|
|
32749
32766
|
function isDuplicateCandidate(value) {
|
|
32750
32767
|
if (typeof value !== "object" || value === null)
|
|
32751
32768
|
return false;
|
|
@@ -33569,9 +33586,13 @@ async function executeDispatchTool(input) {
|
|
|
33569
33586
|
...Array.isArray(labels) ? { labels } : {},
|
|
33570
33587
|
actor
|
|
33571
33588
|
});
|
|
33572
|
-
const
|
|
33573
|
-
|
|
33574
|
-
|
|
33589
|
+
const componentGuidance = await architectureGuidance(client, created.project, created.components);
|
|
33590
|
+
const adviceLines = [
|
|
33591
|
+
...renderAdvice(input.tool, created.key, created.advice, {
|
|
33592
|
+
isPrimarySpec: spec !== undefined
|
|
33593
|
+
}),
|
|
33594
|
+
...componentGuidance === undefined ? [] : [componentGuidance]
|
|
33595
|
+
];
|
|
33575
33596
|
return {
|
|
33576
33597
|
text: [
|
|
33577
33598
|
`Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
|
|
@@ -34611,6 +34632,40 @@ function onEnvoyRoleRegained(listener) {
|
|
|
34611
34632
|
legionRoleClaimBridge().regained = listener;
|
|
34612
34633
|
}
|
|
34613
34634
|
|
|
34635
|
+
// src/subagent-session.ts
|
|
34636
|
+
import fs from "fs";
|
|
34637
|
+
import path2 from "path";
|
|
34638
|
+
import * as host from "@oh-my-pi/pi-coding-agent";
|
|
34639
|
+
var LEGION_BOOTSTRAPPED_SESSION = Symbol.for("legion.pi-envoy.bootstrapped-session");
|
|
34640
|
+
var bootstrappedSessionStore = globalThis;
|
|
34641
|
+
function recordBootstrappedSession(sessionFile) {
|
|
34642
|
+
bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION] = sessionFile;
|
|
34643
|
+
}
|
|
34644
|
+
async function isSubagentSession(context) {
|
|
34645
|
+
await context.sessionManager.ensureOnDisk();
|
|
34646
|
+
const sessionFile = context.sessionManager.getSessionFile();
|
|
34647
|
+
if (sessionFile !== undefined && fs.existsSync(sessionFile)) {
|
|
34648
|
+
return fs.existsSync(`${path2.dirname(sessionFile)}.jsonl`);
|
|
34649
|
+
}
|
|
34650
|
+
const bootstrapped = bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION];
|
|
34651
|
+
return bootstrapped !== undefined && bootstrapped !== sessionFile;
|
|
34652
|
+
}
|
|
34653
|
+
function isRegisteredSubagent(context) {
|
|
34654
|
+
const registry = host.AgentRegistry?.global();
|
|
34655
|
+
if (registry === undefined)
|
|
34656
|
+
return false;
|
|
34657
|
+
const sessionID = context.sessionManager.getSessionId();
|
|
34658
|
+
const sessionFile = context.sessionManager.getSessionFile();
|
|
34659
|
+
return registry.list().some((ref) => ref.kind !== "main" && (ref.session === null ? sessionFile !== undefined && ref.sessionFile === sessionFile : ref.session.sessionManager.getSessionId() === sessionID));
|
|
34660
|
+
}
|
|
34661
|
+
function subagentSessionCheck() {
|
|
34662
|
+
let subagentSession;
|
|
34663
|
+
return (context) => {
|
|
34664
|
+
subagentSession ??= isSubagentSession(context);
|
|
34665
|
+
return subagentSession;
|
|
34666
|
+
};
|
|
34667
|
+
}
|
|
34668
|
+
|
|
34614
34669
|
// src/tool-result.ts
|
|
34615
34670
|
function toolSuccess(text, details = {}) {
|
|
34616
34671
|
return { content: [{ type: "text", text }], details };
|
|
@@ -35112,7 +35167,11 @@ function envoyExtension(pi) {
|
|
|
35112
35167
|
sessionID: () => activeSessionContext?.sessionManager.getSessionId() ?? sessionID
|
|
35113
35168
|
};
|
|
35114
35169
|
bridge.instances.push(claimInstance);
|
|
35170
|
+
const isSubagentTranscript = subagentSessionCheck();
|
|
35171
|
+
const isSubagent = async (context) => await isSubagentTranscript(context) || isRegisteredSubagent(context);
|
|
35115
35172
|
pi.on("session_start", async (_event, context) => {
|
|
35173
|
+
if (await isSubagent(context))
|
|
35174
|
+
return;
|
|
35116
35175
|
const previousSessionID = sessionID;
|
|
35117
35176
|
restoreLocalSessionState(context);
|
|
35118
35177
|
if (dispatchConfig.error !== null) {
|
|
@@ -35148,6 +35207,8 @@ function envoyExtension(pi) {
|
|
|
35148
35207
|
}
|
|
35149
35208
|
});
|
|
35150
35209
|
const rebind = async (reason, context) => {
|
|
35210
|
+
if (await isSubagent(context))
|
|
35211
|
+
return;
|
|
35151
35212
|
const previousID = sessionID;
|
|
35152
35213
|
restoreLocalSessionState(context);
|
|
35153
35214
|
if (defaults.natsUrls.length === 0)
|
package/dist/legion.js
CHANGED
|
@@ -16113,8 +16113,7 @@ var require_mod4 = __commonJS(function(exports) {
|
|
|
16113
16113
|
|
|
16114
16114
|
// extensions/legion.ts
|
|
16115
16115
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
16116
|
-
import
|
|
16117
|
-
import path3 from "path";
|
|
16116
|
+
import path4 from "path";
|
|
16118
16117
|
|
|
16119
16118
|
// ../envoy-client/src/defaults.ts
|
|
16120
16119
|
var DEFAULT_ENVOY_URL = "http://127.0.0.1:9020";
|
|
@@ -16145,7 +16144,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
16145
16144
|
// package.json
|
|
16146
16145
|
var package_default = {
|
|
16147
16146
|
name: "@sjawhar/pi-legion-envoy",
|
|
16148
|
-
version: "1.
|
|
16147
|
+
version: "1.59.1",
|
|
16149
16148
|
type: "module",
|
|
16150
16149
|
omp: {
|
|
16151
16150
|
extensions: [
|
|
@@ -30702,6 +30701,40 @@ function createLegionTool(deps) {
|
|
|
30702
30701
|
};
|
|
30703
30702
|
}
|
|
30704
30703
|
|
|
30704
|
+
// src/subagent-session.ts
|
|
30705
|
+
import fs from "fs";
|
|
30706
|
+
import path3 from "path";
|
|
30707
|
+
import * as host from "@oh-my-pi/pi-coding-agent";
|
|
30708
|
+
var LEGION_BOOTSTRAPPED_SESSION = Symbol.for("legion.pi-envoy.bootstrapped-session");
|
|
30709
|
+
var bootstrappedSessionStore = globalThis;
|
|
30710
|
+
function recordBootstrappedSession(sessionFile) {
|
|
30711
|
+
bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION] = sessionFile;
|
|
30712
|
+
}
|
|
30713
|
+
async function isSubagentSession(context) {
|
|
30714
|
+
await context.sessionManager.ensureOnDisk();
|
|
30715
|
+
const sessionFile = context.sessionManager.getSessionFile();
|
|
30716
|
+
if (sessionFile !== undefined && fs.existsSync(sessionFile)) {
|
|
30717
|
+
return fs.existsSync(`${path3.dirname(sessionFile)}.jsonl`);
|
|
30718
|
+
}
|
|
30719
|
+
const bootstrapped = bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION];
|
|
30720
|
+
return bootstrapped !== undefined && bootstrapped !== sessionFile;
|
|
30721
|
+
}
|
|
30722
|
+
function isRegisteredSubagent(context) {
|
|
30723
|
+
const registry = host.AgentRegistry?.global();
|
|
30724
|
+
if (registry === undefined)
|
|
30725
|
+
return false;
|
|
30726
|
+
const sessionID = context.sessionManager.getSessionId();
|
|
30727
|
+
const sessionFile = context.sessionManager.getSessionFile();
|
|
30728
|
+
return registry.list().some((ref) => ref.kind !== "main" && (ref.session === null ? sessionFile !== undefined && ref.sessionFile === sessionFile : ref.session.sessionManager.getSessionId() === sessionID));
|
|
30729
|
+
}
|
|
30730
|
+
function subagentSessionCheck() {
|
|
30731
|
+
let subagentSession;
|
|
30732
|
+
return (context) => {
|
|
30733
|
+
subagentSession ??= isSubagentSession(context);
|
|
30734
|
+
return subagentSession;
|
|
30735
|
+
};
|
|
30736
|
+
}
|
|
30737
|
+
|
|
30705
30738
|
// extensions/legion.ts
|
|
30706
30739
|
var exitProcess = (code) => process.exit(code);
|
|
30707
30740
|
function setLegionBootstrapExitForTests(hook) {
|
|
@@ -30767,29 +30800,11 @@ async function persistedTranscript(context) {
|
|
|
30767
30800
|
if (!sessionFile?.endsWith(".jsonl")) {
|
|
30768
30801
|
throw new Error("Legion session must have a persisted transcript");
|
|
30769
30802
|
}
|
|
30770
|
-
const agentId =
|
|
30803
|
+
const agentId = path4.basename(sessionFile, ".jsonl");
|
|
30771
30804
|
if (!agentId)
|
|
30772
30805
|
throw new Error("Legion session transcript has no agent id");
|
|
30773
30806
|
return { sessionFile, agentId };
|
|
30774
30807
|
}
|
|
30775
|
-
var LEGION_BOOTSTRAPPED_SESSION = Symbol.for("legion.pi-envoy.bootstrapped-session");
|
|
30776
|
-
var bootstrappedSessionStore = globalThis;
|
|
30777
|
-
function recordBootstrappedSession(sessionFile) {
|
|
30778
|
-
bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION] = sessionFile;
|
|
30779
|
-
}
|
|
30780
|
-
function resetLegionBootstrappedSessionForTests() {
|
|
30781
|
-
delete bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION];
|
|
30782
|
-
}
|
|
30783
|
-
async function isSubagentSession(context) {
|
|
30784
|
-
await context.sessionManager.ensureOnDisk();
|
|
30785
|
-
const sessionFile = context.sessionManager.getSessionFile();
|
|
30786
|
-
if (sessionFile === undefined)
|
|
30787
|
-
return false;
|
|
30788
|
-
const bootstrapped = bootstrappedSessionStore[LEGION_BOOTSTRAPPED_SESSION];
|
|
30789
|
-
if (bootstrapped !== undefined && bootstrapped !== sessionFile)
|
|
30790
|
-
return true;
|
|
30791
|
-
return fs.existsSync(`${path3.dirname(sessionFile)}.jsonl`);
|
|
30792
|
-
}
|
|
30793
30808
|
function isSingleLegionCommand(command) {
|
|
30794
30809
|
if (typeof command !== "string")
|
|
30795
30810
|
return false;
|
|
@@ -30937,11 +30952,7 @@ function legionExtension(pi) {
|
|
|
30937
30952
|
const controlCodec = import_nats.StringCodec();
|
|
30938
30953
|
let capability;
|
|
30939
30954
|
let bootstrap;
|
|
30940
|
-
|
|
30941
|
-
const checkSubagentSession = (context) => {
|
|
30942
|
-
subagentSession ??= isSubagentSession(context);
|
|
30943
|
-
return subagentSession;
|
|
30944
|
-
};
|
|
30955
|
+
const checkSubagentSession = subagentSessionCheck();
|
|
30945
30956
|
let phaseWorkerPane;
|
|
30946
30957
|
let daemonClient;
|
|
30947
30958
|
const roleDaemon = () => {
|
|
@@ -31305,6 +31316,5 @@ function legionExtension(pi) {
|
|
|
31305
31316
|
}
|
|
31306
31317
|
export {
|
|
31307
31318
|
legionExtension as default,
|
|
31308
|
-
resetLegionBootstrappedSessionForTests,
|
|
31309
31319
|
setLegionBootstrapExitForTests
|
|
31310
31320
|
};
|
|
@@ -302,7 +302,9 @@ that follow from it:
|
|
|
302
302
|
the question carries `dispatch://KEY/artifact/<slug>` (or `ref`), never just its filename. Text
|
|
303
303
|
they must read to decide belongs in the spec in the first place — see [Artifacts](#artifacts).
|
|
304
304
|
|
|
305
|
-
|
|
305
|
+
## Architecture components
|
|
306
|
+
|
|
307
|
+
Attach every architectural issue to the components it changes. Attach the root before decomposing it: children inherit the root's effective attachment unless they deliberately set their own, so one root attachment classifies the whole tree.
|
|
306
308
|
|
|
307
309
|
```
|
|
308
310
|
dispatch_issue({ project: "CORE", title: "...", components: { mode: "explicit", ids: ["dispatch-server", "web"] } })
|
|
@@ -311,7 +313,9 @@ dispatch_issue_update({ issue: "CORE-13", components: { mode: "none", reason: "h
|
|
|
311
313
|
dispatch_issue_update({ issue: "CORE-14", components: { mode: "inherit" } }) // back to the parent chain's attachment
|
|
312
314
|
```
|
|
313
315
|
|
|
314
|
-
|
|
316
|
+
No native Dispatch tool lists component ids. Read the configured model with `GET /api/v1/projects/{key}/architecture` and use each non-external `components[].id`; when its source repository is your checkout, those ids are the file names under `.dispatch/architecture/` (`web.md` → `web`). An id absent from the model, or an `external` component, is refused with `COMPONENTS_INPUT`.
|
|
317
|
+
|
|
318
|
+
Use `mode: "none"` with a concrete reason only when the work is genuinely non-architectural (for example hiring, process, or operations work). A closed issue can be classified without reopening. Change code and its architecture description (`.dispatch/architecture/<id>.md`) in the same review.
|
|
315
319
|
|
|
316
320
|
Import a project's architecture model from its configured source repository now (a human configures the source in Settings):
|
|
317
321
|
```ts
|