opencode-courier 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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.32 # OpenCode V2, at the version courier is tested on
24
+ npm install -g @opencode/cli@2.1.0 # 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
  ```
@@ -42,7 +42,7 @@ sometimes stall on a request; if nothing moves for a few minutes, send the promp
42
42
 
43
43
  Courier is built on OpenCode V2's plugin API (`@opencode/plugin`), not the V1 one
44
44
  (`@opencode-ai/plugin`). Each release is pinned to one V2 version and tested against it end to end;
45
- the current one is tested on **OpenCode 2.0.32**. CI also runs the live suite on the newest OpenCode
45
+ the current one is tested on **OpenCode 2.1.0**. CI also runs the live suite on the newest OpenCode
46
46
  release, on `main` and nightly. Older releases: [every release](docs/reference.md#the-opencode-version).
47
47
 
48
48
  ## Courier or the built-in subagent tool?
@@ -108,20 +108,20 @@ version than the pinned one writes one line to the server log
108
108
  <!-- compat-table:start rows=3: generated by script/compat-table.ts from the release tags -->
109
109
  | opencode-courier | OpenCode V2 (`opencode` and `@opencode/plugin`) |
110
110
  |---|---|
111
- | 0.7.3, 0.8.0, 0.9.0 | 2.0.32 |
111
+ | 0.10.1 | 2.1.0 |
112
+ | 0.7.3, 0.8.0, 0.9.0, 0.10.0 | 2.0.32 |
112
113
  | 0.7.2 | 2.0.31 |
113
- | 0.7.1 | 2.0.30 |
114
114
  <!-- compat-table:end -->
115
115
 
116
116
  Older releases and their versions: [every release](docs/reference.md#the-opencode-version).
117
117
 
118
118
  The [terminal part](#in-the-terminal) relies on the host to provide its UI modules and is tested on
119
- OpenCode 2.0.32 only; the server part works on older V2 hosts from 2.0.4 on.
119
+ OpenCode 2.1.0 only; the server part works on older V2 hosts from 2.0.4 on.
120
120
 
121
121
  Check yours with `opencode --version`, and install the matching CLI with:
122
122
 
123
123
  ```bash
124
- npm install -g @opencode/cli@2.0.32
124
+ npm install -g @opencode/cli@2.1.0
125
125
  ```
126
126
 
127
127
  ### The plugin
@@ -11,6 +11,10 @@ export type Status = (typeof STATUSES)[number];
11
11
  export declare const isStatus: (value: unknown) => value is Status;
12
12
  /** The one line a session states its split decision in, so a transcript tells a decision from one never made. */
13
13
  export declare const SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed> or split: <n> parts: \u2026 (one phrase per part)";
14
+ /** The third form, for the root alone: it hands a heavy task whole to one session, to keep its own context. */
15
+ export declare const ONE_PART_LINE = "split: 1 part: <the task>, to keep my context";
16
+ /** `SPLIT_LINE` with the root's third form: the root's pointer offers it, a child's brief and role part never do. */
17
+ export declare const ROOT_SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed> or split: <n> parts: \u2026 (one phrase per part) or split: 1 part: <the task>, to keep my context";
14
18
  export declare const SKILL_ID = "courier-orchestrate";
15
19
  export declare function envelope(from: string, message: string, attributes?: Record<string, string>): string;
16
20
  export declare function clipText(text: string, max?: number): string;
@@ -10,6 +10,10 @@ export const STATUSES = ["done", "partial", "blocked", "failed"];
10
10
  export const isStatus = (value) => typeof value === "string" && STATUSES.includes(value);
11
11
  /** The one line a session states its split decision in, so a transcript tells a decision from one never made. */
12
12
  export const SPLIT_LINE = "split: no, because …; considered: <the parts you weighed> or split: <n> parts: … (one phrase per part)";
13
+ /** The third form, for the root alone: it hands a heavy task whole to one session, to keep its own context. */
14
+ export const ONE_PART_LINE = "split: 1 part: <the task>, to keep my context";
15
+ /** `SPLIT_LINE` with the root's third form: the root's pointer offers it, a child's brief and role part never do. */
16
+ export const ROOT_SPLIT_LINE = `${SPLIT_LINE} or ${ONE_PART_LINE}`;
13
17
  export const SKILL_ID = "courier-orchestrate";
14
18
  export function envelope(from, message, attributes = {}) {
15
19
  const extra = Object.entries(attributes)
package/dist/skill.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import type { Plugin } from "@opencode/plugin";
2
2
  /** Why a model loads the skill, listed with every session's available skills. */
3
3
  export declare const SKILL_DESCRIPTION: string;
4
- /** The skill's text; it names no limits, which the role part and the brief give for the session's own tree. */
4
+ /**
5
+ * The skill's text; it names no limits, which the role part and the brief give for the session's own tree.
6
+ * Steps 3, 4 and 6 repeat the brief's split rule on purpose: the root has no brief, so only the skill gives it.
7
+ */
5
8
  export declare const SKILL_CONTENT: string;
6
9
  /** The editor OpenCode hands a skill transform. */
7
10
  export type SkillEditor = Parameters<Parameters<Plugin.Context["skill"]["transform"]>[0]>[0];
package/dist/skill.js CHANGED
@@ -1,34 +1,43 @@
1
- import { SKILL_ID, SPLIT_LINE } from "./kernel/protocol.js";
1
+ import { ONE_PART_LINE, SKILL_ID, SPLIT_LINE } from "./kernel/protocol.js";
2
2
  // The courier-orchestrate skill: the playbook a session loads with its skill tool.
3
3
  /** Why a model loads the skill, listed with every session's available skills. */
4
4
  export const SKILL_DESCRIPTION = "Load at the start of any implementation task that may have independent parts, or when a session started you, to " +
5
5
  "decide whether to split it across OpenCode sessions with courier_spawn and state the decision, and how: plan the " +
6
6
  "parts, start them, end your turn, read the reports, verify the work, integrate it and report upward, and what to do " +
7
7
  "when a part goes wrong.";
8
- /** The skill's text; it names no limits, which the role part and the brief give for the session's own tree. */
8
+ /**
9
+ * The skill's text; it names no limits, which the role part and the brief give for the session's own tree.
10
+ * Steps 3, 4 and 6 repeat the brief's split rule on purpose: the root has no brief, so only the skill gives it.
11
+ */
9
12
  export const SKILL_CONTENT = [
10
13
  "How to split a task across OpenCode sessions with opencode-courier, and bring the results back up. Your brief, or the " +
11
14
  "opencode-courier role line in your system prompt, names your depth in the session tree and its limits.",
12
15
  "",
13
16
  "1. Decide: split or do. First call courier_recall once: it lists how earlier trees in this project were split and how " +
14
17
  "each part went, so weigh that; one call, before you decide, never in a loop. Split when the task has 2 or more " +
15
- "independent, substantial parts touching separate files or areas; do it yourself when it is small, sequential or tightly coupled. Never start exactly one session for the whole " +
16
- "task: it only adds a hop. What you read and write yourself stays in your context for every later turn, so keep the " +
17
- "deciding, checking and integrating, and hand off the long reading and writing. An implementation task with one tightly " +
18
- "coupled core usually still has independent parts: finding out what the change needs (a session with isolate: false " +
19
- "reads the code, or a library's or a host's source, tries things and reports what it found), its e2e scenario, its " +
20
- "documentation, sometimes a second test file, and a review of the core by a session with fresh context, which reads the " +
21
- "committed diff against the task and the repository's rules and reports its findings (isolate: false, it edits nothing). " +
22
- "Weigh each of them. When the other parts depend on names and texts the core fixes, split in stages: the core first, " +
23
- "yourself when it is small or by one session when it is big, merged and committed before the rest start; then, if 2 or " +
24
- "more parts remain, one session per remaining part, in one group. A part that is itself big and divisible is split again " +
25
- "by the session that gets it; at the deepest level a session does its part itself. " +
26
- `Say which in one line of your reply, before you start: ${SPLIT_LINE}, so that a reasoned decision to do it ` +
27
- "yourself can be told from one never made.",
18
+ "independent, substantial parts touching separate files or areas; do it yourself when it is small, sequential or " +
19
+ "tightly coupled. What you read and write yourself stays in your context for every later turn, so keep the deciding, " +
20
+ "checking and integrating, and hand off the long reading and writing. Only the root, the session nobody started, may " +
21
+ "start exactly one session for the whole task, and only when the task is heavy (long reading, many edits, long runs) " +
22
+ "and its own context is worth keeping: it still verifies and integrates that session's report itself (steps 5 to 7), " +
23
+ "and does a small task itself. Every other session never starts exactly one session for the whole task: it only adds " +
24
+ "a hop, and a chain of single hops uses up the tree's depth. A part that is itself big and divisible is split again by " +
25
+ "the session that gets it; at the deepest level a session does its part itself. " +
26
+ `Say which in one line of your reply, before you start, so that a reasoned decision to do it yourself can be told ` +
27
+ `from one never made: ${SPLIT_LINE}; the root has a third form, ${ONE_PART_LINE}.`,
28
+ " When the core is coupled: an implementation task with one tightly coupled core usually still has independent parts: " +
29
+ "finding out what the change needs (a session with isolate: false reads the code, or a library's or a host's source, " +
30
+ "tries things and reports what it found), its e2e scenario, its documentation, sometimes a second test file, and a " +
31
+ "review of the core by a session with fresh context, which reads the committed diff against the task and the " +
32
+ "repository's rules and reports its findings (isolate: false, it edits nothing). Weigh each of them. When the other " +
33
+ "parts depend on names and texts the core fixes, split in stages: the core first, yourself when it is small or by one " +
34
+ "session when it is big, merged and committed before the rest start; then, if 2 or more parts remain, one session per " +
35
+ "remaining part, in one group. A staged core is one part of the task, not the whole task, so the rule against starting exactly one " +
36
+ "session for the whole task does not apply to it when 2 or more parts follow it.",
28
37
  "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 " +
29
38
  "produce and where, how to check it, what to report. Say if it may split its part further, and to run its checks " +
30
39
  "through the project's scripts (step 6): each permission request of a session below you wakes you and waits on the person.",
31
- "3. Start them: one courier_spawn per part, all with the same group name, so you are woken once with every report; " +
40
+ "3. Start them: one courier_spawn per part that runs at once, all with the same group name, so you are woken once with every report; " +
32
41
  "isolate: true for each part that edits files in parallel with another (commit your own work first if you are isolated " +
33
42
  "yourself). For long work, courier_later a check-in for yourself, and courier_cancel it when the reports arrive.",
34
43
  "4. End your turn: reply without calling more tools. Do not poll with courier_status, courier_children, courier_tree or " +
@@ -6,7 +6,7 @@ export declare function lastTool(messages: Messages): {
6
6
  name: string;
7
7
  at: number;
8
8
  } | undefined;
9
- /** How full a session's context is, as OpenCode's session view reads it (packages/tui/src/util/session.ts, v2.0.32): the last usage since the last compaction over its model's window. */
9
+ /** How full a session's context is, as OpenCode's session view reads it (packages/tui/src/util/session.ts, v2.1.0): the last usage since the last compaction over its model's window. */
10
10
  export declare function contextOf(messages: Messages, windows: ReadonlyArray<ModelWindow>): number | undefined;
11
11
  /** A request as the panel says what a paused session waits on: a permission request, or its first question. */
12
12
  export declare const waitOf: (pending: Pending) => Wait;
@@ -15,7 +15,7 @@ export function lastTool(messages) {
15
15
  }
16
16
  return undefined;
17
17
  }
18
- /** How full a session's context is, as OpenCode's session view reads it (packages/tui/src/util/session.ts, v2.0.32): the last usage since the last compaction over its model's window. */
18
+ /** How full a session's context is, as OpenCode's session view reads it (packages/tui/src/util/session.ts, v2.1.0): the last usage since the last compaction over its model's window. */
19
19
  export function contextOf(messages, windows) {
20
20
  const compacted = messages.findLastIndex((message) => message.type === "compaction" && message.status === "completed");
21
21
  const last = messages.findLast((message, index) => index > compacted && message.type === "assistant" && message.tokens !== undefined);
package/dist/tree/text.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /** The tree's model-facing texts: the child brief, the role part, refusals, reports, notices and tool results. */
2
2
  import { obj, str } from "../platform/json.js";
3
- import { clipText, defuse, END_TURN, SKILL_ID, SPLIT_LINE, STATUSES } from "../kernel/protocol.js";
3
+ import { clipText, defuse, END_TURN, ROOT_SPLIT_LINE, SKILL_ID, SPLIT_LINE, STATUSES } from "../kernel/protocol.js";
4
4
  // The envelope and the child brief.
5
5
  /** How a text names the statuses a report may carry. */
6
6
  const STATUS_LIST = `${STATUSES.slice(0, -1).join(", ")} or ${STATUSES.at(-1)}`;
@@ -47,10 +47,12 @@ 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 for the whole task.",
50
+ "- Do it yourself when it is small, sequential or tightly coupled. Never start exactly one session for the whole task; a core you stage before the rest is one part of it, not the whole, when 2 or more parts follow it.",
51
51
  `- Say which in one line of your reply: ${SPLIT_LINE}.`,
52
- "- If you split, you orchestrate: end your turn while they work, check and integrate each part yourself when it " +
53
- `reports (never hand that checking to another session), then send ${parentID} one combined report.`,
52
+ "- If you split, you orchestrate: start the parts that run at once in one group, with the same group name for each, " +
53
+ "then end your turn while they work. Do not poll with courier_status, courier_children, courier_tree or sleep: the group's " +
54
+ "reports wake you. Check and integrate each part yourself when it reports (never hand that checking to another " +
55
+ `session), then send ${parentID} one combined report.`,
54
56
  ];
55
57
  }
56
58
  /** How every part the context hook adds begins, so a second copy of the plugin does not add its own. */
@@ -65,14 +67,15 @@ export function rolePart(depth, limits) {
65
67
  if (depth === null)
66
68
  return (`${ROLE_PREFIX} root, no sessions started yet. Before a task that touches several files or areas, load the ` +
67
69
  `${SKILL_ID} skill, call courier_recall once for how earlier trees in this project were split and how they went, ` +
68
- `and decide whether to split it across sessions (doing it yourself is fine), and say ` +
69
- `which in one line of your reply: ${SPLIT_LINE}`);
70
+ `and decide whether to split it across sessions (doing it yourself is fine; so is handing a heavy task, with long ` +
71
+ `reading, many edits or long runs, whole to one session to keep your context, but never a small one), and say ` +
72
+ `which in one line of your reply: ${ROOT_SPLIT_LINE}`);
70
73
  if (depth === 0)
71
74
  return (`${ROLE_PREFIX} root orchestrator. You started sessions with courier_spawn, and they may start their own, ` +
72
75
  `down to ${maxDepth} levels below you. ${budget} Check and integrate what each one reports yourself.`);
73
76
  if (depth < maxDepth)
74
- return (`${ROLE_PREFIX} sub-orchestrator, at depth ${depth} of at most ${maxDepth}. Split your task with courier_spawn or ` +
75
- `do it yourself, as your brief says; sessions can be started ${count(maxDepth - depth, "level")} below you. ${budget}`);
77
+ return (`${ROLE_PREFIX} sub-orchestrator, at depth ${depth} of at most ${maxDepth}. Before you split, load the ${SKILL_ID} ` +
78
+ `skill. Split your task with courier_spawn or do it yourself, as your brief says; sessions can be started ${count(maxDepth - depth, "level")} below you. ${budget}`);
76
79
  return (`${ROLE_PREFIX} leaf, at depth ${depth}, the deepest the session tree goes. You cannot start sessions: do your ` +
77
80
  "task yourself and report with courier_send to the session that started you.");
78
81
  }
package/dist/tui.js CHANGED
@@ -61,6 +61,9 @@ var memoryRpc = Rpc.define({
61
61
 
62
62
  // src/kernel/protocol.ts
63
63
  var STATUSES = ["done", "partial", "blocked", "failed"];
64
+ var SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed> or split: <n> parts: \u2026 (one phrase per part)";
65
+ var ONE_PART_LINE = "split: 1 part: <the task>, to keep my context";
66
+ var ROOT_SPLIT_LINE = `${SPLIT_LINE} or ${ONE_PART_LINE}`;
64
67
 
65
68
  // src/memory/domain.ts
66
69
  var RETENTION_MS = 90 * 24 * 60 * 60000;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-courier",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
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,14 +56,14 @@
56
56
  "test:e2e": "e2e/run.sh"
57
57
  },
58
58
  "peerDependencies": {
59
- "@opencode/plugin": "2.0.32",
59
+ "@opencode/plugin": "2.1.0",
60
60
  "effect": "4.0.1"
61
61
  },
62
62
  "devDependencies": {
63
- "@opencode/plugin": "2.0.32",
64
- "@opencode/theme": "2.0.32",
65
- "@opentui/core": "0.5.17",
66
- "@opentui/solid": "0.5.17",
63
+ "@opencode/plugin": "2.1.0",
64
+ "@opencode/theme": "2.1.0",
65
+ "@opentui/core": "0.6.6",
66
+ "@opentui/solid": "0.6.6",
67
67
  "@types/bun": "1.3.14",
68
68
  "@types/node": "26.6.4",
69
69
  "effect": "4.0.1",