@dougbots/avenor-opencode 0.9.0 → 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
@@ -58,17 +58,36 @@ Dispatch an agent run in the current working directory.
58
58
 
59
59
  | Argument | Required | Description |
60
60
  |---|---|---|
61
- | `agent` | yes | Agent name (no default — must be specified) |
61
+ | `agent` | no | Agent name; omission uses the supplied model or runtime defaults |
62
62
  | `prompt` | no | Prompt text |
63
63
  | `prompt_file` | no | Path to a prompt file |
64
64
  | `label` | no | Human-readable label for the run |
65
65
  | `timeout` | no | Timeout duration (e.g. `3600s`, `30m`) |
66
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` |
67
71
  | `wait` | no | Block until complete (default `true`) |
68
72
  | `supervisor_id` | no | Reuse an existing supervisor by socket path |
69
73
 
70
74
  The working directory (`dir`) is injected automatically from your OpenCode session context — it is not a user-facing argument.
71
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
+
72
91
  ### `avenor_status`
73
92
 
74
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.
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,6 @@
1
1
  import { tool } from "@opencode-ai/plugin";
2
2
  import * as crypto from "node:crypto";
3
- import { Supervisor, answerPermissionTool, createRunSnapshot, dial, eventsTool, extractEventText, 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
4
  //#region src/render.ts
5
5
  const FINAL_OUTPUT_CHARS$1 = 1500;
6
6
  const SCALAR_CHARS = 240;
@@ -462,7 +462,20 @@ function formatTranscriptPreview(snapshot) {
462
462
  if (lines.length === 0) return void 0;
463
463
  return clipTail(lines.join("\n"), METADATA_TEXT_CHARS);
464
464
  }
465
- 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) {
466
479
  const metadata = {};
467
480
  if (snapshot.phase) metadata.phase = snapshot.phase;
468
481
  if (snapshot.latest_seq !== void 0) metadata.latest_seq = snapshot.latest_seq;
@@ -472,6 +485,7 @@ function buildSnapshotMetadata(snapshot) {
472
485
  if (transcriptPreview) metadata.transcript_preview = transcriptPreview;
473
486
  const pendingPermission = clipTail(snapshot.pending_permission?.description ?? snapshot.pending_permission?.question ?? snapshot.phase_label, METADATA_TEXT_CHARS);
474
487
  if (pendingPermission) metadata.pending_permission = pendingPermission;
488
+ Object.assign(metadata, hostIdentityMetadata(run, status));
475
489
  return metadata;
476
490
  }
477
491
  function buildWaitingText(run, snapshot) {
@@ -544,6 +558,16 @@ const AvenorPlugin = async (ctx) => {
544
558
  if (snapshot.latest_seq !== void 0) run.latestSeq = snapshot.latest_seq;
545
559
  registerSessionId(snapshot.identity.session_id, snapshot.identity.runtime_id, run.runId);
546
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
+ }
547
571
  async function primeTrackedRun(run, options = {}) {
548
572
  try {
549
573
  const raw = await statusTool({
@@ -553,6 +577,7 @@ const AvenorPlugin = async (ctx) => {
553
577
  const status = Array.isArray(raw) ? raw[0] : raw;
554
578
  if (!status) return void 0;
555
579
  registerSessionId(status.session_id, status.runtime_id, run.runId);
580
+ updateRunIdentity(run, status);
556
581
  if (status.latest_seq !== void 0 && options.captureLatestSeq) run.latestSeq = status.latest_seq;
557
582
  return status;
558
583
  } catch {
@@ -578,10 +603,12 @@ const AvenorPlugin = async (ctx) => {
578
603
  }
579
604
  async function loadInspection(run) {
580
605
  try {
581
- return await inspectTool({
606
+ const inspection = await inspectTool({
582
607
  runId: run.runId,
583
608
  supervisorId: run.supervisorId
584
609
  });
610
+ updateRunIdentity(run, inspection.status);
611
+ return inspection;
585
612
  } catch {
586
613
  return;
587
614
  }
@@ -842,7 +869,7 @@ const AvenorPlugin = async (ctx) => {
842
869
  avenor_spawn: tool({
843
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.",
844
871
  args: {
845
- 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"),
846
873
  prompt: tool.schema.string().optional().describe("Prompt text"),
847
874
  prompt_file: tool.schema.string().optional().describe("Path to prompt file"),
848
875
  dir: tool.schema.string().optional().describe("Working directory for the run (defaults to the session project directory)"),
@@ -859,24 +886,35 @@ const AvenorPlugin = async (ctx) => {
859
886
  "max"
860
887
  ]).optional().describe("Thinking level; omission uses the backend default"),
861
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"),
862
891
  server_url: tool.schema.string().optional().describe("Backend server URL"),
863
892
  supervisor_id: tool.schema.string().optional().describe("Reuse an existing supervisor by socket path"),
864
893
  wait: tool.schema.boolean().default(true).describe("Block until complete with live status updates. False = fire-and-forget.")
865
894
  },
866
895
  async execute(args, context) {
867
- const ownerRunId = ensureParentRunId();
868
- const result = await spawnTool({
896
+ validateSpawnSelection({
869
897
  agent: args.agent,
870
- prompt: args.prompt,
871
- promptFile: args.prompt_file,
872
- label: args.label,
873
- dir: args.dir ?? context.directory,
874
- timeout: args.timeout,
875
898
  model: args.model,
876
- thinking: args.thinking,
877
899
  backend: args.backend,
878
- serverUrl: args.server_url,
879
- 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 } : {},
880
918
  parent_run_id: ownerRunId
881
919
  });
882
920
  if (result.broker_url) brokerUrl = result.broker_url;
@@ -885,6 +923,10 @@ const AvenorPlugin = async (ctx) => {
885
923
  runId: result.run_id,
886
924
  runtimeId: result.runtime_id,
887
925
  agent: args.agent,
926
+ model: args.model,
927
+ backend: args.backend,
928
+ rosterFile: args.roster_file,
929
+ rosterEntry: args.roster_entry,
888
930
  orchestratorSessionId: context.sessionID,
889
931
  label: result.label,
890
932
  supervisorId: result.supervisor_id || args.supervisor_id,
@@ -897,7 +939,8 @@ const AvenorPlugin = async (ctx) => {
897
939
  monitorRun(run).catch(console.error);
898
940
  return {
899
941
  title: `${result.label} — dispatched`,
900
- 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)
901
944
  };
902
945
  }
903
946
  context.metadata({ title: `Starting ${result.label}…` });
@@ -906,7 +949,7 @@ const AvenorPlugin = async (ctx) => {
906
949
  onSnapshot: (snapshot) => {
907
950
  context.metadata({
908
951
  title: snapshot.pending_permission ? `[blocked] ${result.label}` : result.label,
909
- metadata: buildSnapshotMetadata(snapshot)
952
+ metadata: buildSnapshotMetadata(snapshot, run)
910
953
  });
911
954
  }
912
955
  });
@@ -921,7 +964,7 @@ const AvenorPlugin = async (ctx) => {
921
964
  run_id: result.run_id,
922
965
  session_id: inspection?.status.session_id ?? snapshot.identity.session_id,
923
966
  final_output: clipTail(inspection?.final_output ?? snapshot.final_output, FINAL_OUTPUT_CHARS),
924
- ...buildSnapshotMetadata(snapshot)
967
+ ...buildSnapshotMetadata(snapshot, run, inspection?.status)
925
968
  }
926
969
  };
927
970
  }
@@ -934,20 +977,26 @@ const AvenorPlugin = async (ctx) => {
934
977
  metadata: {
935
978
  run_id: result.run_id,
936
979
  session_id: inspection?.status.session_id ?? snapshot.identity.session_id,
937
- ...buildSnapshotMetadata(snapshot)
980
+ ...buildSnapshotMetadata(snapshot, run, inspection?.status)
938
981
  }
939
982
  };
940
983
  }
941
984
  if (outcome.kind === "error") return {
942
985
  title: `${result.label} — monitoring error`,
943
986
  output: `Monitoring failed: ${outcome.error ?? "unknown error"}. The run may still be active — use avenor_status to check.`,
944
- metadata: { run_id: result.run_id }
987
+ metadata: {
988
+ run_id: result.run_id,
989
+ ...hostIdentityMetadata(run)
990
+ }
945
991
  };
946
992
  unregisterRun(result.run_id);
947
993
  return {
948
994
  title: `${result.label} — aborted`,
949
995
  output: `Monitoring of "${result.label}" (run_id: ${result.run_id}) was interrupted. The run may still be active — use avenor_status to check.`,
950
- metadata: { run_id: result.run_id }
996
+ metadata: {
997
+ run_id: result.run_id,
998
+ ...hostIdentityMetadata(run)
999
+ }
951
1000
  };
952
1001
  }
953
1002
  }),
@@ -1095,4 +1144,4 @@ const AvenorPlugin = async (ctx) => {
1095
1144
  };
1096
1145
  };
1097
1146
  //#endregion
1098
- 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.9.0",
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.9.0"
30
+ "@dougbots/avenor-core": "^0.10.0"
31
31
  },
32
32
  "peerDependencies": {
33
33
  "@opencode-ai/plugin": "1.15.3"