opencode-herdr-orchestration 0.1.4 → 0.1.6

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
@@ -19,6 +19,7 @@ The installer:
19
19
 
20
20
  - locates the global OpenCode config directory;
21
21
  - installs an exact package version there;
22
+ - interactively asks which models the shepherd, sheep worker, and shearer reviewer roles should use when run in a terminal;
22
23
  - preserves existing JSONC comments, trailing commas, plugins, and tuple options;
23
24
  - adds the stable file URL required by current OpenCode npm plugin loading;
24
25
  - creates timestamped backups of changed config and npm manifest files;
@@ -35,12 +36,15 @@ Inspect or remove the installation:
35
36
 
36
37
  ```bash
37
38
  npx -y opencode-herdr-orchestration@latest status
39
+ npx -y opencode-herdr-orchestration@latest configure-agents
38
40
  npx -y opencode-herdr-orchestration@latest uninstall
39
41
  npx -y opencode-herdr-orchestration@latest uninstall --with-hooks
40
42
  ```
41
43
 
42
44
  After installation or update, quit and restart OpenCode intentionally when ready. Existing processes keep their already-loaded configuration.
43
45
 
46
+ `configure-agents` updates the same model choices after installation. Press Enter to keep the displayed value, or enter `-` to restore that role's package default. Model names must include their provider prefix, such as `anthropic/claude-sonnet-4-6`. The sheep worker reasoning effort accepts the provider's variant names, such as `low`, `medium`, or `high`; leaving it unset uses the model's default reasoning behavior. Shepherd agents always inherit the active model and its reasoning behavior, and reviewer reasoning stays fixed at `low` and `medium`.
47
+
44
48
  ## Manual Installation
45
49
 
46
50
  Add the published package to the global OpenCode configuration at `~/.config/opencode/opencode.json` or `~/.config/opencode/opencode.jsonc`:
@@ -148,7 +152,9 @@ Plugin tuple options can override model defaults:
148
152
  [
149
153
  "opencode-herdr-orchestration",
150
154
  {
155
+ "shepherdModel": "anthropic/claude-sonnet-4-6",
151
156
  "workerModel": "litellm/glm-5.3-flash",
157
+ "workerVariant": "medium",
152
158
  "reviewerModel": "litellm-responses/gpt-5.6-terra"
153
159
  }
154
160
  ]
@@ -1,16 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { chmodSync, copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
3
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { execFileSync } from "node:child_process";
8
+ import { createInterface } from "node:readline/promises";
8
9
  import {
9
10
  configDirectory,
11
+ findConfigFile,
10
12
  installPackage,
11
13
  latestVersion,
12
14
  PACKAGE_NAME,
13
15
  packageVersion,
16
+ orchestrationOptions,
14
17
  restoreBackup,
15
18
  status as installationStatus,
16
19
  uninstallPackage,
@@ -24,6 +27,13 @@ const sourceHook = join(packageRoot, "hooks", "pre-push");
24
27
  const installedHook = join(hooksPath, "pre-push");
25
28
  const [command, ...flags] = process.argv.slice(2);
26
29
 
30
+ const MODEL_ROLES = [
31
+ ["shepherdModel", "Shepherd agents", "OpenCode active model", "model"],
32
+ ["workerModel", "Sheep worker agents", "litellm/glm-5.3-flash", "model"],
33
+ ["workerVariant", "Sheep worker reasoning effort", "model default", "variant"],
34
+ ["reviewerModel", "Shearer review agents", "litellm-responses/gpt-5.6-terra", "model"],
35
+ ];
36
+
27
37
  function git(args, options = {}) {
28
38
  return execFileSync("git", args, { encoding: "utf8", windowsHide: true, ...options }).trim();
29
39
  }
@@ -59,11 +69,43 @@ function uninstallHooks() {
59
69
  }
60
70
  }
61
71
 
62
- function installOrUpdate(useLatest) {
72
+ function currentOptions(configDir) {
73
+ const file = findConfigFile(configDir);
74
+ return existsSync(file) ? orchestrationOptions(readFileSync(file, "utf8")) : {};
75
+ }
76
+
77
+ async function promptForModels(configDir) {
78
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
79
+ throw new Error("Agent model configuration requires an interactive terminal.");
80
+ }
81
+ const existing = currentOptions(configDir);
82
+ const answers = {};
83
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
84
+ process.stdout.write("Configure agent models. Press Enter to keep the shown value; enter - to restore the package default.\n");
85
+ try {
86
+ for (const [key, label, packageDefault, kind] of MODEL_ROLES) {
87
+ const shown = existing[key] || packageDefault;
88
+ const answer = (await prompt.question(`${label} [${shown}]: `)).trim();
89
+ const value = answer === "-" ? "" : (answer || existing[key] || "");
90
+ if (value && kind === "model" && !value.includes("/")) {
91
+ throw new Error(`${label} model must include a provider prefix, for example provider/model.`);
92
+ }
93
+ answers[key] = value;
94
+ }
95
+ } finally {
96
+ prompt.close();
97
+ }
98
+ return answers;
99
+ }
100
+
101
+ async function installOrUpdate(useLatest) {
63
102
  const configDir = configDirectory();
103
+ const models = command === "install" && process.stdin.isTTY && process.stdout.isTTY
104
+ ? await promptForModels(configDir)
105
+ : undefined;
64
106
  const version = useLatest ? latestVersion() : packageVersion(packageRoot);
65
107
  const packageBackups = installPackage(configDir, version);
66
- const result = writePluginConfig(configDir);
108
+ const result = writePluginConfig(configDir, false, models);
67
109
  try {
68
110
  validateOpenCode(configDir);
69
111
  } catch (error) {
@@ -77,6 +119,24 @@ function installOrUpdate(useLatest) {
77
119
  process.stdout.write("Restart OpenCode intentionally to load the new configuration.\n");
78
120
  }
79
121
 
122
+ async function configureAgents() {
123
+ const configDir = configDirectory();
124
+ if (!existsSync(join(configDir, "node_modules", PACKAGE_NAME, "package.json"))) {
125
+ throw new Error(`${PACKAGE_NAME} is not installed. Run the install command first.`);
126
+ }
127
+ const models = await promptForModels(configDir);
128
+ const result = writePluginConfig(configDir, false, models);
129
+ try {
130
+ validateOpenCode(configDir);
131
+ } catch (error) {
132
+ restoreBackup(result.file, result.backup, result.existed);
133
+ throw new Error(`OpenCode validation failed; restored the previous config. ${error.message}`);
134
+ }
135
+ process.stdout.write(`Configured agent models in ${result.file}\n`);
136
+ if (result.backup) process.stdout.write(`Backup: ${result.backup}\n`);
137
+ process.stdout.write("Restart OpenCode intentionally to load the new configuration.\n");
138
+ }
139
+
80
140
  function uninstallOrchestration() {
81
141
  const configDir = configDirectory();
82
142
  const result = writePluginConfig(configDir, true);
@@ -102,14 +162,15 @@ function captureHookStatus() {
102
162
  }
103
163
 
104
164
  try {
105
- if (command === "install") installOrUpdate(false);
106
- else if (command === "update") installOrUpdate(true);
165
+ if (command === "install") await installOrUpdate(false);
166
+ else if (command === "update") await installOrUpdate(true);
167
+ else if (command === "configure-agents") await configureAgents();
107
168
  else if (command === "uninstall") uninstallOrchestration();
108
169
  else if (command === "install-hooks") installHooks();
109
170
  else if (command === "uninstall-hooks") uninstallHooks();
110
171
  else if (command === "status") fullStatus();
111
172
  else {
112
- process.stderr.write("Usage: opencode-herdr-orchestration <install|update|status|uninstall|install-hooks|uninstall-hooks> [--with-hooks] [--force]\n");
173
+ process.stderr.write("Usage: opencode-herdr-orchestration <install|update|configure-agents|status|uninstall|install-hooks|uninstall-hooks> [--with-hooks] [--force]\n");
113
174
  process.exitCode = 2;
114
175
  }
115
176
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-herdr-orchestration",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Capability-separated Herdr orchestration agents for OpenCode",
5
5
  "author": "CodingJinxx",
6
6
  "repository": {
package/src/agents.js CHANGED
@@ -81,7 +81,9 @@ export function mergeAgent(defaults, override) {
81
81
  }
82
82
 
83
83
  export function createAgents(options = {}) {
84
+ const shepherdModel = options.shepherdModel;
84
85
  const workerModel = options.workerModel ?? "litellm/glm-5.3-flash";
86
+ const workerVariant = options.workerVariant;
85
87
  const reviewerModel = options.reviewerModel ?? "litellm-responses/gpt-5.6-terra";
86
88
  const shepherdBuildPermissions = options.shepherdBuildPermissions ?? {};
87
89
  const shepherdBuildPrompt = appendPrompt(SHEPHERD_BUILD_PROMPT, options.shepherdBuildPromptAppend);
@@ -89,6 +91,7 @@ export function createAgents(options = {}) {
89
91
  return {
90
92
  "shepherd-plan": {
91
93
  mode: "primary",
94
+ ...(shepherdModel ? { model: shepherdModel } : {}),
92
95
  description: "Researches and presents implementation-ready plans through sheep-plan workers without implementing them.",
93
96
  prompt: SHEPHERD_PLAN_PROMPT,
94
97
  permission: {
@@ -135,6 +138,7 @@ export function createAgents(options = {}) {
135
138
 
136
139
  "shepherd-build": {
137
140
  mode: "primary",
141
+ ...(shepherdModel ? { model: shepherdModel } : {}),
138
142
  description: "Executes approved plans through planning, implementation, and independent review workers.",
139
143
  prompt: shepherdBuildPrompt,
140
144
  permission: {
@@ -188,6 +192,7 @@ export function createAgents(options = {}) {
188
192
  "sheep-plan": {
189
193
  mode: "primary",
190
194
  model: workerModel,
195
+ ...(workerVariant ? { variant: workerVariant } : {}),
191
196
  description: "Performs read-only repository research for a shepherd.",
192
197
  prompt: SHEEP_PLAN_PROMPT,
193
198
  permission: {
@@ -207,6 +212,7 @@ export function createAgents(options = {}) {
207
212
  "sheep-build": {
208
213
  mode: "primary",
209
214
  model: workerModel,
215
+ ...(workerVariant ? { variant: workerVariant } : {}),
210
216
  description: "Implements a bounded task, verifies it, and hands a local commit to shepherd-build.",
211
217
  prompt: SHEEP_BUILD_PROMPT,
212
218
  permission: {
package/src/installer.js CHANGED
@@ -65,6 +65,38 @@ export function updatePluginConfig(text, entry, remove = false) {
65
65
  return source;
66
66
  }
67
67
 
68
+ export function orchestrationOptions(text) {
69
+ const config = parse(text || "{}", [], { allowTrailingComma: true, disallowComments: false });
70
+ const entry = Array.isArray(config?.plugin)
71
+ ? config.plugin.find((candidate) => isOrchestrationPlugin(candidate))
72
+ : undefined;
73
+ return Array.isArray(entry) && entry[1] && typeof entry[1] === "object" ? entry[1] : {};
74
+ }
75
+
76
+ export function updateAgentModels(text, entry, models) {
77
+ const original = parse(text || "{}", [], { allowTrailingComma: true, disallowComments: false });
78
+ const matches = Array.isArray(original?.plugin)
79
+ ? original.plugin.map((candidate, index) => isOrchestrationPlugin(candidate) ? index : -1).filter((index) => index >= 0)
80
+ : [];
81
+ let updated = text || "{}";
82
+ const formattingOptions = { insertSpaces: true, tabSize: 2, eol: updated.includes("\r\n") ? "\r\n" : "\n" };
83
+ if (matches.length === 1 && Array.isArray(original.plugin[matches[0]])) {
84
+ updated = applyEdits(updated, modify(updated, ["plugin", matches[0], 0], entry, { formattingOptions }));
85
+ } else {
86
+ updated = updatePluginConfig(updated, entry);
87
+ }
88
+ const config = parse(updated, [], { allowTrailingComma: true, disallowComments: false });
89
+ const index = config.plugin.findIndex((candidate) => isOrchestrationPlugin(candidate));
90
+ if (!Array.isArray(config.plugin[index])) {
91
+ const options = Object.fromEntries(Object.entries(models).filter(([, value]) => value));
92
+ return applyEdits(updated, modify(updated, ["plugin", index], [entry, options], { formattingOptions }));
93
+ }
94
+ for (const [key, value] of Object.entries(models)) {
95
+ updated = applyEdits(updated, modify(updated, ["plugin", index, 1, key], value || undefined, { formattingOptions }));
96
+ }
97
+ return updated;
98
+ }
99
+
68
100
  export function findConfigFile(configDir) {
69
101
  for (const name of ["opencode.jsonc", "opencode.json"]) {
70
102
  const candidate = join(configDir, name);
@@ -132,12 +164,14 @@ export function uninstallPackage(configDir) {
132
164
  return backups;
133
165
  }
134
166
 
135
- export function writePluginConfig(configDir, remove = false) {
167
+ export function writePluginConfig(configDir, remove = false, models) {
136
168
  mkdirSync(configDir, { recursive: true });
137
169
  const file = findConfigFile(configDir);
138
170
  const existed = existsSync(file);
139
171
  const previous = existed ? readFileSync(file, "utf8") : '{\n "$schema": "https://opencode.ai/config.json"\n}\n';
140
- const next = updatePluginConfig(previous, packageEntry(configDir), remove);
172
+ const next = models && !remove
173
+ ? updateAgentModels(previous, packageEntry(configDir), models)
174
+ : updatePluginConfig(previous, packageEntry(configDir), remove);
141
175
  if (next === previous) return { file, backup: null, changed: false, existed };
142
176
  const backup = backupFile(file);
143
177
  writeFileSync(file, next, "utf8");