@sjawhar/pi-legion-envoy 1.55.1 → 1.56.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 +107 -17
- package/dist/legion.js +1 -1
- package/dist/skills/dispatch/SKILL.md +51 -6
- package/package.json +1 -1
package/dist/envoy.js
CHANGED
|
@@ -32567,6 +32567,36 @@ function notSubscribed(owner) {
|
|
|
32567
32567
|
function followsAsk(owner) {
|
|
32568
32568
|
return `You follow this ask: its answer and replies reach you directly. For every event on ${owner.label}: envoy_subscribe ${owner.topic}`;
|
|
32569
32569
|
}
|
|
32570
|
+
var triageAdviceShown = new Set;
|
|
32571
|
+
function renderAdvice(tool, key, advice, opts) {
|
|
32572
|
+
if (advice === undefined)
|
|
32573
|
+
return [];
|
|
32574
|
+
const hasIssueAdvice = advice.issue_status !== undefined;
|
|
32575
|
+
const openAsks = advice.your_open_asks;
|
|
32576
|
+
const writesSinceHuman = advice.session_writes_since_human;
|
|
32577
|
+
const lines = [];
|
|
32578
|
+
if (advice.decision_blocks === 0 && opts.isPrimarySpec === true && (tool === "dispatch_issue" || tool === "dispatch_artifact")) {
|
|
32579
|
+
lines.push('No decision blocks in this spec \u2014 nothing here reaches a human\'s inbox. Want human feedback? See the `dispatch` skill, "Decision blocks".');
|
|
32580
|
+
}
|
|
32581
|
+
if (hasIssueAdvice && writesSinceHuman !== undefined && writesSinceHuman >= 3 && (tool === "dispatch_message" || tool === "dispatch_ask" || tool === "dispatch_comment" && opts.isAskReply !== true)) {
|
|
32582
|
+
const middle = writesSinceHuman >= 6 ? "Stop posting here until a human replies." : "Progress ledger or scratchpad? If so, stop.";
|
|
32583
|
+
lines.push(`You've sent ${writesSinceHuman} messages on ${key} with no human response. ${middle} See the \`dispatch\` skill, "Structure over stream".`);
|
|
32584
|
+
}
|
|
32585
|
+
if (advice.issue_status === "triage" && tool !== "dispatch_issue" && !(tool === "dispatch_issue_update" && opts.setsStatus === true) && !triageAdviceShown.has(key)) {
|
|
32586
|
+
triageAdviceShown.add(key);
|
|
32587
|
+
lines.push(`${key} is still in triage \u2014 nobody can see its development status. See the \`dispatch\` skill, "Issue status is yours to move".`);
|
|
32588
|
+
}
|
|
32589
|
+
if (hasIssueAdvice && openAsks !== undefined && openAsks.length > 0 && (tool === "dispatch_message" || tool === "dispatch_doc_edit" || tool === "dispatch_issue_update" || tool === "dispatch_comment" && opts.replyToOwnAsk !== true)) {
|
|
32590
|
+
const askCount = Math.min(openAsks.length, 2);
|
|
32591
|
+
for (let index = 0;index < askCount; index += 1) {
|
|
32592
|
+
const ask = openAsks[index];
|
|
32593
|
+
if (ask === undefined)
|
|
32594
|
+
break;
|
|
32595
|
+
lines.push(`You still have an open ask on ${key}: "${ask.question.slice(0, 80)}" (${ask.id}). Still needed? See the \`dispatch\` skill, "Close what you opened".`);
|
|
32596
|
+
}
|
|
32597
|
+
}
|
|
32598
|
+
return lines;
|
|
32599
|
+
}
|
|
32570
32600
|
function documentResultDetails(artifact) {
|
|
32571
32601
|
return {
|
|
32572
32602
|
project: artifact.project,
|
|
@@ -33455,9 +33485,19 @@ async function executeDispatchTool(input) {
|
|
|
33455
33485
|
...Array.isArray(labels) ? { labels } : {},
|
|
33456
33486
|
actor
|
|
33457
33487
|
});
|
|
33488
|
+
const adviceLines = renderAdvice(input.tool, created.key, created.advice, {
|
|
33489
|
+
isPrimarySpec: spec !== undefined
|
|
33490
|
+
});
|
|
33458
33491
|
return {
|
|
33459
|
-
text:
|
|
33460
|
-
|
|
33492
|
+
text: [
|
|
33493
|
+
`Created ${created.key}: ${created.title} ${notSubscribed(issueTopic(created.key))}`,
|
|
33494
|
+
...adviceLines
|
|
33495
|
+
].join(`
|
|
33496
|
+
`),
|
|
33497
|
+
details: {
|
|
33498
|
+
issue: created.key,
|
|
33499
|
+
...created.advice === undefined ? {} : { advice: created.advice }
|
|
33500
|
+
}
|
|
33461
33501
|
};
|
|
33462
33502
|
} catch (error) {
|
|
33463
33503
|
if (!(error instanceof DispatchServiceError) || error.code !== "POSSIBLE_DUPLICATE") {
|
|
@@ -33514,12 +33554,20 @@ async function executeDispatchTool(input) {
|
|
|
33514
33554
|
...parent === undefined ? [] : [after.parent === null ? "parent cleared" : `parent -> ${after.parent}`],
|
|
33515
33555
|
...components === undefined ? [] : [componentsChange(components, after.components)]
|
|
33516
33556
|
];
|
|
33557
|
+
const adviceLines = renderAdvice(input.tool, after.key, after.advice, {
|
|
33558
|
+
setsStatus: status !== undefined
|
|
33559
|
+
});
|
|
33517
33560
|
return {
|
|
33518
|
-
text:
|
|
33561
|
+
text: [
|
|
33562
|
+
`${after.key}: ${changes.join("; ")} ${notSubscribed(issueTopic(after.key))}`,
|
|
33563
|
+
...adviceLines
|
|
33564
|
+
].join(`
|
|
33565
|
+
`),
|
|
33519
33566
|
details: {
|
|
33520
33567
|
issue: after.key,
|
|
33521
33568
|
status: after.status,
|
|
33522
|
-
external_links: after.external_links.map((link) => link.url)
|
|
33569
|
+
external_links: after.external_links.map((link) => link.url),
|
|
33570
|
+
...after.advice === undefined ? {} : { advice: after.advice }
|
|
33523
33571
|
}
|
|
33524
33572
|
};
|
|
33525
33573
|
} catch (error) {
|
|
@@ -33650,10 +33698,18 @@ async function executeDispatchTool(input) {
|
|
|
33650
33698
|
};
|
|
33651
33699
|
const ask = resolved?.owner.kind === "project" ? await client.artifactAsk(resolved.artifact.id, askInput) : await client.ask(issue(), askInput);
|
|
33652
33700
|
const askOwner = ask.issue_key !== null ? issueTopic(ask.issue_key) : resolved === undefined ? issueTopic(issue()) : documentTopic(resolved.artifact);
|
|
33701
|
+
const adviceLines = renderAdvice(input.tool, askOwner.label, ask.advice, {});
|
|
33653
33702
|
return {
|
|
33654
|
-
text:
|
|
33703
|
+
text: [
|
|
33704
|
+
`Asked ${ask.id} on ${askOwner.label} (urgency ${ask.urgency}): ${ask.question}
|
|
33655
33705
|
${followsAsk(askOwner)}`,
|
|
33656
|
-
|
|
33706
|
+
...adviceLines
|
|
33707
|
+
].join(`
|
|
33708
|
+
`),
|
|
33709
|
+
details: {
|
|
33710
|
+
...await followedAskDetails(client, ask, resolved?.artifact),
|
|
33711
|
+
...ask.advice === undefined ? {} : { advice: ask.advice }
|
|
33712
|
+
}
|
|
33657
33713
|
};
|
|
33658
33714
|
}
|
|
33659
33715
|
case "dispatch_edit_ask": {
|
|
@@ -33698,21 +33754,34 @@ ${followsAsk(askOwner)}`,
|
|
|
33698
33754
|
const comment = resolved?.owner.kind === "project" ? await client.artifactComment(resolved.artifact.id, commentInput) : await client.comment(issue(), commentInput);
|
|
33699
33755
|
const commentOwner = resolved === undefined ? issueTopic(issue()) : resolvedTopic(resolved);
|
|
33700
33756
|
const commentDetails = resolved === undefined ? { issue: comment.issue_key, comment: comment.id } : writeResultDetails(resolved, { comment: comment.id });
|
|
33757
|
+
const adviceLines = renderAdvice(input.tool, commentOwner.label, comment.advice, {
|
|
33758
|
+
isAskReply: replyToAsk !== undefined,
|
|
33759
|
+
replyToOwnAsk: replyToAsk !== undefined && (comment.advice?.your_open_asks?.some((ask) => ask.id === replyToAsk) ?? false)
|
|
33760
|
+
});
|
|
33701
33761
|
if (replyToAsk !== undefined) {
|
|
33702
33762
|
const askState = comment.turn === null ? "" : `; ask now waiting on ${comment.turn}`;
|
|
33703
33763
|
return {
|
|
33704
|
-
text:
|
|
33764
|
+
text: [
|
|
33765
|
+
`Replied on ask ${replyToAsk} (comment ${comment.id}${askState}). ${followsAsk(commentOwner)}`,
|
|
33766
|
+
...adviceLines
|
|
33767
|
+
].join(`
|
|
33768
|
+
`),
|
|
33705
33769
|
details: {
|
|
33706
33770
|
...commentDetails,
|
|
33707
33771
|
ask: replyToAsk,
|
|
33708
33772
|
follows: { ask: replyToAsk },
|
|
33709
|
-
...comment.turn === null ? {} : { ask_waiting_on: comment.turn }
|
|
33773
|
+
...comment.turn === null ? {} : { ask_waiting_on: comment.turn },
|
|
33774
|
+
...comment.advice === undefined ? {} : { advice: comment.advice }
|
|
33710
33775
|
}
|
|
33711
33776
|
};
|
|
33712
33777
|
}
|
|
33713
33778
|
return {
|
|
33714
|
-
text: `Posted comment ${comment.id} ${notSubscribed(commentOwner)}`,
|
|
33715
|
-
|
|
33779
|
+
text: [`Posted comment ${comment.id} ${notSubscribed(commentOwner)}`, ...adviceLines].join(`
|
|
33780
|
+
`),
|
|
33781
|
+
details: {
|
|
33782
|
+
...commentDetails,
|
|
33783
|
+
...comment.advice === undefined ? {} : { advice: comment.advice }
|
|
33784
|
+
}
|
|
33716
33785
|
};
|
|
33717
33786
|
}
|
|
33718
33787
|
case "dispatch_suggest": {
|
|
@@ -33742,9 +33811,18 @@ ${followsAsk(askOwner)}`,
|
|
|
33742
33811
|
actor
|
|
33743
33812
|
});
|
|
33744
33813
|
const messageRef = dispatchChildRef(dispatchIssueRef(issueKey), "message", message.id);
|
|
33814
|
+
const adviceLines = renderAdvice(input.tool, issueKey, message.advice, {});
|
|
33745
33815
|
return {
|
|
33746
|
-
text:
|
|
33747
|
-
|
|
33816
|
+
text: [
|
|
33817
|
+
`Posted message ${message.id} (${messageRef}) ${notSubscribed(issueTopic(issueKey))}`,
|
|
33818
|
+
...adviceLines
|
|
33819
|
+
].join(`
|
|
33820
|
+
`),
|
|
33821
|
+
details: {
|
|
33822
|
+
issue: issueKey,
|
|
33823
|
+
message: message.id,
|
|
33824
|
+
...message.advice === undefined ? {} : { advice: message.advice }
|
|
33825
|
+
}
|
|
33748
33826
|
};
|
|
33749
33827
|
}
|
|
33750
33828
|
case "dispatch_doc_edit": {
|
|
@@ -33762,11 +33840,14 @@ ${followsAsk(askOwner)}`,
|
|
|
33762
33840
|
const retyped = ops.filter((operation) => operation.op === "retype").length;
|
|
33763
33841
|
const versionText = edited.version === null ? "no new version" : `version ${edited.version.number}`;
|
|
33764
33842
|
const applied = retyped === 0 ? `Applied ${edited.applied} ops (${versionText})` : `Applied ${edited.applied} ops; retyped ${retyped} block${retyped === 1 ? "" : "s"} (${versionText})`;
|
|
33843
|
+
const adviceLines = renderAdvice(input.tool, resolvedTopic(resolved).label, edited.advice, {});
|
|
33765
33844
|
return {
|
|
33766
|
-
text: `${applied} ${notSubscribed(resolvedTopic(resolved))}`,
|
|
33845
|
+
text: [`${applied} ${notSubscribed(resolvedTopic(resolved))}`, ...adviceLines].join(`
|
|
33846
|
+
`),
|
|
33767
33847
|
details: writeResultDetails(resolved, {
|
|
33768
33848
|
applied: edited.applied,
|
|
33769
|
-
...edited.version === null ? {} : { version: edited.version.number }
|
|
33849
|
+
...edited.version === null ? {} : { version: edited.version.number },
|
|
33850
|
+
...edited.advice === undefined ? {} : { advice: edited.advice }
|
|
33770
33851
|
})
|
|
33771
33852
|
};
|
|
33772
33853
|
}
|
|
@@ -33838,15 +33919,24 @@ ${trailer.join(`
|
|
|
33838
33919
|
const result = artifactOwner.kind === "project" ? await client.projectArtifact(artifactOwner.project, artifactInput) : await client.artifact(issue(), artifactInput);
|
|
33839
33920
|
const artifactRef = dispatchDocumentRef(artifactOwner.kind === "project" ? artifactOwner.project : issue(), result.artifact.slug);
|
|
33840
33921
|
const uploadOwner = artifactOwner.kind === "project" ? documentTopic(result.artifact) : issueTopic(issue());
|
|
33922
|
+
const adviceLines = renderAdvice(input.tool, uploadOwner.label, result.advice, {
|
|
33923
|
+
isPrimarySpec: result.artifact.primary || result.artifact.name === "spec.md"
|
|
33924
|
+
});
|
|
33841
33925
|
return {
|
|
33842
|
-
text:
|
|
33926
|
+
text: [
|
|
33927
|
+
`Uploaded ${result.artifact.name} as version ${result.version.number} (artifact slug ${result.artifact.slug}; ${artifactRef}) ${notSubscribed(uploadOwner)}`,
|
|
33928
|
+
...adviceLines
|
|
33929
|
+
].join(`
|
|
33930
|
+
`),
|
|
33843
33931
|
details: artifactOwner.kind === "project" ? {
|
|
33844
33932
|
...documentResultDetails(result.artifact),
|
|
33845
|
-
version: result.version.number
|
|
33933
|
+
version: result.version.number,
|
|
33934
|
+
...result.advice === undefined ? {} : { advice: result.advice }
|
|
33846
33935
|
} : {
|
|
33847
33936
|
issue: issue(),
|
|
33848
33937
|
artifact: result.artifact.id,
|
|
33849
|
-
version: result.version.number
|
|
33938
|
+
version: result.version.number,
|
|
33939
|
+
...result.advice === undefined ? {} : { advice: result.advice }
|
|
33850
33940
|
}
|
|
33851
33941
|
};
|
|
33852
33942
|
}
|
package/dist/legion.js
CHANGED
|
@@ -16145,7 +16145,7 @@ import { logger } from "@oh-my-pi/pi-utils";
|
|
|
16145
16145
|
// package.json
|
|
16146
16146
|
var package_default = {
|
|
16147
16147
|
name: "@sjawhar/pi-legion-envoy",
|
|
16148
|
-
version: "1.
|
|
16148
|
+
version: "1.56.1",
|
|
16149
16149
|
type: "module",
|
|
16150
16150
|
omp: {
|
|
16151
16151
|
extensions: [
|
|
@@ -75,13 +75,11 @@ route"}`; when you see that, you typed the path wrong — read the index rather
|
|
|
75
75
|
|
|
76
76
|
## Writing a spec
|
|
77
77
|
|
|
78
|
-
A spec has two readers: the human who decides reads the top; the implementer who builds reads the
|
|
79
|
-
rest. Use these headings in this order.
|
|
78
|
+
A spec has two readers: the human who decides reads the **Summary** and **New since we talked** at the top, then each decision through its [`:::ask` block](#decision-blocks) where it arises; the implementer who builds reads the rest. Use these headings in this order.
|
|
80
79
|
|
|
81
80
|
| Section | Required content | Form |
|
|
82
81
|
| --- | --- | --- |
|
|
83
82
|
| **Summary** | The problem, what changes for whom, and how we will know it worked — in plain words. | Three sentences at most. |
|
|
84
|
-
| **Decisions needed** | Only decisions that need human authority, taste, or risk appetite: each one plain question, two or three options with what each costs, and your recommendation with its reason — written as an `ask` block directly under those options, so it is answered in context (see [Design changes are brainstormed here](#design-changes-are-brainstormed-here)). An answered item moves into Requirements with its provenance. | One `ask` block per decision; empty is fine. |
|
|
85
83
|
| **New since we talked** | Every design point the human did not settle in conversation, marked `inferred:` with the reasoning. Empty is fine. | One plain sentence per point. |
|
|
86
84
|
| **Acceptance** | Each outcome names what a user will observe and the check that proves it (browser scenario, API call, or command). An outcome without a check is not acceptance. | Numbered lines. |
|
|
87
85
|
| **Requirements** | What must hold, and where each came from: a quoted human sentence, or `inferred:` plus the reasoning. Readers treat inferred requirements as hypotheses. | `requirement \| where it comes from` table, or prose if the reader follows it more easily. |
|
|
@@ -94,13 +92,33 @@ rest. Use these headings in this order.
|
|
|
94
92
|
|
|
95
93
|
- The spec is the issue's one primary document. Extend it in place — a new version that keeps the
|
|
96
94
|
human's own text — never a second "spec" artifact beside it.
|
|
97
|
-
- No hedging ("might", "could consider"). No TBD, TODO, or placeholders: an open item is either
|
|
98
|
-
|
|
95
|
+
- No hedging ("might", "could consider"). No TBD, TODO, or placeholders: an open item is either
|
|
96
|
+
an ask block or a question for the platform PO whose ruling becomes a Requirement (see
|
|
99
97
|
[Before you ask](#before-you-ask) under Asking).
|
|
100
98
|
- Keep each section to one screen; work that exceeds one screen per section is two specs.
|
|
101
99
|
- Update the spec as decisions land: the spec is the record, comments are the discussion.
|
|
102
100
|
- Before sending it: no sections conflict, every requirement has exactly one reading, and the
|
|
103
|
-
Summary and
|
|
101
|
+
Summary and every ask block pass the phone test above.
|
|
102
|
+
|
|
103
|
+
## Decision blocks
|
|
104
|
+
|
|
105
|
+
A decision a human must make is an `:::ask` block where the decision arises in the spec: inside
|
|
106
|
+
the section whose content it is about, never gathered into a list at the top or bottom. Sami,
|
|
107
|
+
LEGION-204 comment, 2026-09-20 14:47Z, verbatim: "Adding a bunch of decision blocks at the top is
|
|
108
|
+
terrible!! Decisions should be in context in the spec".
|
|
109
|
+
|
|
110
|
+
The block is what reaches the human's Inbox. A question phrased as prose in the spec reaches
|
|
111
|
+
nobody. A spec with no ask blocks is fine only when the issue genuinely needs no human decision.
|
|
112
|
+
When `dispatch_issue` or `dispatch_artifact` answers `No decision blocks in this spec …`, read it
|
|
113
|
+
as a question, not an error: either no decision is needed and you say nothing, or you forgot to
|
|
114
|
+
make the decision a block and must fix the spec.
|
|
115
|
+
|
|
116
|
+
**Wrong:** a **Decisions needed** list at the top of the spec with three bullets.
|
|
117
|
+
**Right:** put each decision in the design section it belongs to as an `:::ask{#slug}` block, with
|
|
118
|
+
2–4 options, a recommendation, and surrounding prose that explains the trade-off.
|
|
119
|
+
|
|
120
|
+
See [Typed blocks](#typed-blocks) for the syntax and [Before you ask](#before-you-ask) under
|
|
121
|
+
[Asking](#asking) to decide whether the question is a real decision at all.
|
|
104
122
|
|
|
105
123
|
## Your owner
|
|
106
124
|
|
|
@@ -152,6 +170,9 @@ renders its state and checks from that link. The call is authenticated with the
|
|
|
152
170
|
other `dispatch_*` tool: a Legion pane reads it from the `DISPATCH_TOKEN_FILE` path the daemon sets on
|
|
153
171
|
the pane; an OMP session outside Legion reads `dispatch.token` from `~/.config/opencode/envoy.json`.
|
|
154
172
|
|
|
173
|
+
A write to an issue still in `triage` answers once with `… is still in triage …`; move the status
|
|
174
|
+
when work has started.
|
|
175
|
+
|
|
155
176
|
## Search first
|
|
156
177
|
|
|
157
178
|
Before you create an issue or start a design document, search:
|
|
@@ -353,6 +374,26 @@ yet — "dispatched two auditors, back with results", "checking the release bran
|
|
|
353
374
|
turn to you. The result names the state (`ask now waiting on agent` / `human`), the delivered `comment.created` carries it as
|
|
354
375
|
`ask_waiting_on`, and every ask read carries it as `waiting_on`.
|
|
355
376
|
|
|
377
|
+
## Close what you opened
|
|
378
|
+
|
|
379
|
+
An ask you opened is yours until it is answered or you resolve it. When the answer arrives some
|
|
380
|
+
other way — Sami said it live, a later comment settled it, or the question became moot because the
|
|
381
|
+
design moved — resolve it yourself with `dispatch_resolve_ask` in the same turn you learn that.
|
|
382
|
+
Never leave it for the human to clear.
|
|
383
|
+
|
|
384
|
+
Sami, AGENTC-27 `rules-derivation-2026-09-17.md` row R04, verbatim: "I think this was answered
|
|
385
|
+
live. If not, please reask." The same pattern left him closing asks as `Dismissed`, `Settled`, and
|
|
386
|
+
`Resolved I think`: noise the human had to clear.
|
|
387
|
+
|
|
388
|
+
Every later write on the issue answers `You still have an open ask on …` and names it. Treat that
|
|
389
|
+
as the checklist: if it is still needed, leave it; if it was answered elsewhere, resolve it with
|
|
390
|
+
the resolving fact as the reason. Before posting a new ask, inspect your open ones. If the new ask
|
|
391
|
+
supersedes one, retract the old one with `dispatch_resolve_ask` and kind `retracted` in the same
|
|
392
|
+
turn.
|
|
393
|
+
|
|
394
|
+
See [Following](#following) for why you receive what happens to asks you open and
|
|
395
|
+
[What comes back](#what-comes-back) for finding them again with `dispatch_open_asks`.
|
|
396
|
+
|
|
356
397
|
## Approval of a spec
|
|
357
398
|
|
|
358
399
|
Approval is a property of a document, not a question you phrase: a human approves a specific version, the way a pull-request
|
|
@@ -592,6 +633,10 @@ high-signal, structured conversation"). The structure IS the product:
|
|
|
592
633
|
- **Never split one deliverable across a message + an ask that points at it.** Ask the question
|
|
593
634
|
with the document reference in the question text; the reader lands on the content in one click.
|
|
594
635
|
|
|
636
|
+
The tool result answers your third consecutive message on an issue with no human reply with
|
|
637
|
+
`You've sent N messages …`. That is the ledger pattern being named; stop and either wait or ask
|
|
638
|
+
once.
|
|
639
|
+
|
|
595
640
|
## Messages
|
|
596
641
|
|
|
597
642
|
Dispatch is a high-signal record for humans, not a log of what you are doing. A message is a reply to a human's message, or a
|