opencode-agent-variants 0.1.0 → 0.2.0-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -75,15 +75,16 @@ The wizard supports:
75
75
  - editing parent overrides
76
76
  - editing variant overrides
77
77
  - enabling or disabling parents and variants
78
- - toggling debug mode
79
- - running diagnostics
80
- - viewing and clearing the debug log
81
78
  - deleting variants
79
+ - running diagnostics
80
+ - opening `Debug & advanced` to toggle debug mode, view/clear logs, and change wizard-only filters
82
81
  - previewing the generated config
83
82
  - saving changes with timestamped backups
84
83
 
85
84
  Agent/variant list changes take effect after restarting OpenCode because agents and plugins are assembled at startup. Debug mode is hot-read and takes effect immediately after the wizard saves it.
86
85
 
86
+ The wizard defaults to showing only subagent-capable parent agents when adding or editing parent entries. Agent Variants are meant for agents callable through OpenCode's `task` tool. You can temporarily show all agents from `Debug & advanced` if you need to inspect or repair existing config.
87
+
87
88
  ## Config File
88
89
 
89
90
  The plugin writes a sidecar config file at:
@@ -187,11 +188,11 @@ Agents defined in `opencode.json`, `.opencode/agent/*.md`, or global agent markd
187
188
  - If a variant resolves to a definitely missing model, it is skipped for that run and a warning toast is shown.
188
189
  - Conflicting aliases are skipped instead of overwriting existing agents.
189
190
 
190
- Use the wizard's `Run diagnostics` action to inspect model validation, alias conflicts, disabled parents, and plugin installation state.
191
+ Use the wizard's `Run diagnostics` action to inspect model validation, alias conflicts, disabled parents, task-callability, and plugin installation state.
191
192
 
192
193
  ## Debug Mode
193
194
 
194
- Debug mode is off by default. Enable it from the wizard with `Debug mode: off`.
195
+ Debug mode is off by default. Enable it from the wizard through `Debug & advanced`.
195
196
 
196
197
  When enabled, the server plugin emits diagnostic log lines and TUI toast notifications for built-in virtual variants:
197
198
 
@@ -202,7 +203,7 @@ When enabled, the server plugin emits diagnostic log lines and TUI toast notific
202
203
 
203
204
  Logs are written to `~/.config/opencode/agent-variants.debug.log`. The plugin does not write debug lines to stdout, because that can corrupt the terminal UI.
204
205
 
205
- Debug mode is stored in `agent-variants.jsonc` and takes effect immediately for future variant calls. The wizard can also view and clear the debug log.
206
+ Debug mode is stored in `agent-variants.jsonc` and takes effect immediately for future variant calls. The wizard can also view and clear the debug log from `Debug & advanced`.
206
207
 
207
208
  ## Development
208
209
 
@@ -230,28 +231,6 @@ Check package contents before publishing:
230
231
  npm pack --dry-run
231
232
  ```
232
233
 
233
- ## Release
234
-
235
- CI runs on pushes and pull requests. It installs with `npm ci`, typechecks, builds, and verifies package contents with `npm pack --dry-run`.
236
-
237
- Automated releases run when a version tag is pushed:
238
-
239
- ```sh
240
- git tag v0.1.0
241
- git push origin v0.1.0
242
- ```
243
-
244
- The release workflow:
245
-
246
- - installs dependencies
247
- - typechecks
248
- - builds `dist`
249
- - verifies package contents
250
- - publishes to npm with provenance
251
- - creates a GitHub release with generated notes
252
-
253
- Before the first automated publish, add an npm automation token as the repository secret `NPM_TOKEN`.
254
-
255
234
  ## License
256
235
 
257
236
  MIT
package/dist/config.d.ts CHANGED
@@ -128,6 +128,7 @@ export type TemplateContext = {
128
128
  model_label?: string;
129
129
  routed_agent?: string;
130
130
  };
131
+ export type AgentMode = "primary" | "subagent" | "all";
131
132
  export declare function defaultConfigDir(): string;
132
133
  export declare function defaultSidecarPath(configDir?: string): string;
133
134
  export declare function debugLogPath(configDir?: string): string;
@@ -195,10 +196,12 @@ export declare function templateContext(parent: string, key: string | undefined,
195
196
  export declare function generatedVariantDescription(parent: string, key: string, variant: VariantConfig, config: SidecarConfig): string;
196
197
  export declare function modelCatalogFromProviders(providers: unknown): ModelCatalog;
197
198
  export declare function validateModel(modelInput: string | undefined, config: SidecarConfig, catalog: ModelCatalog): string | undefined;
199
+ export declare function isSubagentCapableMode(mode: AgentMode | undefined): mode is "subagent" | "all" | undefined;
198
200
  export declare function diagnoseConfig(config: SidecarConfig, input: {
199
201
  agents: string[];
200
202
  providers: unknown;
201
203
  pluginEntries?: unknown[];
204
+ agentModes?: Record<string, AgentMode>;
202
205
  }): Diagnostic[];
203
206
  export declare function fingerprint(input: {
204
207
  parentSessionID: string;
package/dist/config.js CHANGED
@@ -176,6 +176,9 @@ export function validateModel(modelInput, config, catalog) {
176
176
  if (catalog.providersWithModelList.has(split.providerID) && !catalog.refs.has(model))
177
177
  return `Model "${model}" was not found in provider "${split.providerID}".`;
178
178
  }
179
+ export function isSubagentCapableMode(mode) {
180
+ return mode !== "primary";
181
+ }
179
182
  export function diagnoseConfig(config, input) {
180
183
  const diagnostics = [];
181
184
  const catalog = modelCatalogFromProviders(input.providers);
@@ -188,10 +191,14 @@ export function diagnoseConfig(config, input) {
188
191
  for (const [agent, entry] of Object.entries(config.agents)) {
189
192
  if (!knownAgents.has(agent))
190
193
  diagnostics.push({ level: "warning", agent, message: `Parent agent "${agent}" is not a known built-in or configured agent.` });
194
+ if (!isSubagentCapableMode(input.agentModes?.[agent]))
195
+ diagnostics.push({ level: "warning", agent, message: `Parent agent "${agent}" is primary-only and will not be callable by the task tool.` });
191
196
  if (entry.disable)
192
197
  diagnostics.push({ level: "info", agent, message: `Parent "${agent}" is disabled in sidecar config.` });
193
198
  for (const [key, variant] of Object.entries(entry.variants)) {
194
199
  const alias = variantName(agent, key, variant);
200
+ if (!isSubagentCapableMode(input.agentModes?.[agent]))
201
+ diagnostics.push({ level: "warning", agent, variant: key, alias, message: `Variant "${alias}" inherits a primary-only parent and will not be callable by the task tool.` });
195
202
  const issue = validateModel(variant.model, config, catalog);
196
203
  if (issue)
197
204
  diagnostics.push({ level: "warning", agent, variant: key, alias, message: `Variant "${alias}" disabled at runtime: ${issue}` });
package/dist/tui.js CHANGED
@@ -1,7 +1,14 @@
1
1
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
- import { BUILTIN_AGENT_DESCRIPTIONS, debugLogPath, defaultConfigDir, defaultSidecarPath, diagnoseConfig, loadSidecar, saveSidecar, variantName, } from "./config.js";
2
+ import { BUILTIN_AGENT_DESCRIPTIONS, debugLogPath, defaultConfigDir, defaultSidecarPath, diagnoseConfig, isSubagentCapableMode, loadSidecar, saveSidecar, variantName, } from "./config.js";
3
3
  // Constants.
4
4
  const BUILTIN_AGENT_KEYS = Object.keys(BUILTIN_AGENT_DESCRIPTIONS);
5
+ const BUILTIN_AGENT_MODES = {
6
+ build: "primary",
7
+ plan: "primary",
8
+ general: "subagent",
9
+ explore: "subagent",
10
+ scout: "subagent",
11
+ };
5
12
  const THEME_COLORS = ["primary", "secondary", "accent", "success", "warning", "error", "info"];
6
13
  const EDITABLE_FIELDS = [
7
14
  { key: "model", label: "Model", type: "string" },
@@ -23,6 +30,18 @@ function agentsFromState(api) {
23
30
  const merged = new Set([...BUILTIN_AGENT_KEYS, ...configured]);
24
31
  return [...merged].sort();
25
32
  }
33
+ function agentMode(api, agent) {
34
+ return api.state.config.agent?.[agent]?.mode ?? BUILTIN_AGENT_MODES[agent] ?? "all";
35
+ }
36
+ function agentModes(api) {
37
+ return Object.fromEntries(agentsFromState(api).map((agent) => [agent, agentMode(api, agent)]));
38
+ }
39
+ function selectableParentAgents(api, config, settings) {
40
+ const aliases = generatedAliasSet(config);
41
+ return agentsFromState(api)
42
+ .filter((agent) => !aliases.has(agent))
43
+ .filter((agent) => !settings.subagentCapableOnly || isSubagentCapableMode(agentMode(api, agent)));
44
+ }
26
45
  function modelOptions(api, config) {
27
46
  const opts = [];
28
47
  for (const [key, raw] of Object.entries(config.models)) {
@@ -154,21 +173,33 @@ function showAlert(ui, props) {
154
173
  });
155
174
  }
156
175
  // Main wizard flows.
157
- async function addVariant(api, config) {
158
- const aliases = generatedAliasSet(config);
159
- const agents = agentsFromState(api).filter((agent) => !aliases.has(agent));
176
+ async function addVariant(api, config, settings) {
177
+ const agents = selectableParentAgents(api, config, settings);
160
178
  if (agents.length === 0) {
161
- await showAlert(api.ui, { title: "No agents", message: "No agents available to add a variant to." });
179
+ await showAlert(api.ui, {
180
+ title: "No agents",
181
+ message: settings.subagentCapableOnly
182
+ ? "No subagent-capable agents are available. Open Debug & advanced and disable the parent filter only if you need to inspect or repair existing config."
183
+ : "No agents available to add a variant to.",
184
+ });
162
185
  return config;
163
186
  }
164
187
  const agentOpts = agents.map((a) => ({
165
188
  title: a,
166
189
  value: a,
167
- description: BUILTIN_AGENT_DESCRIPTIONS[a] ?? api.state.config.agent?.[a]?.description,
190
+ description: `${agentMode(api, a)} - ${BUILTIN_AGENT_DESCRIPTIONS[a] ?? api.state.config.agent?.[a]?.description ?? "Configured agent"}`,
168
191
  }));
169
192
  const agent = await showSelect(api.ui, { title: "Add variant - pick parent agent", options: agentOpts });
170
193
  if (!agent)
171
194
  return config;
195
+ if (settings.subagentCapableOnly && !isSubagentCapableMode(agentMode(api, agent))) {
196
+ await showAlert(api.ui, {
197
+ title: "Primary-only agent",
198
+ message: `"${agent}" is primary-only and cannot be used by the task tool. Agent Variants are intended for subagents.`,
199
+ });
200
+ api.ui.toast({ variant: "warning", title: "Variant not added", message: `${agent} is primary-only.` });
201
+ return config;
202
+ }
172
203
  const existingKeys = Object.keys(config.agents[agent]?.variants ?? {});
173
204
  const defaultKey = existingKeys.length === 0 ? "light" : undefined;
174
205
  const key = await showPrompt(api.ui, {
@@ -584,6 +615,7 @@ async function runDiagnostics(api, config) {
584
615
  agents: agentsFromState(api).filter((agent) => !generatedAliases.has(agent)),
585
616
  providers: api.state.provider,
586
617
  pluginEntries: api.state.config.plugin,
618
+ agentModes: agentModes(api),
587
619
  });
588
620
  const errors = diagnostics.filter((item) => item.level === "error").length;
589
621
  const warnings = diagnostics.filter((item) => item.level === "warning").length;
@@ -729,7 +761,7 @@ function formatInputValue(value) {
729
761
  return JSON.stringify(value);
730
762
  }
731
763
  // Main menu loop.
732
- async function mainMenu(api, config) {
764
+ async function mainMenu(api, config, settings) {
733
765
  const agentCount = Object.keys(config.agents).length;
734
766
  const vCount = variantCount(config);
735
767
  const opts = [
@@ -737,15 +769,9 @@ async function mainMenu(api, config) {
737
769
  { title: "Edit parent fields...", value: "edit-parent", description: "Override fields on an agent parent" },
738
770
  { title: "Edit variant...", value: "edit-variant", description: "Change fields on an existing variant" },
739
771
  { title: "Toggle disable...", value: "toggle", description: "Enable or disable agents/variants" },
740
- { title: "Run diagnostics", value: "diagnostics", description: "Check models, conflicts, plugin install, and disabled variants" },
741
- {
742
- title: `Debug mode: ${config.debug ? "on" : "off"}`,
743
- value: "debug",
744
- description: "Toggle routing/model diagnostic toasts immediately",
745
- },
746
- { title: "View debug log", value: "view-log", description: "Show recent agent-variants.debug.log entries" },
747
- { title: "Clear debug log", value: "clear-log", description: "Empty agent-variants.debug.log" },
748
772
  { title: "Delete variant...", value: "delete", description: "Remove a variant" },
773
+ { title: "Run diagnostics", value: "diagnostics", description: "Check models, conflicts, plugin install, and task-callability" },
774
+ { title: "Debug & advanced...", value: "advanced", description: "Debug mode, logs, and wizard-only parent filter" },
749
775
  { title: "Preview configuration", value: "preview", description: `View current config (${agentCount} agents, ${vCount} variants)` },
750
776
  { title: "Save & exit", value: "save", description: "Write to disk with backup" },
751
777
  ];
@@ -756,29 +782,23 @@ async function mainMenu(api, config) {
756
782
  });
757
783
  switch (action) {
758
784
  case "add":
759
- return mainMenu(api, await addVariant(api, config));
785
+ return mainMenu(api, await addVariant(api, config, settings), settings);
760
786
  case "edit-parent":
761
- return editParentFlow(api, config);
787
+ return editParentFlow(api, config, settings);
762
788
  case "edit-variant":
763
- return mainMenu(api, await editVariant(api, config));
789
+ return mainMenu(api, await editVariant(api, config), settings);
764
790
  case "toggle":
765
- return mainMenu(api, await toggleDisable(api, config));
791
+ return mainMenu(api, await toggleDisable(api, config), settings);
766
792
  case "diagnostics":
767
793
  await runDiagnostics(api, config);
768
- return mainMenu(api, config);
769
- case "debug":
770
- return mainMenu(api, await toggleDebug(api, config));
771
- case "view-log":
772
- await viewDebugLog(api);
773
- return mainMenu(api, config);
774
- case "clear-log":
775
- await clearDebugLog(api);
776
- return mainMenu(api, config);
794
+ return mainMenu(api, config, settings);
795
+ case "advanced":
796
+ return mainMenu(api, await debugAdvancedMenu(api, config, settings), settings);
777
797
  case "delete":
778
- return mainMenu(api, await deleteVariant(api, config));
798
+ return mainMenu(api, await deleteVariant(api, config), settings);
779
799
  case "preview":
780
800
  await previewConfig(api, config);
781
- return mainMenu(api, config);
801
+ return mainMenu(api, config, settings);
782
802
  case "save":
783
803
  await saveConfig(api, config);
784
804
  return config;
@@ -786,16 +806,15 @@ async function mainMenu(api, config) {
786
806
  return config;
787
807
  }
788
808
  }
789
- async function editParentFlow(api, config) {
790
- const aliases = generatedAliasSet(config);
791
- const agents = agentsFromState(api).filter((agent) => !aliases.has(agent));
809
+ async function editParentFlow(api, config, settings) {
810
+ const agents = selectableParentAgents(api, config, settings);
792
811
  const opts = agents.map((a) => {
793
812
  const entry = config.agents[a];
794
813
  const overrides = entry ? Object.keys(entry.parent).length : 0;
795
814
  return {
796
815
  title: a,
797
816
  value: a,
798
- description: overrides > 0 ? `${overrides} parent override(s)` : "No overrides",
817
+ description: `${agentMode(api, a)} - ${overrides > 0 ? `${overrides} parent override(s)` : "No overrides"}`,
799
818
  };
800
819
  });
801
820
  opts.push({
@@ -805,9 +824,47 @@ async function editParentFlow(api, config) {
805
824
  });
806
825
  const agent = await showSelect(api.ui, { title: "Edit parent fields - pick agent", options: opts });
807
826
  if (!agent || agent === "__back__")
808
- return mainMenu(api, config);
827
+ return mainMenu(api, config, settings);
809
828
  const updated = await editParentFields(api, config, agent);
810
- return mainMenu(api, updated);
829
+ return mainMenu(api, updated, settings);
830
+ }
831
+ async function debugAdvancedMenu(api, config, settings) {
832
+ const opts = [
833
+ {
834
+ title: `Debug mode: ${config.debug ? "on" : "off"}`,
835
+ value: "debug",
836
+ description: "Toggle routing/model diagnostic toasts immediately",
837
+ },
838
+ { title: "View debug log", value: "view-log", description: "Show recent agent-variants.debug.log entries" },
839
+ { title: "Clear debug log", value: "clear-log", description: "Empty agent-variants.debug.log" },
840
+ {
841
+ title: `Parent picker filter: ${settings.subagentCapableOnly ? "subagent-capable only" : "all agents"}`,
842
+ value: "filter",
843
+ description: "Wizard-only filter for adding/editing parent entries",
844
+ },
845
+ { title: "< Back", value: "__back__", description: "Return to main menu" },
846
+ ];
847
+ const action = await showSelect(api.ui, { title: "Debug & advanced", options: opts });
848
+ switch (action) {
849
+ case "debug":
850
+ return debugAdvancedMenu(api, await toggleDebug(api, config), settings);
851
+ case "view-log":
852
+ await viewDebugLog(api);
853
+ return debugAdvancedMenu(api, config, settings);
854
+ case "clear-log":
855
+ await clearDebugLog(api);
856
+ return debugAdvancedMenu(api, config, settings);
857
+ case "filter":
858
+ settings.subagentCapableOnly = !settings.subagentCapableOnly;
859
+ api.ui.toast({
860
+ variant: "info",
861
+ title: "Parent filter updated",
862
+ message: settings.subagentCapableOnly ? "Showing subagent-capable parents only." : "Showing all parent agents.",
863
+ });
864
+ return debugAdvancedMenu(api, config, settings);
865
+ default:
866
+ return config;
867
+ }
811
868
  }
812
869
  // Plugin entrypoint.
813
870
  function registerConfigureCommand(api, run) {
@@ -840,7 +897,7 @@ function registerConfigureCommand(api, run) {
840
897
  const tui = async (api) => {
841
898
  const unregister = registerConfigureCommand(api, async () => {
842
899
  const config = loadSidecar(defaultSidecarPath());
843
- await mainMenu(api, config);
900
+ await mainMenu(api, config, { subagentCapableOnly: true });
844
901
  });
845
902
  api.lifecycle.onDispose(() => {
846
903
  unregister?.();
package/docs/CONFIG.md CHANGED
@@ -67,6 +67,8 @@ Variants also support:
67
67
 
68
68
  The sidecar intentionally does not configure `permission`, `tools`, or `mode`. Those come from the parent.
69
69
 
70
+ Agent Variants are intended for agents callable by OpenCode's `task` tool. The wizard defaults to a subagent-capable parent filter (`mode: "subagent"` or `mode: "all"`) and hides primary-only agents from add/edit parent pickers. This is a wizard-only setting, not a sidecar field. Existing sidecar entries for primary-only parents still appear in edit/delete/toggle flows so they can be repaired.
71
+
70
72
  ## Description Generation
71
73
 
72
74
  If a variant does not set `description`, the plugin generates:
@@ -119,7 +121,8 @@ The plugin skips problematic variants instead of producing ambiguous agents:
119
121
  - variant alias equals parent name,
120
122
  - two variants generate the same alias,
121
123
  - generated alias conflicts with an existing agent,
122
- - variant model is definitely missing.
124
+ - variant model is definitely missing,
125
+ - parent is primary-only and will not be callable by `task`.
123
126
 
124
127
  Run `Agent Variants: Configure` -> `Run diagnostics` to inspect these issues.
125
128
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-agent-variants",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-dev.2",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for creating and managing model variants of agents",
6
6
  "license": "MIT",
@@ -55,8 +55,12 @@
55
55
  "scripts": {
56
56
  "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
57
57
  "build": "npm run clean && tsc --outDir dist --declaration",
58
- "ci": "npm run typecheck && npm run build && npm pack --dry-run",
58
+ "ci": "npm run release:intent:check && npm run typecheck && npm run build && npm pack --dry-run",
59
+ "commit:check": "npm run ci",
60
+ "hooks:install": "node scripts/install-git-hooks.mjs",
59
61
  "pack:dry-run": "npm pack --dry-run",
62
+ "release:intent": "node scripts/set-release-intent.mjs",
63
+ "release:intent:check": "node scripts/check-release-intent.mjs",
60
64
  "typecheck": "tsc --noEmit",
61
65
  "prepublishOnly": "npm run typecheck && npm run build"
62
66
  },