@dougbots/avenor-opencode 0.8.3 → 0.10.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 CHANGED
@@ -42,6 +42,9 @@ When a run is dispatched with `avenor_spawn`, the plugin:
42
42
  1. **Blocks by default** — the tool call stays open, updating its title and metadata as the sub-agent runs, exactly like OpenCode's own sub-agent tool calls. The tool returns when the run reaches a terminal state.
43
43
  2. **Re-prompts on completion** — when a `wait=false` (fire-and-forget) run finishes, the plugin automatically injects a completion message into the orchestrating session so the LLM picks up without manual polling.
44
44
  3. **Routes permissions** — when a sub-agent running under the `opencode-http` backend requests a permission, the plugin injects a re-prompt into the orchestrating session. The LLM can call `avenor_answer_permission` to respond; the normal permission dialog is also shown as a fallback.
45
+ 4. **Formats results** — OpenCode returns concise, bounded prose in its shared `output` for `avenor_status`, `avenor_result`, `avenor_answer_permission`, `avenor_follow_up`, `avenor_events`, `avenor_inspect`, and `avenor_shutdown`. Structured metadata stays in host/session state.
46
+
47
+ MCP clients render their own results.
45
48
 
46
49
  ## Tools
47
50
 
@@ -55,17 +58,36 @@ Dispatch an agent run in the current working directory.
55
58
 
56
59
  | Argument | Required | Description |
57
60
  |---|---|---|
58
- | `agent` | yes | Agent name (no default — must be specified) |
61
+ | `agent` | no | Agent name; omission uses the supplied model or runtime defaults |
59
62
  | `prompt` | no | Prompt text |
60
63
  | `prompt_file` | no | Path to a prompt file |
61
64
  | `label` | no | Human-readable label for the run |
62
65
  | `timeout` | no | Timeout duration (e.g. `3600s`, `30m`) |
63
66
  | `model` | no | Model override |
67
+ | `thinking` | no | Run-level reasoning control; unsupported effective backends reject explicit values |
68
+ | `backend` | no | Backend override; direct mode defaults to Avenor's `opencode-acp` backend when omitted |
69
+ | `roster_file` | no | Path to a top-level roster map |
70
+ | `roster_entry` | no | Key within `roster_file` |
64
71
  | `wait` | no | Block until complete (default `true`) |
65
72
  | `supervisor_id` | no | Reuse an existing supervisor by socket path |
66
73
 
67
74
  The working directory (`dir`) is injected automatically from your OpenCode session context — it is not a user-facing argument.
68
75
 
76
+ #### Direct and roster modes
77
+
78
+ Use `roster_file` plus `roster_entry` to select a complete identity. Every roster entry requires `backend` and at least one of `agent` or `model`; roster mode rejects direct `agent`, `model`, and `backend` overrides. Direct mode allows all three identity fields to be omitted independently, including backend-only selection. Roster mode does not apply the direct `opencode-acp` default.
79
+
80
+ ```json
81
+ {
82
+ "prompt": "Review the repository",
83
+ "roster_file": "/repo/roster.json",
84
+ "roster_entry": "reviewer",
85
+ "wait": true
86
+ }
87
+ ```
88
+
89
+ Roster entries currently support only `backend`, `agent`, and `model`. `system` and `thinking` are rejected; `thinking` remains a run-level value and is validated against the effective backend. A resumed session cannot change backend. The plugin passes `roster_file` as supplied, so an absolute path avoids ambiguity when the OpenCode process directory differs from the project directory.
90
+
69
91
  ### `avenor_status`
70
92
 
71
93
  Get status of a specific run or all active runs. Use the compact `lifecycle` view for progress and pending permission checks; `full` remains the compatibility default.
@@ -89,6 +111,17 @@ Wait for a run to finish and return its complete final output without transcript
89
111
 
90
112
  A blocked run returns its pending permission instead of waiting forever. If this tool's own timeout expires, it returns the latest status with `ready: false` and `timed_out: true`.
91
113
 
114
+ ### `avenor_inspect`
115
+
116
+ Use `avenor_inspect` to review a bounded snapshot. The snapshot includes transcript, completed and live tools, permissions, and final output. Use `avenor_events` when raw event records are required.
117
+
118
+ | Argument | Required | Description |
119
+ |---|---|---|
120
+ | `run_id` | yes | Run ID to inspect |
121
+ | `limit` | no | Max events to reduce into the snapshot |
122
+ | `after_seq` | no | Only include events after this sequence number |
123
+ | `supervisor_id` | no | Reuse an existing supervisor by socket path |
124
+
92
125
  ### `avenor_answer_permission`
93
126
 
94
127
  Answer a pending permission request. The `option_id` comes from the `pending_permission.options` array returned by `avenor_status`.
package/dist/index.d.ts CHANGED
@@ -6,7 +6,14 @@ type TrackedRun = {
6
6
  runId: string;
7
7
  runtimeId?: string;
8
8
  sessionId?: string;
9
- agent: string;
9
+ agent?: string;
10
+ model?: string;
11
+ backend?: string;
12
+ rosterFile?: string;
13
+ rosterEntry?: string;
14
+ effectiveAgent?: string;
15
+ effectiveModel?: string;
16
+ effectiveBackend?: string;
10
17
  orchestratorSessionId: string;
11
18
  label: string;
12
19
  supervisorId?: string;
@@ -15,7 +22,12 @@ type TrackedRun = {
15
22
  lastSnapshot?: RunSnapshot;
16
23
  };
17
24
  declare function terminalStatus(snapshot: RunSnapshot, fallback?: StatusResult): string;
25
+ type HostIdentityMetadataInput = Pick<TrackedRun, 'agent' | 'model' | 'backend' | 'rosterFile' | 'rosterEntry' | 'effectiveAgent' | 'effectiveModel' | 'effectiveBackend'>;
26
+ type HostIdentityStatus = Pick<StatusResult, 'roster_file' | 'roster_entry' | 'agent' | 'model' | 'backend' | 'effective_agent' | 'effective_model' | 'effective_backend'>;
27
+ /** Project shared spawn identity into OpenCode metadata without resolving rosters here. */
28
+ declare function hostIdentityMetadata(run?: HostIdentityMetadataInput, status?: HostIdentityStatus): Record<string, unknown>;
29
+ declare function buildSnapshotMetadata(snapshot: RunSnapshot, run?: HostIdentityMetadataInput, status?: HostIdentityStatus): Record<string, unknown>;
18
30
  declare function buildWaitingText(run: TrackedRun, snapshot: RunSnapshot): string;
19
31
  declare const AvenorPlugin: Plugin;
20
32
  //#endregion
21
- export { AvenorPlugin, buildWaitingText, terminalStatus };
33
+ export { AvenorPlugin, buildSnapshotMetadata, buildWaitingText, hostIdentityMetadata, terminalStatus };
package/dist/index.js CHANGED
@@ -1,6 +1,414 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import * as crypto from "node:crypto";
3
- import { Supervisor, answerPermissionTool, createRunSnapshot, dial, eventsTool, followUpTool, inspectTool, observeRun, resultTool, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
3
+ import { Supervisor, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, followUpTool, inspectTool, observeRun, resultTool, shutdownTool, spawnTool, statusTool, validateSpawnSelection } from "@dougbots/avenor-core";
4
+ //#region src/render.ts
5
+ const FINAL_OUTPUT_CHARS$1 = 1500;
6
+ const SCALAR_CHARS = 240;
7
+ const EVENT_CHARS = 600;
8
+ const PREVIEW_LINES = 12;
9
+ const STATUS_ROWS = 12;
10
+ const COLLECTION_ITEMS = 8;
11
+ const INSPECT_ITEMS = 5;
12
+ const CLEANUP_ITEMS = 8;
13
+ const TERMINAL_STATUSES$1 = new Set([
14
+ "done",
15
+ "failed",
16
+ "timeout",
17
+ "killed"
18
+ ]);
19
+ const ANSI_PATTERN = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
20
+ function asRecord(value) {
21
+ return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
22
+ }
23
+ function stringValue(value) {
24
+ return typeof value === "string" && value.trim() ? value : void 0;
25
+ }
26
+ function numberValue(value) {
27
+ if (typeof value === "number" && Number.isFinite(value)) return value;
28
+ if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
29
+ }
30
+ function sanitizeText(text) {
31
+ return text.replace(ANSI_PATTERN, "").replaceAll(" ", " ").replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "");
32
+ }
33
+ function itemMarker(count) {
34
+ return `… ${count} items omitted`;
35
+ }
36
+ function lineMarker(count) {
37
+ return `… ${count} lines omitted`;
38
+ }
39
+ function characterMarker(count) {
40
+ return `[preview clipped: ${count} characters omitted]`;
41
+ }
42
+ function cleanMultiline(value) {
43
+ if (typeof value !== "string") return void 0;
44
+ return sanitizeText(value).trim() || void 0;
45
+ }
46
+ function tailClip(value, limit) {
47
+ if (value.length <= limit) return { text: value };
48
+ let markerLength = characterMarker(value.length).length;
49
+ for (let attempt = 0; attempt < 3; attempt++) {
50
+ const visibleLength = Math.max(0, limit - markerLength);
51
+ const nextMarkerLength = characterMarker(value.length - visibleLength).length;
52
+ if (nextMarkerLength === markerLength) return {
53
+ text: value.slice(value.length - visibleLength),
54
+ omitted: value.length - visibleLength
55
+ };
56
+ markerLength = nextMarkerLength;
57
+ }
58
+ const visibleLength = Math.max(0, limit - markerLength);
59
+ return {
60
+ text: value.slice(value.length - visibleLength),
61
+ omitted: value.length - visibleLength
62
+ };
63
+ }
64
+ function scalar(value, fallback = "unknown", limit = SCALAR_CHARS) {
65
+ const clean = cleanMultiline(typeof value === "string" ? value : void 0);
66
+ if (!clean) return fallback;
67
+ const clipped = tailClip(clean.replace(/[\r\n]+/g, " ").replace(/\s+/g, " ").trim(), limit);
68
+ return clipped.omitted === void 0 ? clipped.text : `${clipped.text}${characterMarker(clipped.omitted)}`;
69
+ }
70
+ function optionalScalar(value, limit = SCALAR_CHARS) {
71
+ const clean = cleanMultiline(typeof value === "string" ? value : void 0);
72
+ return clean ? scalar(clean, "unknown", limit) : void 0;
73
+ }
74
+ function preview(value, limit) {
75
+ const clean = cleanMultiline(value);
76
+ if (!clean) return void 0;
77
+ const clipped = tailClip(clean, limit);
78
+ const lines = clipped.text.split("\n").slice(-PREVIEW_LINES);
79
+ const omittedLines = Math.max(0, clean.split("\n").length - lines.length);
80
+ return [
81
+ ...lines,
82
+ ...clipped.omitted === void 0 ? [] : [characterMarker(clipped.omitted)],
83
+ ...omittedLines === 0 ? [] : [lineMarker(omittedLines)]
84
+ ];
85
+ }
86
+ function selected(record, key, fallback, defaultValue = "unknown") {
87
+ return scalar(stringValue(record?.[key]) ?? stringValue(fallback), defaultValue);
88
+ }
89
+ function selectedOptional(record, key) {
90
+ return optionalScalar(record?.[key]);
91
+ }
92
+ function hasStatusIdentity(record) {
93
+ return !!record && !!(stringValue(record.run_id) || stringValue(record.label) || stringValue(record.status));
94
+ }
95
+ function fallback(tool) {
96
+ return [`Avenor ${tool} result unavailable.`, "Retry the tool or use avenor_status/avenor_inspect."];
97
+ }
98
+ function output(lines, tool) {
99
+ return (lines ?? fallback(tool)).join("\n");
100
+ }
101
+ function isTerminal(status) {
102
+ return TERMINAL_STATUSES$1.has(status);
103
+ }
104
+ function permission(record) {
105
+ return asRecord(record?.pending_permission);
106
+ }
107
+ function permissionOptions(record) {
108
+ const options = record?.options;
109
+ return Array.isArray(options) ? options.map(asRecord).filter((value) => !!value) : [];
110
+ }
111
+ function optionRows(options) {
112
+ return options.slice(0, COLLECTION_ITEMS).map((option) => `${selected(option, "option_id")} (${selected(option, "label")})`).join(", ");
113
+ }
114
+ function optionLine(prefix, options) {
115
+ const rows = optionRows(options);
116
+ const omitted = options.length - Math.min(options.length, COLLECTION_ITEMS);
117
+ return `${prefix}${rows ? ` ${rows}` : ""}${omitted > 0 ? ` ${itemMarker(omitted)}` : ""}`;
118
+ }
119
+ function guidance(status, pending, runId) {
120
+ if (status === "waiting") {
121
+ const options = permissionOptions(pending);
122
+ if (options.length > 0) return `Guidance: Call avenor_answer_permission with run_id "${runId}" and option_id "${selected(options[0], "option_id")}".`;
123
+ if (pending) return `Guidance: Permission input is required, but no options are available; call avenor_inspect with run_id "${runId}".`;
124
+ return `Guidance: Call avenor_follow_up or avenor_status with run_id "${runId}".`;
125
+ }
126
+ if (isTerminal(status)) return `Guidance: Call avenor_result or avenor_events with run_id "${runId}".`;
127
+ return `Guidance: Call avenor_result with run_id "${runId}".`;
128
+ }
129
+ function statusExpanded(status) {
130
+ return [
131
+ ["Phase", "phase"],
132
+ ["Phase label", "phase_label"],
133
+ ["Session", "session_id"],
134
+ ["Backend", "backend"],
135
+ ["Agent", "agent"],
136
+ ["Model", "model"],
137
+ ["Latest sequence", "latest_seq"],
138
+ ["Stop reason", "stop_reason"]
139
+ ].flatMap(([label, key]) => {
140
+ const value = key === "latest_seq" ? numberValue(status[key])?.toString() : selectedOptional(status, key);
141
+ return value === void 0 ? [] : [`${label}: ${value}`];
142
+ });
143
+ }
144
+ function statusLines(details, args) {
145
+ if (Array.isArray(details)) {
146
+ const statuses = details.map(asRecord);
147
+ if (statuses.some((status) => !hasStatusIdentity(status))) return void 0;
148
+ const displayed = statuses.slice(0, STATUS_ROWS);
149
+ const lines = [
150
+ `Avenor status — ${statuses.length} runs`,
151
+ ...displayed.map((status) => {
152
+ const runId = selected(status, "run_id", args.run_id);
153
+ return `${selected(status, "label", void 0, runId)} — ${selected(status, "status")} (run_id: ${runId})`;
154
+ }),
155
+ ...statuses.length > displayed.length ? [itemMarker(statuses.length - displayed.length)] : []
156
+ ];
157
+ for (const status of displayed) {
158
+ const runId = selected(status, "run_id", args.run_id);
159
+ lines.push(`Run: ${runId}`, `Label: ${selected(status, "label", void 0, runId)}`, `Status: ${selected(status, "status")}`, ...statusExpanded(status));
160
+ }
161
+ return lines;
162
+ }
163
+ const status = asRecord(details);
164
+ if (!hasStatusIdentity(status)) return void 0;
165
+ const runId = selected(status, "run_id", args.run_id);
166
+ const label = selected(status, "label", void 0, runId);
167
+ const state = selected(status, "status");
168
+ const pending = permission(status);
169
+ const options = permissionOptions(pending);
170
+ const finalPreview = preview(status.final_output, FINAL_OUTPUT_CHARS$1);
171
+ return [
172
+ `Status: ${label} — ${state}`,
173
+ `Run: ${runId}`,
174
+ ...pending ? [`Permission: ${selected(pending, "description")}`, optionLine("Options:", options)] : [],
175
+ ...status.final_output_truncated === true ? [`Warning: final output preview is truncated; call avenor_result with run_id "${runId}".`] : [],
176
+ ...finalPreview ? ["Final output:", ...finalPreview] : ["Final output: unavailable."],
177
+ guidance(state, pending, runId),
178
+ ...statusExpanded(status)
179
+ ];
180
+ }
181
+ function resultLines(details, args) {
182
+ const result = asRecord(details);
183
+ if (!hasStatusIdentity(result)) return void 0;
184
+ const runId = selected(result, "run_id", args.run_id);
185
+ const label = selected(result, "label", void 0, runId);
186
+ const state = selected(result, "status");
187
+ const pending = permission(result);
188
+ const finalPreview = preview(result.output, FINAL_OUTPUT_CHARS$1);
189
+ const session = selectedOptional(result, "session_id");
190
+ const stopReason = selectedOptional(result, "stop_reason");
191
+ return [
192
+ `Result: ${label} — ${state}`,
193
+ `Run: ${runId}`,
194
+ ...result.timed_out === true ? ["Warning: wait timed out; the run is still active."] : [],
195
+ ...pending ? [`Permission: ${selected(pending, "description")}`] : [],
196
+ guidance(state, pending, runId),
197
+ ...result.output_truncated === true ? [stringValue(result.output_event_path) ? `Warning: final output may be truncated; retry avenor_result or read the durable event path "${scalar(result.output_event_path)}".` : `Warning: final output may be truncated; call avenor_events with run_id "${runId}".`] : [],
198
+ ...finalPreview ? ["Final output:", ...finalPreview] : ["Final output: unavailable."],
199
+ ...session ? [`Session: ${session}`] : [],
200
+ ...stopReason ? [`Stop reason: ${stopReason}`] : []
201
+ ];
202
+ }
203
+ function permissionLines(details, args) {
204
+ const result = asRecord(details);
205
+ if (!result || typeof result.ok !== "boolean") return void 0;
206
+ const runId = scalar(args.run_id);
207
+ return [
208
+ `Permission answer: ${result.ok ? "accepted" : "failed"}.`,
209
+ `Run: ${runId}`,
210
+ `Option: ${scalar(args.option_id)}`,
211
+ ...stringValue(args.request_id) ? [`Request: ${scalar(args.request_id)}`] : [],
212
+ `Guidance: Call avenor_status with run_id "${runId}" to verify the permission state.`
213
+ ];
214
+ }
215
+ function followUpLines(details, args) {
216
+ const result = asRecord(details);
217
+ if (!result || !stringValue(result.run_id) || !stringValue(result.label)) return void 0;
218
+ const runId = selected(result, "run_id", args.run_id);
219
+ const label = selected(result, "label", args.label, runId);
220
+ return [
221
+ `Follow-up: ${label} — dispatched.`,
222
+ `Source run: ${scalar(args.run_id)}`,
223
+ `Run: ${runId}`,
224
+ `Label: ${label}`,
225
+ `Guidance: Call avenor_status with run_id "${runId}".`
226
+ ];
227
+ }
228
+ function eventSequence(value) {
229
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
230
+ return typeof value === "string" && value.trim() && Number.isFinite(Number(value)) ? value : void 0;
231
+ }
232
+ function eventRow(value) {
233
+ const name = stringValue(value.event) ?? stringValue(value.type) ?? "unknown";
234
+ const sequence = eventSequence(value.seq);
235
+ const text = extractEventText(value);
236
+ const previewText = text ? scalar(text, "unknown", EVENT_CHARS) : void 0;
237
+ return `Event ${sequence ?? "?"}: ${scalar(name)}${previewText ? ` — ${previewText}` : ""}`;
238
+ }
239
+ function eventsLines(details, args) {
240
+ const result = asRecord(details);
241
+ if (!result || !Array.isArray(result.events) || result.events.some((event) => !asRecord(event))) return void 0;
242
+ const events = result.events.map((event) => event);
243
+ const runId = scalar(args.run_id);
244
+ const types = Array.isArray(args.types) ? args.types : [];
245
+ const displayedTypes = types.slice(0, COLLECTION_ITEMS).map((type) => scalar(type)).join(", ");
246
+ const lines = [`Events: ${runId} — ${events.length} events`, ...types.length > 0 ? [`Filter: ${displayedTypes}${types.length > COLLECTION_ITEMS ? ` ${itemMarker(types.length - COLLECTION_ITEMS)}` : ""}`] : []];
247
+ if (events.length === 0) return [...lines, "No events."];
248
+ const displayed = events.slice(0, STATUS_ROWS);
249
+ lines.push(...displayed.map(eventRow));
250
+ if (events.length > displayed.length) lines.push(itemMarker(events.length - displayed.length));
251
+ return lines;
252
+ }
253
+ function inspectTranscriptRow(value) {
254
+ const kind = stringValue(value.kind);
255
+ const text = optionalScalar(value.text);
256
+ if (!kind) return void 0;
257
+ if (kind === "tool") {
258
+ const title = selected(value, "title", stringValue(value.kind) ?? stringValue(value.key));
259
+ const status = optionalScalar(value.status);
260
+ return `${title}${status ? ` (${status})` : ""}${text ? `: ${text}` : ""}`;
261
+ }
262
+ if (!text) return void 0;
263
+ if ([
264
+ "assistant",
265
+ "thought",
266
+ "user",
267
+ "permission",
268
+ "status",
269
+ "session"
270
+ ].includes(kind)) return `${kind}: ${text}`;
271
+ }
272
+ function inspectToolRow(prefix, value) {
273
+ const title = selected(value, "title", stringValue(value.kind) ?? stringValue(value.key));
274
+ const status = optionalScalar(value.status);
275
+ const text = optionalScalar(value.preview);
276
+ return `${prefix}: ${title}${status ? ` (${status})` : ""}${text ? `: ${text}` : ""}`;
277
+ }
278
+ function inspectPermissionRow(value) {
279
+ const description = scalar(stringValue(value.description) ?? stringValue(value.question) ?? stringValue(value.tool) ?? stringValue(value.kind));
280
+ const options = permissionOptions(value);
281
+ const rows = optionRows(options);
282
+ return `Permission: ${description} (request_id: ${selected(value, "request_id")}); options:${rows ? ` ${rows}` : ""}${options.length > COLLECTION_ITEMS ? ` ${itemMarker(options.length - COLLECTION_ITEMS)}` : ""}`;
283
+ }
284
+ function inspectGroup(title, values, row) {
285
+ const rows = (Array.isArray(values) ? values.map(asRecord).filter((value) => !!value) : []).map(row).filter((value) => !!value);
286
+ return [
287
+ `${title}:`,
288
+ ...rows.slice(0, INSPECT_ITEMS),
289
+ ...rows.length > INSPECT_ITEMS ? [itemMarker(rows.length - INSPECT_ITEMS)] : []
290
+ ];
291
+ }
292
+ function inspectLines(details, args) {
293
+ const inspect = asRecord(details);
294
+ const status = asRecord(inspect?.status);
295
+ if (!inspect || !status || !hasStatusIdentity(inspect) && !hasStatusIdentity(status)) return void 0;
296
+ const runId = selected(inspect, "run_id", args.run_id);
297
+ const label = selected(inspect, "label", void 0, runId);
298
+ const state = selected(status, "status");
299
+ const finalPreview = preview(inspect.final_output, FINAL_OUTPUT_CHARS$1);
300
+ const transcript = Array.isArray(inspect.transcript) ? inspect.transcript : [];
301
+ const tools = Array.isArray(inspect.tools) ? inspect.tools : [];
302
+ const liveTools = Array.isArray(inspect.live_tools) ? inspect.live_tools : [];
303
+ const permissions = Array.isArray(inspect.permissions) ? inspect.permissions : [];
304
+ return [
305
+ `Inspect: ${label} — ${state}`,
306
+ `Run: ${runId}`,
307
+ `Counts: transcript=${transcript.length}, tools=${tools.length}, live_tools=${liveTools.length}, permissions=${permissions.length}`,
308
+ ...finalPreview ? ["Final output:", ...finalPreview] : ["Final output: unavailable."],
309
+ `Guidance: Call avenor_events with run_id "${runId}" for raw events.`,
310
+ ...inspectGroup("Transcript", transcript, inspectTranscriptRow),
311
+ ...inspectGroup("Tools", tools, (value) => inspectToolRow("Tool", value)),
312
+ ...inspectGroup("Live tools", liveTools, (value) => inspectToolRow("Live tool", value)),
313
+ ...inspectGroup("Permissions", permissions, inspectPermissionRow)
314
+ ];
315
+ }
316
+ function shutdownLines(details, args) {
317
+ const result = asRecord(details);
318
+ if (!result || typeof result.ok !== "boolean" || !Array.isArray(result.cleaned_up)) return void 0;
319
+ const paths = result.cleaned_up.filter((path) => typeof path === "string");
320
+ const supervisor = stringValue(args.supervisor_id) ? scalar(args.supervisor_id) : "singleton";
321
+ return [
322
+ `Avenor shutdown: ${result.ok ? "succeeded" : "failed"}.`,
323
+ `Supervisor: ${supervisor}`,
324
+ `Mode: ${args.force === true ? "force" : "graceful"}`,
325
+ paths.length > 0 ? `Cleanup: ${paths.length} paths` : "Cleanup: none",
326
+ ...!result.ok ? ["Warning: shutdown did not complete.", "Guidance: retry avenor_shutdown."] : [],
327
+ ...paths.slice(0, CLEANUP_ITEMS).map((path) => `Cleanup path: ${scalar(path)}`),
328
+ ...paths.length > CLEANUP_ITEMS ? [itemMarker(paths.length - CLEANUP_ITEMS)] : []
329
+ ];
330
+ }
331
+ function formatStatusOutput(args, result) {
332
+ const statuses = Array.isArray(result) ? result : void 0;
333
+ if (statuses) return {
334
+ title: `Avenor status — ${statuses.length} runs`,
335
+ output: output(statusLines(statuses, args), "status"),
336
+ metadata: {
337
+ results: statuses,
338
+ count: statuses.length
339
+ }
340
+ };
341
+ const record = result;
342
+ return {
343
+ title: `${selected(record, "label", void 0, selected(record, "run_id", args.run_id))} — ${selected(record, "status")}`,
344
+ output: output(statusLines(result, args), "status"),
345
+ metadata: { ...result }
346
+ };
347
+ }
348
+ function formatResultOutput(args, result) {
349
+ const record = result;
350
+ return {
351
+ title: `${selected(record, "label", void 0, selected(record, "run_id", args.run_id))} — ${selected(record, "status")}${result.timed_out === true ? " (wait timed out)" : ""}`,
352
+ output: output(resultLines(result, args), "result"),
353
+ metadata: { ...result }
354
+ };
355
+ }
356
+ function formatAnswerPermissionOutput(args, result) {
357
+ return {
358
+ title: `${scalar(args.run_id)} — permission answered`,
359
+ output: output(permissionLines(result, args), "answer_permission"),
360
+ metadata: {
361
+ ...result,
362
+ run_id: args.run_id,
363
+ option_id: args.option_id,
364
+ request_id: args.request_id
365
+ }
366
+ };
367
+ }
368
+ function formatFollowUpOutput(args, result) {
369
+ const record = result;
370
+ const runId = selected(record, "run_id", args.run_id);
371
+ return {
372
+ title: `${selected(record, "label", args.label, runId)} — dispatched`,
373
+ output: output(followUpLines(result, args), "follow_up"),
374
+ metadata: {
375
+ ...result,
376
+ source_run_id: args.run_id
377
+ }
378
+ };
379
+ }
380
+ function formatEventsOutput(args, result) {
381
+ return {
382
+ title: `${scalar(args.run_id)} — ${result.events.length} events`,
383
+ output: output(eventsLines(result, args), "events"),
384
+ metadata: {
385
+ ...result,
386
+ run_id: args.run_id,
387
+ count: result.events.length
388
+ }
389
+ };
390
+ }
391
+ function formatInspectOutput(args, result) {
392
+ const record = result;
393
+ const status = result.status;
394
+ return {
395
+ title: `${selected(record, "label", void 0, selected(record, "run_id", args.run_id))} — ${selected(status, "status")}`,
396
+ output: output(inspectLines(result, args), "inspect"),
397
+ metadata: { ...result }
398
+ };
399
+ }
400
+ function formatShutdownOutput(args, result) {
401
+ return {
402
+ title: "Avenor — shut down",
403
+ output: output(shutdownLines(result, args), "shutdown"),
404
+ metadata: {
405
+ ...result,
406
+ supervisor_id: args.supervisor_id,
407
+ force: args.force ?? false
408
+ }
409
+ };
410
+ }
411
+ //#endregion
4
412
  //#region src/plugin.ts
5
413
  const TERMINAL_STATUSES = new Set([
6
414
  "done",
@@ -54,7 +462,20 @@ function formatTranscriptPreview(snapshot) {
54
462
  if (lines.length === 0) return void 0;
55
463
  return clipTail(lines.join("\n"), METADATA_TEXT_CHARS);
56
464
  }
57
- function buildSnapshotMetadata(snapshot) {
465
+ /** Project shared spawn identity into OpenCode metadata without resolving rosters here. */
466
+ function hostIdentityMetadata(run = {}, status) {
467
+ const metadata = {};
468
+ const add = (key, value) => {
469
+ if (typeof value === "string" && value.length > 0) metadata[key] = value;
470
+ };
471
+ add("roster_file", status?.roster_file ?? run.rosterFile);
472
+ add("roster_entry", status?.roster_entry ?? run.rosterEntry);
473
+ add("effective_agent", status?.effective_agent ?? status?.agent ?? run.effectiveAgent);
474
+ add("effective_model", status?.effective_model ?? status?.model ?? run.effectiveModel);
475
+ add("effective_backend", status?.effective_backend ?? status?.backend ?? run.effectiveBackend);
476
+ return metadata;
477
+ }
478
+ function buildSnapshotMetadata(snapshot, run, status) {
58
479
  const metadata = {};
59
480
  if (snapshot.phase) metadata.phase = snapshot.phase;
60
481
  if (snapshot.latest_seq !== void 0) metadata.latest_seq = snapshot.latest_seq;
@@ -64,6 +485,7 @@ function buildSnapshotMetadata(snapshot) {
64
485
  if (transcriptPreview) metadata.transcript_preview = transcriptPreview;
65
486
  const pendingPermission = clipTail(snapshot.pending_permission?.description ?? snapshot.pending_permission?.question ?? snapshot.phase_label, METADATA_TEXT_CHARS);
66
487
  if (pendingPermission) metadata.pending_permission = pendingPermission;
488
+ Object.assign(metadata, hostIdentityMetadata(run, status));
67
489
  return metadata;
68
490
  }
69
491
  function buildWaitingText(run, snapshot) {
@@ -136,6 +558,16 @@ const AvenorPlugin = async (ctx) => {
136
558
  if (snapshot.latest_seq !== void 0) run.latestSeq = snapshot.latest_seq;
137
559
  registerSessionId(snapshot.identity.session_id, snapshot.identity.runtime_id, run.runId);
138
560
  }
561
+ function updateRunIdentity(run, status) {
562
+ run.rosterFile = status.roster_file ?? run.rosterFile;
563
+ run.rosterEntry = status.roster_entry ?? run.rosterEntry;
564
+ run.effectiveAgent = status.effective_agent ?? status.agent ?? run.effectiveAgent;
565
+ run.effectiveModel = status.effective_model ?? status.model ?? run.effectiveModel;
566
+ run.effectiveBackend = status.effective_backend ?? status.backend ?? run.effectiveBackend;
567
+ run.agent = run.effectiveAgent ?? run.agent;
568
+ run.model = run.effectiveModel ?? run.model;
569
+ run.backend = run.effectiveBackend ?? run.backend;
570
+ }
139
571
  async function primeTrackedRun(run, options = {}) {
140
572
  try {
141
573
  const raw = await statusTool({
@@ -145,6 +577,7 @@ const AvenorPlugin = async (ctx) => {
145
577
  const status = Array.isArray(raw) ? raw[0] : raw;
146
578
  if (!status) return void 0;
147
579
  registerSessionId(status.session_id, status.runtime_id, run.runId);
580
+ updateRunIdentity(run, status);
148
581
  if (status.latest_seq !== void 0 && options.captureLatestSeq) run.latestSeq = status.latest_seq;
149
582
  return status;
150
583
  } catch {
@@ -170,10 +603,12 @@ const AvenorPlugin = async (ctx) => {
170
603
  }
171
604
  async function loadInspection(run) {
172
605
  try {
173
- return await inspectTool({
606
+ const inspection = await inspectTool({
174
607
  runId: run.runId,
175
608
  supervisorId: run.supervisorId
176
609
  });
610
+ updateRunIdentity(run, inspection.status);
611
+ return inspection;
177
612
  } catch {
178
613
  return;
179
614
  }
@@ -432,33 +867,54 @@ const AvenorPlugin = async (ctx) => {
432
867
  },
433
868
  tool: {
434
869
  avenor_spawn: tool({
435
- description: "Dispatch an agent run via avenor. Blocks by default, showing live progress as an updating tool call. Set wait=false for fire-and-forget — you will be re-prompted automatically on completion.",
870
+ description: "Dispatch an agent run via avenor. Optional thinking accepts off, minimal, low, medium, high, xhigh, or max; unsupported backends reject explicit values. Blocks by default, showing live progress as an updating tool call. Set wait=false for fire-and-forget — you will be re-prompted automatically on completion.",
436
871
  args: {
437
- agent: tool.schema.string().describe("Agent name (required, no default)"),
872
+ agent: tool.schema.string().optional().describe("Optional agent name; omission uses the supplied model or runtime defaults"),
438
873
  prompt: tool.schema.string().optional().describe("Prompt text"),
439
874
  prompt_file: tool.schema.string().optional().describe("Path to prompt file"),
440
875
  dir: tool.schema.string().optional().describe("Working directory for the run (defaults to the session project directory)"),
441
876
  label: tool.schema.string().optional().describe("Human-readable label for the run"),
442
877
  timeout: tool.schema.string().optional().describe("Timeout duration (e.g. 3600s)"),
443
878
  model: tool.schema.string().optional().describe("Model override"),
879
+ thinking: tool.schema.enum([
880
+ "off",
881
+ "minimal",
882
+ "low",
883
+ "medium",
884
+ "high",
885
+ "xhigh",
886
+ "max"
887
+ ]).optional().describe("Thinking level; omission uses the backend default"),
444
888
  backend: tool.schema.string().optional().describe("Backend override"),
889
+ roster_file: tool.schema.string().optional().describe("Path to the roster map"),
890
+ roster_entry: tool.schema.string().optional().describe("Roster entry to select"),
445
891
  server_url: tool.schema.string().optional().describe("Backend server URL"),
446
892
  supervisor_id: tool.schema.string().optional().describe("Reuse an existing supervisor by socket path"),
447
893
  wait: tool.schema.boolean().default(true).describe("Block until complete with live status updates. False = fire-and-forget.")
448
894
  },
449
895
  async execute(args, context) {
450
- const ownerRunId = ensureParentRunId();
451
- const result = await spawnTool({
896
+ validateSpawnSelection({
452
897
  agent: args.agent,
453
- prompt: args.prompt,
454
- promptFile: args.prompt_file,
455
- label: args.label,
456
- dir: args.dir ?? context.directory,
457
- timeout: args.timeout,
458
898
  model: args.model,
459
899
  backend: args.backend,
460
- serverUrl: args.server_url,
461
- supervisorId: args.supervisor_id,
900
+ roster_file: args.roster_file,
901
+ roster_entry: args.roster_entry
902
+ });
903
+ const ownerRunId = ensureParentRunId();
904
+ const result = await spawnTool({
905
+ ...args.agent !== void 0 ? { agent: args.agent } : {},
906
+ ...args.prompt !== void 0 ? { prompt: args.prompt } : {},
907
+ ...args.prompt_file !== void 0 ? { promptFile: args.prompt_file } : {},
908
+ ...args.label !== void 0 ? { label: args.label } : {},
909
+ dir: args.dir ?? context.directory,
910
+ ...args.timeout !== void 0 ? { timeout: args.timeout } : {},
911
+ ...args.model !== void 0 ? { model: args.model } : {},
912
+ ...args.thinking !== void 0 ? { thinking: args.thinking } : {},
913
+ ...args.backend !== void 0 ? { backend: args.backend } : {},
914
+ ...args.roster_file !== void 0 ? { rosterFile: args.roster_file } : {},
915
+ ...args.roster_entry !== void 0 ? { rosterEntry: args.roster_entry } : {},
916
+ ...args.server_url !== void 0 ? { serverUrl: args.server_url } : {},
917
+ ...args.supervisor_id !== void 0 ? { supervisorId: args.supervisor_id } : {},
462
918
  parent_run_id: ownerRunId
463
919
  });
464
920
  if (result.broker_url) brokerUrl = result.broker_url;
@@ -467,6 +923,10 @@ const AvenorPlugin = async (ctx) => {
467
923
  runId: result.run_id,
468
924
  runtimeId: result.runtime_id,
469
925
  agent: args.agent,
926
+ model: args.model,
927
+ backend: args.backend,
928
+ rosterFile: args.roster_file,
929
+ rosterEntry: args.roster_entry,
470
930
  orchestratorSessionId: context.sessionID,
471
931
  label: result.label,
472
932
  supervisorId: result.supervisor_id || args.supervisor_id,
@@ -479,7 +939,8 @@ const AvenorPlugin = async (ctx) => {
479
939
  monitorRun(run).catch(console.error);
480
940
  return {
481
941
  title: `${result.label} — dispatched`,
482
- output: `Dispatched "${result.label}" (run_id: ${result.run_id}). Completion will be delivered automatically; use avenor_status with view "lifecycle" for progress or avenor_result to wait for the final result.`
942
+ output: `Dispatched "${result.label}" (run_id: ${result.run_id}). Completion will be delivered automatically; use avenor_status with view "lifecycle" for progress or avenor_result to wait for the final result.`,
943
+ metadata: hostIdentityMetadata(run)
483
944
  };
484
945
  }
485
946
  context.metadata({ title: `Starting ${result.label}…` });
@@ -488,7 +949,7 @@ const AvenorPlugin = async (ctx) => {
488
949
  onSnapshot: (snapshot) => {
489
950
  context.metadata({
490
951
  title: snapshot.pending_permission ? `[blocked] ${result.label}` : result.label,
491
- metadata: buildSnapshotMetadata(snapshot)
952
+ metadata: buildSnapshotMetadata(snapshot, run)
492
953
  });
493
954
  }
494
955
  });
@@ -503,7 +964,7 @@ const AvenorPlugin = async (ctx) => {
503
964
  run_id: result.run_id,
504
965
  session_id: inspection?.status.session_id ?? snapshot.identity.session_id,
505
966
  final_output: clipTail(inspection?.final_output ?? snapshot.final_output, FINAL_OUTPUT_CHARS),
506
- ...buildSnapshotMetadata(snapshot)
967
+ ...buildSnapshotMetadata(snapshot, run, inspection?.status)
507
968
  }
508
969
  };
509
970
  }
@@ -516,20 +977,26 @@ const AvenorPlugin = async (ctx) => {
516
977
  metadata: {
517
978
  run_id: result.run_id,
518
979
  session_id: inspection?.status.session_id ?? snapshot.identity.session_id,
519
- ...buildSnapshotMetadata(snapshot)
980
+ ...buildSnapshotMetadata(snapshot, run, inspection?.status)
520
981
  }
521
982
  };
522
983
  }
523
984
  if (outcome.kind === "error") return {
524
985
  title: `${result.label} — monitoring error`,
525
986
  output: `Monitoring failed: ${outcome.error ?? "unknown error"}. The run may still be active — use avenor_status to check.`,
526
- metadata: { run_id: result.run_id }
987
+ metadata: {
988
+ run_id: result.run_id,
989
+ ...hostIdentityMetadata(run)
990
+ }
527
991
  };
528
992
  unregisterRun(result.run_id);
529
993
  return {
530
994
  title: `${result.label} — aborted`,
531
995
  output: `Monitoring of "${result.label}" (run_id: ${result.run_id}) was interrupted. The run may still be active — use avenor_status to check.`,
532
- metadata: { run_id: result.run_id }
996
+ metadata: {
997
+ run_id: result.run_id,
998
+ ...hostIdentityMetadata(run)
999
+ }
533
1000
  };
534
1001
  }
535
1002
  }),
@@ -541,12 +1008,11 @@ const AvenorPlugin = async (ctx) => {
541
1008
  supervisor_id: tool.schema.string().optional().describe("Reuse an existing supervisor by socket path")
542
1009
  },
543
1010
  async execute(args, _context) {
544
- const result = await statusTool({
1011
+ return formatStatusOutput(args, await statusTool({
545
1012
  runId: args.run_id,
546
1013
  supervisorId: args.supervisor_id,
547
1014
  view: args.view
548
- });
549
- return JSON.stringify(result, null, 2);
1015
+ }));
550
1016
  }
551
1017
  }),
552
1018
  avenor_result: tool({
@@ -570,7 +1036,7 @@ const AvenorPlugin = async (ctx) => {
570
1036
  });
571
1037
  if (result.ready) unregisterRun(args.run_id);
572
1038
  else if (run) monitorRun(run).catch(console.error);
573
- return JSON.stringify(result, null, 2);
1039
+ return formatResultOutput(args, result);
574
1040
  } catch (error) {
575
1041
  if (run) monitorRun(run).catch(console.error);
576
1042
  throw error;
@@ -587,14 +1053,13 @@ const AvenorPlugin = async (ctx) => {
587
1053
  message: tool.schema.string().optional().describe("Optional write-in response text. Required only when the selected option declares requiresMessage: true.")
588
1054
  },
589
1055
  async execute(args, _context) {
590
- const result = await answerPermissionTool({
1056
+ return formatAnswerPermissionOutput(args, await answerPermissionTool({
591
1057
  runId: args.run_id,
592
1058
  optionId: args.option_id,
593
1059
  requestId: args.request_id,
594
1060
  supervisorId: args.supervisor_id,
595
1061
  message: args.message
596
- });
597
- return JSON.stringify(result, null, 2);
1062
+ }));
598
1063
  }
599
1064
  }),
600
1065
  avenor_follow_up: tool({
@@ -624,7 +1089,7 @@ const AvenorPlugin = async (ctx) => {
624
1089
  trackedRuns.set(result.run_id, run);
625
1090
  await primeTrackedRun(run);
626
1091
  monitorRun(run).catch(console.error);
627
- return JSON.stringify(result, null, 2);
1092
+ return formatFollowUpOutput(args, result);
628
1093
  }
629
1094
  }),
630
1095
  avenor_events: tool({
@@ -636,13 +1101,12 @@ const AvenorPlugin = async (ctx) => {
636
1101
  supervisor_id: tool.schema.string().optional().describe("Reuse an existing supervisor by socket path")
637
1102
  },
638
1103
  async execute(args, _context) {
639
- const result = await eventsTool({
1104
+ return formatEventsOutput(args, await eventsTool({
640
1105
  runId: args.run_id,
641
1106
  types: args.types,
642
1107
  limit: args.limit,
643
1108
  supervisorId: args.supervisor_id
644
- });
645
- return JSON.stringify(result, null, 2);
1109
+ }));
646
1110
  }
647
1111
  }),
648
1112
  avenor_inspect: tool({
@@ -654,13 +1118,12 @@ const AvenorPlugin = async (ctx) => {
654
1118
  supervisor_id: tool.schema.string().optional().describe("Reuse an existing supervisor by socket path")
655
1119
  },
656
1120
  async execute(args, _context) {
657
- const result = await inspectTool({
1121
+ return formatInspectOutput(args, await inspectTool({
658
1122
  runId: args.run_id,
659
1123
  limit: args.limit,
660
1124
  after_seq: args.after_seq,
661
1125
  supervisorId: args.supervisor_id
662
- });
663
- return JSON.stringify(result, null, 2);
1126
+ }));
664
1127
  }
665
1128
  }),
666
1129
  avenor_shutdown: tool({
@@ -671,15 +1134,14 @@ const AvenorPlugin = async (ctx) => {
671
1134
  },
672
1135
  async execute(args, _context) {
673
1136
  await stopTrackedRuns((run) => !args.supervisor_id || run.supervisorId === args.supervisor_id);
674
- const result = await shutdownTool({
1137
+ return formatShutdownOutput(args, await shutdownTool({
675
1138
  supervisorId: args.supervisor_id,
676
1139
  force: args.force
677
- });
678
- return JSON.stringify(result, null, 2);
1140
+ }));
679
1141
  }
680
1142
  })
681
1143
  }
682
1144
  };
683
1145
  };
684
1146
  //#endregion
685
- export { AvenorPlugin, buildWaitingText, terminalStatus };
1147
+ export { AvenorPlugin, buildSnapshotMetadata, buildWaitingText, hostIdentityMetadata, terminalStatus };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dougbots/avenor-opencode",
3
- "version": "0.8.3",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,7 +27,7 @@
27
27
  "dev": "tsdown --watch"
28
28
  },
29
29
  "dependencies": {
30
- "@dougbots/avenor-core": "^0.8.3"
30
+ "@dougbots/avenor-core": "^0.10.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@opencode-ai/plugin": "1.15.3"