opencode-courier 0.2.2 → 0.3.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 (60) hide show
  1. package/README.md +247 -108
  2. package/dist/bounded.d.ts +8 -0
  3. package/dist/bounded.js +19 -0
  4. package/dist/cleanup.d.ts +21 -21
  5. package/dist/cleanup.js +61 -30
  6. package/dist/courier.d.ts +116 -15
  7. package/dist/courier.js +239 -74
  8. package/dist/group.d.ts +86 -0
  9. package/dist/group.js +247 -0
  10. package/dist/hub.d.ts +300 -0
  11. package/dist/hub.js +343 -0
  12. package/dist/index.d.ts +5 -13
  13. package/dist/index.js +82 -313
  14. package/dist/json.d.ts +8 -0
  15. package/dist/json.js +15 -0
  16. package/dist/later.d.ts +7 -7
  17. package/dist/later.js +8 -7
  18. package/dist/limits.d.ts +65 -0
  19. package/dist/limits.js +109 -0
  20. package/dist/notices.d.ts +356 -0
  21. package/dist/notices.js +678 -0
  22. package/dist/question/answer.d.ts +35 -0
  23. package/dist/question/answer.js +214 -0
  24. package/dist/question/index.d.ts +6 -0
  25. package/dist/question/index.js +7 -0
  26. package/dist/question/lifecycle.d.ts +43 -0
  27. package/dist/question/lifecycle.js +91 -0
  28. package/dist/question/pure.d.ts +48 -0
  29. package/dist/question/pure.js +74 -0
  30. package/dist/question/relay.d.ts +5 -0
  31. package/dist/question/relay.js +269 -0
  32. package/dist/question/runtime.d.ts +20 -0
  33. package/dist/question/runtime.js +74 -0
  34. package/dist/question/shared.d.ts +24 -0
  35. package/dist/question/shared.js +24 -0
  36. package/dist/relay.d.ts +32 -36
  37. package/dist/relay.js +6 -56
  38. package/dist/report.d.ts +48 -0
  39. package/dist/report.js +88 -0
  40. package/dist/roster.d.ts +48 -8
  41. package/dist/roster.js +140 -17
  42. package/dist/skill.d.ts +13 -0
  43. package/dist/skill.js +9 -0
  44. package/dist/stop.d.ts +41 -0
  45. package/dist/stop.js +120 -0
  46. package/dist/storage.d.ts +8 -1
  47. package/dist/storage.js +8 -5
  48. package/dist/tools.d.ts +94 -0
  49. package/dist/tools.js +297 -0
  50. package/dist/tree.d.ts +102 -0
  51. package/dist/tree.js +58 -0
  52. package/dist/version.d.ts +4 -10
  53. package/dist/version.js +4 -10
  54. package/dist/watch.d.ts +70 -42
  55. package/dist/watch.js +346 -51
  56. package/dist/webhook.d.ts +11 -28
  57. package/dist/webhook.js +46 -146
  58. package/package.json +16 -5
  59. package/dist/question.d.ts +0 -158
  60. package/dist/question.js +0 -768
package/dist/limits.js ADDED
@@ -0,0 +1,109 @@
1
+ import { setBounded } from "./bounded.js";
2
+ import { isNotFound, millis, obj } from "./json.js";
3
+ import { childrenRefusal, depthRefusal, ROLE_PREFIX, rolePart, totalRefusal } from "./notices.js";
4
+ import { reportOf } from "./report.js";
5
+ import { allEntries, bySession, children, indexedLineage, lineageBy } from "./roster.js";
6
+ export const DEFAULT_LIMITS = { maxDepth: 3, maxChildren: 5, maxTotal: 20 };
7
+ /** The limits in the plugin's options; one that is not a positive integer keeps its default and is named in `problems`. */
8
+ export function readLimits(options) {
9
+ const given = obj(options);
10
+ const problems = [];
11
+ const read = (name) => {
12
+ const value = given[name];
13
+ if (value === undefined)
14
+ return DEFAULT_LIMITS[name];
15
+ if (typeof value === "number" && Number.isInteger(value) && value > 0)
16
+ return value;
17
+ problems.push(`${name} must be a positive integer, not ${JSON.stringify(value)}; using ${DEFAULT_LIMITS[name]}`);
18
+ return DEFAULT_LIMITS[name];
19
+ };
20
+ const limits = { maxDepth: read("maxDepth"), maxChildren: read("maxChildren"), maxTotal: read("maxTotal") };
21
+ return { limits, problems };
22
+ }
23
+ /** Whether a session with these times runs: it has never finished a turn, or something reached it after the last one ended. */
24
+ export const busy = (time) => time.idle === undefined || millis(time.updated) > millis(time.idle);
25
+ /**
26
+ * Whether a spawned session counts against the limits: it runs, unless its turn failed or was
27
+ * interrupted since its last prompt, or it owes its parent a report. One OpenCode no longer knows does not.
28
+ */
29
+ async function live(ports, sessionID) {
30
+ const [time, report] = await Promise.all([
31
+ ports.session.get({ sessionID }).then((info) => info.time, (error) => (isNotFound(error) ? null : undefined)),
32
+ reportOf(ports.storage, sessionID).catch(() => undefined),
33
+ ]);
34
+ if (time === null)
35
+ return false;
36
+ // One whose first prompt failed never ran, and has no idle time to tell so.
37
+ const runs = time !== undefined && busy(time) && !report?.ended;
38
+ return runs || (report?.owes ?? false);
39
+ }
40
+ /**
41
+ * Checks the limits for a spawn from `parentID`, then holds its place in `gate` until `release`;
42
+ * throws a refusal naming the limit. Checks run one at a time, so spawns made together count each other.
43
+ */
44
+ export async function admit(ports, parentID) {
45
+ const gate = ports.gate;
46
+ const checked = gate.turn.then(() => check(ports, parentID));
47
+ gate.turn = checked.catch(() => undefined);
48
+ const { depth, reservation } = await checked;
49
+ const release = () => {
50
+ gate.reserved.delete(reservation);
51
+ };
52
+ return { depth, reservation, release };
53
+ }
54
+ async function check(ports, parentID) {
55
+ const { maxDepth, maxChildren, maxTotal } = ports.limits;
56
+ const entries = await allEntries(ports.storage);
57
+ const parents = bySession(entries);
58
+ const chain = lineageBy(parents, parentID);
59
+ if (chain.length >= maxDepth)
60
+ throw new Error(depthRefusal(chain.length, maxDepth));
61
+ const top = chain.at(-1)?.parentID ?? parentID;
62
+ const chains = new Map(entries.map((entry) => [entry.sessionID, lineageBy(parents, entry.sessionID)]));
63
+ const tree = entries.filter((entry) => chains.get(entry.sessionID).at(-1)?.parentID === top);
64
+ const runs = await Promise.all(tree.map((entry) => live(ports, entry.sessionID)));
65
+ // A session waiting on a live descendant is live too: the brief tells it to end its turn meanwhile.
66
+ const active = new Set(tree.filter((_, i) => runs[i]).flatMap((entry) => [entry.sessionID, ...chains.get(entry.sessionID).map((above) => above.parentID)]));
67
+ const counted = tree.filter((entry) => active.has(entry.sessionID));
68
+ // A spawn whose child is on the roster already is counted there, if it is live, not by its reservation.
69
+ const recorded = new Set(tree.map((entry) => entry.sessionID));
70
+ const pending = [...ports.gate.reserved].filter((held) => held.top === top && !(held.sessionID && recorded.has(held.sessionID)));
71
+ const mine = counted.filter((entry) => entry.parentID === parentID).length + pending.filter((held) => held.parentID === parentID).length;
72
+ if (mine >= maxChildren)
73
+ throw new Error(childrenRefusal(mine, maxChildren));
74
+ if (counted.length + pending.length >= maxTotal)
75
+ throw new Error(totalRefusal(counted.length + pending.length, maxTotal));
76
+ const reservation = { parentID, top };
77
+ ports.gate.reserved.add(reservation);
78
+ return { depth: chain.length + 1, reservation };
79
+ }
80
+ /** How many sessions' depths an instance remembers. */
81
+ const ROLES_MAX = 1000;
82
+ /**
83
+ * The `context` hook: names a session's role in its tree in a system part, and hides courier_spawn
84
+ * from one at `maxDepth`. A session nobody spawned that has started none is pointed to the skill, if it could spawn.
85
+ */
86
+ export async function shapeContext(ports, event) {
87
+ try {
88
+ const depth = await depthOf(ports, event.sessionID);
89
+ if (depth === null && !event.tools.courier_spawn)
90
+ return;
91
+ if (depth !== null && depth >= ports.limits.maxDepth)
92
+ delete event.tools.courier_spawn;
93
+ if (!event.system.some((part) => part.text.startsWith(ROLE_PREFIX)))
94
+ event.system.push({ type: "text", text: rolePart(depth, ports.limits) });
95
+ }
96
+ catch (error) {
97
+ ports.log(`courier: cannot tell the role of session ${event.sessionID}: ${String(error)}`);
98
+ }
99
+ }
100
+ async function depthOf(ports, sessionID) {
101
+ const known = ports.roles.get(sessionID);
102
+ if (known !== undefined)
103
+ return known;
104
+ // The reverse index alone: a scan here would run for every session's first request, courier or not.
105
+ const chain = await indexedLineage(ports.storage, sessionID);
106
+ const depth = chain.length || ((await children(ports.storage, sessionID)).length ? 0 : null);
107
+ setBounded(ports.roles, sessionID, depth, ROLES_MAX);
108
+ return depth;
109
+ }
@@ -0,0 +1,356 @@
1
+ /** Closes a tool result after which the caller most likely has nothing left to do. */
2
+ export declare const END_TURN = "If nothing else is left to do now, end your turn by replying without calling more tools.";
3
+ /** The limits on the tree of sessions courier_spawn builds, from the plugin's options. */
4
+ export interface Limits {
5
+ readonly maxDepth: number;
6
+ readonly maxChildren: number;
7
+ readonly maxTotal: number;
8
+ }
9
+ /** The statuses a report carries: the task is done, partly done, waits on the parent, or failed. */
10
+ export declare const STATUSES: readonly ["done", "partial", "blocked", "failed"];
11
+ export type Status = (typeof STATUSES)[number];
12
+ export declare const isStatus: (value: unknown) => value is Status;
13
+ /** The one line a session states its split decision in, so a transcript tells a decision from one never made. */
14
+ export declare const SPLIT_LINE = "split: no, because \u2026; considered: <the parts you weighed> or split: <n> parts: \u2026 (one phrase per part)";
15
+ /** The branch an isolated child commits its work on, and names in the `branch` of its report. */
16
+ export declare const childBranch: (sessionID: string) => string;
17
+ /** What a spawned session at `depth` is told; `depth` 1 is a child of a session nobody spawned. `branch` is for an isolated one. */
18
+ export declare function childBrief(parentID: string, task: string, depth: number, limits: Limits, branch?: string): string;
19
+ /** How every part the context hook adds begins, so a second copy of the plugin does not add its own. */
20
+ export declare const ROLE_PREFIX = "opencode-courier role:";
21
+ /**
22
+ * The system part naming a session's place in its tree: `depth` 0 for a session nobody spawned that
23
+ * has started one, `null` for one that has started none, which is pointed to the skill. The same on every request.
24
+ */
25
+ export declare function rolePart(depth: number | null, limits: Limits): string;
26
+ export declare const SKILL_ID = "courier-orchestrate";
27
+ /** Why a model loads the skill, listed with every session's available skills. */
28
+ export declare const SKILL_DESCRIPTION: string;
29
+ /** The skill's text; it names no limits, which the role part and the brief give for the session's own tree. */
30
+ export declare const SKILL_CONTENT: string;
31
+ export declare const depthRefusal: (depth: number, maxDepth: number) => string;
32
+ export declare const childrenRefusal: (running: number, maxChildren: number) => string;
33
+ export declare const totalRefusal: (running: number, maxTotal: number) => string;
34
+ export declare function envelope(from: string, message: string, attributes?: Record<string, string>): string;
35
+ /** What a report lists besides its text; `checks` are the commands the session ran and what each gave. */
36
+ export interface Artifacts {
37
+ readonly branch?: string;
38
+ readonly commits?: ReadonlyArray<string>;
39
+ readonly files?: ReadonlyArray<string>;
40
+ readonly checks?: ReadonlyArray<{
41
+ readonly command: string;
42
+ readonly result: string;
43
+ }>;
44
+ }
45
+ /**
46
+ * The body of a report: its text, then the artifacts in a fixed layout, the same for every report,
47
+ * so a parent finds the branch, the commits, the files and the checks without reading prose.
48
+ */
49
+ export declare function reportBody(message: string, artifacts: Artifacts | undefined): string;
50
+ export declare function spawnText(child: {
51
+ readonly sessionID: string;
52
+ readonly directory: string;
53
+ readonly group?: string;
54
+ readonly rosterError?: string;
55
+ readonly groupError?: string;
56
+ readonly fromParent?: boolean;
57
+ readonly uncommitted?: boolean;
58
+ }): string;
59
+ /** A report held with its group: which group, and how many of its members have reported so far, when that could be read. */
60
+ export interface Held {
61
+ readonly group: string;
62
+ readonly reported?: number;
63
+ readonly members?: number;
64
+ }
65
+ /**
66
+ * What courier_send did; `report` says whether the message was noted as the sender's report to the session
67
+ * that started it, and `held` that it was held with the sender's group rather than delivered, with no `messageID`.
68
+ */
69
+ export interface Sent {
70
+ readonly messageID?: string;
71
+ readonly status?: Status;
72
+ readonly report?: boolean;
73
+ readonly held?: Held;
74
+ }
75
+ /** courier_send's result: for a message to the session that started the sender, whether it was its report. */
76
+ export declare function sendText(sessionID: string, sent?: Pick<Sent, "status" | "report" | "held">): string;
77
+ /** courier_status's result: the session's state, as JSON. */
78
+ export declare const statusText: (status: unknown) => string;
79
+ export declare const childrenText: (listed: ReadonlyArray<unknown>) => string;
80
+ /** What courier_cleanup did: removed the worktree, found it gone, kept it and why, or forgot a child with none; `group`, one it dropped the child from. */
81
+ export type CleanupResult = {
82
+ readonly sessionID: string;
83
+ readonly directory: string;
84
+ readonly outcome: "removed" | "gone" | "forgotten";
85
+ readonly group?: string;
86
+ } | {
87
+ readonly sessionID: string;
88
+ readonly directory: string;
89
+ readonly outcome: "kept";
90
+ readonly reason: string;
91
+ readonly changes: readonly string[];
92
+ readonly commits: readonly string[];
93
+ };
94
+ export declare function cleanupText(result: CleanupResult): string;
95
+ /** courier_tree's result: the sessions under a session, drawn depth first, as JSON. */
96
+ export declare function treeText(tree: {
97
+ readonly sessions: ReadonlyArray<unknown>;
98
+ readonly truncated?: number;
99
+ }): string;
100
+ /** What courier_stop did to each session, and to the messages and worktrees. */
101
+ export declare function stopText(result: {
102
+ readonly sessionID: string;
103
+ readonly stopped: ReadonlyArray<{
104
+ readonly sessionID: string;
105
+ readonly outcome: string;
106
+ readonly error?: string;
107
+ }>;
108
+ readonly cancelled: ReadonlyArray<string>;
109
+ readonly cleanup?: ReadonlyArray<CleanupResult | {
110
+ readonly sessionID: string;
111
+ readonly outcome: "failed";
112
+ readonly error: string;
113
+ }>;
114
+ readonly told?: string;
115
+ }): string;
116
+ /** What the parent of a stopped session is told when the stop was ordered by a session above both. */
117
+ export declare const stoppedNotice: (title: string, by: string) => string;
118
+ /** Why a worktree in this state must be kept, or undefined when removing it loses nothing. */
119
+ export declare function keepReason(state: {
120
+ readonly changes: readonly string[];
121
+ readonly commits: readonly string[];
122
+ }): string | undefined;
123
+ /**
124
+ * What courier_answer did: a permission request's result carries the reply, a question's whether its
125
+ * answers went out as the result of its call or, since that call had been cut off, as a message.
126
+ */
127
+ export type AnswerResult = PermissionAnswered | QuestionAnswered;
128
+ /**
129
+ * What answering a permission request did; `answered` is false when no location loaded here held the
130
+ * request: answered already, no longer waited for, or waiting in another server on the same data dir.
131
+ */
132
+ export interface PermissionAnswered {
133
+ readonly sessionID: string;
134
+ readonly requestID: string;
135
+ readonly reply: Reply;
136
+ readonly answered: boolean;
137
+ }
138
+ /** What answering a question did; `answered` is false when it no longer waited. */
139
+ export type QuestionAnswered = {
140
+ readonly sessionID: string;
141
+ readonly requestID: string;
142
+ readonly answered: false;
143
+ } | {
144
+ readonly sessionID: string;
145
+ readonly requestID: string;
146
+ readonly answered: true;
147
+ readonly by: "result" | "message";
148
+ readonly answers: Answers;
149
+ };
150
+ export declare function answerText(result: AnswerResult): string;
151
+ /** `own` when the message goes to the session that scheduled it. */
152
+ export declare function laterText(entry: {
153
+ readonly id: string;
154
+ readonly sessionID: string;
155
+ }, fireAt: string, own: boolean): string;
156
+ export declare const cancelText: (id: string, cancelled: boolean) => string;
157
+ /** `receiving` says whether a webhook receiver runs in this server. */
158
+ export declare function subscribeText(sessionID: string, topic: string, receiving: boolean): string;
159
+ export declare const unsubscribeText: (sessionID: string, dropped: readonly string[]) => string;
160
+ /** The error of OpenCode's `session.execution.failed` event. */
161
+ export interface ExecutionError {
162
+ readonly type: string;
163
+ readonly message: string;
164
+ readonly status?: number;
165
+ }
166
+ export declare function failureNotice(title: string, error: ExecutionError, group?: string): string;
167
+ /**
168
+ * What the parent is told when a session it started ends its turn without a report to it;
169
+ * `progressed` when it messaged the parent without a status since its last prompt.
170
+ */
171
+ export declare function silentNotice(title: string, lastText: string | undefined, progressed?: boolean, group?: string): string;
172
+ /** A report held with its group: its status, text and artifacts, as courier_send was given them, and when. */
173
+ export interface HeldReport {
174
+ readonly at: number;
175
+ readonly status: Status;
176
+ readonly message: string;
177
+ readonly artifacts?: Artifacts;
178
+ }
179
+ /** A member of a group that left it without a report, and why: its turn failed or was interrupted, it was deleted, or courier_cleanup dropped it. */
180
+ export interface LeftMember {
181
+ readonly sessionID: string;
182
+ readonly title: string;
183
+ readonly by: "failed" | "interrupted" | "deleted" | "dropped";
184
+ }
185
+ /**
186
+ * What the parent gets when a group it named is released: every report, with its status, text and artifacts in
187
+ * `reportBody`'s layout, and the members that left without one, in the order they joined.
188
+ */
189
+ export declare function groupNotice(group: string, reports: ReadonlyArray<HeldReport & {
190
+ readonly sessionID: string;
191
+ readonly title: string;
192
+ }>, left?: ReadonlyArray<LeftMember>): string;
193
+ /** The answers to a permission request, as OpenCode's own prompt offers them. */
194
+ export declare const REPLIES: readonly ["once", "always", "reject"];
195
+ export type Reply = (typeof REPLIES)[number];
196
+ /** A permission request, as OpenCode's `permission.asked` event carries it. */
197
+ export interface PermissionRequest {
198
+ readonly id: string;
199
+ readonly sessionID: string;
200
+ readonly action: string;
201
+ readonly resources: ReadonlyArray<string>;
202
+ readonly save?: ReadonlyArray<string>;
203
+ readonly message?: string;
204
+ }
205
+ /**
206
+ * Sent with a rejection that has no reason of its own. OpenCode ends the child's turn on a bare
207
+ * rejection, and the child would never report back; with a message, the call fails and it carries on.
208
+ */
209
+ export declare const REJECTED = "Refused by the session that started you. Do without it, or report back why you cannot.";
210
+ /** How a notice names where the asking session came from, to the session at the top of its lineage. */
211
+ export declare const origin: (startedBy?: string) => string;
212
+ /** For a notice that a session's turn ended without it finishing. */
213
+ export declare const STAYS_QUIET = "and then it does not report back on its own: if it stays quiet, message it with courier_send to have it carry on.";
214
+ /**
215
+ * What the session at the top is told. `startedBy` names the session that started the asking one
216
+ * when that is not the top session itself, but one started from it.
217
+ */
218
+ export declare function permissionNotice(title: string, request: PermissionRequest, startedBy?: string): string;
219
+ /** What the session told about a permission request is told once it is answered without courier_answer. */
220
+ export declare function permissionSettledNotice(title: string, requestID: string, reply: Reply): string;
221
+ /** The `metadata.kind` of the forms OpenCode's web search asks its provider with. */
222
+ export declare const WEBSEARCH_FORM = "websearch.provider";
223
+ /** One field of an OpenCode form, as much of it as a notice shows. */
224
+ export interface FormField {
225
+ readonly key: string;
226
+ readonly type: string;
227
+ readonly title?: string;
228
+ readonly description?: string;
229
+ readonly options?: ReadonlyArray<{
230
+ readonly value: string;
231
+ readonly label: string;
232
+ readonly description?: string;
233
+ }>;
234
+ readonly url?: string;
235
+ /** Not shown to the person. */
236
+ readonly hidden?: boolean;
237
+ /** Conditions on earlier answers; the field is shown only when they hold. */
238
+ readonly when?: ReadonlyArray<unknown>;
239
+ }
240
+ /** An OpenCode form, as its `form.created` event carries it. */
241
+ export interface Form {
242
+ readonly id: string;
243
+ readonly sessionID: string;
244
+ readonly title: string;
245
+ readonly metadata?: Readonly<Record<string, unknown>>;
246
+ readonly fields: ReadonlyArray<FormField>;
247
+ }
248
+ /** A form's `metadata.kind`, when it has one that can go in a notice's envelope. */
249
+ export declare const kindOf: (form: Form) => string | undefined;
250
+ /**
251
+ * What the session at the top is told about a form OpenCode shows in a session started with
252
+ * courier_spawn, which the plugin cannot pass on or answer: only the person can, in that session.
253
+ */
254
+ export declare function formNotice(title: string, form: Form, startedBy?: string): string;
255
+ /** What the session told about a form is told once it is answered or withdrawn. */
256
+ export declare function formSettledNotice(title: string, formID: string, settled: "answered" | "cancelled"): string;
257
+ /** One question of OpenCode's question tool, as the asking model wrote it. */
258
+ export interface Prompt {
259
+ readonly question: string;
260
+ readonly header: string;
261
+ readonly options: ReadonlyArray<{
262
+ readonly label: string;
263
+ readonly description: string;
264
+ }>;
265
+ readonly multiple?: boolean;
266
+ }
267
+ /** The answers to a question call, as the question tool returns them: one list per question. */
268
+ export type Answers = string[][];
269
+ /** A question a spawned session asked with its question tool, stored until it is answered. */
270
+ export interface Asked {
271
+ readonly requestID: string;
272
+ readonly sessionID: string;
273
+ /** The session told, at the top of the sessions started with courier_spawn; it alone answers. */
274
+ readonly top: string;
275
+ readonly title: string;
276
+ /** The session that started the asking one, when that is not the top session itself. */
277
+ readonly startedBy?: string;
278
+ readonly questions: ReadonlyArray<Prompt>;
279
+ readonly askedAt: number;
280
+ }
281
+ /** How a question was settled without the top session: in the asking session, or not at all. */
282
+ export type Elsewhere = {
283
+ readonly by: "child";
284
+ readonly answers: Answers;
285
+ } | {
286
+ readonly by: "dismissed";
287
+ } | {
288
+ readonly by: "failed";
289
+ readonly error: string;
290
+ };
291
+ /** What the top session's answer does to a call that waits: answers it, or dismisses it. */
292
+ export type Outcome = {
293
+ readonly answers: Answers;
294
+ } | {
295
+ readonly dismissed: true;
296
+ };
297
+ /**
298
+ * Why a linked call is withdrawn: its question was settled elsewhere, or the top session's outcome
299
+ * reached it another way (courier_answer, or a dismissal through an earlier call linked to it).
300
+ */
301
+ export type Withdrawal = Elsewhere | {
302
+ readonly by: "top";
303
+ readonly outcome: Outcome;
304
+ };
305
+ /** What OpenCode's question tool tells the asking model once it has the answers. */
306
+ export declare function answeredText(questions: ReadonlyArray<Prompt>, answers: ReadonlyArray<ReadonlyArray<string>>): string;
307
+ declare const CUT_OFF: {
308
+ stopped: string;
309
+ restarted: string;
310
+ };
311
+ /** Why a question's call was cut off: its turn was stopped, or OpenCode restarted or closed its location. */
312
+ export type CutOff = keyof typeof CUT_OFF;
313
+ /** What the top session is told about a question, when it is asked or after its call was cut off. */
314
+ export declare function questionNotice(asked: Asked, cutOff?: CutOff): string;
315
+ /** What the top session is told about a question settled without it. */
316
+ export declare function questionSettledNotice(asked: Asked, how: Elsewhere): string;
317
+ /** The result of a spawned session's question call that the person dismissed through the top session. */
318
+ export declare const DISMISSED = "The person dismissed this question without answering it. Carry on without the answers, or report back with courier_send to the session that started you why you cannot.";
319
+ /** The message a spawned session whose question call was cut off gets, with the top session's outcome. */
320
+ export declare function cutOffAnswer(asked: Asked, outcome: Outcome): string;
321
+ /**
322
+ * What a top session's question call that no waiting question was linked to adds to its result, when
323
+ * questions with the same choices wait: `waiting` lists them.
324
+ */
325
+ export declare function unlinkedNote(waiting: ReadonlyArray<Pick<Asked, "sessionID" | "requestID">>): string;
326
+ /** The result of a linked call withdrawn because its question was settled without it. */
327
+ export declare function withdrawnText(linked: Asked, how: Withdrawal): string;
328
+ /**
329
+ * What a linked call adds to the person's answers about passing them on: `passed` is "result",
330
+ * "message", undefined when it no longer waited, or the error as `error: <message>`.
331
+ */
332
+ export declare function passedNote(linked: Asked, passed: string | undefined): string;
333
+ export declare function clipText(text: string, max?: number): string;
334
+ /** The GitHub CI events, with how a summary names each. */
335
+ export declare const CI_EVENTS: Record<string, string>;
336
+ /** What every GitHub delivery carries, read once for the summary of its event. */
337
+ export interface Delivery {
338
+ readonly name: string;
339
+ readonly body: Record<string, any>;
340
+ readonly repo: string;
341
+ readonly action: string | undefined;
342
+ readonly sender: string | undefined;
343
+ /** ` by <sender>`, or nothing when the delivery names no sender. */
344
+ readonly by: string;
345
+ /** The pull request and issue numbers it concerns, which `webhook.ts` reads for its topics. */
346
+ readonly numbers: readonly number[];
347
+ }
348
+ /** The summary of a GitHub delivery that wakes its subscribers. */
349
+ export declare function githubSummary(delivery: Delivery): string;
350
+ /** The summary of a delivery to `/hook/<name>`: its text, clipped. */
351
+ export declare const hookSummary: (text: string) => string;
352
+ /** Defuses `<courier` and `</courier>` in outside text, so it cannot close the envelope or forge another. */
353
+ export declare function defuse(text: string): string;
354
+ /** The text a subscribed session is woken with for a webhook delivery, before its envelope. */
355
+ export declare const webhookText: (summary: string) => string;
356
+ export {};