opencode-courier 0.3.0 → 0.4.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 (141) hide show
  1. package/README.md +175 -274
  2. package/dist/index.js +47 -73
  3. package/dist/kernel/protocol.d.ts +20 -0
  4. package/dist/kernel/protocol.js +36 -0
  5. package/dist/kernel/tool.d.ts +5 -0
  6. package/dist/kernel/tool.js +5 -0
  7. package/dist/person/events.d.ts +23 -0
  8. package/dist/person/events.js +113 -0
  9. package/dist/person/form.d.ts +26 -0
  10. package/dist/person/form.js +64 -0
  11. package/dist/person/index.d.ts +17 -0
  12. package/dist/person/index.js +5 -0
  13. package/dist/{relay.d.ts → person/permission.d.ts} +27 -21
  14. package/dist/{relay.js → person/permission.js} +9 -5
  15. package/dist/person/ports.d.ts +58 -0
  16. package/dist/person/ports.js +2 -0
  17. package/dist/{question → person/question}/answer.d.ts +33 -4
  18. package/dist/{question → person/question}/answer.js +43 -8
  19. package/dist/person/question/index.d.ts +7 -0
  20. package/dist/{question → person/question}/index.js +3 -3
  21. package/dist/{question → person/question}/lifecycle.d.ts +1 -1
  22. package/dist/{question → person/question}/pure.d.ts +1 -2
  23. package/dist/{question → person/question}/pure.js +1 -1
  24. package/dist/{question → person/question}/relay.d.ts +1 -1
  25. package/dist/{question → person/question}/relay.js +14 -8
  26. package/dist/{question → person/question}/shared.d.ts +13 -3
  27. package/dist/person/question/shared.js +52 -0
  28. package/dist/person/question/types.d.ts +121 -0
  29. package/dist/person/question/types.js +1 -0
  30. package/dist/person/rpc-handlers.d.ts +160 -0
  31. package/dist/person/rpc-handlers.js +62 -0
  32. package/dist/person/rpc.d.ts +227 -0
  33. package/dist/person/rpc.js +70 -0
  34. package/dist/person/text.d.ts +68 -0
  35. package/dist/person/text.js +262 -0
  36. package/dist/person/tools.d.ts +20 -0
  37. package/dist/person/tools.js +55 -0
  38. package/dist/person/tui.d.ts +46 -0
  39. package/dist/{bounded.d.ts → platform/bounded.d.ts} +4 -0
  40. package/dist/{bounded.js → platform/bounded.js} +4 -0
  41. package/dist/platform/events.d.ts +34 -0
  42. package/dist/platform/events.js +121 -0
  43. package/dist/platform/hub.d.ts +195 -0
  44. package/dist/{hub.js → platform/hub.js} +27 -71
  45. package/dist/{version.js → platform/version.js} +1 -1
  46. package/dist/rpc.d.ts +2 -0
  47. package/dist/rpc.js +3 -0
  48. package/dist/scheduling/index.d.ts +6 -0
  49. package/dist/scheduling/index.js +5 -0
  50. package/dist/{later.d.ts → scheduling/later.d.ts} +7 -3
  51. package/dist/{later.js → scheduling/later.js} +12 -4
  52. package/dist/scheduling/text.d.ts +6 -0
  53. package/dist/scheduling/text.js +10 -0
  54. package/dist/scheduling/tools.d.ts +14 -0
  55. package/dist/scheduling/tools.js +48 -0
  56. package/dist/skill.d.ts +4 -0
  57. package/dist/skill.js +54 -1
  58. package/dist/{cleanup.d.ts → tree/cleanup.d.ts} +1 -27
  59. package/dist/{cleanup.js → tree/cleanup.js} +9 -16
  60. package/dist/tree/context.d.ts +15 -0
  61. package/dist/tree/context.js +34 -0
  62. package/dist/tree/domain.d.ts +101 -0
  63. package/dist/tree/domain.js +68 -0
  64. package/dist/{watch.d.ts → tree/events.d.ts} +10 -37
  65. package/dist/tree/events.js +204 -0
  66. package/dist/tree/forget.d.ts +24 -0
  67. package/dist/tree/forget.js +59 -0
  68. package/dist/tree/group.d.ts +82 -0
  69. package/dist/tree/group.js +203 -0
  70. package/dist/tree/index.d.ts +10 -0
  71. package/dist/tree/index.js +11 -0
  72. package/dist/tree/limits.d.ts +10 -0
  73. package/dist/{limits.js → tree/limits.js} +6 -55
  74. package/dist/tree/panel.d.ts +13 -0
  75. package/dist/tree/panel.js +75 -0
  76. package/dist/tree/ports.d.ts +276 -0
  77. package/dist/tree/ports.js +1 -0
  78. package/dist/tree/report.d.ts +23 -0
  79. package/dist/tree/report.js +33 -0
  80. package/dist/tree/roster.d.ts +27 -0
  81. package/dist/tree/roster.js +62 -0
  82. package/dist/tree/rpc-handlers.d.ts +201 -0
  83. package/dist/tree/rpc-handlers.js +20 -0
  84. package/dist/tree/rpc.d.ts +344 -0
  85. package/dist/tree/rpc.js +83 -0
  86. package/dist/tree/send.d.ts +14 -0
  87. package/dist/tree/send.js +99 -0
  88. package/dist/tree/spawn.d.ts +21 -0
  89. package/dist/tree/spawn.js +173 -0
  90. package/dist/tree/status.d.ts +128 -0
  91. package/dist/tree/status.js +62 -0
  92. package/dist/{stop.d.ts → tree/stop.d.ts} +1 -9
  93. package/dist/{stop.js → tree/stop.js} +9 -13
  94. package/dist/tree/store.d.ts +24 -0
  95. package/dist/tree/store.js +264 -0
  96. package/dist/{tree.d.ts → tree/subtree.d.ts} +5 -6
  97. package/dist/{tree.js → tree/subtree.js} +7 -5
  98. package/dist/tree/summary.d.ts +7 -0
  99. package/dist/tree/summary.js +35 -0
  100. package/dist/tree/text.d.ts +77 -0
  101. package/dist/tree/text.js +287 -0
  102. package/dist/tree/tools.d.ts +54 -0
  103. package/dist/{tools.js → tree/tools.js} +12 -142
  104. package/dist/tree/tui.d.ts +53 -0
  105. package/dist/tui.d.ts +3 -0
  106. package/dist/tui.js +1219 -0
  107. package/dist/webhook/index.d.ts +5 -0
  108. package/dist/webhook/index.js +3 -0
  109. package/dist/{webhook.d.ts → webhook/receiver.d.ts} +17 -23
  110. package/dist/{webhook.js → webhook/receiver.js} +58 -50
  111. package/dist/webhook/subscriptions.d.ts +29 -0
  112. package/dist/webhook/subscriptions.js +52 -0
  113. package/dist/webhook/text.d.ts +23 -0
  114. package/dist/webhook/text.js +82 -0
  115. package/dist/webhook/tools.d.ts +19 -0
  116. package/dist/webhook/tools.js +46 -0
  117. package/package.json +18 -7
  118. package/dist/courier.d.ts +0 -200
  119. package/dist/courier.js +0 -315
  120. package/dist/group.d.ts +0 -86
  121. package/dist/group.js +0 -247
  122. package/dist/hub.d.ts +0 -300
  123. package/dist/limits.d.ts +0 -65
  124. package/dist/notices.d.ts +0 -356
  125. package/dist/notices.js +0 -678
  126. package/dist/question/index.d.ts +0 -6
  127. package/dist/question/shared.js +0 -24
  128. package/dist/report.d.ts +0 -48
  129. package/dist/report.js +0 -88
  130. package/dist/roster.d.ts +0 -93
  131. package/dist/roster.js +0 -206
  132. package/dist/tools.d.ts +0 -94
  133. package/dist/watch.js +0 -474
  134. /package/dist/{question → person/question}/lifecycle.js +0 -0
  135. /package/dist/{question → person/question}/runtime.d.ts +0 -0
  136. /package/dist/{question → person/question}/runtime.js +0 -0
  137. /package/dist/{json.d.ts → platform/json.d.ts} +0 -0
  138. /package/dist/{json.js → platform/json.js} +0 -0
  139. /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
  140. /package/dist/{storage.js → platform/storage.js} +0 -0
  141. /package/dist/{version.d.ts → platform/version.d.ts} +0 -0
package/README.md CHANGED
@@ -22,7 +22,7 @@ prompt](#demos)).
22
22
  ## Quickstart
23
23
 
24
24
  ```bash
25
- npm install -g @opencode/cli@2.0.26 # OpenCode V2, at the version courier is tested on
25
+ npm install -g @opencode/cli@2.0.28 # OpenCode V2, at the version courier is tested on
26
26
  opencode plugin add opencode-courier # by name, so OpenCode offers new releases
27
27
  opencode # in any folder
28
28
  ```
@@ -39,43 +39,31 @@ If OpenCode asks to allow a child's command, the parent passes the question to y
39
39
  sometimes stall on a request; if nothing moves for a few minutes, send the prompt again. Next:
40
40
  [Using it](#using-it).
41
41
 
42
+
42
43
  ## OpenCode V2 native
43
44
 
44
45
  Courier is built on OpenCode V2's plugin API (`@opencode/plugin`), not the V1 one
45
46
  (`@opencode-ai/plugin`), and needs OpenCode V2. Each release is pinned to one V2 version and tested
46
- against it end to end; the current one is tested on **OpenCode 2.0.26**. CI also runs the live
47
+ against it end to end; the current one is tested on **OpenCode 2.0.28**. CI also runs the live
47
48
  suite on the newest OpenCode release, so a host release that breaks the plugin shows up there
48
49
  first. Older releases and their versions: [Supported OpenCode version](#supported-opencode-version).
49
50
 
50
- > **Status: early.** Passes an end-to-end test inside a live OpenCode V2 server driven by a
51
- > scripted stand-in model, and a smoke test with real (free) models: see
52
- > [Development](#development).
53
-
54
51
  ## Courier or the built-in subagent tool?
55
52
 
56
53
  OpenCode V2 has its own [`subagent` tool](https://opencode.ai/v2/docs/agents): it starts an agent
57
54
  with fresh context in a child session, and the parent either waits for its final answer or, with
58
- `background: true`, carries on and is notified when the child finishes. Passing the child's
59
- `sessionID` back sends it another prompt, steering it if it is running and waking it if it is idle.
60
- Use it when one result back is all you need: a search, a review, a self-contained task that needs
61
- nothing from you on the way.
55
+ `background: true`, carries on and is notified when the child finishes; passing the child's
56
+ `sessionID` back prompts it again. Use it when one result back is all you need: a search, a review,
57
+ a self-contained task that needs nothing from you on the way. Use courier when the work needs more:
62
58
 
63
- Use courier when the work needs more than one result back. On top of what the built-in tool does,
64
- it adds:
65
-
66
- - **Messages from the child, mid-run:** with the built-in tool a child's one message to its parent
67
- is its final answer. With courier a child reports progress, or asks its parent something,
59
+ - **Messages from the child, mid-run:** a child reports progress, or asks its parent something,
68
60
  whenever it likes with `courier_send`, and any session can message any other.
69
- - **Waking an idle session from anywhere:** a message from a child, a schedule or a webhook starts a
70
- new turn in a session that has ended its own.
71
- - **Scheduled messages:** `courier_later` wakes a session at a set time, for check-ins and
72
- reminders.
73
- - **Webhooks:** GitHub reviews, comments and CI runs, or any signed POST, wake the
74
- [subscribed](#webhooks) session.
61
+ - **Waking an idle session from anywhere:** a message from a child, a schedule (`courier_later`) or
62
+ a [webhook](#webhooks) (GitHub reviews, comments and CI runs, or any signed POST) starts a new turn
63
+ in a session that has ended its own.
75
64
  - **Worktree isolation:** `isolate: true` gives a child its own git worktree, so children can edit
76
- files in parallel. An isolated child that splits hands its committed work down: its isolated
77
- children start from its HEAD, and each isolated child commits on `courier/<its session ID>` and
78
- reports that branch for its parent to merge ([nested isolation](docs/reference.md#nested-isolation)).
65
+ files in parallel; each commits on `courier/<its session ID>` and reports that branch for its
66
+ parent to merge ([nested isolation](docs/reference.md#nested-isolation)).
79
67
  - **Relaying to you:** a child's permission requests and questions reach you through the parent,
80
68
  and your answers go back; a form only you can answer is pointed out to you.
81
69
 
@@ -83,101 +71,65 @@ it adds:
83
71
 
84
72
  A parent session calls `courier_spawn`, gets a session id back immediately and ends its turn. The
85
73
  child works on its own and, when it is done or stuck, calls `courier_send` with the parent's id, a
86
- status and what it produced. That message lands in the parent's inbox and OpenCode starts a new
87
- turn for the parent if it is idle. A child whose turn fails instead, so that it cannot report, or ends without reporting, is
88
- reported by the plugin, and a child that waits for a permission or asks a question has it passed to the parent, who asks you and
89
- passes your answer back. A child that waits on a form only you can answer, such as OpenCode asking
90
- which web search provider to use, has its parent told so.
91
-
92
- There is no polling anywhere: a message is admitted into the target session's inbox through
93
- OpenCode's own API, which wakes the session, the same way OpenCode's background subagents report to
94
- their parent. Delivery is `steer` by default (injected into the target's running turn, or starts
95
- one if idle); `queue: true` waits until the current turn ends.
74
+ status and what it produced. That message lands in the parent's inbox and OpenCode starts a new turn
75
+ for the parent if it is idle. There is no polling anywhere: a message is admitted into the target
76
+ session's inbox through OpenCode's own API, the way OpenCode's background subagents report to their
77
+ parent. Delivery is `steer` by default (injected into the target's running turn, or starts one if
78
+ idle); `queue: true` waits until the current turn ends.
96
79
 
97
80
  ## Demos
98
81
 
99
82
  **The parent does not wait.** Prompt: *"Spawn two children: one runs `sleep 30` then lists the
100
83
  exports of math.js, the other runs `sleep 45` then lists the exports of text.js. Don't wait for
101
84
  them: meanwhile write a short README.md for this folder yourself, and add their reports to it when
102
- they arrive."*
103
-
104
- The recording at the top is this run. Watch the parent write its README and finish its turn 16
105
- seconds in, while both children still sleep. Each report then starts a new turn on its own (the
106
- `Message from ses_…` line is the child's message): the first after about 30 seconds, the second
107
- after about 45, and the parent folds each into the README and cancels the check-in it had scheduled
108
- for itself.
85
+ they arrive."* The recording at the top is this run: the parent writes its README and finishes its
86
+ turn 16 seconds in, while both children still sleep; each report (the `Message from ses_…` line)
87
+ then starts a new turn on its own, and the parent folds it into the README.
109
88
 
110
89
  **A child's question reaches you.** Prompt: *"Spawn a child session to add a multiply function to
111
90
  math.js. It must ask me first whether to name it multiply, times or product."*
112
91
 
113
92
  ![A child's question opens as a form in the parent's session, times is picked, and the child carries on](docs/demo-question-tui.gif)
114
93
 
115
- Watch the child's question open in the parent's session as OpenCode's own question form, where
116
- `times` is picked. The parent passes it back, and the child adds `times` to math.js and reports.
117
-
118
- Both recordings are real runs with a free model on [OpenCode Zen](https://opencode.ai/zen) (Muse
119
- Spark 1.3).
94
+ The child's question opens in the parent's session as OpenCode's own question form, `times` is
95
+ picked, and the child adds `times` to math.js and reports. Both recordings are real runs with a free
96
+ model on [OpenCode Zen](https://opencode.ai/zen) (Muse Spark 1.3).
120
97
 
121
98
  ## Recursive orchestration
122
99
 
123
- A session splits a goal across child sessions, each child decides in turn whether to split its
124
- part again, and the results are checked and integrated at every level on the way back up. The
125
- plugin ships the playbook as an OpenCode skill, `courier-orchestrate`, listed in every session with
126
- the plugin loaded: how to decide whether to split, and to say which in one line of the reply
127
- (`split: no, because …; considered: <the parts you weighed>` or `split: 3 parts: …`), plan the
128
- parts, start them in one join group, end the turn, read each report by its status, verify
129
- first-hand through the project's scripts, integrate and report upward, and what to do when a part
130
- fails, ends quietly, blocks or hits a limit. It names the parts an implementation task with one
131
- tightly coupled core usually still has: its e2e scenario, its documentation, sometimes a second
132
- test file, and a review of the core by a session with fresh context (`isolate: false`, it edits
133
- nothing). The session does the core itself first and commits it, then starts one session per
134
- remaining part, if 2 or more remain; a review session's findings are a report to act on, and
135
- running the checks stays the session's own. Its description asks a model to load it at the start
136
- of any implementation task that may have independent parts, and a session nobody spawned is pointed to it by its system prompt as well
137
- ([Session trees](#session-trees)); `/skills` in the TUI attaches it to your prompt. Its text:
138
- [the reference](docs/reference.md#the-courier-orchestrate-skill).
139
-
140
- The smoke test's recursive scenario (`COURIER_SCENARIO=recursive`, [docs/real-model.md](docs/real-model.md#recursive-orchestration))
141
- asks a free model:
142
-
143
- > Have this job done through helper sessions started with courier_spawn, one session per part, and
144
- > do no part yourself. It has two parts. Part "numbers" is itself two independent halves, which the
145
- > session that gets it hands to two sessions of its own: one runs `` `sleep 20; echo $((17 * 23))` ``
146
- > and writes the number it printed to numbers/a.txt, the other runs `` `sleep 20; echo $((2 ** 10))` ``
147
- > and writes the number it printed to numbers/b.txt. Part "count" runs `` `sleep 20; echo $((99 - 57))` ``
148
- > and writes the number it printed to count/total.txt. When the parts have reported back, read the
149
- > three files yourself and reply with one line: RESULTS \<a\> \<b\> \<total\>
150
-
151
- The root starts two sessions and ends its turn; the "numbers" session starts two of its own, ends
152
- its turn, checks both files when their reports wake it and reports once to the root; the "count"
153
- session does its part itself. The root is woken by the two reports, reads the three files and
154
- replies `RESULTS 391 1024 42`. The checker verifies the shape of the tree, that no session polled or
155
- went past `maxDepth`, that every report carries a status and each sub-orchestrator reported once,
156
- after its children, and the files and the final line.
100
+ A session can split a goal across child sessions, each child deciding in turn whether to split its
101
+ part again, with the results checked and integrated at every level on the way back up. The plugin
102
+ ships the playbook as an OpenCode skill, `courier-orchestrate`, listed in every session: models are
103
+ told to load it at the start of an implementation task that may have independent parts, and
104
+ `/skills` in the TUI attaches it to your prompt. What it says: [the
105
+ reference](docs/reference.md#the-courier-orchestrate-skill).
157
106
 
158
107
  ## Install
159
108
 
160
109
  ### Supported OpenCode version
161
110
 
162
- Requires OpenCode V2: the `opencode` command from `@opencode/cli` (also installed as `opencode2`,
163
- the name of the beta line). Each release of this plugin is built and tested against exactly one
164
- OpenCode V2 version, and a release that moves the pin adds a row here. OpenCode itself never
165
- checks the version: the plugin installs and loads on any V2 host. When it loads on a version other
166
- than the pinned one, it writes one line to the server log naming both versions, so a mismatch is
167
- named before a tool fails. More in [the reference](docs/reference.md#the-opencode-version).
111
+ Requires OpenCode V2: the `opencode` command from `@opencode/cli` (also installed as `opencode2`).
112
+ Each release is built and tested against exactly one OpenCode V2 version; a row lists the releases
113
+ pinned to its version. OpenCode never checks it:
114
+ the plugin installs and loads on any V2 host, and on a version other than the pinned one writes one
115
+ line to the server log naming both ([more](docs/reference.md#the-opencode-version)).
168
116
 
169
117
  | opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
170
118
  |---|---|
171
- | 0.2.3 | 2.0.26 |
119
+ | 0.4.0 | 2.0.28 |
120
+ | 0.2.3, 0.3.0 | 2.0.26 |
172
121
  | 0.2.2 | 2.0.24 |
173
122
  | 0.2.1 | 2.0.23 |
174
123
  | 0.2.0 | 2.0.22 |
175
124
  | 0.1.6 | 0.0.0-beta-19271 (the beta line: `opencode2` from `@opencode-ai/cli`, and `@opencode-ai/plugin`) |
176
125
 
126
+ The [terminal part](#in-the-terminal) relies on the host to provide its UI modules and is tested on
127
+ OpenCode 2.0.28 only; the server part works as before on older V2 hosts from 2.0.4 on.
128
+
177
129
  Check yours with `opencode --version`, and install the matching CLI with:
178
130
 
179
131
  ```bash
180
- npm install -g @opencode/cli@2.0.26
132
+ npm install -g @opencode/cli@2.0.28
181
133
  ```
182
134
 
183
135
  ### The plugin
@@ -187,24 +139,14 @@ opencode plugin add opencode-courier
187
139
  ```
188
140
 
189
141
  This installs the package from npm and adds `"opencode-courier"` to `plugins` in the global
190
- configuration (`~/.config/opencode/opencode.json`). To receive webhooks, replace that entry with the
191
- object form shown under [Webhooks](#webhooks), which carries a `webhook` option; the [session tree
192
- limits](#session-trees) are options of the same entry.
142
+ configuration (`~/.config/opencode/opencode.json`); its options ([session trees](#session-trees),
143
+ [webhooks](#webhooks)) go in the object form of that entry. Install it by name, without a version:
144
+ OpenCode only checks plugins for updates when their entry is not an exact version, so
145
+ `opencode-courier@0.2.1` is never offered a newer release ([moving such an
146
+ entry](docs/reference.md#updating-the-plugin)).
193
147
 
194
- Install it by name, without a version: OpenCode only checks plugins for updates when their entry is
195
- not an exact version, so `opencode-courier@0.2.1` is never offered a newer release. How to move an
196
- entry that carries a version to the name, and what to look at when an update still does not show:
197
- [Updating the plugin](docs/reference.md#updating-the-plugin).
198
-
199
- ### From a local clone
200
-
201
- ```bash
202
- git clone https://github.com/ivopogace/opencode-courier && cd opencode-courier
203
- bun install && npm run build
204
- ```
205
-
206
- Then list it in `opencode.json` (V2 uses `plugins`, plural). A local plugin path must be a
207
- **directory**; OpenCode loads its `index.js`, and ignores a path to a file with a warning:
148
+ From a local clone, `bun install && npm run build`, then list its `dist` **directory** in
149
+ `opencode.json` (V2 uses `plugins`, plural; a path to a file is ignored with a warning):
208
150
 
209
151
  ```jsonc
210
152
  {
@@ -215,111 +157,77 @@ Then list it in `opencode.json` (V2 uses `plugins`, plural). A local plugin path
215
157
  ## Using it
216
158
 
217
159
  1. Keep the background server running so sessions can be woken while you are away (`opencode
218
- service start`; `opencode service status` to check).
219
- 2. Give the agents that run children the permissions their work needs. A child that hits an approval
220
- prompt has it passed to its parent, which asks you, but the child waits until you answer, so keep
221
- prompts for what you want to decide yourself. A child's question goes to the parent the same
222
- way, so let the agents that run children use the `question` tool if they should be able to ask
223
- you; OpenCode's default agent may. Choose a web search provider before children search the web
224
- (run one web search in your own session and answer OpenCode's prompt, or use its "Third-party
225
- search" setting): otherwise the first child to search shows that prompt in its own session,
226
- which only you can answer there.
160
+ service start`), and run one OpenCode server per data directory where you can: a second one, such
161
+ as `opencode serve` next to it, cannot pass your answer on to a child the other runs
162
+ ([why](docs/reference.md#two-servers-on-one-data-directory)).
163
+ 2. Give the agents that run children the permissions their work needs: a child that hits an approval
164
+ prompt waits until you answer it through its parent. Let them use the `question` tool if they
165
+ should be able to ask you. Choose a web search provider before children search the web (run one
166
+ web search in your own session, or use OpenCode's "Third-party search" setting), or the first
167
+ child to search shows that prompt in its own session, which only you can answer there.
227
168
  3. Your own session's prompts are relayed nowhere, and each one stalls the whole tree until you
228
- answer it. Before an unattended run, allow the orchestrating session's toolchain up front: the
229
- repository's `bun run` and `npm run` scripts, `git`, and whatever its checks need. Prefer one rule
230
- on `bun run *` or `npm run *` over a rule per binary; the skill tells a session that orchestrates
231
- to run checks through the project's scripts rather than `./node_modules/.bin/<tool>`, so one rule
232
- covers them.
233
- 4. Use `isolate: true` whenever children edit files in parallel. The child's worktree is made from
234
- the last commit (from an isolated session, its HEAD), so an uncommitted `opencode.json` is not there and the child falls back to your
235
- global config: keep providers and models in the global config, or commit the file. When you are
236
- done with an isolated child, `courier_cleanup` it so its worktree does not linger.
237
- 5. A child whose turn fails, or ends without `courier_send`, is reported to its parent by the
238
- plugin. What the plugin cannot see, such as a server that stops while a child runs, a
239
- `courier_later` check-in still covers: for a long-running child, schedule one for yourself and
240
- `courier_cancel` it when the child reports.
169
+ answer it. Before an unattended run, allow the orchestrating session's toolchain up front: one rule
170
+ on `bun run *` or `npm run *` (the skill has checks run through the project's scripts), `git`, and
171
+ whatever else its checks need.
172
+ 4. Use `isolate: true` whenever children edit files in parallel. The worktree is made from the last
173
+ commit (from an isolated session, its HEAD), so an uncommitted `opencode.json` is not there: keep
174
+ providers and models in the global config, or commit the file. A worktree is kept until you
175
+ `courier_cleanup` it, which refuses to drop uncommitted changes or unbranched commits unless told to.
176
+ 5. For a long-running child, schedule a `courier_later` check-in for yourself and `courier_cancel` it
177
+ when the child reports: it covers what the plugin cannot see, such as a server that stops while a
178
+ child runs.
241
179
 
242
180
  ## Tools
243
181
 
244
182
  | Tool | Does |
245
183
  |---|---|
246
- | `courier_spawn` | Creates a session (optionally in its own git worktree with `isolate: true`, which from an isolated session starts at its HEAD, [nested isolation](docs/reference.md#nested-isolation)) on the parent's model, sends it the task plus a brief naming the parent, how to report back and when to split the task further, and returns at once. With a `group` name, the reports of every session started under that name are held and delivered together, in one message ([reference](docs/reference.md#join-groups)). Refused past the [session tree limits](#session-trees). |
247
- | `courier_send` | Delivers a message to a session, signed with the sender's id, waking it if idle. From a child to its parent, a `status` (`done`, `partial`, `blocked` or `failed`) makes it the child's report, carried as an attribute of the message (`<courier from="…" status="done">`), and `artifacts` (`branch`, `commits`, `files`, `checks`) are listed in its body in a fixed layout; a message without a status is progress, not the report ([reference](docs/reference.md#a-childs-report)). From a member of a join group, a report with `done`, `partial` or `failed` is held with the group instead, and the result says so. |
248
- | `courier_status` | One look at a session: outcome, idle time, last reply and the permission requests and questions it waits on. For check-ins, not for waiting. |
249
- | `courier_children` | Lists the sessions this one (or a given `sessionID`) started with `courier_spawn`, each with what `courier_status` reports plus its directory, whether it is isolated, when it was started and, for one started in a group, the group and whether its report is `held`, `out` or `released`. |
250
- | `courier_tree` | Lists the whole tree under this session (or a given `sessionID`): every session started from it with `courier_spawn`, directly or through others, depth first, each with what `courier_children` shows plus its depth, its parent, and its report state (whether it owes a report, sent progress, ended without one, and the status of its last report). Cut at `maxTotal` sessions ([reference](docs/reference.md#seeing-and-stopping-a-subtree)). |
251
- | `courier_stop` | Interrupts a session you started, directly or through others, and every session under it, deepest first, and cancels the `courier_later` messages scheduled for them. With `cleanup: true` it also removes their worktrees, by `courier_cleanup`'s rules (kept ones are listed). You are not told of what you stopped; a session between you and the target is, once, as `ended="stopped"` ([reference](docs/reference.md#seeing-and-stopping-a-subtree)). |
252
- | `courier_cleanup` | Removes the git worktree of a child started with `isolate: true` and drops the child from `courier_children`. Keeps a worktree with uncommitted changes or commits on no branch, tag or remote and lists them, unless `force: true` is passed ([reference](docs/reference.md#worktree-cleanup)). A child without a worktree is only dropped from `courier_children`; either way, a join group member that has not reported is dropped from its group, which is released without it ([reference](docs/reference.md#join-groups)). |
253
- | `courier_answer` | Passes your answer to a permission request or a question that a session started from this one waits on: `reply` (`once`, `always` or `reject`, with an optional `message`) for a permission request, `answers` for a question. |
254
- | `courier_later` | Schedules a message for a session (this one by default) in `delayMinutes` or `at` an ISO time, and returns an id. When due it is delivered like `courier_send`, queued behind any running turn and waking the session if idle. |
255
- | `courier_cancel` | Drops a message scheduled with `courier_later`, e.g. because the child it was waiting for reported first. |
256
- | `courier_subscribe` | Subscribes a session (this one by default) to webhook deliveries for a `topic`: `owner/repo`, `owner/repo#12` (one pull request or issue) or a generic name. Each matching delivery arrives as a message, queued behind any running turn and waking the session if idle. Needs the [webhook receiver](#webhooks). |
257
- | `courier_unsubscribe` | Drops one topic, or all of a session's, e.g. once its pull request is merged. |
184
+ | `courier_spawn` | Starts a session on the parent's model with the task and a brief, and returns at once; `isolate: true` gives it its own git worktree, `group` holds its report with its siblings'. |
185
+ | `courier_send` | Messages a session, waking it if idle; from a child to its parent, a `status` and `artifacts` make it the child's [report](docs/reference.md#a-childs-report). |
186
+ | `courier_status` | One look at a session: outcome, idle time, last reply and what it waits on. For check-ins, not for waiting. |
187
+ | `courier_children` | Lists the sessions this one (or a given one) started, with their state, worktree and join group. |
188
+ | `courier_tree` | Lists the whole tree under a session, depth first, with each one's depth and report state. |
189
+ | `courier_stop` | Interrupts a session below you and everything under it, and cancels their scheduled messages; `cleanup: true` removes their worktrees too. |
190
+ | `courier_cleanup` | Removes an isolated child's worktree, unless it holds unsaved work, and forgets the child. |
191
+ | `courier_answer` | Passes your answer to a permission request or a question that a session under this one waits on. |
192
+ | `courier_later` | Schedules a message for a session (this one by default) in `delayMinutes` or `at` a time. |
193
+ | `courier_cancel` | Drops a message scheduled with `courier_later`. |
194
+ | `courier_subscribe` | Subscribes a session to webhook deliveries for a topic (`owner/repo`, `owner/repo#12` or a name). |
195
+ | `courier_unsubscribe` | Drops one topic, or all of a session's. |
196
+
197
+ Each tool's parameters: [the reference](docs/reference.md#the-tools).
258
198
 
259
199
  ## How children reach you
260
200
 
261
- The short version; the long one, with every edge, is [docs/reference.md](docs/reference.md).
262
-
263
- - **A child runs on its parent's model**, not on OpenCode's default, so a parent you moved to
264
- another model starts children that can reach theirs too. An `agent` with a model of its own keeps
265
- it. [More](docs/reference.md#the-childs-model).
266
- - **A child can split its task** with `courier_spawn` of its own, within the [limits](#session-trees):
267
- its brief says when to split and when to do the work itself, and a session that splits checks
268
- its children's work and sends one combined report. [More](docs/reference.md#session-trees-and-their-limits).
269
201
  - **A child reports once, with a status**: `done`, `partial`, `blocked` (it needs a decision from
270
- its parent, and says what) or `failed`, as an attribute of the message, with its artifacts listed
271
- in the body. A message without a status is progress, so a child that sends "halfway" and then
272
- stops is still reported as ending without a report. [More](docs/reference.md#a-childs-report).
273
- - **A join group wakes you once.** Start a batch of sessions with the same `group` name, and
274
- their `done`, `partial` and `failed` reports are held until the last one is in and your turn has ended, then delivered
275
- in one message, marked `group="<name>"`, listing every report with its status and artifacts in
276
- the order the sessions were started. A `blocked` report, progress, a failed turn, a silent end, a
277
- permission request and a question reach you at once, as ever; a member whose turn fails or is
278
- interrupted leaves the group, which is released without it. A member that will not report (it
279
- ended its turn silently, or you gave up on it) holds the others back: drop it with
280
- `courier_cleanup`, and the group is released without it, the message naming it; a report it
281
- sends after all comes on its own. Held reports survive a restart, and the name is free again
282
- once the group has been delivered. [More](docs/reference.md#join-groups).
283
- - **A child that fails** cannot report, so the plugin does: every failed turn of a spawned session
284
- sends its parent a message marked `failed="<error type>"`, with the error, waking it if idle.
285
- [More](docs/reference.md#a-child-that-fails).
286
- - **A child that ends its turn without reporting** to its parent has the parent told, marked
287
- `ended="without-report"`, with its last reply, waking it if idle; it counts toward the limits
288
- until it reports. One that waits, on a request, a session it started, a scheduled message or a
289
- webhook it just subscribed to, is not reported, nor is one that answers what you typed in its session.
290
- [More](docs/reference.md#a-child-that-ends-without-a-report).
291
- - **A child that asks for permission** has the request passed to the session at the top, with what
292
- it asks for and the choices OpenCode offers (`once`, `always`, `reject`). That session asks you
293
- and answers with `courier_answer`; the child carries on.
202
+ its parent, and says what) or `failed`, with its artifacts (branch, commits, files, checks) listed
203
+ in the message. A message without a status is progress, not the report.
204
+ [More](docs/reference.md#a-childs-report).
205
+ - **A join group wakes you once.** Start a batch of sessions with the same `group` name, and their
206
+ `done`, `partial` and `failed` reports are held until the last one is in and your turn has ended,
207
+ then delivered in one message. A `blocked` report, a failure, a question or a permission request
208
+ still reaches you at once. A member that will not report holds the others back: drop it with
209
+ `courier_cleanup`, and the group is released without it. [More](docs/reference.md#join-groups).
210
+ - **A child that fails, or ends its turn without reporting, is reported** to its parent by the
211
+ plugin, with the error or its last reply, waking it if idle; one that waits on a request, its own
212
+ children, a scheduled message or a webhook is not, nor is one that answers what you typed in its
213
+ session. [More](docs/reference.md#a-child-that-fails).
214
+ - **Permission requests and questions reach you.** A child's request or question is passed to the
215
+ session at the top of its tree, any number of levels down; that session asks you and passes your
216
+ answer back, or links its own question to the child's so your pick goes straight to it. Whichever
217
+ answer comes first counts, and the other is withdrawn.
294
218
  [More](docs/reference.md#a-child-that-asks-for-permission).
295
- - **A child that asks a question** with OpenCode's question tool has it shown in the top session.
296
- When that session asks you the same question, your answer goes to the child's waiting call as if
297
- you had answered there. Whichever answer reaches the child first counts, in its session, in the
298
- parent's or by `courier_answer`, and the other question is withdrawn; a question stays answerable
299
- across an interrupted turn or a server restart.
300
- [More](docs/reference.md#a-child-that-asks-a-question).
301
- - **A child that shows a form** the plugin cannot pass on, such as OpenCode's web search asking
302
- for a provider, has the top session told, marked `asks="form"`, with the form's choices: only you
303
- can answer it, in the child's session. [More](docs/reference.md#a-child-that-shows-a-form).
304
- - **You can see and stop a whole subtree.** `courier_tree` lists every session under yours with its
305
- depth and report state; `courier_stop` interrupts one of them and everything under it, deepest
306
- first, and cancels their scheduled messages. A stop is neither a failure nor a silent end, and
307
- only a session above the target may order it. [More](docs/reference.md#seeing-and-stopping-a-subtree).
308
- - **The plugin remembers.** Each parent's children (`courier_children`) and whether each owes a
309
- report, held group reports, pending `courier_later` messages and open questions survive a compaction or a restart; entries are dropped after 14 days.
310
- [Roster](docs/reference.md#roster), [Scheduled messages](docs/reference.md#scheduled-messages).
311
- - **One OpenCode server per data directory, preferably.** A second server on the same one, such as
312
- `opencode serve` next to `opencode service`, shares the plugin's storage: one of the two delivers
313
- `courier_later` messages and released groups, but a child's permission request can only be answered from the server
314
- that runs the child. [More](docs/reference.md#two-servers-on-one-data-directory).
315
- - **Worktrees are yours to remove.** An isolated child's worktree is kept until `courier_cleanup`,
316
- which refuses to drop uncommitted changes or unbranched commits unless told to.
317
- [More](docs/reference.md#worktree-cleanup).
219
+ - **Some forms only you can answer**, such as OpenCode's web search asking for a provider: the top
220
+ session is told, and you answer in the child's session. [More](docs/reference.md#a-child-that-shows-a-form).
221
+ - **You can see and stop a whole subtree** with `courier_tree` and `courier_stop`, or in the
222
+ [terminal](#in-the-terminal). [More](docs/reference.md#seeing-and-stopping-a-subtree).
223
+
224
+ Children, held reports, scheduled messages and open questions survive a compaction or a restart;
225
+ entries are dropped after 14 days ([roster](docs/reference.md#roster)).
318
226
 
319
227
  ## Session trees
320
228
 
321
- A spawned session may start sessions of its own, so the sessions form a tree, and three options
322
- bound it. Set them in the plugin's `options`, in the global config (`~/.config/opencode/opencode.json`):
229
+ A spawned session may start sessions of its own, so the sessions form a tree, bounded by three
230
+ options in the plugin's `options`, in the global config:
323
231
 
324
232
  ```jsonc
325
233
  {
@@ -335,19 +243,64 @@ bound it. Set them in the plugin's `options`, in the global config (`~/.config/o
335
243
 
336
244
  `courier_spawn` refuses a spawn past a limit, naming it and saying what to do instead: do the work
337
245
  itself, or wait for a child to report. A session counts while it is live: its turn runs, it owes
338
- its parent a report (its task or a message reached it after it last reported, failed or was interrupted), or a
339
- session below it is live. One that has reported, with nothing live below it, does not. Each
340
- spawned session's model is also told its role on every request: `sub-orchestrator`, which may split
341
- its task, or `leaf`, at `maxDepth`, which does not see `courier_spawn` at all. Your own session is
342
- told, in one line, to load the `courier-orchestrate` skill before a task that touches several files
343
- or areas, decide whether to split it, and say which in one line of its reply (`split: no, because …;
344
- considered: <the parts you weighed>` or `split: 3 parts: …`); once it has started a session, that
345
- line becomes a `root orchestrator` part with the limits. The details: [the reference](docs/reference.md#session-trees-and-their-limits).
246
+ its parent a report (its task or a message reached it after it last reported, failed or was
247
+ interrupted), or a session below it is live. One that has reported, with nothing live below it,
248
+ does not. Each spawned session's brief says when to split its task and when to do it itself
249
+ ([more](docs/reference.md#session-trees-and-their-limits)).
250
+
251
+ ## In the terminal
252
+
253
+ The package ships a part for OpenCode's terminal UI, which the TUI loads from the server's plugin
254
+ list; the server half behaves the same whether or not a TUI is attached. In a session that started
255
+ sessions, the prompt's footer shows what the tree under it is doing: how many run, wait on you or
256
+ are idle, and for each [join group](docs/reference.md#join-groups) how many of its members have
257
+ reported:
258
+
259
+ ```
260
+ courier 2 running · 1 waiting on you · group pair 1/2
261
+ ```
262
+
263
+ `/courier tree` opens a panel beside the session with the tree under it, a row per session indented
264
+ by depth, with what it does, its report and its group's standing; a session that started others gets
265
+ a bar of the parts reported, a leaf how long it ran, its last tool call and its cost. `↑`/`↓` select,
266
+ `enter` opens the session, `s` stops it and everything under it after you confirm, as `courier_stop`
267
+ would, and `q` closes the panel; `/courier stop <session>` stops from the prompt.
268
+
269
+ `ctrl+x k` (the leader key, then `k`), `/asks`, the palette's "Courier: what waits on you" or a click
270
+ on "waiting on you" opens the asks inbox: every permission request, question and form waiting
271
+ anywhere in the tree, oldest first, with where it comes from, answered in a dialog with no model in
272
+ between; the session that started the tree hears once that you settled it. The TUI plays OpenCode's
273
+ sounds for a child's report, a group's release and a new ask ([more](docs/reference.md#in-the-terminal)).
274
+
275
+ ## Native child sessions
276
+
277
+ Off by default. With `"nativeChildren": true` in the plugin's `options`, a child started without
278
+ `isolate` is also created with OpenCode's own `parentID`, so OpenCode treats it as the parent's
279
+ subagent: the TUI's view of the top session lists and answers the permission requests and forms of
280
+ every session under it, and the paired web, desktop or mobile app's request dock answers a
281
+ grandchild's ask from the top session, on a phone too. Isolated children are created as before, and
282
+ the courier's own tree, limits and relays work as without the option. What changes
283
+ ([more](docs/reference.md#native-child-sessions)):
284
+
285
+ - **Deleting a session deletes its native children**, as OpenCode does with subagents.
286
+ - **A child does not take its parent's session rules**: courier passes it an empty ruleset, so it
287
+ may do what it could without the option.
288
+ - **Providers see a subagent**: the child's model requests carry its parent's id for cache routing
289
+ and two headers naming the parent; **on GitHub Copilot**, which meters such requests differently
290
+ from a person's, courier was not tested.
291
+ - **A child's asks show in the top session's view**, not in its own, and it leaves the TUI's session
292
+ lists. Its permission requests and questions are answered once, in OpenCode's own prompt there; the
293
+ top session's model is only told. `opencode run` and the TUI's auto-accept mode answer them as their
294
+ own subagents'.
295
+ - **Turn it on only with OpenCode's TUI or the paired app**, which show those prompts; with a bare
296
+ API client nobody prompts you, and only `courier_answer` is left.
346
297
 
347
298
  ## Webhooks
348
299
 
349
- The receiver is off unless the plugin has a `webhook` option. Put it in the **global** config
350
- (`~/.config/opencode/opencode.json`), since there is one receiver per OpenCode server:
300
+ GitHub reviews, comments, pull requests and completed CI runs, or anything signed posted to
301
+ `/hook/<name>`, can wake a session. The receiver is off unless the plugin has a `webhook` option, in
302
+ the **global** config, since there is one receiver per OpenCode server (`"webhook": true` takes
303
+ every default):
351
304
 
352
305
  ```jsonc
353
306
  {
@@ -360,78 +313,26 @@ The receiver is off unless the plugin has a `webhook` option. Put it in the **gl
360
313
  }
361
314
  ```
362
315
 
363
- From a local clone, `package` is the path to its `dist` directory instead. `"webhook": true` takes
364
- every default. If the option is given more than once, for example in a project's config as well, the
365
- first location to load wins, and the others log that their settings are ignored.
366
-
367
- | Option | Default | |
368
- |---|---|---|
369
- | `port` | `4097` | Port to listen on. |
370
- | `host` | `127.0.0.1` | Address to bind. Only this machine can reach the default. |
371
- | `secretFile` | | File holding the shared secret (`~` is expanded). |
372
- | `secretEnv` | `COURIER_WEBHOOK_SECRET` | Environment variable holding it, when there is no `secretFile`. |
373
- | `maxBytes` | `1048576` | Largest body accepted. |
374
-
375
- The secret is never read from `opencode.json` itself (a `secret` key is refused), so the config can
376
- be committed. Make one with `openssl rand -hex 32 > ~/.config/opencode/courier-webhook-secret` and
377
- `chmod 600` it. A file is the safer choice with `opencode service start`, whose environment may not
378
- be your shell's. Without a usable secret the receiver does not start, and the server log says why.
379
-
380
- On GitHub, add a webhook to the repository (Settings → Webhooks) with content type
381
- `application/json`, the same secret, and the events you want (pull request reviews, review comments,
382
- issue comments, pull requests, check suites or workflow runs). GitHub must reach the receiver, and
383
- by default it only listens on `127.0.0.1`: forward a public URL to it with a tunnel you trust
384
- (`cloudflared tunnel --url http://127.0.0.1:4097`, `ngrok http 4097`, or `smee --url
385
- https://smee.io/<channel> --target http://127.0.0.1:4097/github`, which needs no inbound port at
386
- all) and use `<public URL>/github` as the payload URL. Whatever you expose, only signed deliveries
387
- are acted on.
388
-
389
- The receiver starts when OpenCode loads the plugin, which after a server start happens the first
390
- time a project is used. Until then deliveries fail; GitHub does not retry them on its own, but lists
391
- them under Recent Deliveries with a Redeliver button.
392
-
393
- Once it runs, `courier_subscribe` ties a session to a topic and every matching delivery arrives as a
394
- message, queued behind any running turn and waking the session if idle:
395
-
396
- - `POST /github` takes GitHub webhook deliveries: reviews, comments, pull requests and issues being
397
- opened, closed or merged, and completed CI runs reach the sessions subscribed to `owner/repo#N`
398
- and `owner/repo`; other events with a repository reach `owner/repo`.
399
- - `POST /hook/<name>` takes anything else, for sessions subscribed to `<name>`: a JSON body's
400
- `text`, `summary` or `message` field, otherwise the body itself.
401
-
402
- Every delivery must be signed (`X-Hub-Signature-256`, HMAC-SHA256 under the shared secret), replays
403
- are refused, and what a session sees is a short summary marked as outside text. Which events wake
404
- whom, the signing scheme for `/hook/<name>`, limits and status codes: [the
316
+ 1. Make the secret with `openssl rand -hex 32 > ~/.config/opencode/courier-webhook-secret` and
317
+ `chmod 600` it. It is never read from `opencode.json` (a `secret` key is refused), so the config
318
+ can be committed; without a usable secret the receiver does not start, and the server log says why.
319
+ 2. On GitHub, add a webhook to the repository (Settings → Webhooks) with content type
320
+ `application/json`, the same secret, the events you want, and `<public URL>/github` as the payload
321
+ URL, forwarded to `127.0.0.1:4097` by a tunnel you trust (`smee`, `cloudflared`, `ngrok`). Only
322
+ signed deliveries are acted on.
323
+ 3. `courier_subscribe` a session to `owner/repo`, `owner/repo#N` or `<name>`: each matching delivery
324
+ arrives as a short summary marked as outside text, queued behind any running turn and waking the
325
+ session if idle.
326
+
327
+ The options, which events wake whom, the tunnels, the signing scheme and status codes: [the
405
328
  reference](docs/reference.md#webhooks).
406
329
 
407
330
  ## Development
408
331
 
409
- ```bash
410
- bun install
411
- bun test # unit tests, with a fake plugin context
412
- npm run typecheck
413
- npm run build # emits dist/
414
- OPENCODE_BIN=$(which opencode) npm run test:e2e # live test, about two minutes, no API key
415
- OPENCODE_BIN=$(which opencode) e2e/real-model.sh # smoke test with a real model, not in CI
416
- ```
417
-
418
- The live test (`e2e/run.sh`) starts a real OpenCode V2 server in a throwaway project and home
419
- directory, with this plugin loaded and a scripted OpenAI-compatible stand-in model, and walks every
420
- tool through the behaviour above. It needs node, npm, bun, git, curl, jq and openssl. New behaviour
421
- gets a scenario there.
422
-
423
- The smoke test (`e2e/real-model.sh`) runs the same server with a real model (by default a free one
424
- on [OpenCode Zen](https://opencode.ai/zen), no key needed) and checks that the parent spawns instead
425
- of doing the work, ends its turn instead of polling, and is woken by each report;
426
- `COURIER_SCENARIO=permission` and `COURIER_SCENARIO=question` exercise the two relays, and
427
- `COURIER_SCENARIO=recursive` the [recursive orchestration](#recursive-orchestration) above. Which
428
- models pass and what was tuned for them: [docs/real-model.md](docs/real-model.md).
429
-
430
- CI runs both suites on every push to `main` and every pull request, with the OpenCode CLI at the
431
- pinned version, and the live suite once more on the newest OpenCode release, where a failure warns
432
- rather than breaks the build. SonarCloud scans `src/` on `main` and on pull requests. Releases start
433
- from GitHub and end with a maintainer's 2FA approval on npm: [docs/releasing.md](docs/releasing.md).
434
- What the plugin API does that the plugin had to work around: [docs/plugin-api-notes.md](docs/plugin-api-notes.md).
332
+ Commands, the suites and CI: [docs/maintainers/development.md](docs/maintainers/development.md). How
333
+ the code is laid out: [architecture.md](docs/maintainers/architecture.md); what the plugin API does
334
+ that courier works around: [plugin-api-notes.md](docs/maintainers/plugin-api-notes.md); releases:
335
+ [releasing.md](docs/maintainers/releasing.md).
435
336
 
436
337
  ## Contributing
437
338