@sjawhar/pi-legion-envoy 0.2.0 → 0.3.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/README.md CHANGED
@@ -64,18 +64,18 @@ OMP at extension files it does not contain.
64
64
 
65
65
  ## Dispatch
66
66
 
67
- Legion sessions use the `envoy_dispatch` tool from `extensions/legion.ts`: it routes through
68
- the Legion daemon's architect-only, tree-scoped `/legion/v1/dispatch-threads` endpoint, which
69
- also registers the thread so replies route back to the tree. The raw dispatch MCP tool is
70
- deliberately not served to Legion sessions — the shared shim exits without serving when it
71
- sees a Legion environment (`LEGION_TREE`/`LEGION_CONTROLLER`), so phase workers cannot bypass
72
- the architect gate with ambient GitHub authority.
73
-
74
- Interactive OMP sessions get the `dispatch` MCP tool the way OpenCode sessions do: the shared
75
- `@legion/envoy-client` shim mounts as a stdio MCP server, and it serves only when
76
- `dispatch.enabled` is true in the shared envoy.json (`~/.config/opencode/envoy.json`,
77
- shallow-merged with `<cwd>/.opencode/envoy.json`) or `DISPATCH_MCP_URL` is set explicitly.
78
- The server URL comes from `dispatch.serverUrl` (default `http://localhost:8766`).
67
+ Every OMP session — Legion sessions included — gets the `dispatch` MCP tool the way
68
+ OpenCode sessions do: the shared `@legion/envoy-client` shim mounts as a stdio MCP server,
69
+ and it serves when `dispatch.enabled` is true in the shared envoy.json
70
+ (`~/.config/opencode/envoy.json`, shallow-merged with `<cwd>/.opencode/envoy.json`) or
71
+ `DISPATCH_MCP_URL` is set explicitly. The server URL comes from `dispatch.serverUrl`
72
+ (default `http://localhost:8766`). Dispatch exists so headless unattended agents —
73
+ Legion architects, planners, phase workers — can raise durable questions to the human.
74
+ Replies route back to the asking session, which is auto-subscribed to the thread's
75
+ GitHub topic; a Legion role's session survives kill/resume because Legion resurrection
76
+ resumes the same OMP session file, so the reply still lands. Lifecycle and scope
77
+ decisions still go through `hub` to the owning architect — Dispatch is for durable
78
+ questions to the human, not for coordination between roles.
79
79
 
80
80
  Mount it in `~/.omp/agent/mcp.json` (user-wide) or `.omp/mcp.json` (per project):
81
81
 
package/dist/envoy.js CHANGED
@@ -29857,21 +29857,6 @@ var LegionDaemonApi = {
29857
29857
  request: architectCapability.extend({ issue: nonEmptyString, comment: exports_external.string().optional() }),
29858
29858
  response: exports_external.object({})
29859
29859
  },
29860
- DispatchThread: {
29861
- request: exports_external.strictObject({
29862
- tree: nonEmptyString,
29863
- issue: nonEmptyString,
29864
- role: legionRole,
29865
- sessionId: nonEmptyString,
29866
- secret: nonEmptyString,
29867
- parent: nonEmptyString,
29868
- subject: nonEmptyString,
29869
- body: nonEmptyString,
29870
- ask: exports_external.unknown().optional(),
29871
- urgency: exports_external.enum(["low", "med", "high", "blocking"]).optional()
29872
- }),
29873
- response: exports_external.object({ thread: exports_external.number().int(), url: nonEmptyString })
29874
- },
29875
29860
  SpawnToken: {
29876
29861
  request: architectCapability.extend({ issue: nonEmptyString, role: legionRole }),
29877
29862
  response: exports_external.object({ spawnToken: nonEmptyString })
@@ -29961,6 +29946,29 @@ function normalizeEnvoyUrl(value) {
29961
29946
  return value.replace(/\/+$/, "");
29962
29947
  }
29963
29948
 
29949
+ // ../envoy-client/src/dispatch-subscribe.ts
29950
+ var ISSUE_URL_RE = /https?:\/\/github\.com\/([^/\s"]+)\/([^/\s"]+)\/issues\/(\d+)/i;
29951
+ var DISPATCH_TOOL_RE = /(^|[-._])dispatch$/i;
29952
+ function isDispatchTool(tool) {
29953
+ return DISPATCH_TOOL_RE.test(tool);
29954
+ }
29955
+ function dispatchThreadTopic(owner, repo, thread) {
29956
+ return `notifications.github.${owner}.${repo}.issue.${thread}.>`;
29957
+ }
29958
+ function dispatchSubscriptionTopic(tool, output) {
29959
+ if (!isDispatchTool(tool))
29960
+ return null;
29961
+ const match = ISSUE_URL_RE.exec(output);
29962
+ if (!match)
29963
+ return null;
29964
+ const owner = match[1];
29965
+ const repo = match[2];
29966
+ const thread = Number(match[3]);
29967
+ if (!Number.isInteger(thread) || thread <= 0)
29968
+ return null;
29969
+ return dispatchThreadTopic(owner, repo, thread);
29970
+ }
29971
+
29964
29972
  // ../envoy-client/src/errors.ts
29965
29973
  function messageFor(error48) {
29966
29974
  return error48 instanceof Error ? error48.message : String(error48);
@@ -30897,6 +30905,18 @@ function envoyExtension(pi) {
30897
30905
  }, defaults.heartbeatMs);
30898
30906
  heartbeatRegistered = true;
30899
30907
  };
30908
+ const recoverRegisteredInterests = async () => {
30909
+ const registry2 = await client.getInterest(sessionID).catch(() => {
30910
+ return;
30911
+ });
30912
+ if (registry2 === undefined)
30913
+ return;
30914
+ for (const topic of registry2.topics) {
30915
+ if (topic === agentSubject(sessionID) || topic.startsWith(ROLE_TOPIC_PREFIX))
30916
+ continue;
30917
+ await subscribe(topic);
30918
+ }
30919
+ };
30900
30920
  const establishSession = async (context) => {
30901
30921
  const previousTopic = sessionID === "" ? undefined : agentSubject(sessionID);
30902
30922
  sessionDirectory = context.cwd;
@@ -30908,6 +30928,9 @@ function envoyExtension(pi) {
30908
30928
  }
30909
30929
  await ensureConnection();
30910
30930
  await subscribe(currentTopic);
30931
+ if ((context.sessionManager.getBranch?.() ?? []).length > 0) {
30932
+ await recoverRegisteredInterests();
30933
+ }
30911
30934
  if (registrationRequired()) {
30912
30935
  await registerSession();
30913
30936
  ensureHeartbeat(context);
@@ -31003,6 +31026,19 @@ function envoyExtension(pi) {
31003
31026
  });
31004
31027
  }
31005
31028
  registerEnvoyWhoamiCommand(pi, () => sessionID);
31029
+ pi.on("tool_result", async (event) => {
31030
+ if (event.isError)
31031
+ return;
31032
+ const topic = dispatchSubscriptionTopic(event.toolName, JSON.stringify(event.details) ?? "");
31033
+ if (topic === null)
31034
+ return;
31035
+ try {
31036
+ if (await subscribe(topic))
31037
+ await registerSession();
31038
+ } catch (error48) {
31039
+ activeSessionContext?.ui.notify(`envoy: dispatch reply auto-subscribe failed (${messageFor(error48)}); run envoy_subscribe ${topic}`, "warning");
31040
+ }
31041
+ });
31006
31042
  async function execute(operation, parameters) {
31007
31043
  try {
31008
31044
  switch (operation) {
@@ -25,9 +25,10 @@ returns that schema to the architect. It writes the same phase-specific payload
25
25
  `.legion/<phase>.json`, verifies it exists, and commits the handoff before reporting completion.
26
26
  The committed predecessor handoff wins after revival or re-creation.
27
27
 
28
- Workers do not run a controller loop, mutate lifecycle labels, or create human dispatch threads.
29
- When an issue needs a product, scope, cross-phase, or human decision, they send the owning
30
- architect the verified observation and decision needed through hub.
28
+ Workers do not run a controller loop or mutate lifecycle labels. Workers coordinate
29
+ lifecycle, scope, and cross-phase decisions with the owning architect through hub, sending
30
+ the verified observation and decision needed. A worker may use the `dispatch` MCP tool
31
+ directly for a durable human question; replies come back to the worker's own session.
31
32
 
32
33
  ## Durable artifacts
33
34
 
@@ -15,7 +15,9 @@ separate coordinator to finish necessary work.
15
15
  - Use the `legion` tool for lifecycle writes. Its issue key format is
16
16
  `owner/repo#number`.
17
17
  - Use `task` for every Legion role spawn and `hub` to direct or revive a known phase
18
- worker. Phase workers escalate inward to you; only you open `envoy_dispatch` threads.
18
+ worker. Phase workers escalate lifecycle, scope, and cross-phase matters inward to you
19
+ through hub. Any role may use `dispatch` directly for a standalone human question;
20
+ replies return to the asking session.
19
21
  - The runtime, not you, appends a machine `<legion-spawn>` block. Each Legion `task`
20
22
  text must start with `Legion-Issue: <owner/repo#n>` on its first line.
21
23
  - Use only the live label vocabulary: `needs-approval`, `human-approved`,
@@ -57,7 +59,7 @@ sub-architect:
57
59
  ```text
58
60
  legion({ op: "post_spec", issue: "<root issue>", body: "<root specification>" })
59
61
  legion({ op: "label_add", issue: "<root issue>", label: "needs-approval" })
60
- envoy_dispatch({
62
+ dispatch({
61
63
  parent: "<root issue>",
62
64
  subject: "Legion design approval requested",
63
65
  body: "<summary, specification, and requested decision>"
@@ -186,7 +188,6 @@ corresponding lifecycle procedure.
186
188
  | `pr-blocked` | Read the failed CI evidence and recovery attempts. Assign a focused implementer or corrective child, then return it through testing and review; do not treat the blocked PR as final. |
187
189
  | `pr-closed-unmerged` | Decide from current scope whether to reopen the work, send a fresh implementer, or cancel it with a reason. Delegate the repository action to the responsible phase worker and keep ownership. |
188
190
  | `issue-comment` | Interpret the comment in the issue's design context. Answer it, adjust the plan, or relay it through `hub` to the responsible worker; scope and product decisions remain with you. |
189
- | `dispatch-reply` | Resolve the question that opened the thread, record the resulting decision in the tree's work, and direct the affected worker through `hub`. |
190
191
  | `catchup-overseer` | Verify its gates, child counts, and PR verdicts against current artifacts, then resume the applicable numbered lifecycle step. It is a current-state snapshot, not a raw-event replay. |
191
192
  | `revive-worker` | The extension has revived the backed worker. Do not create a duplicate; direct the restored worker through `hub` if action is needed and rely on its committed handoff over recollection. |
192
193
  | `reopened` | Reopen the root lifecycle: inspect the reason and current artifacts, reassess scope and children, and resume at the first applicable numbered step. |
@@ -195,5 +196,6 @@ corresponding lifecycle procedure.
195
196
 
196
197
  Controller-actionable matters are exactly re-filing a genuinely independent child,
197
198
  capacity, and cross-tree conflict. Use the Legion escalation operation for those. Handle
198
- everything else in the tree or, for a human question, use `envoy_dispatch`; workers never
199
- open dispatch threads. Do not create a wait loop for any wake source.
199
+ everything else in the tree, or use `dispatch` for a human question; workers may reach
200
+ Sami directly with `dispatch` the same way. Do not create a wait loop for any wake
201
+ source.
@@ -46,7 +46,7 @@ into a state holder: daemon state and GitHub artifacts remain authoritative.
46
46
  |---|---|---|
47
47
  | New issue added to the project board (webhook: issue opened / project item added; resync heals misses) | issue ref + triage context (incl. pre-existing children) | Triage: spawn root process via daemon admission, or park in the daemon-state backlog |
48
48
  | Backlog eligibility | slot freed / priority change | Reconsider parked items; deliberately-backlogged issues carry a marker so resync doesn't re-flag them |
49
- | Architect escalation (controller-actionable only: re-file a child as a root issue, capacity, cross-tree conflicts) | request + context | Judge and act; human Q&A never routes here architects open dispatch threads |
49
+ | 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` from the owning architect, not here |
50
50
  | Resync report | artifact-driven anomaly list (zero-owner trees, erroring issues) | Verify against fresh state, then dispatch/heal |
51
51
  | Mention | Slack/GitHub @mention text | Answer, or route to the owning issue's architect role |
52
52
  | Approval interpretation | ambiguous human comment on a gated issue | Decide whether it's an approval; if so, apply `human-approved` via the daemon |
@@ -82,8 +82,8 @@ is a deliberate controller decision, not a no-op.
82
82
  ## Architect escalation
83
83
 
84
84
  Only decide controller-actionable escalations: re-filing independent work, capacity, and
85
- cross-tree conflicts. Architects handle ordinary human Q&A through their own dispatch
86
- threads.
85
+ cross-tree conflicts. Issue-scoped human Q&A goes through `dispatch` from the owning
86
+ architect, not the controller.
87
87
 
88
88
  For an independence judgment, verify the child and its parent against GitHub and current
89
89
  daemon state. If the work belongs in an independent root:
@@ -19,9 +19,10 @@ its own role again.
19
19
 
20
20
  Read the current issue and its acceptance criteria before changing the workspace. Work only
21
21
  on this phase's artifact. You may use ordinary scouts, reviewers, and oracle subagents for
22
- phase work; never spawn legion-role workers or open human dispatch threads. Escalate a product,
23
- scope, cross-phase, or human decision to the owning architect through hub, with the verified
24
- facts and the decision needed.
22
+ phase work; never spawn legion-role workers. Escalate a product, scope, cross-phase, or
23
+ lifecycle decision to the owning architect through hub, with the verified facts and the
24
+ decision needed. For a durable question that needs Sami directly, you may use the raw
25
+ `dispatch` MCP tool yourself; replies return to your own session.
25
26
 
26
27
  ## Workspace and handoff precedence
27
28
 
@@ -171,5 +172,6 @@ pipeline labels, run a controller loop, or notify a controller with an invented
171
172
  protocol. A direct worker delivery belongs to its role; overseers receive only derived
172
173
  verdicts.
173
174
 
174
- When blocked, send the owning architect a concise hub message: issue, phase, verified
175
- observation, what you tried, and the decision required. Do not dispatch a human thread yourself.
175
+ When blocked on lifecycle, scope, or cross-phase matters, send the owning architect a concise
176
+ hub message: issue, phase, verified observation, what you tried, and the decision required.
177
+ Reach for `dispatch` yourself only for a standalone human question outside that coordination.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-legion-envoy",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "omp": {
6
6
  "extensions": [