@sjawhar/claude-envoy 0.0.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.claude-plugin/plugin.json +6 -0
  2. package/.mcp.json +11 -0
  3. package/.omp-plugin/plugin.json +6 -0
  4. package/AGENTS.md +343 -0
  5. package/README.md +639 -0
  6. package/bin/dispatch +6 -0
  7. package/bin/envoy-channel.ts +9 -0
  8. package/bin/envoy-send.ts +33 -0
  9. package/dist/THIRD_PARTY_NOTICES +816 -0
  10. package/dist/dispatch.js +27707 -0
  11. package/dist/envoy-channel.js +57695 -0
  12. package/dist/session-hook.js +26328 -0
  13. package/dist/session-title-hook.js +116 -0
  14. package/hooks/hooks.json +60 -0
  15. package/hooks/session-hook.ts +512 -0
  16. package/hooks/session-title-hook.ts +46 -0
  17. package/legion/.claude-plugin/plugin.json +4 -0
  18. package/legion/agents/deep-worker.md +68 -0
  19. package/legion/agents/oracle.md +35 -0
  20. package/legion/agents/plan-gap-analyst.md +65 -0
  21. package/legion/agents/plan-reviewer.md +104 -0
  22. package/legion/agents/thermonuclear-code-quality.md +30 -0
  23. package/legion/agents/thermonuclear-deep-review.md +30 -0
  24. package/legion/hooks/hooks.json +89 -0
  25. package/legion/skills/ce-simplify-code/LICENSE +21 -0
  26. package/legion/skills/ce-simplify-code/SKILL.md +66 -0
  27. package/legion/skills/ce-simplify-code/references/personas/code-quality-reviewer.md +17 -0
  28. package/legion/skills/ce-simplify-code/references/personas/code-reuse-reviewer.md +7 -0
  29. package/legion/skills/ce-simplify-code/references/personas/efficiency-reviewer.md +11 -0
  30. package/legion/skills/leading-a-work-stream/SKILL.md +96 -0
  31. package/legion/skills/leading-a-work-stream/references/daily-plan.md +8 -0
  32. package/legion/skills/leading-a-work-stream/references/decision-rights.md +6 -0
  33. package/legion/skills/leading-a-work-stream/references/direction-and-coaching.md +13 -0
  34. package/legion/skills/leading-a-work-stream/references/dispatch-upkeep.md +8 -0
  35. package/legion/skills/leading-a-work-stream/references/sources.md +9 -0
  36. package/legion/skills/leading-a-work-stream/references/speed-tick.md +44 -0
  37. package/legion/skills/leading-a-work-stream/references/user-quotes.md +7 -0
  38. package/legion/skills/legion-architect/SKILL.md +86 -0
  39. package/legion/skills/legion-architect/references/acceptance.md +208 -0
  40. package/legion/skills/legion-architect/references/decomposition.md +129 -0
  41. package/legion/skills/legion-architect/references/design-gate.md +206 -0
  42. package/legion/skills/legion-architect/references/merge-notices.md +91 -0
  43. package/legion/skills/legion-architect/references/wake-routing.md +41 -0
  44. package/legion/skills/legion-controller/SKILL.md +151 -0
  45. package/legion/skills/legion-controller/references/admission.md +22 -0
  46. package/legion/skills/legion-controller/references/backlog-stewardship.md +101 -0
  47. package/legion/skills/legion-controller/references/daily-report.md +75 -0
  48. package/legion/skills/legion-controller/references/escalation.md +82 -0
  49. package/legion/skills/legion-controller/references/finding-asks.md +181 -0
  50. package/legion/skills/legion-controller/references/parked-decisions.md +198 -0
  51. package/legion/skills/legion-controller/references/session.md +100 -0
  52. package/legion/skills/legion-controller/references/slack-mentions.md +154 -0
  53. package/legion/skills/legion-controller/references/triage.md +96 -0
  54. package/legion/skills/legion-controller/references/wakes.md +132 -0
  55. package/legion/skills/legion-oracle/SKILL.md +48 -0
  56. package/legion/skills/legion-retro/SKILL.md +112 -0
  57. package/legion/skills/legion-retro/references/learnings.md +60 -0
  58. package/legion/skills/legion-retro/references/pr-body-content.md +56 -0
  59. package/legion/skills/legion-worker/SKILL.md +69 -0
  60. package/legion/skills/legion-worker/references/cleanup-deletion.md +22 -0
  61. package/legion/skills/legion-worker/references/conflicts-and-rewrites.md +164 -0
  62. package/legion/skills/legion-worker/references/default-branch-check.md +53 -0
  63. package/legion/skills/legion-worker/references/findings.md +199 -0
  64. package/legion/skills/legion-worker/references/merge-gate.md +353 -0
  65. package/legion/skills/legion-worker/references/pr-body.md +218 -0
  66. package/legion/skills/legion-worker/references/review-threads.md +77 -0
  67. package/legion/skills/legion-worker/references/systematic-rename.md +19 -0
  68. package/legion/skills/legion-worker/references/tools.md +708 -0
  69. package/legion/skills/thermonuclear-code-quality/LICENSE +21 -0
  70. package/legion/skills/thermonuclear-code-quality/SKILL.md +60 -0
  71. package/legion/skills/thermonuclear-deep-review/LICENSE +21 -0
  72. package/legion/skills/thermonuclear-deep-review/SKILL.md +80 -0
  73. package/package.json +49 -1
  74. package/scripts/build-inputs.txt +84 -0
  75. package/scripts/build.ts +415 -0
  76. package/scripts/check-packed-skills.sh +50 -0
  77. package/scripts/pack-skills.sh +146 -0
  78. package/scripts/smoke-channel.sh +178 -0
  79. package/scripts/smoke-clear-rebind.sh +164 -0
  80. package/scripts/smoke-session-title.sh +245 -0
  81. package/scripts/verify-build.sh +88 -0
  82. package/skills/agent-secrets/SKILL.md +180 -0
  83. package/skills/asd-ste100/LICENSE +21 -0
  84. package/skills/asd-ste100/SKILL.md +140 -0
  85. package/skills/asd-ste100/examples/before-after.md +92 -0
  86. package/skills/asd-ste100/references/writing-rules.md +59 -0
  87. package/skills/dispatch/SKILL.md +89 -0
  88. package/skills/dispatch-first/SKILL.md +46 -0
  89. package/skills/dispatch-writing-specs/SKILL.md +80 -0
  90. package/skills/envoy/SKILL.md +82 -0
  91. package/skills/envoy/references/check-settlement.md +62 -0
  92. package/skills/envoy/references/deliveries.md +67 -0
  93. package/skills/envoy/references/legion.md +14 -0
  94. package/skills/envoy/references/roles.md +12 -0
  95. package/skills/envoy/references/sources.md +41 -0
  96. package/skills/outside-message/SKILL.md +111 -0
  97. package/skills/plain-words/SKILL.md +88 -0
  98. package/src/channel-forwarder.ts +262 -0
  99. package/src/claude-channel.ts +705 -0
  100. package/src/claude-session.ts +38 -0
  101. package/src/envoy-channel-server.ts +2115 -0
  102. package/src/envoy-client.ts +26 -0
  103. package/src/envoy-off.ts +62 -0
  104. package/src/legion-claim.ts +211 -0
  105. package/src/legion-inbox.ts +110 -0
  106. package/src/legion-phase-hook.ts +554 -0
  107. package/src/legion-tool.ts +69 -0
  108. package/src/replay-state.ts +97 -0
  109. package/src/send-arguments.ts +13 -0
  110. package/src/session-identity.ts +165 -0
  111. package/src/session-state.ts +142 -0
  112. package/src/session-title.ts +251 -0
  113. package/src/subscription-topics.ts +16 -0
@@ -0,0 +1,89 @@
1
+ ---
2
+ name: dispatch
3
+ description: "Use before your first dispatch command in a session: a message, comment, ask, artifact, document edit, claim or status change."
4
+ ---
5
+
6
+ # Dispatch
7
+
8
+ Dispatch is a record for the people who decide, never a log of your work.
9
+
10
+ - Use the `dispatch` command; the HTTP API only for what no command does. `dispatch <command> --help` gives its rules and an example.
11
+ - Put long text on stdin: `--<field>-file -` with a quoted here-document (`<<'EOF'`); give object and list fields as JSON.
12
+ - Exit 1 is a refusal, 2 a usage error. A refusal lists each problem: fix them all, then call once.
13
+
14
+ ## Asks and ask blocks
15
+
16
+ - A design question a document records is an ask block (a decision block) at the end of its section:
17
+
18
+ ```md
19
+ :::ask{urgency="med"}
20
+ The question and the recommendation, as `skill://dispatch-writing-specs` writes them.
21
+
22
+ - Option one: what it costs.
23
+ - Option two: what it costs.
24
+ :::
25
+ ```
26
+
27
+ - An ask is short and asks one thing; its reasoning goes in the document.
28
+ - An open ask is the request; never repeat it per message.
29
+ - Change an open ask's block with `dispatch edit-ask`, or `doc-edit`'s `replace` or `move`. An answered ask is fixed: ask anew.
30
+ - Reply on an open ask with `dispatch comment --reply-to-ask`; `--turn agent` (a progress note) needs it. An approval request a newer version moved needs `request-approval` again.
31
+ - When an answer's option and text disagree, the text wins.
32
+ - An ask block has a block id, for `doc-edit`, and an ask id, for the ask commands. `dispatch read --ref dispatch://KEY/ask/<id>` takes either.
33
+ - An ask reaches the issue's assignee; if none, name who answers.
34
+
35
+ ## Choosing what to work on
36
+
37
+ - When you finish an issue, take the top ready one: `todo`, by priority then board rank, per project.
38
+ - `todo` means specced and waiting on nothing; one that waits goes in `backlog`, saying on what.
39
+ - Hold at most three issues in flight; move the rest to `backlog`, saying where each is.
40
+ - Never reorder the board: one agent keeps each project's order (`issue-update --before`/`--after`). If the top looks wrong, tell `notifications.role.legion-<project>-controller`.
41
+
42
+ ## Issue status is yours to move
43
+
44
+ Outside Legion, claim your issue (`dispatch claim`) and move it and your children: `in_progress` at implementation, `testing` on a production-like surface, `needs_review` while its PR waits, `done` with `--reason` once driven in production. Never write or route the status of an issue with the `legion` label or under one, and claim one only as a role of its tree: an outside claim can take the tree out of the workflow. A status write is set back or stands.
45
+
46
+ `unresponsive` holders keep their claims 20 minutes (`skill://envoy`).
47
+
48
+ A claim, or `dispatch issue-update --route session:<your id>` (`--route ''` on release), brings a person's answers, comments and messages; for agents' activity, `envoy_subscribe notifications.dispatch.issue.<KEY>.>`, which shows no quiet event.
49
+
50
+ ## Close what you opened
51
+
52
+ - Resolve your open ask (`dispatch resolve-ask`) the turn it turns needless: another answer, a later write, dropped work, a replacement. If a person said keep it open, say why in its thread; they close it.
53
+
54
+ ## Structure over stream
55
+
56
+ - One issue per deliverable, its content the issue's document.
57
+ - A message replies to a person or tells them what they must know now.
58
+ - Text a person must read to decide belongs in the spec. An artifact is a real file.
59
+
60
+ ## Architecture components
61
+
62
+ Attach each architectural issue to its components (`--components-json`), the root before decomposing it. Change code and `.dispatch/architecture/<id>.md` in the same review.
63
+
64
+ ## References
65
+
66
+ Link each item you name:
67
+
68
+ ```text
69
+ dispatch://KEY
70
+ dispatch://KEY/spec
71
+ dispatch://KEY/spec#b-<block>[@vN]
72
+ dispatch://KEY/artifact/<slug>[@vN]
73
+ dispatch://KEY/ask/<id>
74
+ dispatch://KEY/ask/<id>#b-<part>[@vN]
75
+ dispatch://KEY/comment/<id>
76
+ dispatch://KEY/comment/<id>#b-<block>[@vN]
77
+ dispatch://KEY/message/<id>
78
+ dispatch://KEY/message/<id>#b-<block>
79
+ dispatch://PROJECT/artifact/<slug>[@vN]
80
+ dispatch://PROJECT/artifact/<slug>/ask/<id>
81
+ dispatch://PROJECT/artifact/<slug>/comment/<id>#b-<block>[@vN]
82
+ ```
83
+
84
+ A bare id, filename or path is not a reference, and outside Dispatch neither is `dispatch://`: give the `https://` link `dispatch` printed. Write each id whole.
85
+
86
+ ## Setup and the HTTP API
87
+
88
+ - Use a personal token from **Settings → Agent tokens**, as `DISPATCH_TOKEN` or `dispatch.token` in the session's `envoy.json`.
89
+ - `GET /api/v1` lists every route; never guess one. Branch on an error's `code`, not its text.
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: dispatch-first
3
+ description: "Use when a session has the dispatch command, before you plan, file an issue, ask a human, post a finding, or start work. Someone may track that work already or hold a decision on it."
4
+ ---
5
+
6
+ # Dispatch first
7
+
8
+ Dispatch holds the open issues, their specs and people's answers.
9
+
10
+ ## Search before you act
11
+
12
+ - Before you plan, file an issue, ask, post a finding or start work, run `dispatch search`.
13
+ - Each query word must match. Use two or three words: the thing and what is wrong with it. Never paste a draft.
14
+
15
+ ```bash
16
+ dispatch search --query 'broadcast send order'
17
+ dispatch search --query 'reviewer threads OR review comments'
18
+ ```
19
+
20
+ - No hits: remove a word before you add one. Try two or three wordings before you decide nothing exists.
21
+ - Open each hit that can be yours with `dispatch read --issue KEY`, then its parent (`child_of` under `Links:`, or a document's `Issue:`).
22
+
23
+ ## What to do with what you find
24
+
25
+ `dispatch issue` and `dispatch ask` also search. Treat a hit in `advice.suggestions` as yours to read.
26
+
27
+ - **Work already tracked:** put the finding on that issue. File a new issue only when no hit covers the work, and cite the nearest one you ruled out (`dispatch://KEY`).
28
+ - **Question already decided:** the answered ask is the record. Cite it (`dispatch://KEY/ask/<id>`). Do not ask again.
29
+ - **Duplicate found:** keep the issue with the spec, copy to it what only the duplicate has, and close the duplicate (`--status done --reason 'Duplicate of dispatch://KEY.'`). When it has the `legion` label or another holder's claim, comment the kept issue's name on it instead.
30
+
31
+ ## Reach a person only through Dispatch
32
+
33
+ Reach a person through an ask, an ask block or an approval request.
34
+
35
+ - The moment a person blocks your work, open an ask: an ask block for a design question its document records, else `dispatch ask`. A blocker that is not an open ask does not exist for them. A Legion phase worker sends decisions to its architect instead. Before you say you wait on a person, run `dispatch open-asks`.
36
+ - A result a person asked for is the same: a document or artifact notifies nobody. End that document with an ask block asking their next step, else `dispatch ask`.
37
+ - Record every request you accept in Dispatch before you start it: an issue, or a line in your issue's `open-items.md` artifact. Before you end a turn, read that record. Each item is done, in progress, or an open ask. Never drop one.
38
+
39
+ ## Which skill to read
40
+
41
+ | Before you | Read |
42
+ | --- | --- |
43
+ | run your first `dispatch` command in this session | `skill://dispatch` |
44
+ | write or change a spec, or write an ask, comment or message a person reads | `skill://dispatch-writing-specs` |
45
+ | plan work whose spec a person approved | superpowers' `writing-plans`, stored as the issue's `plan.md` (`skill://dispatch-writing-specs`, "The plan") |
46
+ | write a Legion architect's own spec | `skill://legion-architect` too |
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: dispatch-writing-specs
3
+ description: "Use in a session with Dispatch before you write or change a spec, ask, comment or message a person reads. It replaces superpowers' brainstorming there."
4
+ ---
5
+
6
+ # Writing specs
7
+
8
+ ## The conversation
9
+
10
+ Reach shared understanding with the person at the least cost of their time. A spec is a conversation, not a document you write and then ask about. A Legion phase worker writes none (`skill://legion-worker`). In order:
11
+
12
+ 1. Read the issue, the code and what the person wrote.
13
+ 2. Send what you found and the questions it leaves open, a few at a time, before the spec states a design.
14
+ 3. Write each part only after the answers it rests on.
15
+ 4. A wrong premise or a changed request changes the request first.
16
+
17
+ ## The outline
18
+
19
+ Find the issue's project with `dispatch projects` or `search`, never the environment. The spec's `##` parts, in order:
20
+
21
+ Part | Content
22
+ --- | ---
23
+ `Summary` | The change in the reader's terms, at most 100 words
24
+ `Problem` | What goes wrong, for whom, with counts
25
+ `Goals and non-goals` | What is in and out of scope
26
+ `Requirements` | a `:::requirement` block each, `**R1.**` on, its source (`From:`)
27
+ `Verification` | the issue's `verification.md`, a `:::check` block each: `serves`, `surface`, `run`, `when`, `negative_control`
28
+ `Design` | How it meets them; a value and its reason for each limit, size and time
29
+ `Decisions` | One line: `The decisions are in dispatch://KEY/artifact/decisions-md.`
30
+
31
+ `serves` and `needs` name a block of this issue or an ancestor. Another agent can build and check the work from these alone. A small change gets a few sentences per part, no matrix; split a large design into child issues.
32
+
33
+ ## Rules for the text
34
+
35
+ For each spec, ask, comment and message:
36
+
37
+ - Write in `skill://asd-ste100`'s strict mode.
38
+ - Write about the current design, never the person or how people reached it, and never quote or upload their words.
39
+ - Link each source where the person wrote it, each id whole. With no link, state the design.
40
+ - In a spec, cite code by file and symbol, never a line number: it goes stale at the next edit.
41
+ - A sentence that cites a gate passing (test, review, check) also says what the pass does not show: "Tests pass: a wrong verdict, not a missing one."
42
+ - Keep only text that helps a reader understand, decide, build, check.
43
+ - Explain each name a new reader may not know, and what each issue you name does for them.
44
+ - In an ask, ask block, comment or message, name a requirement or check by what it is, never its number.
45
+
46
+ ## Questions
47
+
48
+ - Name the concrete case and its numbers, then the choice in those terms, never "Which option?".
49
+ - A choice inside what the person asked for, or one their messages favour, is yours.
50
+ - Ask only for the person's authority, taste or risk appetite, when options differ on what they care about.
51
+ - Ask with an [ask block](skill://dispatch/SKILL.md#asks-and-ask-blocks): 2 to 4 options, answerable on a phone.
52
+
53
+ ## Each version
54
+
55
+ 1. After a person's answer, comment or edit, reread the problem and rewrite what the answer touches, deleting what it contradicts: an answer is integrated, never appended. Answer each comment in its thread. Never narrate an edit: versions and comments hold the history.
56
+ 2. Before each spec write, paste the whole spec, never a path, into a fresh subagent: it lists gaps, conflicts, uncheckable requirements, unclear text and needless questions. Fix or record each.
57
+ 3. Before each approval request, `wait` for a read-only fact check: `task(agent="scout")`.
58
+ 4. Name each version with `dispatch doc-edit --summary`.
59
+
60
+ ## Decisions
61
+
62
+ Choices—answered asks included—earn `decisions.md` `##` sections only if a disagreeing reader would act differently; keep others to one spec line each.
63
+
64
+ - The heading names the choice; the text gives the reason and the options weighed.
65
+ - Link each spec block it shaped, `dispatch://KEY/spec#b-<id>`, and a person's decision its ask, `dispatch://KEY/ask/<id>`.
66
+ - An unsure section adds a paragraph that starts `Unsure:`: what would change your mind.
67
+ - A replaced section moves under a final `## Replaced` as a `###`.
68
+
69
+ ## Old specs
70
+
71
+ Rewrite a spec outside the outline whole in one version, its ask blocks moved with `doc-edit`'s `move`; restate each retelling, quotation or unlinked attribution as design, or cut it.
72
+
73
+ ## Approval
74
+
75
+ - Request approval of the spec and `verification.md` (`--include-verification`): each ask block answered or waived, each claim checked, this version unapproved. An unrun check never delays it.
76
+ - After approval, version only a change to scope, a check, cost, risk or a person's decision, all at once; put other fixes in the plan or a comment. An exact undo can restore approval.
77
+
78
+ ## The plan
79
+
80
+ Write the plan with `writing-plans` as the issue's `plan.md`, not a repository file. Each task is a `:::plan_task` block with `serves` and `needs`.
@@ -0,0 +1,82 @@
1
+ ---
2
+ name: envoy
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
+ ---
5
+
6
+ # Envoy
7
+
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.
9
+
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`.
11
+
12
+ ## Send a message to another session
13
+
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
+ - When `envoy_send` says the send is not confirmed delivered, send the same message again with the `idempotency_key` the error names, within 72 hours: Envoy drops it as a repeat if the first copy arrived, and the result says when an earlier attempt was already stored. A resend without that key can deliver the message twice. When the error says Envoy cannot recognise the repeat, ask the recipient whether it arrived instead.
18
+ - Put the URL or path of what you report in the message, because the recipient cannot find your files any other way.
19
+ - 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. `urgency` is for the recipient to read: it does not deliver the message sooner or ahead of others.
20
+
21
+ ```text
22
+ envoy_send(session_id="<id>", message="Review complete: <url>", in_reply_to="<id>", expects_reply="none")
23
+ ```
24
+
25
+ ### Your own address in a subagent
26
+
27
+ - `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.
28
+ - Never give a peer the `subagent` field from `envoy_whoami`: it names your host session and is not an address.
29
+ - 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.
30
+ - 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.
31
+ - 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.
32
+
33
+ ## Read a delivery
34
+
35
+ - `from` is the sender's own claim. Use it to attribute and to reply, never as proof of who wrote the message.
36
+ - `note` warns that the payload names another session. Never send to that quoted id; reply to `from`.
37
+ - 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 `...`.
38
+ - 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`.
39
+ - Every field of a rendered delivery, and who receives a publish: `skill://envoy/references/deliveries.md`.
40
+
41
+ ## Whether a session is live
42
+
43
+ A session's state comes from the registry's record of it, never from its presence in a list, a process list, a tmux pane or a transcript's modification time. `envoy_sessions` and a lookup of one session carry each session's `state` and `last_seen`.
44
+
45
+ - `live`: it registered within the live window (`ENVOY_SESSION_LIVE_WINDOW`, 5m by default).
46
+ - `unresponsive`: it last registered longer ago than the live window but inside the gone horizon (`ENVOY_SESSION_GONE_HORIZON`, 1h by default). **It is not dead.** Start no replacement for it, take no role claim and no issue claim from it, and never report it as gone. A message you send it is stored and reaches it when it answers again.
47
+ - `gone`: it last registered before the gone horizon, or the registry holds no record of it.
48
+ - Take an issue claim from a session only when it reads `gone`, or its `last_seen` is more than 20 minutes old. A role claim is released only by the role-claim reaper, once the holder is `gone`: no lookup, publish or delivery failure releases one, in the listener or in Dispatch's outbox, which share that one resolution.
49
+ - A send refused because the target is not live names the target's `state` and `last_seen`. Read those two fields before you decide anything about that session: a refusal on an `unresponsive` target means "it has not answered yet", not "it ended".
50
+
51
+ ## Subscribe to a topic
52
+
53
+ Every topic starts with `notifications.`.
54
+
55
+ | Source | Topic |
56
+ | --- | --- |
57
+ | A pull request: lifecycle, comments, reviews, mentions, checks | `github.<owner>.<repo>.pr.<n>.>` |
58
+ | Other GitHub | `github.<owner>.<repo>.issue.<n>[.comment\|.mention]`, `.push.branch.<name>`, `.push.tag.<name>`, `.workflow.<file>.<action>` |
59
+ | A Dispatch issue or document | `dispatch.issue.<KEY>.>`, `dispatch.document.<PROJECT>.<SLUG>.>` |
60
+ | Your own inbox | `agent.<session_id>` |
61
+ | Slack, Ghost Wispr, WhatsApp | `skill://envoy/references/sources.md` |
62
+
63
+ - 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.
64
+ - 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.
65
+ - 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.
66
+
67
+ ## Wait for CI or a merge
68
+
69
+ - 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.
70
+ - 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 commit whose message carries one of GitHub's skip keywords (`[skip ci]` and its kin) starts no workflow, so no settlement of it may ever arrive.
71
+ - 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.
72
+ - A `pr.<n>` event with `closed` and `merged: true` means the PR merged; it carries `merge_commit_sha`, `merged_by` and `head_sha`.
73
+ - A later settlement of the same commit can carry `superseded_settlement: "true"`. Code that orders settlements follows `skill://envoy/references/check-settlement.md`.
74
+ - 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`).
75
+
76
+ ## Roles and team topics
77
+
78
+ - 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.
79
+ - 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 once the holder's session confirmed it, with the live `holder` and its `holder_state`. When nobody holds the role, Envoy keeps the message for the session that claims the role next: the answer says it was kept, and that session is handed it once, within one hour of the keep. A holder that is not `live` returns an error instead: that refusal names the holder, its `holder_state` (`unresponsive` or `gone`), its `last_seen` and `claim_released: false`, and nothing was forwarded and nothing kept, so publish the message again once the holder answers rather than starting a replacement for it or taking anything from it. A live holder that did not confirm the message within about 4 seconds errs too, and you see that one after about 8 seconds, because the client tries once more. No error releases the role's claim: a holder whose `holder_state` is `unresponsive` keeps its role. `envoy_role_get(role=...)` finds the holder first. The stream keeps a forward on the holder's inbox, so a holder that was forwarded to gets it once from its own durable consumer, also after a restart or a reconnect.
80
+ - 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 every session subscribed when it is published, yours too when you subscribe. The listener also asks each other session that holds its own NATS connection and registered the topic (every session that follows it with `envoy_subscribe`) to confirm the message; your own session is never asked. A publish returns an error naming each such session that did not confirm within about 4 seconds, and you see it after about 8 seconds, because the client tries once more. A session named with `receipt_timeout` still gets the message: the listener kept a copy in that session's inbox, which the session reads back once its connection returns. An `unresponsive` session is sent the message the same way, asked for no receipt and named under `unresponsive` in the answer, which is no failure. A session that stopped without unregistering stays in the registry until the reaper finds it `gone`, and is named until then; a named session is not a failed delivery to the others. A team topic is not kept for a session that subscribes later. A topic outside `notifications.` is refused.
81
+ - How a role claim survives a resume, a fork or a dead process: `skill://envoy/references/roles.md`.
82
+ - 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,67 @@
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
+ delivered: "2026-09-07T04:41:13Z"
15
+ id: agent-message-2
16
+ by: "2026-09-07T05:00:00Z"
17
+ urgency: high
18
+ expects_reply: required
19
+ re: agent-message-1
20
+ supersedes: agent-message-0
21
+ reply_with:
22
+ tool: envoy_send
23
+ args:
24
+ session_id: 01a0bbbb-cccc-7ddd-eeee-0123456789ab
25
+ in_reply_to: agent-message-2
26
+ message: ...
27
+ reply_role:
28
+ tool: envoy_publish
29
+ args:
30
+ topic: notifications.role.legion-reviewer
31
+ message: ...
32
+ summary: Deployment needs confirmation.
33
+ message: "Confirm the listener health check passed.\n\nThen publish the release."
34
+ note: body names session 01a0cccc-dddd-7eee-ffff-0123456789ab; the sender is 01a0bbbb-cccc-7ddd-eeee-0123456789ab
35
+ ```
36
+
37
+ - `to`: the local inbox that receives this delivery.
38
+ - `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.
39
+ - `at`: when the server that accepted the delivery took it, by that server's clock: the Envoy listener the sender sent through for a session's message or a webhook event, Dispatch's server for a Dispatch event. The sending session sets no time of its own; its machine's clock counts only where that listener runs on the same machine. It is not the present time: a delivery read late carries an old `at`.
40
+ - `delivered`: when this session got the delivery, by this machine's clock. An Oh My Pi session shows it in every model request, beside `at`, or beside `published_at` on a notice that names a message the session could not be shown. A session the host moves to shows when it got the delivery there, which can be later than another session got it. A session that got the delivery before its process restarted can show an estimate of that time, taken from its transcript, never later than the delivery.
41
+ - `waited`: present when `delivered` is 15 minutes or more after the send time, less any clock difference `clocks` states: how long the delivery waited, in words. An old send time on a late delivery says the delivery waited, not that a clock is wrong.
42
+ - `clocks`: present when this machine's clock runs 15 minutes or more ahead of or behind the Envoy listener's, read from the listener's replies, or when `delivered` is 15 minutes or more before the send time less that difference: which way the clocks differ, and by how much. Only this machine's clock and the listener's are measured, so a send time still after the delivery once the measured difference is taken off says the sending server's own clock runs ahead of the listener's. A difference under 15 minutes is not shown and stays in `waited`.
43
+ - `id`: the delivery id; pass it as `in_reply_to` when you reply.
44
+ - `by`: the expiry deadline, when the sender set one.
45
+ - `urgency`: how soon the sender asks you to act: `low`, `med`, `high` or `blocking`. It is for you to read only: it does not change when a delivery reaches you, or its place among others.
46
+ - `expects_reply`: `none`, `optional` or `required`.
47
+ - `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.
48
+ - `supersedes`: an earlier delivery this one replaces; act on this one in its place.
49
+ - `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.
50
+ - `reply_role`: the role-publish reply call, when the sender holds a role.
51
+ - `summary`: the one-line source summary.
52
+ - `message`: the complete payload; it can hold several paragraphs.
53
+ - `note`: the payload names another session; never use that quoted id as the recipient.
54
+ - `unrecognised`: present when the payload failed validation or came from an unknown source. It names what failed, not every unknown key.
55
+
56
+ ## Who receives a publish
57
+
58
+ It depends on where a session takes its deliveries, not on whether the topic is built in or your own.
59
+
60
+ - An Oh My Pi or Claude Code session takes its messages from a durable consumer the listener keeps for it on Envoy's stream and relays to it in order, acknowledging each one only once the session wrote it. So every message the stream keeps — your inbox, and each topic you follow with `envoy_subscribe` — reaches you once, whoever sent it: your own publish included, and a subagent's publish to a topic its parent follows. One published while you were restarting, disconnected or not running reaches you when you are back, up to 72 hours later. A session does not take its messages from a NATS subscription of its own; `packages/envoy/AGENTS.md`, "Session consumers", is the rule.
61
+ - A team or other ad-hoc topic is the exception: the stream keeps none, so such a topic reaches only the sessions connected and subscribed when it is published, and nothing recovers it. The listener also forwards the message to the inbox of every other session registered on the topic with `envoy_subscribe`, and the stream keeps that copy, so a session that registered it gets it once after a restart too. What a team topic you subscribed to without registering carried meanwhile is gone.
62
+ - 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. The stream keeps the forward on the holder's inbox, so the holder's session gets it once, also after a restart. A role message no live holder can take is kept for the session that claims the role next, which is handed it once, within one hour of the keep.
63
+ - A session whose plugin, or whose listener, is of an earlier release falls back to the core path while the rollout lasts (dispatch://INFRA-771 removes it): there it receives live on its own NATS subscriptions, and recovers what the stream stored for it itself, marked as published while its connection was down. It does not recover a role message that way.
64
+
65
+ ## Listener API errors
66
+
67
+ 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,14 @@
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`, `holder_lapsed`, `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 each issue's handoffs on its Dispatch issue (`dispatch handoff-read`).
9
+ - `no_holder`: no session holds the role's claim. Only the reaper releases a claim, once its holder is `gone`; a Legion role whose agent process exits is `gone` at once, so its successor's soft claim takes the role without waiting out the live window.
10
+ - `holder_lapsed`: a session holds the claim but has not registered within the live window, so the listener refused the publish `404` and forwarded nothing. The error names the holder, its `holder_state` and its `last_seen`, and says `claim_released: false`. An `unresponsive` holder is not dead: it keeps the role, nothing replaces it, and the daemon holds the packet and publishes it again.
11
+ - `delivery_failed`: the message is not known to have reached the holder: the holder lookup failed, the holder's session could not be reached, the publish errored, or the stream did not store the forward. A role message published straight onto NATS gives it for an unresponsive holder too. In the stream case the holder can have seen the message live, and its dedupe drops a re-sent copy. It releases no claim.
12
+ - `receipt_timeout`: the listener forwarded the message to a live holder and no receipt arrived in time. The error names the holder. 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.
13
+ - A role message forwarded to you is kept on your inbox in the stream, which your own durable consumer reads, so your session gets it once, also after the pane restarts or reconnects.
14
+ - Never forward these failure payloads to an architect, because the daemon already acts on them.
@@ -0,0 +1,12 @@
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 Envoy keeps a role publish because no live session holds the role.
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 reaper removed the old session's row (the reaper removes a row only once that session is `gone`: past the gone horizon, `ENVOY_SESSION_GONE_HORIZON`, 1h by default).
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 holder that has not registered for longer than the live window (`ENVOY_SESSION_LIVE_WINDOW`, 5m by default) reads `unresponsive`, and keeps its role: a role publish to it is refused `404 holder_lapsed`, which names the holder, its `holder_state`, its `last_seen` and `claim_released: false`, and nothing is forwarded and nothing kept. An unresponsive holder is not dead, so hold the message and publish it again later, start no replacement for that session, and take nothing from it. Only the role-claim reaper releases a claim, once the holder is `gone`; from then Envoy keeps each role publish for the session that claims the role next, and the answer says it was kept, until that transcript resumes or another session claims the role. A client that stops cleanly deregisters its own session and sends its client instance id, and the listener removes the registration only when the registration carries the same id or none, so a departing client never removes a registration another process made under the same session id.
8
+ - A role message the listener forwarded to your session while you held the role is kept on your inbox in the stream, which your own durable consumer reads, so your session gets it once after a resume or a reconnect within 72 hours, also when the role moved to another session in the meantime.
9
+ - An automatic reclaim never steals. It is a soft claim, which the listener grants when the role is unheld, when its holder is not `live` — `gone`, or `unresponsive` past the live window, which is how a relaunched Legion role recovers it — and when its holder is the id this session continues (a fork's parent) and that predecessor is this client instance's: the predecessor is not `live`, or its live registration carries this instance's own id, or one of the two sides carries no instance id. A client instance is one Oh My Pi Envoy extension instance, or one Claude Code channel-server process; each mints its id when it starts. Any other `live` session keeps the role — the parent of a `/fork` whose role moved to the child, a second process on the same transcript, a named predecessor another client instance still runs — and the listener refuses atomically, naming that holder, while the resumed session logs a warning and holds nothing.
10
+ - A refusal does not stop the session. The session keeps running, and it stays registered, but without the role. It does not claim again at each heartbeat. The Oh My Pi plugin takes the role at its next session event, once the holder is no longer live. The Claude Code channel takes the role at an explicit claim. Use `envoy_role_get` to read who holds the role now.
11
+ - Only an explicit `envoy_role_set` is last-claim-wins. Running it again for a role this session already holds is harmless.
12
+ - 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. Real WhatsApp delivery also needs the listener's MCP bridge configured with a `whatsapp` server (`packages/envoy/internal/mcpbridge`).
@@ -0,0 +1,111 @@
1
+ ---
2
+ name: outside-message
3
+ description: "Use before any text reaches a person outside the company: a comment on an issue or pull request of a repository the company does not own, an email, a chat message to another company, a release note, an enrollment reply, a bug report, a disclosure. Use it also before you open an ask that asks the operator to approve such text. Not for a message to the operator, to another agent, or inside the company's own organization."
4
+ ---
5
+
6
+ # A message to someone outside the company
7
+
8
+ The operator approves every message that leaves the company, and four steps come before you ask.
9
+ The rules below are the operator's own, from the answers that set them; this skill is where they
10
+ are written down.
11
+
12
+ ## The exclusions
13
+
14
+ An exclusion exists only where the operator has stated it. Never infer one from the shape of a
15
+ message.
16
+
17
+ **A project the operator has excluded by name** goes out without approval. Only the operator's own
18
+ answer names such a project, and your stream's brief is where that answer reaches you. With no such
19
+ answer for the project in front of you, there is no exclusion.
20
+
21
+ **A pull request to a project that has already enrolled**, with its description and its comments to
22
+ that project's maintainers. The operator does not want to be asked for approval every time for an
23
+ enrolled project, and ceremony around a pull request must not stop the work. The four steps still
24
+ apply. The approval ask does not. The work does not wait for the pull request either: prepare and
25
+ sign the changes locally, and send them when the work they belong to ends.
26
+
27
+ What counts as enrolled is the enrolling project's own record, and its stream's brief says where
28
+ that record is. Do not decide enrollment from a message's tone, or from a maintainer having replied
29
+ to the company before.
30
+
31
+ Everything else waits for the operator: a first contact with a project that has not enrolled, a bug
32
+ report, a disclosure, and a reply to a maintainer who wrote first.
33
+
34
+ ## The four steps
35
+
36
+ 1. **Research the audience.** Read the repository's contributing guide, its issue and pull-request
37
+ templates, its code of conduct, and its last ten issues or pull requests. Read how the
38
+ maintainers write to each other. Search for what has already been discussed about your subject; a
39
+ question they answered last month, asked again, costs you the reader.
40
+ 2. **Write for that audience, in their conventions.** Use their template where they have one, their
41
+ terms, their level of detail. Run `skill://asd-ste100` and `skill://plain-words` over the text.
42
+ Neither removes AI writing tells; where your session has a pass for those, run it as well.
43
+ 3. **Follow their conventions in form as well as words.** A pull request on their repository uses
44
+ their pull-request template. An issue uses their issue template. A thread reply matches the
45
+ thread.
46
+ 4. **A fresh reviewer reads it and cuts.** Dispatch a subagent that has not seen the drafting, give
47
+ it the message and the research, and tell it to remove everything that is not needed to
48
+ understand the core message. A reviewer that only approves has not done step 4. A subagent with
49
+ no model of its own runs on yours. Where your session can give the reviewer a different model,
50
+ do: a reader of the same model as the writer is the weaker check, though no measurement here
51
+ separates the two.
52
+
53
+ **Step 4 ends with a list of what it removed, and that list goes in the ask.** Measured on
54
+ 2026-10-30 over 18 drafts of three real messages: drafts written with this skill met the bar 9
55
+ times in 9, drafts with no guidance 8 times in 9, so the first three steps changed almost nothing
56
+ — the research teaches them. The step that changed the message was the cut, and it is the step
57
+ agents skip: 2 of 9 with the skill and 3 of 9 without had no cut at all. Only the cut drafts lost
58
+ a promise about a future release, and they lost half their length. An instruction alone did not
59
+ make the cut happen, so the list is what proves it did: three lines naming what came out, or the
60
+ sentence "the reviewer removed nothing", which is itself an answer.
61
+
62
+ The form of the text is not in this skill. Where the stream that owns the relationship keeps its own
63
+ rules for writing to maintainers — in its project's `AGENTS.md`, in its skills, or in its brief —
64
+ those rules decide how the message reads, and some of them are owned by people through that
65
+ repository's code owners. Read them before you draft, and send a change to the form there rather
66
+ than into this file.
67
+
68
+ This skill covers when approval is needed and what comes before the ask. The project's own rules
69
+ cover how the text reads.
70
+
71
+ ## Then the ask
72
+
73
+ **Put the full message text in the ask itself.** Not a link to a document holding it, not a summary
74
+ of it, not "the draft is at …". The operator has to open another document to read the message in
75
+ almost every one of these asks, and cannot judge the message from the ask.
76
+
77
+ An ask for an outside message has, in this order:
78
+
79
+ - who the reader is, and one line on what they already know about this;
80
+ - **the message, in full, as it would be sent**;
81
+ - **what the reviewer removed in step 4**, in three lines or fewer, or "the reviewer removed
82
+ nothing";
83
+ - what changes if the answer is no.
84
+
85
+ Link the research, not the message. An ask with no removal line has not had step 4, whatever it
86
+ says.
87
+
88
+ ## Never write these
89
+
90
+ Delete the empty verb phrases and say the literal thing instead. These shapes:
91
+
92
+ | Never | Write instead |
93
+ |---|---|
94
+ | "to name the problem" | say the problem |
95
+ | "land the change" | merge it, release it, deploy it — whichever it is |
96
+ | "carry the finding to X" | tell X, send X, put it on the issue |
97
+ | "this shows the gap" | the gap is … |
98
+ | "that pays for itself" | it costs $X and returns $Y |
99
+
100
+ The test: delete the verb phrase and ask what literally happens. If you cannot say what happens, you
101
+ did not know, and the phrase hid that from both of you. Write what happens.
102
+
103
+ The same rule runs over asks, specs, messages, commit bodies and pull-request descriptions, not only
104
+ outside messages. `skill://plain-words` has the full list and the rewrites.
105
+
106
+ ## What this skill does not cover
107
+
108
+ - Text written in a person's own name, which that person signs off.
109
+ - A message to another agent or to the operator: write it plainly and send it; no approval step.
110
+ - Posting a release the pipeline publishes from approved template text: the template was approved
111
+ once; the release needs no new ask unless its text changes.