@sjawhar/pi-envoy 10.24.1 → 10.25.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/README.md CHANGED
@@ -79,8 +79,8 @@ pre-split package beside the new ones.
79
79
 
80
80
  The tarball is self-contained: it ships `dist/envoy.js` — bundling every dependency except the OMP
81
81
  host packages (`@legion/pi-shared`, `@legion/contracts` and `@legion/envoy-client` are inlined) —
82
- `dist/THIRD_PARTY_NOTICES`, and the four skills every session with Dispatch reads, `dispatch`,
83
- `dispatch-first`, `dispatch-brainstorming` and `envoy`, staged from the repository's `skills/` at
82
+ `dist/THIRD_PARTY_NOTICES`, and the five skills every session with Dispatch reads, `asd-ste100`,
83
+ `dispatch`, `dispatch-first`, `dispatch-brainstorming` and `envoy`, staged from the repository's `skills/` at
84
84
  `dist/skills`. The manifest declares `omp.skills: ["dist/skills"]` alongside `omp.extensions`:
85
85
  OMP's own plugin discovery only reads `skill://`-resolvable skills from `<plugin root>/skills` or
86
86
  from directories a manifest's `omp.skills` array names, and this package ships skills at
package/dist/dispatch.js CHANGED
@@ -18778,6 +18778,10 @@ function writeMemory(dir, sessionId, follows) {
18778
18778
  writeAtomically(join(dir, "state.json"), `${JSON.stringify(state)}
18779
18779
  `);
18780
18780
  }
18781
+ function appendResult(dir, entry) {
18782
+ ensureDirectory(dir);
18783
+ writeResult(dir, entry);
18784
+ }
18781
18785
  function writeResult(dir, entry) {
18782
18786
  const line = { at: new Date().toISOString(), ...entry };
18783
18787
  appendFileSync(join(dir, "results.jsonl"), `${JSON.stringify(line)}
@@ -19820,45 +19824,49 @@ async function runDispatchCli(argv, rawEnv, io) {
19820
19824
  if (value !== undefined && value !== "")
19821
19825
  env[name] = value;
19822
19826
  }
19827
+ const refuse = (text2, code2, tool2) => {
19828
+ print(text2);
19829
+ const sessionId2 = env.CLAUDE_CODE_SESSION_ID ?? env.DISPATCH_SESSION_ID;
19830
+ if (sessionId2 !== undefined) {
19831
+ try {
19832
+ appendResult(sessionDirectory(env, sessionId2), {
19833
+ tool: tool2 ?? "dispatch",
19834
+ error: text2.trimEnd()
19835
+ });
19836
+ } catch {}
19837
+ }
19838
+ return code2;
19839
+ };
19823
19840
  let parsed;
19824
19841
  try {
19825
19842
  parsed = parseCommand(argv, io);
19826
19843
  } catch (error48) {
19827
- print(`dispatch: ${messageFor(error48)}`);
19828
- return USAGE;
19844
+ return refuse(`dispatch: ${messageFor(error48)}`, USAGE);
19829
19845
  }
19830
19846
  if (parsed.kind === "help") {
19831
19847
  print(parsed.tool === undefined ? commandsHelp() : commandHelp(parsed.tool));
19832
19848
  return OK;
19833
19849
  }
19834
19850
  if (parsed.kind === "refused") {
19835
- if (parsed.tool === undefined) {
19836
- print([`dispatch: ${parsed.problems.join(`
19851
+ return parsed.tool === undefined ? refuse([`dispatch: ${parsed.problems.join(`
19837
19852
  `)}`, "Run dispatch --help."].join(`
19838
- `));
19839
- return USAGE;
19840
- }
19841
- print(new ToolInputError(parsed.tool, parsed.problems).message);
19842
- return REFUSED;
19853
+ `), USAGE) : refuse(new ToolInputError(parsed.tool, parsed.problems).message, REFUSED, parsed.tool);
19843
19854
  }
19844
19855
  const hostName = env.DISPATCH_HOST ?? (env.CLAUDE_CODE_CHILD_SESSION === "1" ? "claude" : undefined);
19845
19856
  const host = HOSTS.find((name) => name === hostName);
19846
19857
  if (host === undefined) {
19847
- print(hostName === undefined ? "dispatch: DISPATCH_HOST is not set. The host plugin sets it in an agent's shell; to run dispatch by hand, set DISPATCH_HOST and DISPATCH_SESSION_ID yourself." : `dispatch: DISPATCH_HOST is ${JSON.stringify(hostName)}, not omp, claude or opencode.`);
19848
- return USAGE;
19858
+ return refuse(hostName === undefined ? "dispatch: DISPATCH_HOST is not set. The host plugin sets it in an agent's shell; to run dispatch by hand, set DISPATCH_HOST and DISPATCH_SESSION_ID yourself." : `dispatch: DISPATCH_HOST is ${JSON.stringify(hostName)}, not omp, claude or opencode.`, USAGE, parsed.tool);
19849
19859
  }
19850
19860
  const sessionVariable = host === "claude" ? "CLAUDE_CODE_SESSION_ID" : "DISPATCH_SESSION_ID";
19851
19861
  const sessionId = env[sessionVariable];
19852
19862
  if (sessionId === undefined) {
19853
- print(`dispatch: ${sessionVariable} is not set, so this call has no session to act as. The host plugin sets it in an agent's shell.`);
19854
- return USAGE;
19863
+ return refuse(`dispatch: ${sessionVariable} is not set, so this call has no session to act as. The host plugin sets it in an agent's shell.`, USAGE, parsed.tool);
19855
19864
  }
19856
19865
  let dir;
19857
19866
  try {
19858
19867
  dir = sessionDirectory(env, sessionId);
19859
19868
  } catch (error48) {
19860
- print(messageFor(error48));
19861
- return USAGE;
19869
+ return refuse(messageFor(error48), USAGE, parsed.tool);
19862
19870
  }
19863
19871
  const file2 = (env.DISPATCH_HOST === undefined ? null : handedConfigFile(env)) ?? sessionConfigFile(host, env);
19864
19872
  if (parsed.dryRun) {
@@ -19871,22 +19879,19 @@ async function runDispatchCli(argv, rawEnv, io) {
19871
19879
  serverUrls: settings?.enabled === true ? dispatchServerUrls(settings) : []
19872
19880
  });
19873
19881
  if (problems2.length > 0) {
19874
- print(new ToolInputError(parsed.tool, problems2).message);
19875
- return REFUSED;
19882
+ return refuse(new ToolInputError(parsed.tool, problems2).message, REFUSED, parsed.tool);
19876
19883
  }
19877
19884
  print(JSON.stringify(parsed.args, null, 2));
19878
19885
  return OK;
19879
19886
  }
19880
19887
  if (file2.path === null) {
19881
- print(`dispatch: ${file2.error}`);
19882
- return USAGE;
19888
+ return refuse(`dispatch: ${file2.error}`, USAGE, parsed.tool);
19883
19889
  }
19884
19890
  const { dispatch: resolution, warnings } = sessionDispatchSettings(file2, env, { cwd: io.cwd });
19885
19891
  for (const warning of warnings)
19886
19892
  print(`dispatch: ${warning}`);
19887
19893
  if (!resolution.enabled) {
19888
- print(resolution.error === null ? `dispatch: ${resolution.notice}` : `dispatch: dispatch config: ${resolution.error}`);
19889
- return USAGE;
19894
+ return refuse(resolution.error === null ? `dispatch: ${resolution.notice}` : `dispatch: dispatch config: ${resolution.error}`, USAGE, parsed.tool);
19890
19895
  }
19891
19896
  const config2 = resolution;
19892
19897
  let topics;
@@ -19916,8 +19921,7 @@ async function runDispatchCli(argv, rawEnv, io) {
19916
19921
  pruneSessions(sessionStateRoot(env), Date.now());
19917
19922
  follows = loadSessionMemory(dir, sessionId);
19918
19923
  } catch (error48) {
19919
- print(`dispatch: ${messageFor(error48)}`);
19920
- return USAGE;
19924
+ return refuse(`dispatch: ${messageFor(error48)}`, USAGE, parsed.tool);
19921
19925
  }
19922
19926
  const { tool, args } = parsed;
19923
19927
  let text;
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dustin Yuchen Teng
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,140 @@
1
+ ---
2
+ name: asd-ste100
3
+ description: "Use when English text must be parsed without a human to resolve ambiguity — tool descriptions, error messages, inter-agent instructions, system prompts, status reports — and misreading has a real cost, or when text reads as dense, hedged, or easy to misparse. Triggers: disambiguate, STE100 rewrite, apply Simplified Technical English, plain-language rewrite, controlled-language rewrite, rewrite so an agent cannot misread this. Not for creative or marketing copy."
4
+ version: 0.4.0
5
+ ---
6
+
7
+ Copied from danyuchn/asd-ste100-skill at 32511c69 under its MIT licence (LICENSE), without its linter.
8
+
9
+ # Simplified Technical English (ASD-STE100)
10
+
11
+ ASD-STE100 is a controlled-language standard built by the aerospace and defense industry (ASD, the AeroSpace and Defense Industries Association of Europe) to stop maintenance technicians from misreading English instructions. The standard removes the two biggest sources of misreading: words with more than one meaning, and sentences with more than one possible structure.
12
+
13
+ This skill borrows that same discipline for a different reader: an **AI agent or a downstream system** that has to parse an English string — an error message, a tool description, an inter-agent instruction, a status report — without a human in the loop to resolve ambiguity. If a maintenance technician can misread "close the valve" as an adjective ("the valve that is near") instead of a command, so can a language model.
14
+
15
+ ## When to Use This Skill
16
+
17
+ - An agent's output (explanation, instruction, log message, tool description) reads as dense, jargon-heavy, or ambiguous.
18
+ - Text will be consumed by another agent, a translation pipeline, or a non-native English reader, and misparsing has a real cost.
19
+ - You are writing a prompt, system message, or tool description and want to remove ambiguity before a model ever sees it.
20
+ - You want a **before/after** comparison showing exactly which rule was violated and how the rewrite fixes it. Ask for it — the default output is the rewritten text alone (see Output Format).
21
+
22
+ This skill is not for creative or marketing copy — STE is deliberately flat and literal. Do not apply it to text where voice, nuance, or persuasion is the point.
23
+
24
+ ## Two Modes
25
+
26
+ Pick a mode before rewriting. If the user does not say which, infer it from the text type. Keep the choice internal unless the user asks for the rule table (see Output Format).
27
+
28
+ **Strict** — procedures, error messages, tool and function descriptions, inter-agent instructions, safety text. Anywhere a wrong reading has a cost. Apply every rule below, including the hard length caps and one-word-one-meaning discipline.
29
+
30
+ **STE-flavored** — READMEs, PR descriptions, changelogs, explanatory prose. Apply the structural rules in full and treat the lexical rules as advisory (see Core Rewrite Rules for that split). In practice that means keeping the sentence length caps, active voice, simple tenses, no phrasal verbs, no semicolons, no nominalization and no marketing adjectives, while dropping the one-word-one-meaning lockdown: prose needs some range, and a strict rewrite of prose reads as a personality transplant rather than a clarification.
31
+
32
+ The two modes and the structural/lexical split are the same distinction seen from two directions. The split says which rules this skill can verify without ASD's dictionary. The modes say which of them to enforce for a given kind of text.
33
+
34
+ ## Source and Scope
35
+
36
+ This skill encodes the **rule categories** of ASD-STE100 Issue 9 (Jan 2025): 53 writing rules across 9 sections covering word choice, grammar, sentence structure, and style, backed by a dictionary of ~900 approved words (one meaning, one part of speech each) and ~1,200 words to avoid with suggested replacements. See `references/writing-rules.md` for the full rule summary and citations.
37
+
38
+ It does **not** reproduce ASD's ~900-word approved dictionary verbatim. ASD-STE100 is free to obtain, but it is not free to redistribute: Issue 9, page 2 states that "no reproduction or publication of it, in whole or in part, shall be made without the written authority of an officer of ASD," and grants free reproduction rights only to eight listed categories (ASD/AIA/AIAC member associations and their member companies and customers, member-state defence ministries, A4A, airworthiness authorities, and universities and research institutes for educational purposes). This project is in none of them, so the dictionary stays out of this repo.
39
+
40
+ Instead, this skill applies the *underlying principle* (pick the plainest, most common word available and use it the same way every time) rather than checking against a fixed word list. When exact ASD-approved wording matters (e.g. actual aircraft maintenance documentation), get the standard and check word-by-word against the real dictionary. Request it from the [official downloads page](https://www.asd-ste100.org/STE_downloads.html) — note that this is a request form that emails you a link, not a direct download.
41
+
42
+ ## Core Rewrite Rules
43
+
44
+ STE's rules divide into two kinds, and this skill can only fully deliver one of them. **Structural rules** are self-contained: they describe sentence shape, and you can apply them from the description alone. **Lexical rules** are defined entirely by the official ~900-word dictionary, which this skill deliberately does not reproduce (see Source and Scope). Without that dictionary, the lexical rules degrade from a checkable standard into a preference for plain words.
45
+
46
+ Apply the structural rules with confidence. Apply the lexical rules as a direction of travel, and say so in your output rather than implying dictionary compliance you cannot verify.
47
+
48
+ ### Structural rules — apply these
49
+
50
+ | Rule | Do | Don't |
51
+ |---|---|---|
52
+ | Active voice | "The agent deletes the file." | "The file is deleted (by the agent)." — unless the actor is genuinely unknown or irrelevant |
53
+ | No phrasal verbs (Rule 9.3) | "Remove the panel." / "Start the job." | "Take off the panel." / "Spin up the job." — a two-word verb has meanings the parts do not predict |
54
+ | One instruction per sentence | "Open the file. Read line 3." | "Open the file and read line 3, then check if it matches." |
55
+ | Sentence length | ≤20 words for instructions/procedures, ≤25 words for descriptions | Long compound/subordinate-clause sentences |
56
+ | No semicolons (Rule 8.1) | Split into separate sentences | Any semicolon at all — STE bans the mark outright, not only as a clause join. (Rule 8.1 permits every other standard punctuation mark. The em dash is *not* banned by STE, though it often signals a sentence that should be split.) |
57
+ | Noun clusters | ≤3 words stacked as a noun phrase ("fuel pump valve") | 4+ word noun stacks ("high pressure fuel pump inlet valve assembly") |
58
+ | No ellipsis | Keep the subject, verb, and article explicit even if it reads longer | Drop words to save space ("Files not backed up will be lost" → ambiguous which files) |
59
+ | Keep modality | "The request **may have** failed." stays "may have" | Promote a hedge to a fact ("The request failed.") or invent a certainty the source did not state |
60
+ | Paragraph limits | One topic per paragraph, ≤6 sentences | Multi-topic paragraphs |
61
+ | Lists for sequences | Use a numbered or bulleted list for 3+ steps or conditions | Bury a sequence inside one prose sentence |
62
+
63
+ ### Lexical rules — direction of travel only
64
+
65
+ | Rule | Do | Don't | Why it is weaker here |
66
+ |---|---|---|---|
67
+ | One word, one meaning | Pick one verb for one action and reuse it every time (e.g. always "check", never mix "check"/"verify"/"confirm" for the same action) | Rotate synonyms for the same idea across a document | Consistency within a document is checkable. Which word is the *approved* one is not, without the dictionary. |
68
+ | One part of speech per word | "Apply oil to the valve" (oil = noun) | "Oil the valve" (oil = verb) | Whether "oil" is approved as a noun only is a dictionary fact. Prefer the noun form when both read equally well. Do not claim compliance. |
69
+ | Verb, not noun (Rule 3.7) | "Analyze the log." | "Perform an analysis of the log." — a noun form of an action makes the sentence longer and hides who acts | Rule 3.7 says "use an **approved** verb to describe an action." Preferring the verb form is safe to apply anywhere. Knowing which verb is the approved one needs the dictionary. |
70
+ | Domain terms | Keep necessary technical nouns/verbs, but define them once if not common English (STE allows a project-specific glossary beyond its base dictionary) | Use jargon without ever defining it | The glossary allowance is real STE, but the base dictionary it extends is absent. |
71
+
72
+ ### Simple tenses — apply with one exception
73
+
74
+ STE permits infinitive, imperative, simple present, simple past, simple future, and past participle as adjective. It excludes present perfect and other compound forms: "we received the report", not "we have received the report".
75
+
76
+ Aircraft manuals never need present perfect, so the exclusion costs the standard nothing. Other text is not always so lucky. "The job has completed" (and its output is available now) and "the job completed" (at some past point) are different statements, and status text frequently needs the first. **Where the compound form carries information the simple form cannot — current relevance, or a hedge as in "may have failed" — keep it and flag the departure.** Elsewhere, follow the rule.
77
+
78
+ ## Scan Checklist
79
+
80
+ These six habits cover most of what makes machine-written English hard to parse. Each one is mechanical: you can point at the exact word or punctuation mark that breaks the rule, with no judgment call. Scan for all six before you rewrite anything.
81
+
82
+ 1. **Synonym rotation** — the same thing gets several names in one document ("the user", "the customer", "the client"). The reader cannot tell whether they are one thing or three. Fix: pick one name, use it every time.
83
+ 2. **Hedge stacking** — helper verbs and qualifiers pile up until the sentence asserts nothing ("it is important to note that this may potentially help to improve"). Fix: state the claim, or delete it.
84
+ 3. **Nominalization** — an action frozen into a noun ("perform an analysis of", "provides assistance to"). Fix: use the verb ("analyze", "helps").
85
+ 4. **Marketing adjectives** — words that claim quality instead of showing it: seamless, robust, powerful, cutting-edge, effortless, blazing-fast. Fix: delete, or replace with the measurement that earns the claim.
86
+ 5. **Run-on sentences** — several ideas joined by semicolons or em dashes. Fix: one idea per sentence.
87
+ 6. **Soft phrasal verbs** — spin up, reach out, dive into, kick off. Fix: use the single plain verb (start, contact, read, begin).
88
+
89
+ ## Process
90
+
91
+ 1. Pick the mode (Strict or STE-flavored). Say which only when the user asked for the rule table — see Output Format.
92
+ 2. Read the input text once for meaning — do not start rewriting before you understand what it must still say afterward.
93
+ 3. Walk it sentence by sentence. Flag every rule violation from the Core Rewrite Rules tables and every habit from the Scan Checklist. In STE-flavored mode, flag the lexical rules but do not enforce them.
94
+ 4. Rewrite each flagged sentence to fix the violation while preserving the original meaning exactly. If a rewrite would drop necessary precision (a safety condition, a scope qualifier, a number), keep the longer phrasing and flag it instead of silently simplifying.
95
+ - **Check modality before you commit to a rewrite.** Hedges ("may", "could", "sometimes", "is likely to") carry the author's confidence, and confidence is content. A shorter sentence that upgrades a hedge to a fact is not a simplification — it is a different claim. This is the most common way a well-intentioned STE rewrite goes wrong, because hedges are exactly what a length cap tempts you to cut.
96
+ - Never add a fact the source did not state. A rewrite that reads better because it supplies a cause, a frequency, or a mechanism has stopped being a rewrite.
97
+ 5. Output the rewritten text (see Output Format). Keep the mode choice and the rule analysis internal unless the user asked to see them.
98
+ 6. If the input already complies, say so — do not force changes onto compliant text.
99
+
100
+ ## Output Format
101
+
102
+ **Default: the rewritten text, and nothing else.** Most callers want a result they can paste straight into a tool description, an error string, or a prompt. Print the simplified text on its own. Do not add a preamble about this skill, a mode announcement, a violation count, a summary of what changed, a rule table, or a closing offer to explain further.
103
+
104
+ The one permitted addition: if step 4 kept a longer phrasing on purpose, add a single line after the text, prefixed `Kept as-is:`, naming the phrase and the precision that would have been lost. Omit the line when there is nothing to report.
105
+
106
+ **On request: the rule table.** When the user asks to see the reasoning — "show the diff", "which rules did it break", "explain the changes", "before/after" — output this table instead:
107
+
108
+ ```markdown
109
+ | Rule violated | Original | Simplified |
110
+ |---|---|---|
111
+ | Present perfect tense | "We have received your request." | "We received your request." |
112
+ | Noun cluster (4+ words) | "the agent task queue priority handler" | "the handler that sets task-queue priority" |
113
+
114
+ Mode: Strict. 7 violations found.
115
+ ```
116
+
117
+ Follow the table with a one-line note on anything you deliberately did **not** simplify, and why (usually: simplifying would lose required precision).
118
+
119
+ ## Boundaries
120
+
121
+ **Will:**
122
+ - Rewrite ambiguous or dense English into short, single-meaning, active-voice sentences.
123
+ - Return the rewritten text alone by default, and name the rules it applied when the user asks.
124
+ - Preserve every fact, condition, and scope qualifier in the original.
125
+ - Preserve the strength of every hedge, and add no claim the source did not make.
126
+ - Suggest a one-line glossary entry for domain terms that must stay.
127
+
128
+ **Will not:**
129
+ - Reproduce ASD's official ~900-word dictionary as if it were memorized verbatim — always treat the official download as the source of truth for exact approved wording.
130
+ - Simplify creative, marketing, or persuasive copy where voice and nuance are the point.
131
+ - Silently drop a safety condition, exception, or scope qualifier to shorten a sentence — it will flag the trade-off instead.
132
+ - Convert "may have failed" into "failed", or "could be caused by X" into "X is the cause" — losing a hedge changes the claim.
133
+ - Guarantee an aerospace/defense-grade STE-compliant document. This is a general-purpose clarity tool inspired by STE, not a certified STE authoring tool.
134
+ - Make weak content true or useful. STE fixes the *form* of a text, not its substance. A hollow paragraph rewritten under these rules becomes a clean, short, well-punctuated hollow paragraph. If the text has nothing to say, no rewrite fixes that — say so instead of polishing it.
135
+ - Shorten past the point of clarity. Cutting words is not the goal. Removing ambiguity is the goal. Past a certain point compression starts costing the reader time rather than saving it, so stop when the sentence is unambiguous, not when it is shortest.
136
+
137
+ ## Additional Resources
138
+
139
+ - **`references/writing-rules.md`** — fuller summary of the 9 rule sections and dictionary structure, with citations to the official standard and secondary sources.
140
+ - **`examples/before-after.md`** — worked examples, including official STE examples and agent-output examples built for this skill.
@@ -0,0 +1,92 @@
1
+ # Before / After Examples
2
+
3
+ ## Part 1 — Official STE Examples
4
+
5
+ These illustrate real ASD-STE100 rules, drawn from public secondary sources (see `references/writing-rules.md`). They are paraphrased illustrations of the rule, not quotes from the standard itself.
6
+
7
+ | Rule | Before | After | Why |
8
+ |---|---|---|---|
9
+ | One meaning per word | "Verify the system." / "Check the connections." / "Confirm receipt." | "Make sure the system is correct." (one approved term used consistently) | Three near-synonyms force the reader to guess whether they mean the same action. |
10
+ | One part of speech per word | "Oil the valve." | "Apply oil to the valve." | If "oil" is approved only as a noun, using it as a verb breaks the one-word-one-role guarantee. |
11
+ | Precise verb meaning | "Follow the safety instructions." | "Obey the safety instructions." | "Follow" can mean "come after" or "obey" — STE picks the unambiguous one. |
12
+ | Simple tense only | "We have received the technical reports from HQ." | "We received the technical reports from HQ." | Present perfect adds a second parse ("received, and still relevant now?") that simple past avoids. |
13
+ | Verb, not noun | "Perform an inspection of the filter." | "Inspect the filter." | The noun form hides the action and adds a filler verb that carries no meaning. |
14
+ | No phrasal verbs | "Take off the access panel." | "Remove the access panel." | "Take off" also means "depart" and "deduct" — the two words together do not predict the meaning. |
15
+
16
+ ## Part 2 — Applied to Agent Output
17
+
18
+ These are original examples built for this skill's actual use case: rewriting AI agent output so another agent, a translation layer, or a non-native reader can parse it without ambiguity. They are illustrations, not quotes from any real system.
19
+
20
+ Word counts below are whitespace-separated tokens (`text.split()`), punctuation not counted separately. A different tokenizer will produce a different number.
21
+
22
+ ### Example A — Tool description
23
+
24
+ **Before:**
25
+ > This tool will attempt to synchronize state across the various backends that have been configured, and if a conflict is detected it may resolve it automatically depending on the strategy that has been set, or otherwise it will surface the conflict for manual review.
26
+
27
+ **Violations flagged:**
28
+ - Two instructions in one sentence (sync + resolve/surface).
29
+ - Present perfect in the relative clauses ("have been configured", "has been set").
30
+ - 44 words, far over the 25-word descriptive cap.
31
+
32
+ Note what is *not* flagged: "will attempt to" and "may resolve". Those are hedges, not violations. The tool is not promised to succeed, and the rewrite must not promise it either.
33
+
34
+ **After:**
35
+ > The tool tries to synchronize state across the configured backends. If it finds a conflict, it reads the configured strategy. If the strategy allows automatic resolution, the tool may resolve the conflict without a user. If the tool does not resolve the conflict, it reports the conflict for manual review.
36
+
37
+ The last sentence branches on whether the conflict was resolved, not on what the strategy allows. That is what "or otherwise" meant in the original: the fallback covers a permitted resolution that still did not happen.
38
+
39
+ ### Example B — Error message
40
+
41
+ **Before:**
42
+ > An error may have occurred while processing your request due to a possible mismatch in the expected data format, which could be caused by an outdated client version.
43
+
44
+ **Violations flagged:**
45
+ - One sentence carrying three separate claims (an error occurred, a format mismatch, a client version).
46
+ - 28 words, over the descriptive cap.
47
+
48
+ Not flagged: "may have occurred" and "could be caused by". The message is written by a system that does not know what went wrong. Both hedges are accurate reporting of that ignorance.
49
+
50
+ **After:**
51
+ > Your request may have failed. The cause may be a data format that does not match what the server expects. An outdated client can cause this mismatch. Check your client version.
52
+
53
+ **This example is the reason the modality rule exists.** An earlier version of this file rewrote the first sentence as "The request failed", and the third as "an outdated client **is the most common cause**". Both read better. Both are wrong: the first asserts a failure the system only suspects, and the second invents a frequency claim that appears nowhere in the input. A rewrite that supplies a cause, a frequency, or a mechanism has stopped being a rewrite.
54
+
55
+ Note also that "may have failed" keeps a compound verb form that the simple-tense rule would otherwise remove. **When the tense rule and the modality rule conflict, modality wins** — dropping the auxiliary here would delete the uncertainty along with the tense.
56
+
57
+ ### Example C — Inter-agent instruction
58
+
59
+ **Before:**
60
+ > Once the upstream job has completed and assuming no errors were raised, the downstream agent should proceed to consume the output artifact, though it is worth noting that partial artifacts are sometimes produced under timeout conditions.
61
+
62
+ **Violations flagged:**
63
+ - Present perfect ("has completed") and subordinate-clause stacking ("assuming...", "though it is worth noting...").
64
+ - One sentence, three separate facts (completion condition, next action, edge-case warning).
65
+ - 36 words, over the 20-word instruction cap.
66
+
67
+ **After:**
68
+ > Wait for the upstream job to finish with no errors. Then read the output artifact. Warning: a timeout can produce a partial artifact. Check that the artifact is complete before you use it.
69
+
70
+ Two deliberate calls worth stating rather than hiding:
71
+ - "should proceed to consume" became the imperative "read". STE permits this for instructions, where a recommendation addressed to the executing agent is a command. Do not make the same move in descriptive text.
72
+ - The final sentence is **new**. The original warned about partial artifacts without saying what to do about it. Adding the check makes the warning actionable, but it is added content, so it is called out here rather than passed off as a rewrite. If the source's silence was deliberate, drop the sentence.
73
+
74
+ ### Example D — README prose (STE-flavored mode)
75
+
76
+ **Before:**
77
+ > Our caching layer is designed to slot seamlessly into your existing stack with minimal friction and no vendor lock-in; it leverages semantic similarity to dramatically reduce the cache misses that traditionally plague LLM workloads.
78
+
79
+ **Violations flagged:**
80
+ - Marketing adjectives and claims without measurement ("seamlessly", "minimal friction", "dramatically").
81
+ - Semicolon joining two separate ideas.
82
+ - Nominalization and soft phrasing ("is designed to slot into", "leverages").
83
+ - 34 words, over the 25-word descriptive cap.
84
+
85
+ **After:**
86
+ > A normal cache matches requests by exact text, so a small change in wording causes a cache miss. This cache compares the meaning of a new prompt against the prompts it already holds. It runs alongside your current stack and stores no data outside it.
87
+
88
+ Flavored mode kept the explanatory rhythm and did not force one fixed term per concept. It still cut the marketing adjectives, the semicolon, and the length.
89
+
90
+ ## How to Read These Examples
91
+
92
+ Part 1 shows the actual rules this skill is built on. Part 2 shows the transfer: the same discipline — one meaning per word, active voice, simple tense, one instruction per sentence, explicit conditions instead of buried subordinate clauses — makes machine-to-machine and cross-language text safer to parse, not just aircraft manuals.
@@ -0,0 +1,59 @@
1
+ # ASD-STE100 Writing Rules — Summary and Sources
2
+
3
+ This file summarizes the public, official description of ASD-STE100 (Simplified Technical English). It paraphrases rule *categories*. It does not reproduce the standard's text or its ~900-word dictionary verbatim. For the authoritative document, request the free download at the official site.
4
+
5
+ ## What ASD-STE100 Is
6
+
7
+ ASD-STE100 is a controlled natural language, first released in 1986 (as AECMA Document PSC-85-16598) by what is now ASD (the AeroSpace and Defense Industries Association of Europe). It was built at the request of European airlines — most staffed by non-native English speakers — who needed maintenance documentation that could not be misread, because a misread instruction on an aircraft can kill people. The standard is maintained by the Simplified Technical English Maintenance Group (STEMG) and has been free to download since Issue 6 (2013). The current edition is Issue 9 (January 2025).
8
+
9
+ ## Structure
10
+
11
+ - **53 writing rules across 9 sections** covering word choice, grammar, sentence structure, and style.
12
+ - **A dictionary** of roughly 900 approved words, each restricted to one meaning and one part of speech, plus roughly 1,200 words to avoid with suggested replacements.
13
+ - **A terminology allowance**: organizations may define their own dictionary of approved technical nouns and verbs beyond the base ~900 words, for domain-specific vocabulary the base dictionary can't cover.
14
+
15
+ ## Rule Categories (Paraphrased)
16
+
17
+ **Word choice**
18
+ - Use approved words only in their approved meaning and part of speech.
19
+ - Each word maps to exactly one meaning — don't rely on context to disambiguate a word that has several dictionary senses.
20
+ - Prefer the plainer, shorter, more common word over a formal or rare synonym.
21
+ - Use an approved verb for an action, not a noun derived from that verb (Rule 3.7).
22
+ - Do not make phrasal verbs by putting a verb and a preposition together (Rule 9.3). Their meaning is not predictable from the parts, and non-native readers and translation systems both mishandle them.
23
+
24
+ **Verb forms**
25
+ - Permitted forms: infinitive, imperative, simple present, simple past, simple future, and past participle used only as an adjective.
26
+ - No present perfect, past perfect, or other compound/auxiliary constructions. "We have received" is not allowed. "We received" is allowed.
27
+ - "-ing" forms are permitted only as a technical noun or as part of a technical noun, not as a verb form.
28
+
29
+ **Voice**
30
+ - Active voice is required for procedures and instructions.
31
+ - Passive voice is allowed only in descriptive text, and only when the actor performing the action is genuinely unknown or irrelevant to the reader.
32
+
33
+ **Sentence structure**
34
+ - One instruction per sentence.
35
+ - Maximum ~20 words per sentence for procedures and instructions. Maximum ~25 words for descriptive text.
36
+ - Do not omit sentence parts (verb, subject, article) just to shorten the sentence — the standard explicitly warns that this creates ambiguity rather than clarity.
37
+ - Noun clusters (strings of nouns stacked as a modifier) are capped at 3 words.
38
+ - Semicolons are not permitted at all (Rule 8.1): "You can use all standard English punctuation marks but not the semicolon (;)." Write separate sentences instead. Every other standard mark, including the em dash, remains permitted.
39
+
40
+ **Paragraph and document structure**
41
+ - One topic per paragraph.
42
+ - Maximum ~6 sentences per paragraph.
43
+ - Use vertical (numbered or bulleted) lists for sequences, conditions, or complex enumerations instead of burying them in prose.
44
+
45
+ **Safety instructions**
46
+ - Safety-critical instructions must open with a clear command or condition, not be buried mid-sentence.
47
+
48
+ ## Why This Skill Repurposes STE for Agent Output
49
+
50
+ STE was designed to eliminate ambiguity for a reader who cannot ask a follow-up question — a technician on a tarmac, working from a manual, with no author to call. An AI agent parsing another agent's output, a tool description, or a system message is in the same position: no back-channel to resolve "does this passive-voice sentence mean the caller does X, or the callee does X?" The same rule set that protects an airline mechanic from a misread torque spec protects a downstream agent from a misread instruction.
51
+
52
+ ## Sources
53
+
54
+ - [ASD-STE100 official site](https://www.asd-ste100.org/)
55
+ - [ASD-STE100 — About STE](https://www.asd-ste100.org/about_STE.html)
56
+ - [ASD Europe — Simplified Technical English](https://www.asd-europe.org/standards-specifications/simplified-technical-english/)
57
+ - [Simplified Technical English — Wikipedia](https://en.wikipedia.org/wiki/Simplified_Technical_English)
58
+ - [TechScribe — ASD-STE100 Simplified Technical English](https://www.techscribe.co.uk/techw/asd-simplified-technical-english.htm)
59
+ - [SKYbrary — Simplified Technical English (STE)](https://skybrary.aero/articles/simplified-technical-english-ste)
@@ -1,310 +1,71 @@
1
1
  ---
2
2
  name: envoy
3
- description: Use when subscribing sessions to Envoy topics, sending agent-to-agent messages, or reasoning about topic formats for Slack/GitHub/agent routing.
3
+ description: Use when you send a message to another agent session, reply to an Envoy delivery, find your own or a peer's session id, subscribe to a topic (a GitHub pull request, CI checks, a Dispatch issue, Slack), publish to a role or team topic, or wait for CI or a merge. Also when you are a subagent that must report to another session, or a subscription stays silent.
4
4
  ---
5
5
 
6
6
  # Envoy
7
7
 
8
- Envoy delivers external signals and session messages. Deliveries are at-least-once and can arrive
9
- out of order across topics: use `id` to deduplicate and `at` to judge freshness.
8
+ If this skill disagrees with the code, a command's output or its `--help`, follow those and say which line of this skill is wrong.
10
9
 
11
- ## The one subscription you need for a PR
10
+ Envoy delivers external events and messages between sessions. Delivery is at-least-once and can come out of order across topics: drop a repeated `id`, and when two deliveries conflict, act on the one with the later `at`.
12
11
 
13
- Subscribe to the whole PR family, not individual event types:
12
+ ## Send a message to another session
14
13
 
15
- ```text
16
- envoy_subscribe([
17
- "notifications.github.example-org.example-repo.pr.42.>"
18
- ])
19
- ```
20
-
21
- The owner and the repository are one token each, with every dot in the name written `_`:
22
- `acme/site.io`'s PR family is `notifications.github.acme.site_io.pr.42.>`, and a topic spelled
23
- `acme.site.io` receives nothing.
24
-
25
- NATS `>` matches **one or more** trailing tokens, so it does not match the lifecycle base
26
- `pr.42` itself. Envoy registers that concrete base automatically when you subscribe to
27
- `<subject>.>`, making `pr.<n>.>` the recommended default: one call receives both the lifecycle
28
- subject and its child events.
29
-
30
- For a typical push, this receives `pr.42` with `synchronize`, then any comments or reviews, then
31
- one `pr.42.checks` event when that head's checks settle. A settlement is published for every
32
- commit of the pull request whose checks settle, the current head or not (a head pushed with
33
- GitHub's `skip-checks` trailer runs none, so the commit before it settles for it), and its `sha`
34
- names the commit: compare it with the head you are waiting on. The family is `pr.42` (lifecycle),
35
- `pr.42.comment`, `pr.42.review`, `pr.42.mention`, and `pr.42.checks`. A closed lifecycle payload
36
- carries `merged`, `merge_commit_sha`, `merged_by`, and `head_sha`.
37
-
38
- The retired literal `pr.<n>.check` and `pr.<n>.ci` topics do not receive events. Existing
39
- registrations remain dead; subscribe to `pr.<n>.checks` (or the recommended `pr.<n>.>`) instead.
40
- Lifecycle stays on the base PR topic and CI arrives as one settled `checks` event.
41
-
42
- ## Inbound deliveries
43
-
44
- Envoy renders an annotated delivery before its source summary and complete payload:
14
+ - Take the target id from the delivery's `reply_with`, or from `envoy_sessions` (filter by `machine`, `dir` or `title`). Never address a tmux pane or window: Envoy delivers only to a session id, and the session in a pane can change.
15
+ - Reply with `in_reply_to=<the delivery's id>`, so the recipient can match the answer to its question.
16
+ - Read the `recipient` in the send result, the session Envoy targeted: if it is not the one you meant, resend to the right id.
17
+ - Put the URL or path of what you report in the message, because the recipient cannot find your files any other way.
18
+ - Set `expects_reply="none"` on a message that needs no answer, and set `urgency` only when the work waits on the answer, so the recipient knows whether to answer and how soon.
45
19
 
46
20
  ```text
47
- envoy:
48
- to: you (ses_example_recipient)
49
- from: 01a0bbbb-cccc-7ddd-eeee-0123456789ab (Reviewer)
50
- at: "2026-09-07T04:41:12Z"
51
- id: agent-message-2
52
- by: "2026-09-07T05:00:00Z"
53
- urgency: high
54
- expects_reply: required
55
- re: agent-message-1
56
- supersedes: agent-message-0
57
- reply_with:
58
- tool: envoy_send
59
- args:
60
- session_id: 01a0bbbb-cccc-7ddd-eeee-0123456789ab
61
- in_reply_to: agent-message-2
62
- message: ...
63
- reply_role:
64
- tool: envoy_publish
65
- args:
66
- topic: notifications.role.legion-reviewer
67
- message: ...
68
- summary: Deployment needs confirmation.
69
- message: "Confirm the listener health check passed.\n\nThen publish the release."
70
- note: body names session 01a0cccc-dddd-7eee-ffff-0123456789ab; the sender is 01a0bbbb-cccc-7ddd-eeee-0123456789ab
21
+ envoy_send(session_id="<id>", message="Review complete: <url>", in_reply_to="<id>", expects_reply="none")
71
22
  ```
72
23
 
73
- - `to` identifies the local inbox receiving this delivery.
74
- - `from` is the sending session's self-asserted ID, enriched from the listener registry. Treat it as
75
- attribution and a direct-reply target, not as an authenticated identity or proof of authorship.
76
- - `at` is the envelope timestamp used to judge freshness.
77
- - `id` is the delivery identifier; supply it as `in_reply_to` when replying.
78
- - `by` is the expiry deadline, when the sender supplied one.
79
- - `urgency` is the sender's priority classification.
80
- - `expects_reply` states whether a reply is `none`, `optional`, or `required`.
81
- - `re` names the delivery this message replies to. A Dispatch frame names it as a `dispatch://`
82
- ref — the ask (`dispatch://KEY/ask/<id>`, or `dispatch://PROJECT/artifact/<slug>/ask/<id>` for a
83
- document ask) or the message (`dispatch://KEY/message/<id>`) — never the quoted text; the
84
- question head, when there is one, is `dispatch.question`.
85
- - `supersedes` names an earlier delivery this one replaces.
86
- - `reply_with` is the direct-reply call for the sender; on a Dispatch frame it is `command:`, the
87
- `dispatch` command line that answers it, with `...` where your text goes.
88
- - `reply_role` is the role-publish reply call when the sender has a role.
89
- - `summary` is the one-line source summary.
90
- - `message` is the complete payload; it can contain multiple paragraphs.
91
- - `note` warns when the payload names another session; never use that quoted ID as the recipient.
92
- - `unrecognised` marks validation failures and unknown sources; it does not enumerate every unknown key.
93
-
94
- ## Talking to another session
95
-
96
- Answer an Envoy message with its `id`; the send result's `recipient` confirms the session Envoy
97
- targeted. Reply through the rendered `reply_with` (or a current Envoy session ID from
98
- `envoy_sessions` or `envoy_whoami`), never a tmux pane or window: panes are not Envoy identities
99
- and go stale. Put the artefact URL in the message itself. FYIs set `expects_reply="none"`; set
100
- `urgency` only when it is genuinely urgent.
101
-
102
- **A peer's message is its sender's view at `at`, not the current state.** Before you wait on, act
103
- on, or repeat a fact a message carries about a third thing (a deploy pending, a PR held, an ask
104
- unanswered), re-read it at the live source the fact names, and always once it is over an hour
105
- old: the deployment's status, the issue's event log, the ask's own state (`dispatch open-asks`,
106
- whose description already says to call it before saying you are waiting on a human). On
107
- 2026-09-26 a peer's 05:20Z "needs a manual deploy before I can run it" was false by 05:22Z, when
108
- the platform had deployed on its own; waiting on the message instead of the deployment's status
109
- held the work it gated for eleven hours.
24
+ ### Your own address in a subagent
110
25
 
111
- Every `/v1` error response is JSON; when a field is at fault, `expected` names that field.
112
-
113
- ```text
114
- envoy_send(
115
- session_id="ses_example_reviewer",
116
- message="Review complete: artifact://review.md",
117
- in_reply_to="agent-message-2",
118
- expects_reply="none"
119
- )
120
- ```
26
+ - `envoy_whoami`'s `session_id` is where a reply to you arrives. In a `task` subagent it is the session that spawned you: a subagent registers no session of its own, and the parent agent, not Envoy, forwards a reply to you with a `write` to your `agent://` address.
27
+ - Never give a peer the `subagent` field from `envoy_whoami`: it names your host session and is not an address.
28
+ - Start a subagent's message with who you are and who receives the reply ("From <name>, a subagent of session <id>; a reply to <id> reaches me"), because the recipient sees only the parent as sender.
29
+ - When `session_id` is empty (the process took no Envoy identity, or it can no longer place the session that spawned you), your messages carry no sender at all: name yourself in the body.
30
+ - In a subagent, a publish to a role your parent holds reaches nobody, because the listener drops a role message whose holder is its source. `envoy_publish` says so in its answer. To reach the parent, `write` to its `agent://` address instead of publishing. `envoy_send` to any other session works.
121
31
 
122
- ### Your own address, and a subagent's
32
+ ## Read a delivery
123
33
 
124
- `envoy_whoami`'s `session_id` is the address a reply to you reaches. Inside a `task` subagent it
125
- is the session that spawned you: a subagent registers no Envoy session of its own, so a peer
126
- answering it reaches that session, which relays to you with a `write` to your `agent://` address.
127
- The `subagent` field in that output carries your own host session id — it is not an address, so
128
- never hand it to a peer.
129
-
130
- `session_id` is empty when nothing can be reached: a process that took no Envoy identity, or a
131
- subagent whose spawning session this process can no longer place (it forked away, and other
132
- top-level sessions are running). Then your messages carry no sender either, so say who you are
133
- in the message body. An empty address is deliberate — being handed an unrelated live session
134
- would send your peers to an agent that never spawned you.
135
-
136
- Whether a publish reaches a session turns on where that session takes its deliveries, not on
137
- whether the topic is a built-in one or your own. A session that holds its own NATS connection —
138
- every Oh My Pi session, and a Claude Code one — receives every message published on a subject it
139
- subscribes to, whoever the source: its own publish, and a subagent's publish to a topic the
140
- session that spawned you subscribes to, included. The listener skips its push to such a session
141
- entirely, so the push path's skip of the source session never decides anything for it, and a
142
- topic the stream retains and a team topic that travels live (below) behave alike here. The
143
- session whose own publish does not come back is one the listener delivers to by push, one
144
- registered with a serve port such as the OpenCode plugin; such a session receives no team topic
145
- at all, since it holds no NATS subscription of its own.
146
-
147
- A role lane is the case that needs the `agent://` hop, because a role is not a subject its holder
148
- subscribes to: the listener resolves the live holder and forwards the message to it, and drops
149
- the message when that holder is the session the message names as its source. Inside a `task`
150
- subagent that source is the session that spawned you, so a publish to a role your parent holds is
151
- accepted, answers with that holder, and reaches nobody — `envoy_publish` says so in its answer.
152
- Use a `write` to the parent's `agent://` address for that one hop. `envoy_send` to any other
153
- session, including a reply, is unaffected.
154
-
155
- ### Delivery capabilities
156
-
157
- Each session row from `envoy_sessions` carries `capabilities`, the targeted-delivery modes that
158
- session's host honours: `aside` (a message queued beside the model's work), `btw` (an ephemeral
159
- question the host answers without disturbing the current turn), and `steer` (an interjection at
160
- the next tool boundary). An OMP session advertises `aside`, `btw`, and `steer` (`aside` and
161
- `steer` on a host that cannot run a side turn); a Claude Code session advertises `aside` only, because
162
- its channel notifications queue for the next turn. Target a session only with a mode it
163
- advertises.
164
-
165
- ## Waiting for CI or a merge
166
-
167
- Subscribe to `notifications.github.example-org.example-repo.pr.42.>` and end the turn. The single
168
- `pr.42.checks` event whose `sha` is the current head wakes you when it settles (an earlier
169
- commit's settlement can arrive first); a `pr.42` `closed` event with
170
- `merged: true` tells you the PR merged. Do not create `gh` pollers. A Legion implementer waits this
171
- way only on a re-run of a red its task names; otherwise it pushes and completes its phase, and the
172
- daemon reads CI for it (`skill://legion-worker`).
173
- Settlement waits for the commit to be quiet for a few seconds, every reported check run to finish,
174
- and every recorded GitHub check suite to be `completed`. It covers those reported checks and suites
175
- for that commit, not GitHub's required-checks set; until then, a silent subscription is normal.
176
-
177
- Check settlement is at-least-once: a settlement can be followed by a `superseded_settlement: "true"` payload. Every settlement carries its attempt set `check_runs` — the latest GitHub check-run id per check name, sorted by name — plus the listener's `generation` (the record's state version) and `snapshot` (the record's hash). Consumers order settlements of one commit by the attempt set, compared per shared name: no id lower and some id higher (or a new name — a new name counts as higher) is newer; every shared id equal and no new name is the same set; no id higher, no new name, and some id lower is older; anything else (a higher or new alongside a lower) is a mixed view and is dropped as a conflict (names only in the stored set are ignored — a check can vanish from GitHub's view, and a record recreated after the seven-day KV TTL starts sparse). Within one producer record per-name ids never decrease, and a consumer's fence is the per-name maximum over every view it has accepted — an accepted set merges into the fence, nothing is pruned — so the fence never decreases either: a newer attempt is newer whatever its completion time, no timestamps take part in ordering, and a name an incomplete view omitted cannot later reappear as new. At the same set the listener's `generation` orders its own settlements: lower is stale; equal is a duplicate when the `snapshot` matches and otherwise a conflict (an equal pair with a different snapshot cannot occur within one record's lifetime; a recreated record may reuse one and is dropped). A live settlement is a possibly incomplete view of the head (a missed webhook, a record recreated after the KV TTL): it decides the outcome of every name it reports — at any id the ordering accepted, including the same run observed in place — and says nothing about the rest: a known failure among them stands (the consumer keeps failure names, not a per-name status map), and the head is red while any failure remains. A consumer that reconciles a verdict from GitHub's rollup compares the rollup's attempt set the same way, but GitHub's read is complete: its failing check runs and failing commit statuses replace the stored ones wholesale. Statuses have no check run and the listener never sees them, so a consumer keeps them apart from check-run failures: a check run that shares a status's name cannot retire it — only GitHub does (likewise a deleted check's failure). A newer rollup set merges into the fence and takes the identity (no listener generation); the same set applies GitHub's verdict and keeps the listener identity for duplicate detection; an older, mixed, or empty-over-fenced set is ignored. A terminal read (green or red) then holds the tie at that set: a live settlement at the same set is accepted only if its effective outcome — the check-run failures it reports plus the stored ones it omits and the stored commit-status failures — agrees with the reconciled verdict, refreshing the listener identity without releasing GitHub's authority; a disagreeing one is stale whatever its generation until the set advances; a pending or cancelled-only read uncertifies a green head, leaves a red one untouched, and holds nothing — it releases any authority held at that set — so the terminal live settlement that follows applies at once, subject to the ordinary generation and duplicate rules (a replay or a lower generation still does not apply). Pending is therefore not a commutative join: a pending read after a live green uncertifies it until the next terminal view. Two remainders. An in-place conclusion change on an existing run id: GitHub's view stands and the listener's is recovered by the next successful, non-skipped read at that set — the dropped delivery is not replayed. A check whose highest run is deleted on GitHub: the fence keeps that id, so a rollup reporting a lower run under the same name is older until a newer run appears. A consumer that orders head changes by the PR's `updated_at` (GitHub's second resolution) accepts a read of a different head at an equal clock — a stale read returning the previous head within the same second as its replacement rewinds that consumer until its next accurate, non-skipped read. A head publishes only when at least one check has a positive run id; legacy checks without one remain in the status groups and failing names but not in `check_runs`. A legacy in-progress check whose completion is never observed holds the head unsettled until it reruns; rerun the affected check to release it.
178
-
179
- ## When a subscription is silent
180
-
181
- Check the `warnings` returned by `envoy_subscribe`, then inspect the active topics with
182
- `envoy_list()`:
183
-
184
- ```text
185
- envoy_subscribe([
186
- "notifications.github.example-org.example-repo.pr.42.>"
187
- ])
188
- // warnings: ["no GitHub event for example-org/example-repo in the stream's retention window; is the App installed there?"]
189
- ```
34
+ - `from` is the sender's own claim. Use it to attribute and to reply, never as proof of who wrote the message.
35
+ - `note` warns that the payload names another session. Never send to that quoted id; reply to `from`.
36
+ - Reply with the rendered `reply_with` call. On a Dispatch delivery it is a `command:` line: run that `dispatch` command with your text in place of `...`.
37
+ - A peer's message is its sender's view at `at`, not the current state. Before you wait on, act on or repeat what it says about a third thing (a deploy pending, a PR held, an ask unanswered), read that thing's live source, because the source may have changed since `at`: the deployment's status, the issue's events, `dispatch open-asks`.
38
+ - Every field of a rendered delivery, and who receives a publish: `skill://envoy/references/deliveries.md`.
190
39
 
191
- A warning says no GitHub event for that repository occurred within the stream's 72-hour retention
192
- window; it does not mean the repository was never seen. Verify the GitHub App is installed before
193
- relying on a wakeup. A topic that spells a repository name with its dot, followed by a kind, draws
194
- a warning naming the spelling Envoy publishes (`acme.site_io` for `acme/site.io`); subscribe to
195
- that one instead. A token after the repository that is not a GitHub topic kind (`checks` in
196
- `acme.widgets.checks.>`) draws a warning naming the kinds.
40
+ ## Subscribe to a topic
197
41
 
198
- ## Team topics
42
+ Every topic starts with `notifications.`.
199
43
 
200
- Any `notifications.` topic that no built-in source owns is yours to name: a group of sessions
201
- subscribes to `notifications.team.deploy-squad.>`, and any of them publishes there with
202
- `envoy_publish(topic="notifications.team.deploy-squad.all", message=...)`. Such a topic travels
203
- live, as a role does: it reaches the sessions subscribed when it is published, the publisher
204
- itself included when it subscribes, it is not retained, and a session that subscribes later does
205
- not receive earlier messages. A topic outside `notifications.` is refused. Use a role for one
206
- owner, and `envoy_send` for one session.
44
+ | Source | Topic |
45
+ | --- | --- |
46
+ | A pull request: lifecycle, comments, reviews, mentions, checks | `github.<owner>.<repo>.pr.<n>.>` |
47
+ | Other GitHub | `github.<owner>.<repo>.issue.<n>[.comment\|.mention]`, `.push.branch.<name>`, `.push.tag.<name>`, `.workflow.<file>.<action>` |
48
+ | A Dispatch issue or document | `dispatch.issue.<KEY>.>`, `dispatch.document.<PROJECT>.<SLUG>.>` |
49
+ | Your own inbox | `agent.<session_id>` |
50
+ | Slack, Ghost Wispr, WhatsApp | `skill://envoy/references/sources.md` |
207
51
 
208
- ## Roles
52
+ - Write the owner, the repository, a branch or tag name and a workflow file as one token each, with every dot written `_`: `acme/site.io` is `github.acme.site_io`, and `ci.yml` is `workflow.ci_yml`. A slash stays. A topic spelled with the dot receives nothing.
53
+ - Subscribe to the pull request's whole family, `pr.<n>.>`. Envoy also subscribes you to the lifecycle subject `pr.<n>`, which NATS `>` alone does not match, because `>` needs one or more trailing tokens.
54
+ - Read the `warnings` that `envoy_subscribe` returns, and confirm the active topics with `envoy_list`. A warning names the spelling Envoy publishes, or a token that is not a GitHub topic kind; fix the topic it names. "No GitHub event ... in the stream's retention window" means nothing came from that repository in the last 72 hours: check that the GitHub App that sends Envoy its webhooks is installed on that repository before you rely on a wake.
209
55
 
210
- Publish to a role; do not subscribe as its holder. A successful `envoy_publish` to a role returns
211
- its live `holder`; an unheld role returns an error. Use `envoy_role_get(role="reviewer")` to find
212
- the live holder first when you need one.
56
+ ## Wait for CI or a merge
213
57
 
214
- A claim belongs to the session, not the process. `omp --resume` re-claims the role recorded in
215
- the session transcript, including after the listener's stale-interest reaper has removed the old
216
- row. A `/fork`, `/branch`, `/handoff`, or other transcript-carrying switch checks the
217
- previous id and moves any held role to the new id while establishing the new session. A `/new`
218
- or `/resume` switch installs an unrelated transcript and carries nothing. A process that dies and
219
- is never resumed does not stay a live holder; once liveness expires, role publishes return
220
- `no holder` until that transcript resumes or another session claims the role.
58
+ - Subscribe to `github.<owner>.<repo>.pr.<n>.>` and end the turn. Never start a `gh` poller: the event wakes you, and a poller burns turns.
59
+ - One `pr.<n>.checks` event arrives per commit whose checks settle, the head or not. Compare its `sha` with the head you wait on, because an earlier commit's settlement can arrive first. A push with GitHub's `skip-checks` trailer runs no checks, and several can sit on top (a Legion handoff push is one): wait for the settlement of the nearest ancestor that is not such a push, the CI head `skill://legion-worker/references/merge-gate.md` reads.
60
+ - Settlement waits until the commit has had no check event for 5 seconds (`ENVOY_CI_DEBOUNCE`) and every reported check run and check suite of it has finished. No event arrives before that. Envoy does not read GitHub's required-checks list, so a green settlement does not mean GitHub will merge the PR.
61
+ - A `pr.<n>` event with `closed` and `merged: true` means the PR merged; it carries `merge_commit_sha`, `merged_by` and `head_sha`.
62
+ - A later settlement of the same commit can carry `superseded_settlement: "true"`. Code that orders settlements follows `skill://envoy/references/check-settlement.md`.
63
+ - A Legion implementer waits this way only on a re-run of a red check its task names. Otherwise it pushes and completes its phase, and the daemon reads CI for it (`skill://legion-worker`).
221
64
 
222
- Automatic reclaim never steals. It is a *soft* claim, which the listener grants only when the role
223
- is unheld, held by a session that is no longer live, or held by the id this session continues (a
224
- fork's parent). If a different live session holds it — the parent of a `/fork` whose role moved to
225
- the child, or a second process on the same transcript — the listener refuses atomically and the
226
- resumed session logs a warning and holds nothing. Only an explicit `envoy_role_set` is
227
- last-claim-wins. Re-running `envoy_role_set` for a role this session already holds is harmless.
228
- Moving to a different role is refused while another plugin holds subscriptions that last only as
229
- long as this session keeps its current role (the refusal names them), and `envoy_unsubscribe`
230
- keeps those subscriptions open, listing them as kept.
231
-
232
- ## Legion role claims
233
-
234
- Legion agents receive through a daemon-minted role token. Claim the assigned role with
235
- `envoy_role_set(role="<assigned-role>")`; a claimant does not manually subscribe to its role
236
- topic. Claims use last-claim-wins semantics, survive parking and worker re-creation, and remain
237
- through the issue's post-close linger. The daemon owns the authoritative token-to-issue map, so do
238
- not construct a token from a partial issue reference.
239
-
240
- ## Legion exception lane
241
-
242
- `no_holder`, `delivery_failed`, and `receipt_timeout` for a Legion role are daemon liveness
243
- signals, not reasons to add a second subscriber or manually retry. `no_holder`: no live session
244
- holds the role. `delivery_failed`: the message was not forwarded at all — the holder lookup
245
- failed, the holder was stale, or the publish errored. `receipt_timeout`: the listener forwarded
246
- the message to a registered, live holder and no receipt arrived inside the window; its payload
247
- keeps `original_topic`, `event_id`, `payload`, and the original `dedupe_key`, so the daemon can
248
- re-send the same message and the receiver's dedupe drops a copy that did arrive. Treat any of
249
- these wakes as the daemon's responsibility to revive or recreate the backing worker and
250
- re-deliver; otherwise it resurrects the root process with derived catch-up and the workspace
251
- handoffs. Raw delivery failures stay out of architect context.
252
-
253
- ## Slack
254
-
255
- Use the real team ID, not a workspace slug. Slack delivers a one-line prose `summary` plus a
256
- structured `message` payload. The payload records `subtype` for edits, deletes, and bot messages;
257
- `thread_ts` for replies; and `bot_id` and `bot_name` when a bot supplied the message.
258
-
259
- ```text
260
- envoy_subscribe([
261
- "notifications.slack.T01234567.C01234567.thread.1_000.>"
262
- ])
263
- ```
264
-
265
- This follows every message and mention in one thread. Channel-level topics end in `.message` or
266
- `.mention`; thread timestamps replace dots with underscores.
267
-
268
- Reply in a thread with `slack_post(channel=<channel_id>, thread_ts=<the message's thread_ts, or
269
- its ts>, text=…)`, which posts as the Legion Slack app with the bot token the secrets broker grants
270
- the session (`LEGION_SLACK_BOT_TOKEN`). A refusal carries Slack's own error code, or the broker's
271
- state when it has not granted the token. The post is sent once; when the answer is that Slack did
272
- not answer, the reply may already be there, so do not post it again.
273
-
274
- ## Ghost Wispr
275
-
276
- Ghost Wispr topics are `notifications.ghostwispr.<session>.<kind>`, where `kind` is
277
- `session.started`, `session.ended`, or `summary.ready`. Their summary is concise prose and their
278
- payload is structured; `summary_ready` includes its status, summary, and summary metadata.
279
-
280
- ```text
281
- envoy_subscribe([
282
- "notifications.ghostwispr.session-example.summary.ready"
283
- ])
284
- ```
285
-
286
- ## WhatsApp
287
-
288
- WhatsApp topics are `notifications.whatsapp.<phone>.<jid>.message` or `.status`. A JID contains
289
- dots, which become additional NATS segments, so use `>` rather than `*` for a chat.
290
-
291
- ```text
292
- envoy_subscribe([
293
- "notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.>"
294
- ])
295
- ```
296
-
297
- ### WhatsApp routing smoke test
298
-
299
- This checks Envoy routing, not real WhatsApp ingestion. In Session A, subscribe as above. From a
300
- different Session B, publish a synthetic message to the same `.message` topic:
301
-
302
- ```text
303
- envoy_publish(
304
- topic="notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.message",
305
- message="Synthetic WhatsApp routing test"
306
- )
307
- ```
65
+ ## Roles and team topics
308
66
 
309
- Session A should receive it. Broadcasts do not echo to their publishing session, so one session
310
- cannot perform both steps. Real WhatsApp delivery additionally requires a configured MCP bridge.
67
+ - Send to one session with `envoy_send`, to the one session that holds a named duty with a role, and to a group with a team topic.
68
+ - Publish to a role with `envoy_publish(topic="notifications.role.<role>", ...)`; never subscribe to a role topic, because the listener forwards a role message to its holder only. A publish returns the live `holder`; an unheld role returns an error. `envoy_role_get(role=...)` finds the holder first.
69
+ - For a team, the group subscribes to `notifications.team.<name>.>`, and any member publishes to `notifications.team.<name>.all`. Envoy treats any other `notifications.` topic that no source in the table uses the same way. It reaches only the sessions subscribed when it is published (yours too, when you subscribe), and is not kept for a session that subscribes later. A topic outside `notifications.` is refused.
70
+ - How a role claim survives a resume, a fork or a dead process: `skill://envoy/references/roles.md`.
71
+ - In a Legion pane, the plugin claims your role at session start; read `skill://envoy/references/legion.md` before you claim, release or publish to a role, or answer a role delivery failure there.
@@ -0,0 +1,62 @@
1
+ # Ordering pull request check settlements
2
+
3
+ Read this when you write or review code that consumes `pr.<n>.checks` settlements and must decide which of several settlements of one commit is current.
4
+
5
+ ## What a settlement carries
6
+
7
+ - A commit can settle more than once. A later settlement can carry `superseded_settlement: "true"`, and an exact copy can arrive, because delivery is at-least-once.
8
+ - `check_runs`, the attempt set: the latest GitHub check-run id per check name, sorted by name.
9
+ - `generation`: the listener record's state version. `snapshot`: the record's hash.
10
+ - A head publishes only when at least one check has a positive run id. Legacy checks without one appear in the status groups and the failing names, never in `check_runs`.
11
+
12
+ ## Order two attempt sets of one commit
13
+
14
+ Compare the ids of the names both sets share:
15
+
16
+ 1. No id lower, and some id higher or a new name: newer. A new name counts as higher.
17
+ 2. Every shared id equal and no new name: the same set.
18
+ 3. No id higher, no new name, and some id lower: older.
19
+ 4. Anything else (a higher or new id alongside a lower one): a mixed view. Drop it as a conflict.
20
+
21
+ Ignore names that only the stored set has: a check can vanish from GitHub's view, and a record recreated after the seven-day KV TTL starts sparse.
22
+
23
+ ## Keep a fence
24
+
25
+ - The fence is the per-name maximum id over every set you accepted. Merge each accepted set into it, and prune nothing.
26
+ - Within one listener record, per-name ids never decrease, so the fence never decreases either.
27
+ - So a newer attempt is newer whatever its completion time, no timestamp takes part in ordering, and a name an incomplete view left out cannot later come back as new.
28
+
29
+ ## At the same set, order by generation
30
+
31
+ - A lower `generation` is stale.
32
+ - An equal `generation` with the same `snapshot` is a duplicate. With a different `snapshot`, drop it as a conflict: that pair occurs only when a record was recreated and reused a generation.
33
+
34
+ ## Apply a live settlement
35
+
36
+ - A live settlement (one the listener sent) may be an incomplete view of the head: a webhook was missed, or the record was recreated after the KV TTL.
37
+ - It decides the outcome of every name it reports, at any id the ordering accepted, the same run seen in place included. It says nothing about the names it leaves out.
38
+ - A known failure among the names it leaves out stands, because you keep failure names, not a per-name status map. The head is red while any failure remains.
39
+
40
+ ## Reconcile with GitHub's rollup
41
+
42
+ - Compare the rollup's attempt set in the same way. GitHub's read is complete: its failing check runs and failing commit statuses replace the stored ones wholesale.
43
+ - Keep commit statuses apart from check-run failures, because statuses have no check run and the listener never sees them. A check run with a status's name cannot retire that status; only GitHub's read can. The same holds for a deleted check's failure.
44
+ - A newer rollup set merges into the fence and takes the identity, with no listener generation.
45
+ - The same set applies GitHub's verdict and keeps the listener identity for duplicate detection.
46
+ - Ignore an older set, a mixed set, and an empty set when the fence already holds ids.
47
+
48
+ ## After a terminal GitHub read
49
+
50
+ A terminal read is a green or red one. GitHub's verdict then holds authority at that set:
51
+
52
+ - Accept a live settlement at the same set only if its effective outcome agrees with GitHub's verdict. The effective outcome is the check-run failures it reports, plus the stored ones it leaves out, plus the stored commit-status failures. Accepting it refreshes the listener identity, and GitHub's verdict keeps its authority.
53
+ - A live settlement that disagrees is stale, whatever its generation, until the set advances.
54
+ - A pending or cancelled-only read takes the green off a green head, leaves a red head red, and ends GitHub's authority at that set. The next terminal live settlement then applies at once, under the usual generation and duplicate rules: a replay or a lower generation still does not apply.
55
+ - So the order of reads matters: a pending read after a live green takes the green off until the next terminal view.
56
+
57
+ ## Known gaps
58
+
59
+ - A run whose conclusion changes in place, on the same run id: GitHub's view stands. The listener's view comes back with the next successful, non-skipped read at that set; the dropped delivery is not replayed.
60
+ - A check whose highest run is deleted on GitHub: the fence keeps that id, so a rollup that reports a lower run under that name is older until a newer run appears.
61
+ - A consumer that orders head changes by the PR's `updated_at` (one-second resolution) accepts a read of another head at an equal clock. A stale read that returns the previous head in the same second as its replacement rewinds that consumer until its next accurate, non-skipped read.
62
+ - A legacy in-progress check whose completion is never seen holds the head unsettled until it reruns: rerun that check to release it.
@@ -0,0 +1,62 @@
1
+ # Envoy deliveries
2
+
3
+ Read this when a delivery field is unclear, or when a publish did not reach a session you expected.
4
+
5
+ ## A rendered delivery
6
+
7
+ Envoy renders an annotated delivery before its source summary and complete payload:
8
+
9
+ ```text
10
+ envoy:
11
+ to: you (ses_example_recipient)
12
+ from: 01a0bbbb-cccc-7ddd-eeee-0123456789ab (Reviewer)
13
+ at: "2026-09-07T04:41:12Z"
14
+ id: agent-message-2
15
+ by: "2026-09-07T05:00:00Z"
16
+ urgency: high
17
+ expects_reply: required
18
+ re: agent-message-1
19
+ supersedes: agent-message-0
20
+ reply_with:
21
+ tool: envoy_send
22
+ args:
23
+ session_id: 01a0bbbb-cccc-7ddd-eeee-0123456789ab
24
+ in_reply_to: agent-message-2
25
+ message: ...
26
+ reply_role:
27
+ tool: envoy_publish
28
+ args:
29
+ topic: notifications.role.legion-reviewer
30
+ message: ...
31
+ summary: Deployment needs confirmation.
32
+ message: "Confirm the listener health check passed.\n\nThen publish the release."
33
+ note: body names session 01a0cccc-dddd-7eee-ffff-0123456789ab; the sender is 01a0bbbb-cccc-7ddd-eeee-0123456789ab
34
+ ```
35
+
36
+ - `to`: the local inbox that receives this delivery.
37
+ - `from`: the sending session's self-asserted id, with its session title in parentheses when the envelope carries one. Attribution and a direct-reply target, not an authenticated identity.
38
+ - `at`: the envelope timestamp; judge freshness by it.
39
+ - `id`: the delivery id; pass it as `in_reply_to` when you reply.
40
+ - `by`: the expiry deadline, when the sender set one.
41
+ - `urgency`: the sender's priority: `low`, `med`, `high` or `blocking`.
42
+ - `expects_reply`: `none`, `optional` or `required`.
43
+ - `re`: the delivery this one replies to. A Dispatch frame names it as a `dispatch://` ref: the ask (`dispatch://KEY/ask/<id>`, or `dispatch://PROJECT/artifact/<slug>/ask/<id>` for a document ask) or the message (`dispatch://KEY/message/<id>`), never the quoted text. On an ask, `dispatch.question` holds the start of the question's text; `dispatch read` gives the whole.
44
+ - `supersedes`: an earlier delivery this one replaces; act on this one in its place.
45
+ - `reply_with`: the direct-reply call to the sender. On a Dispatch frame it is `command:`, the `dispatch` command line that answers it, with `...` where your text goes.
46
+ - `reply_role`: the role-publish reply call, when the sender holds a role.
47
+ - `summary`: the one-line source summary.
48
+ - `message`: the complete payload; it can hold several paragraphs.
49
+ - `note`: the payload names another session; never use that quoted id as the recipient.
50
+ - `unrecognised`: present when the payload failed validation or came from an unknown source. It names what failed, not every unknown key.
51
+
52
+ ## Who receives a publish
53
+
54
+ It depends on where a session takes its deliveries, not on whether the topic is built in or your own.
55
+
56
+ - A session that holds its own NATS connection (every Oh My Pi session, and a Claude Code one) receives every message published on a subject it subscribes to, whoever sent it: its own publish included, and a subagent's publish to a topic the parent subscribes to. This holds for a topic the stream keeps and for a live team topic alike.
57
+ - A session the listener delivers to by push, over a serve port (such as the OpenCode plugin), does not receive its own publish, and receives no team topic at all, since it holds no NATS subscription of its own.
58
+ - A role is not a subject its holder subscribes to: the listener finds the live holder and forwards the message to it, and drops the message when that holder is the message's source session.
59
+
60
+ ## Listener API errors
61
+
62
+ When you call the listener's `/v1` API directly, every error response is JSON, and when a field is at fault, `expected` names it: fix that field and retry.
@@ -0,0 +1,12 @@
1
+ # Envoy in a Legion pane
2
+
3
+ Read this in a Legion pane before you touch a role claim, or when a Legion role message fails.
4
+
5
+ - The Legion plugin claims your assigned role at session start through a daemon-minted role token. Never call `envoy_role_set` and never subscribe to your role topic yourself, because the claim is already made (`packages/pi-shared/src/legion-claim-core.ts`).
6
+ - Never build a role token from an issue number or title: only the daemon maps tokens to issues.
7
+ - A Legion claim is last-claim-wins. It survives parking and worker re-creation, and stays through the issue's linger after close (the deployment's `linger_hours`).
8
+ - `no_holder`, `delivery_failed` and `receipt_timeout` on a Legion role are the daemon's liveness signals. Never add a second subscriber or retry by hand: the daemon revives or recreates the worker and re-delivers, or relaunches the root process, which gets a `catch-up` notice of its tree and reads the handoffs under `.legion/<issue>/`.
9
+ - `no_holder`: no live session holds the role.
10
+ - `delivery_failed`: the message was not forwarded at all: the holder lookup failed, the holder was stale, or the publish errored.
11
+ - `receipt_timeout`: the listener forwarded the message to a live holder and no receipt arrived in time. Its payload keeps `original_topic`, `event_id`, `payload` and the original `dedupe_key`, so the daemon can re-send it and the receiver's dedupe drops a copy that did arrive.
12
+ - Never forward these failure payloads to an architect, because the daemon already acts on them.
@@ -0,0 +1,10 @@
1
+ # How a role claim lives
2
+
3
+ Read this when a role you held is gone after a resume, fork or restart, when `envoy_role_set` is refused, or when a role publish returns `no holder`.
4
+
5
+ - A claim belongs to the session, not the process. `omp --resume` re-claims the role recorded in the session transcript, also after the listener removed the dead session's old row (a session's registration lapses 5 minutes after its last heartbeat).
6
+ - `/fork`, `/branch`, `/handoff` and any other switch that carries the transcript read the role the previous session id held and move it to the new id while the new session starts. `/new` and `/resume` install an unrelated transcript and carry nothing.
7
+ - A process that dies and is never resumed stops being a live holder. Once its registration lapses, role publishes return `no holder` until that transcript resumes or another session claims the role.
8
+ - An automatic reclaim never steals. It is a soft claim, which the listener grants only when the role is unheld, held by a session that is no longer live, or held by the id this session continues (a fork's parent). When a different live session holds it (the parent of a `/fork` whose role moved to the child, or a second process on the same transcript), the listener refuses atomically, and the resumed session logs a warning and holds nothing.
9
+ - Only an explicit `envoy_role_set` is last-claim-wins. Running it again for a role this session already holds is harmless.
10
+ - If another plugin made subscriptions that depend on your current role, `envoy_role_set` to a different role is refused, and the refusal names those subscriptions. `envoy_unsubscribe` keeps those subscriptions and lists them as kept.
@@ -0,0 +1,41 @@
1
+ # Slack, Ghost Wispr and WhatsApp topics
2
+
3
+ Read this when you subscribe to one of these sources or test its routing.
4
+
5
+ ## Slack
6
+
7
+ - Use the real team id (`T...`), never a workspace slug: the topic carries the id, so a slug receives nothing.
8
+ - Channel topics: `notifications.slack.<team>.<channel>.message` or `.mention`. One thread: `notifications.slack.<team>.<channel>.thread.<ts>.>`, with the dot in the thread timestamp written `_`.
9
+ - A delivery has a one-line prose `summary` and a structured `message`. The payload records `subtype` for edits, deletes and bot messages, `thread_ts` for replies, and `bot_id` and `bot_name` when a bot sent it.
10
+ - Reply in a thread with `slack_post`; its tool description gives the arguments and refusals.
11
+
12
+ ```text
13
+ envoy_subscribe(["notifications.slack.T01234567.C01234567.thread.1_000.>"])
14
+ ```
15
+
16
+ ## Ghost Wispr
17
+
18
+ Topics are `notifications.ghostwispr.<session>.<kind>`, where `kind` is `session.started`, `session.ended` or `summary.ready`. The summary is prose and the payload structured; A `summary.ready` payload carries `status`, `summary` (capped; `body_truncated` when cut), `summary_preset`, `timestamp` and `version`.
19
+
20
+ ```text
21
+ envoy_subscribe(["notifications.ghostwispr.session-example.summary.ready"])
22
+ ```
23
+
24
+ ## WhatsApp
25
+
26
+ Topics are `notifications.whatsapp.<phone>.<jid>.message` or `.status`. A JID contains dots, which become extra NATS tokens, so match a chat with `>`, never `*`.
27
+
28
+ ```text
29
+ envoy_subscribe(["notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.>"])
30
+ ```
31
+
32
+ To test Envoy routing without real WhatsApp ingestion, subscribe as above, then publish a synthetic message to the same `.message` topic:
33
+
34
+ ```text
35
+ envoy_publish(
36
+ topic="notifications.whatsapp.15551234567.5551234567@s.whatsapp.net.message",
37
+ message="Synthetic WhatsApp routing test"
38
+ )
39
+ ```
40
+
41
+ The subscribed session should receive it. An Oh My Pi or Claude Code session receives its own publish, so it can run both steps; a session delivered over a serve port cannot, and needs a second session to publish. Real WhatsApp delivery also needs the listener's MCP bridge configured with a `whatsapp` server (`packages/envoy/internal/mcpbridge`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjawhar/pi-envoy",
3
- "version": "10.24.1",
3
+ "version": "10.25.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "omp": {