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.
- package/README.md +175 -274
- package/dist/index.js +47 -73
- package/dist/kernel/protocol.d.ts +20 -0
- package/dist/kernel/protocol.js +36 -0
- package/dist/kernel/tool.d.ts +5 -0
- package/dist/kernel/tool.js +5 -0
- package/dist/person/events.d.ts +23 -0
- package/dist/person/events.js +113 -0
- package/dist/person/form.d.ts +26 -0
- package/dist/person/form.js +64 -0
- package/dist/person/index.d.ts +17 -0
- package/dist/person/index.js +5 -0
- package/dist/{relay.d.ts → person/permission.d.ts} +27 -21
- package/dist/{relay.js → person/permission.js} +9 -5
- package/dist/person/ports.d.ts +58 -0
- package/dist/person/ports.js +2 -0
- package/dist/{question → person/question}/answer.d.ts +33 -4
- package/dist/{question → person/question}/answer.js +43 -8
- package/dist/person/question/index.d.ts +7 -0
- package/dist/{question → person/question}/index.js +3 -3
- package/dist/{question → person/question}/lifecycle.d.ts +1 -1
- package/dist/{question → person/question}/pure.d.ts +1 -2
- package/dist/{question → person/question}/pure.js +1 -1
- package/dist/{question → person/question}/relay.d.ts +1 -1
- package/dist/{question → person/question}/relay.js +14 -8
- package/dist/{question → person/question}/shared.d.ts +13 -3
- package/dist/person/question/shared.js +52 -0
- package/dist/person/question/types.d.ts +121 -0
- package/dist/person/question/types.js +1 -0
- package/dist/person/rpc-handlers.d.ts +160 -0
- package/dist/person/rpc-handlers.js +62 -0
- package/dist/person/rpc.d.ts +227 -0
- package/dist/person/rpc.js +70 -0
- package/dist/person/text.d.ts +68 -0
- package/dist/person/text.js +262 -0
- package/dist/person/tools.d.ts +20 -0
- package/dist/person/tools.js +55 -0
- package/dist/person/tui.d.ts +46 -0
- package/dist/{bounded.d.ts → platform/bounded.d.ts} +4 -0
- package/dist/{bounded.js → platform/bounded.js} +4 -0
- package/dist/platform/events.d.ts +34 -0
- package/dist/platform/events.js +121 -0
- package/dist/platform/hub.d.ts +195 -0
- package/dist/{hub.js → platform/hub.js} +27 -71
- package/dist/{version.js → platform/version.js} +1 -1
- package/dist/rpc.d.ts +2 -0
- package/dist/rpc.js +3 -0
- package/dist/scheduling/index.d.ts +6 -0
- package/dist/scheduling/index.js +5 -0
- package/dist/{later.d.ts → scheduling/later.d.ts} +7 -3
- package/dist/{later.js → scheduling/later.js} +12 -4
- package/dist/scheduling/text.d.ts +6 -0
- package/dist/scheduling/text.js +10 -0
- package/dist/scheduling/tools.d.ts +14 -0
- package/dist/scheduling/tools.js +48 -0
- package/dist/skill.d.ts +4 -0
- package/dist/skill.js +54 -1
- package/dist/{cleanup.d.ts → tree/cleanup.d.ts} +1 -27
- package/dist/{cleanup.js → tree/cleanup.js} +9 -16
- package/dist/tree/context.d.ts +15 -0
- package/dist/tree/context.js +34 -0
- package/dist/tree/domain.d.ts +101 -0
- package/dist/tree/domain.js +68 -0
- package/dist/{watch.d.ts → tree/events.d.ts} +10 -37
- package/dist/tree/events.js +204 -0
- package/dist/tree/forget.d.ts +24 -0
- package/dist/tree/forget.js +59 -0
- package/dist/tree/group.d.ts +82 -0
- package/dist/tree/group.js +203 -0
- package/dist/tree/index.d.ts +10 -0
- package/dist/tree/index.js +11 -0
- package/dist/tree/limits.d.ts +10 -0
- package/dist/{limits.js → tree/limits.js} +6 -55
- package/dist/tree/panel.d.ts +13 -0
- package/dist/tree/panel.js +75 -0
- package/dist/tree/ports.d.ts +276 -0
- package/dist/tree/ports.js +1 -0
- package/dist/tree/report.d.ts +23 -0
- package/dist/tree/report.js +33 -0
- package/dist/tree/roster.d.ts +27 -0
- package/dist/tree/roster.js +62 -0
- package/dist/tree/rpc-handlers.d.ts +201 -0
- package/dist/tree/rpc-handlers.js +20 -0
- package/dist/tree/rpc.d.ts +344 -0
- package/dist/tree/rpc.js +83 -0
- package/dist/tree/send.d.ts +14 -0
- package/dist/tree/send.js +99 -0
- package/dist/tree/spawn.d.ts +21 -0
- package/dist/tree/spawn.js +173 -0
- package/dist/tree/status.d.ts +128 -0
- package/dist/tree/status.js +62 -0
- package/dist/{stop.d.ts → tree/stop.d.ts} +1 -9
- package/dist/{stop.js → tree/stop.js} +9 -13
- package/dist/tree/store.d.ts +24 -0
- package/dist/tree/store.js +264 -0
- package/dist/{tree.d.ts → tree/subtree.d.ts} +5 -6
- package/dist/{tree.js → tree/subtree.js} +7 -5
- package/dist/tree/summary.d.ts +7 -0
- package/dist/tree/summary.js +35 -0
- package/dist/tree/text.d.ts +77 -0
- package/dist/tree/text.js +287 -0
- package/dist/tree/tools.d.ts +54 -0
- package/dist/{tools.js → tree/tools.js} +12 -142
- package/dist/tree/tui.d.ts +53 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.js +1219 -0
- package/dist/webhook/index.d.ts +5 -0
- package/dist/webhook/index.js +3 -0
- package/dist/{webhook.d.ts → webhook/receiver.d.ts} +17 -23
- package/dist/{webhook.js → webhook/receiver.js} +58 -50
- package/dist/webhook/subscriptions.d.ts +29 -0
- package/dist/webhook/subscriptions.js +52 -0
- package/dist/webhook/text.d.ts +23 -0
- package/dist/webhook/text.js +82 -0
- package/dist/webhook/tools.d.ts +19 -0
- package/dist/webhook/tools.js +46 -0
- package/package.json +18 -7
- package/dist/courier.d.ts +0 -200
- package/dist/courier.js +0 -315
- package/dist/group.d.ts +0 -86
- package/dist/group.js +0 -247
- package/dist/hub.d.ts +0 -300
- package/dist/limits.d.ts +0 -65
- package/dist/notices.d.ts +0 -356
- package/dist/notices.js +0 -678
- package/dist/question/index.d.ts +0 -6
- package/dist/question/shared.js +0 -24
- package/dist/report.d.ts +0 -48
- package/dist/report.js +0 -88
- package/dist/roster.d.ts +0 -93
- package/dist/roster.js +0 -206
- package/dist/tools.d.ts +0 -94
- package/dist/watch.js +0 -474
- /package/dist/{question → person/question}/lifecycle.js +0 -0
- /package/dist/{question → person/question}/runtime.d.ts +0 -0
- /package/dist/{question → person/question}/runtime.js +0 -0
- /package/dist/{json.d.ts → platform/json.d.ts} +0 -0
- /package/dist/{json.js → platform/json.js} +0 -0
- /package/dist/{storage.d.ts → platform/storage.d.ts} +0 -0
- /package/dist/{storage.js → platform/storage.js} +0 -0
- /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.
|
|
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.
|
|
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
|
|
59
|
-
`sessionID` back
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
77
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|

|
|
114
93
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
124
|
-
part again,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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.
|
|
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.
|
|
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`)
|
|
191
|
-
|
|
192
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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:
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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` |
|
|
247
|
-
| `courier_send` |
|
|
248
|
-
| `courier_status` | One look at a session: outcome, idle time, last reply and
|
|
249
|
-
| `courier_children` | Lists the sessions this one (or a given
|
|
250
|
-
| `courier_tree` | Lists the whole tree under
|
|
251
|
-
| `courier_stop` | Interrupts a session you
|
|
252
|
-
| `courier_cleanup` | Removes
|
|
253
|
-
| `courier_answer` | Passes your answer to a permission request or a question that a session
|
|
254
|
-
| `courier_later` | Schedules a message for a session (this one by default) in `delayMinutes` or `at`
|
|
255
|
-
| `courier_cancel` | Drops a message scheduled with `courier_later
|
|
256
|
-
| `courier_subscribe` | Subscribes a session
|
|
257
|
-
| `courier_unsubscribe` | Drops one topic, or all of a session's
|
|
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`,
|
|
271
|
-
in the
|
|
272
|
-
|
|
273
|
-
- **A join group wakes you once.** Start a batch of sessions with the same `group` name, and
|
|
274
|
-
|
|
275
|
-
in one message
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
- **
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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,
|
|
322
|
-
|
|
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
|
|
339
|
-
session below it is live. One that has reported, with nothing live below it,
|
|
340
|
-
spawned session's
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
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
|
-
|
|
350
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|