opencode-courier 0.7.0 → 0.7.2
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 +12 -6
- package/dist/person/events.d.ts +2 -2
- package/dist/person/events.js +4 -3
- package/dist/skill.js +11 -6
- package/dist/tree/text.js +1 -1
- package/dist/tree/wake.d.ts +3 -2
- package/dist/tree/wake.js +5 -2
- package/dist/tui.js +2 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ The edge cases on the live terminal UI, narrated ([what it shows](#demos)).
|
|
|
21
21
|
## Quickstart
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm install -g @opencode/cli@2.0.
|
|
24
|
+
npm install -g @opencode/cli@2.0.31 # OpenCode V2, at the version courier is tested on
|
|
25
25
|
opencode plugin add opencode-courier # by name, so OpenCode offers new releases
|
|
26
26
|
opencode # in any folder
|
|
27
27
|
```
|
|
@@ -43,7 +43,7 @@ sometimes stall on a request; if nothing moves for a few minutes, send the promp
|
|
|
43
43
|
|
|
44
44
|
Courier is built on OpenCode V2's plugin API (`@opencode/plugin`), not the V1 one
|
|
45
45
|
(`@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.
|
|
46
|
+
against it end to end; the current one is tested on **OpenCode 2.0.31**. CI also runs the live
|
|
47
47
|
suite on the newest OpenCode release, on `main` and nightly, so a host release that breaks the
|
|
48
48
|
plugin shows up there first. Older releases and their versions: [Supported OpenCode version](#supported-opencode-version).
|
|
49
49
|
|
|
@@ -111,8 +111,11 @@ pinned to its version. OpenCode never checks it:
|
|
|
111
111
|
the plugin installs and loads on any V2 host, and on a version other than the pinned one writes one
|
|
112
112
|
line to the server log naming both ([more](docs/reference.md#the-opencode-version)).
|
|
113
113
|
|
|
114
|
+
<!-- compat-table:start: generated by script/compat-table.ts from the release tags -->
|
|
114
115
|
| opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
|
|
115
116
|
|---|---|
|
|
117
|
+
| 0.7.2 | 2.0.31 |
|
|
118
|
+
| 0.7.1 | 2.0.30 |
|
|
116
119
|
| 0.5.0, 0.6.0, 0.7.0 | 2.0.29 |
|
|
117
120
|
| 0.4.0, 0.4.1 | 2.0.28 |
|
|
118
121
|
| 0.2.3, 0.3.0 | 2.0.26 |
|
|
@@ -120,14 +123,15 @@ line to the server log naming both ([more](docs/reference.md#the-opencode-versio
|
|
|
120
123
|
| 0.2.1 | 2.0.23 |
|
|
121
124
|
| 0.2.0 | 2.0.22 |
|
|
122
125
|
| 0.1.6 | 0.0.0-beta-19271 (the beta line: `opencode2` from `@opencode-ai/cli`, and `@opencode-ai/plugin`) |
|
|
126
|
+
<!-- compat-table:end -->
|
|
123
127
|
|
|
124
128
|
The [terminal part](#in-the-terminal) relies on the host to provide its UI modules and is tested on
|
|
125
|
-
OpenCode 2.0.
|
|
129
|
+
OpenCode 2.0.31 only; the server part works as before on older V2 hosts from 2.0.4 on.
|
|
126
130
|
|
|
127
131
|
Check yours with `opencode --version`, and install the matching CLI with:
|
|
128
132
|
|
|
129
133
|
```bash
|
|
130
|
-
npm install -g @opencode/cli@2.0.
|
|
134
|
+
npm install -g @opencode/cli@2.0.31
|
|
131
135
|
```
|
|
132
136
|
|
|
133
137
|
### The plugin
|
|
@@ -210,7 +214,7 @@ Each tool's parameters: [the reference](docs/reference.md#the-tools).
|
|
|
210
214
|
children, a scheduled message or a webhook is not, nor is one that answers what you typed in its
|
|
211
215
|
session. [More](docs/reference.md#a-child-that-fails).
|
|
212
216
|
- **A message that fails to wake its session is sent again.** A report, notice or scheduled message
|
|
213
|
-
that waits in an idle session's inbox for 30 seconds without a turn starting for it is sent once
|
|
217
|
+
(but not a note that a request was answered elsewhere) that waits in an idle session's inbox for 30 seconds without a turn starting for it is sent once
|
|
214
218
|
more under the same inbox id, which OpenCode takes for the same message, not a second one. If the
|
|
215
219
|
session still does not take it, the footer and the tree panel show it as `stalled`, for you to
|
|
216
220
|
prompt it. [More](docs/reference.md#a-missed-wake).
|
|
@@ -220,7 +224,9 @@ Each tool's parameters: [the reference](docs/reference.md#the-tools).
|
|
|
220
224
|
and under its rules, before anything is passed on: its model cannot approve, only propose. For a
|
|
221
225
|
question, it asks you and passes your answer back, or links its own question to the child's so your
|
|
222
226
|
pick goes straight to it. Whichever answer comes first counts, and the other is withdrawn.
|
|
223
|
-
|
|
227
|
+
A request starts a turn in the top session if it is idle; if you answer it somewhere else, the
|
|
228
|
+
note that it is settled waits for that session's next turn, usually the child's report or its
|
|
229
|
+
silence, instead of starting one of its own. [More](docs/reference.md#a-child-that-asks-for-permission).
|
|
224
230
|
- **Some forms only you can answer**, such as OpenCode's web search asking for a provider: the top
|
|
225
231
|
session is told, and you answer in the child's session. [More](docs/reference.md#a-child-that-shows-a-form).
|
|
226
232
|
- **You can see and stop a whole subtree** with `courier_tree` and `courier_stop`, or in the
|
package/dist/person/events.d.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { type PersonPorts, type PersonState } from "./ports.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function reportAsked(ports: PersonPorts, state: PersonState, event: PermissionAsked): Promise<string[]>;
|
|
9
9
|
/**
|
|
10
|
-
* Tells the session told about a request that it was answered some other way, so it does not pass on
|
|
11
|
-
*
|
|
10
|
+
* Tells the session told about a request that it was answered some other way, so it does not pass on a stale
|
|
11
|
+
* question; requests answered through courier_answer are skipped. Sent with `resume: false`, so it starts no turn.
|
|
12
12
|
*/
|
|
13
13
|
export declare function reportReplied(ports: PersonPorts, state: PersonState, event: PermissionReplied): Promise<string[]>;
|
|
14
14
|
/**
|
package/dist/person/events.js
CHANGED
|
@@ -36,8 +36,8 @@ export async function reportAsked(ports, state, event) {
|
|
|
36
36
|
return [topOf(chain)];
|
|
37
37
|
}
|
|
38
38
|
/**
|
|
39
|
-
* Tells the session told about a request that it was answered some other way, so it does not pass on
|
|
40
|
-
*
|
|
39
|
+
* Tells the session told about a request that it was answered some other way, so it does not pass on a stale
|
|
40
|
+
* question; requests answered through courier_answer are skipped. Sent with `resume: false`, so it starts no turn.
|
|
41
41
|
*/
|
|
42
42
|
export async function reportReplied(ports, state, event) {
|
|
43
43
|
if (!claim(state.seen, event.id))
|
|
@@ -54,8 +54,9 @@ export async function reportReplied(ports, state, event) {
|
|
|
54
54
|
sessionID: topOf(chain),
|
|
55
55
|
text: envelope(sessionID, permissionSettledNotice(chain[0].title, requestID, reply), { answered: reply, request: requestID }),
|
|
56
56
|
description: `Session ${sessionID} no longer asks for permission`,
|
|
57
|
-
metadata: { source: "courier", from: sessionID, answered: reply, requestID },
|
|
57
|
+
metadata: { source: "courier", from: sessionID, answered: reply, requestID, resume: false },
|
|
58
58
|
delivery: "steer",
|
|
59
|
+
resume: false,
|
|
59
60
|
});
|
|
60
61
|
return [topOf(chain)];
|
|
61
62
|
}
|
package/dist/skill.js
CHANGED
|
@@ -11,17 +11,22 @@ export const SKILL_CONTENT = [
|
|
|
11
11
|
"opencode-courier role line in your system prompt, names your depth in the session tree and its limits.",
|
|
12
12
|
"",
|
|
13
13
|
"1. Decide: split or do. Split when the task has 2 or more independent, substantial parts touching separate files or " +
|
|
14
|
-
"areas; do it yourself when it is small, sequential or tightly coupled. Never start exactly one session
|
|
15
|
-
"
|
|
14
|
+
"areas; do it yourself when it is small, sequential or tightly coupled. Never start exactly one session for the whole " +
|
|
15
|
+
"task: it only adds a hop. What you read and write yourself stays in your context for every later turn, so keep the " +
|
|
16
|
+
"deciding, checking and integrating, and hand off the long reading and writing. An implementation task with one tightly " +
|
|
17
|
+
"coupled core usually still has independent parts: finding out what the change needs (a session with isolate: false " +
|
|
18
|
+
"reads the code, or a library's or a host's source, tries things and reports what it found), its e2e scenario, its " +
|
|
16
19
|
"documentation, sometimes a second test file, and a review of the core by a session with fresh context, which reads the " +
|
|
17
20
|
"committed diff against the task and the repository's rules and reports its findings (isolate: false, it edits nothing). " +
|
|
18
|
-
"Weigh each of them
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
+
"Weigh each of them. When the other parts depend on names and texts the core fixes, split in stages: the core first, " +
|
|
22
|
+
"yourself when it is small or by one session when it is big, merged and committed before the rest start; then, if 2 or " +
|
|
23
|
+
"more parts remain, one session per remaining part, in one group. A part that is itself big and divisible is split again " +
|
|
24
|
+
"by the session that gets it; at the deepest level a session does its part itself. " +
|
|
21
25
|
`Say which in one line of your reply, before you start: ${SPLIT_LINE}, so that a reasoned decision to do it ` +
|
|
22
26
|
"yourself can be told from one never made.",
|
|
23
27
|
"2. Plan the parts so that no two edit the same files. Write each task so the session can do it without you: what to " +
|
|
24
|
-
"produce and where, how to check it, what to report. Say if it may split its part further
|
|
28
|
+
"produce and where, how to check it, what to report. Say if it may split its part further, and to run its checks " +
|
|
29
|
+
"through the project's scripts (step 6): each permission request of a session below you wakes you and waits on the person.",
|
|
25
30
|
"3. Start them: one courier_spawn per part, all with the same group name, so you are woken once with every report; " +
|
|
26
31
|
"isolate: true for each part that edits files in parallel with another (commit your own work first if you are isolated " +
|
|
27
32
|
"yourself). For long work, courier_later a check-in for yourself, and courier_cancel it when the reports arrive.",
|
package/dist/tree/text.js
CHANGED
|
@@ -47,7 +47,7 @@ function splitRule(parentID, depth, limits) {
|
|
|
47
47
|
`You are at depth ${depth} of a session tree that goes at most ${limits.maxDepth} deep. Split your task or do it yourself:`,
|
|
48
48
|
"- Split it when it has 2 or more independent parts, each substantial and touching separate files or areas: start one " +
|
|
49
49
|
`session per part with courier_spawn, at most ${limits.maxChildren} of yours running at once and ${limits.maxTotal} in the whole tree.`,
|
|
50
|
-
"- Do it yourself when it is small, sequential or tightly coupled. Never start exactly one session.",
|
|
50
|
+
"- Do it yourself when it is small, sequential or tightly coupled. Never start exactly one session for the whole task.",
|
|
51
51
|
`- Say which in one line of your reply: ${SPLIT_LINE}.`,
|
|
52
52
|
"- If you split, you orchestrate: end your turn while they work, check and integrate each part yourself when it " +
|
|
53
53
|
`reports (never hand that checking to another session), then send ${parentID} one combined report.`,
|
package/dist/tree/wake.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface Enqueued {
|
|
|
14
14
|
readonly description?: unknown;
|
|
15
15
|
readonly metadata?: {
|
|
16
16
|
readonly source?: unknown;
|
|
17
|
+
readonly resume?: unknown;
|
|
17
18
|
};
|
|
18
19
|
};
|
|
19
20
|
};
|
|
@@ -34,8 +35,8 @@ export interface Execution {
|
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
37
|
-
* Watches a courier message enqueued for a session until it is delivered, noting whether the session runs
|
|
38
|
-
*
|
|
38
|
+
* Watches a courier message enqueued for a session until it is delivered, noting whether the session runs.
|
|
39
|
+
* Returns whether it is watched, which one sent with `resume: false` is not.
|
|
39
40
|
*/
|
|
40
41
|
export declare function watchWake(ports: WatchPorts, event: Enqueued): Promise<boolean>;
|
|
41
42
|
/** Stops watching a message once it is delivered or cancelled. */
|
package/dist/tree/wake.js
CHANGED
|
@@ -4,14 +4,17 @@ export const WAKE_MS = 30_000;
|
|
|
4
4
|
/** Whether a session's stored messages end with the marker of an ended turn; one with none yet is taken to run. */
|
|
5
5
|
const endsIdle = (messages) => messages.at(-1)?.type === "idle";
|
|
6
6
|
/**
|
|
7
|
-
* Watches a courier message enqueued for a session until it is delivered, noting whether the session runs
|
|
8
|
-
*
|
|
7
|
+
* Watches a courier message enqueued for a session until it is delivered, noting whether the session runs.
|
|
8
|
+
* Returns whether it is watched, which one sent with `resume: false` is not.
|
|
9
9
|
*/
|
|
10
10
|
export async function watchWake(ports, event) {
|
|
11
11
|
const { sessionID, inboxID, item } = event.data;
|
|
12
12
|
const payload = item.payload;
|
|
13
13
|
if (item.type !== "synthetic" || payload?.metadata?.source !== "courier" || typeof payload.text !== "string")
|
|
14
14
|
return false;
|
|
15
|
+
// Sent without a wake: it waits for the next turn, which nothing owes it.
|
|
16
|
+
if (payload.metadata.resume === false)
|
|
17
|
+
return false;
|
|
15
18
|
const at = event.created ?? ports.now();
|
|
16
19
|
// Read after the enqueue: a turn it started has stored messages since, and an unknown session is taken to run.
|
|
17
20
|
const idle = await ports.session.context({ sessionID }).then(endsIdle, () => false);
|
package/dist/tui.js
CHANGED
|
@@ -1151,7 +1151,7 @@ async function confirmForget(ctx, courier, location, by, target, done) {
|
|
|
1151
1151
|
return true;
|
|
1152
1152
|
}
|
|
1153
1153
|
var FOCUS_PROMPT = "pane.focus.left";
|
|
1154
|
-
var SPLIT_WIDTH =
|
|
1154
|
+
var SPLIT_WIDTH = 108;
|
|
1155
1155
|
var oneLine = (title) => title.replace(/[\r\n]+/g, " ");
|
|
1156
1156
|
function TreePanel(props) {
|
|
1157
1157
|
const {
|
|
@@ -1197,7 +1197,7 @@ function TreePanel(props) {
|
|
|
1197
1197
|
presentation,
|
|
1198
1198
|
focused
|
|
1199
1199
|
} = input;
|
|
1200
|
-
view.note(ctx.renderer.width
|
|
1200
|
+
view.note(ctx.renderer.width >= SPLIT_WIDTH ? {
|
|
1201
1201
|
presentation,
|
|
1202
1202
|
focused
|
|
1203
1203
|
} : {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-courier",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Multi-session orchestration for OpenCode V2: spawn child sessions, be woken by them. No polling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ivo Pogace <71701666+ivopogace@users.noreply.github.com>",
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
"test:e2e": "e2e/run.sh"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@opencode/plugin": "2.0.
|
|
59
|
+
"@opencode/plugin": "2.0.31",
|
|
60
60
|
"effect": "4.0.1"
|
|
61
61
|
},
|
|
62
62
|
"devDependencies": {
|
|
63
|
-
"@opencode/plugin": "2.0.
|
|
64
|
-
"@opencode/theme": "2.0.
|
|
63
|
+
"@opencode/plugin": "2.0.31",
|
|
64
|
+
"@opencode/theme": "2.0.31",
|
|
65
65
|
"@opentui/core": "0.5.17",
|
|
66
66
|
"@opentui/solid": "0.5.17",
|
|
67
67
|
"@types/bun": "1.3.14",
|