opencode-herdr-orchestration 0.1.3 → 0.1.5

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
@@ -4,7 +4,7 @@ Capability-separated OpenCode agents for planning, implementation, independent r
4
4
 
5
5
  This package registers the agents, provides complete structured worker-response retrieval, injects session-specific orchestration mode into shell environments, and ships a reproducible Git `pre-push` policy for new and existing repositories.
6
6
 
7
- Requires Node.js 22.22.2 or newer when running the package CLI or tests.
7
+ Requires Node.js 20 or newer when running the package CLI or tests.
8
8
 
9
9
  ## Installation
10
10
 
@@ -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`.
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,6 +152,7 @@ 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",
152
157
  "reviewerModel": "litellm-responses/gpt-5.6-terra"
153
158
  }
@@ -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,12 @@ 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"],
32
+ ["workerModel", "Sheep worker agents", "litellm/glm-5.3-flash"],
33
+ ["reviewerModel", "Shearer review agents", "litellm-responses/gpt-5.6-terra"],
34
+ ];
35
+
27
36
  function git(args, options = {}) {
28
37
  return execFileSync("git", args, { encoding: "utf8", windowsHide: true, ...options }).trim();
29
38
  }
@@ -59,11 +68,41 @@ function uninstallHooks() {
59
68
  }
60
69
  }
61
70
 
62
- function installOrUpdate(useLatest) {
71
+ function currentOptions(configDir) {
72
+ const file = findConfigFile(configDir);
73
+ return existsSync(file) ? orchestrationOptions(readFileSync(file, "utf8")) : {};
74
+ }
75
+
76
+ async function promptForModels(configDir) {
77
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
78
+ throw new Error("Agent model configuration requires an interactive terminal.");
79
+ }
80
+ const existing = currentOptions(configDir);
81
+ const answers = {};
82
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
83
+ process.stdout.write("Configure agent models. Press Enter to keep the shown value; enter - to restore the package default.\n");
84
+ try {
85
+ for (const [key, label, packageDefault] of MODEL_ROLES) {
86
+ const shown = existing[key] || packageDefault;
87
+ const answer = (await prompt.question(`${label} [${shown}]: `)).trim();
88
+ const value = answer === "-" ? "" : (answer || existing[key] || "");
89
+ if (value && !value.includes("/")) throw new Error(`${label} model must include a provider prefix, for example provider/model.`);
90
+ answers[key] = value;
91
+ }
92
+ } finally {
93
+ prompt.close();
94
+ }
95
+ return answers;
96
+ }
97
+
98
+ async function installOrUpdate(useLatest) {
63
99
  const configDir = configDirectory();
100
+ const models = command === "install" && process.stdin.isTTY && process.stdout.isTTY
101
+ ? await promptForModels(configDir)
102
+ : undefined;
64
103
  const version = useLatest ? latestVersion() : packageVersion(packageRoot);
65
104
  const packageBackups = installPackage(configDir, version);
66
- const result = writePluginConfig(configDir);
105
+ const result = writePluginConfig(configDir, false, models);
67
106
  try {
68
107
  validateOpenCode(configDir);
69
108
  } catch (error) {
@@ -77,6 +116,24 @@ function installOrUpdate(useLatest) {
77
116
  process.stdout.write("Restart OpenCode intentionally to load the new configuration.\n");
78
117
  }
79
118
 
119
+ async function configureAgents() {
120
+ const configDir = configDirectory();
121
+ if (!existsSync(join(configDir, "node_modules", PACKAGE_NAME, "package.json"))) {
122
+ throw new Error(`${PACKAGE_NAME} is not installed. Run the install command first.`);
123
+ }
124
+ const models = await promptForModels(configDir);
125
+ const result = writePluginConfig(configDir, false, models);
126
+ try {
127
+ validateOpenCode(configDir);
128
+ } catch (error) {
129
+ restoreBackup(result.file, result.backup, result.existed);
130
+ throw new Error(`OpenCode validation failed; restored the previous config. ${error.message}`);
131
+ }
132
+ process.stdout.write(`Configured agent models in ${result.file}\n`);
133
+ if (result.backup) process.stdout.write(`Backup: ${result.backup}\n`);
134
+ process.stdout.write("Restart OpenCode intentionally to load the new configuration.\n");
135
+ }
136
+
80
137
  function uninstallOrchestration() {
81
138
  const configDir = configDirectory();
82
139
  const result = writePluginConfig(configDir, true);
@@ -102,14 +159,15 @@ function captureHookStatus() {
102
159
  }
103
160
 
104
161
  try {
105
- if (command === "install") installOrUpdate(false);
106
- else if (command === "update") installOrUpdate(true);
162
+ if (command === "install") await installOrUpdate(false);
163
+ else if (command === "update") await installOrUpdate(true);
164
+ else if (command === "configure-agents") await configureAgents();
107
165
  else if (command === "uninstall") uninstallOrchestration();
108
166
  else if (command === "install-hooks") installHooks();
109
167
  else if (command === "uninstall-hooks") uninstallHooks();
110
168
  else if (command === "status") fullStatus();
111
169
  else {
112
- process.stderr.write("Usage: opencode-herdr-orchestration <install|update|status|uninstall|install-hooks|uninstall-hooks> [--with-hooks] [--force]\n");
170
+ process.stderr.write("Usage: opencode-herdr-orchestration <install|update|configure-agents|status|uninstall|install-hooks|uninstall-hooks> [--with-hooks] [--force]\n");
113
171
  process.exitCode = 2;
114
172
  }
115
173
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-herdr-orchestration",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Capability-separated Herdr orchestration agents for OpenCode",
5
5
  "author": "CodingJinxx",
6
6
  "repository": {
@@ -31,13 +31,7 @@
31
31
  "check": "node --check src/plugin.js && node --check src/index.js && node --check src/agents.js && node --check src/prompts.js && node --check src/response.js && node --check bin/orchestration.js"
32
32
  },
33
33
  "engines": {
34
- "node": ">=22.22.2"
35
- },
36
- "peerDependencies": {
37
- "@opencode-ai/plugin": ">=1.3.0 <2"
38
- },
39
- "devDependencies": {
40
- "@opencode-ai/plugin": "1.3.14"
34
+ "node": ">=20"
41
35
  },
42
36
  "keywords": [
43
37
  "opencode",
@@ -47,6 +41,8 @@
47
41
  ],
48
42
  "license": "MIT",
49
43
  "dependencies": {
44
+ "@opencode-ai/plugin": "1.3.14",
45
+ "cross-spawn": "7.0.6",
50
46
  "jsonc-parser": "3.3.1"
51
47
  }
52
48
  }
package/src/agents.js CHANGED
@@ -81,6 +81,7 @@ 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";
85
86
  const reviewerModel = options.reviewerModel ?? "litellm-responses/gpt-5.6-terra";
86
87
  const shepherdBuildPermissions = options.shepherdBuildPermissions ?? {};
@@ -89,6 +90,7 @@ export function createAgents(options = {}) {
89
90
  return {
90
91
  "shepherd-plan": {
91
92
  mode: "primary",
93
+ ...(shepherdModel ? { model: shepherdModel } : {}),
92
94
  description: "Researches and presents implementation-ready plans through sheep-plan workers without implementing them.",
93
95
  prompt: SHEPHERD_PLAN_PROMPT,
94
96
  permission: {
@@ -135,6 +137,7 @@ export function createAgents(options = {}) {
135
137
 
136
138
  "shepherd-build": {
137
139
  mode: "primary",
140
+ ...(shepherdModel ? { model: shepherdModel } : {}),
138
141
  description: "Executes approved plans through planning, implementation, and independent review workers.",
139
142
  prompt: shepherdBuildPrompt,
140
143
  permission: {
package/src/installer.js CHANGED
@@ -1,14 +1,13 @@
1
- import { execFileSync } from "node:child_process";
2
1
  import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
2
  import { homedir } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
3
+ import { join, resolve } from "node:path";
5
4
  import { pathToFileURL } from "node:url";
5
+ import spawn from "cross-spawn";
6
6
  import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
7
7
 
8
8
  export const PACKAGE_NAME = "opencode-herdr-orchestration";
9
- const NPM_COMMAND = process.platform === "win32" ? process.execPath : "npm";
10
- const NPM_PREFIX = process.platform === "win32" ? [resolveNpmCli()] : [];
11
- const OPENCODE_COMMAND = process.platform === "win32" ? "opencode.exe" : "opencode";
9
+ const NPM_COMMAND = "npm";
10
+ const OPENCODE_COMMAND = "opencode";
12
11
  export const AGENT_NAMES = [
13
12
  "shepherd-plan",
14
13
  "shepherd-build",
@@ -18,22 +17,6 @@ export const AGENT_NAMES = [
18
17
  "shearer-review-medium",
19
18
  ];
20
19
 
21
- function resolveWindowsCommand(name) {
22
- try {
23
- return execFileSync("where.exe", [name], { encoding: "utf8", windowsHide: true })
24
- .split(/\r?\n/)
25
- .find(Boolean)
26
- .trim();
27
- } catch {
28
- return name;
29
- }
30
- }
31
-
32
- function resolveNpmCli() {
33
- const npmShim = resolveWindowsCommand("npm.cmd");
34
- return join(dirname(npmShim), "node_modules", "npm", "bin", "npm-cli.js");
35
- }
36
-
37
20
  export function configDirectory(env = process.env) {
38
21
  if (env.OPENCODE_CONFIG_DIR) return resolve(env.OPENCODE_CONFIG_DIR);
39
22
  const home = env.HOME || env.USERPROFILE || homedir();
@@ -82,6 +65,38 @@ export function updatePluginConfig(text, entry, remove = false) {
82
65
  return source;
83
66
  }
84
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
+
85
100
  export function findConfigFile(configDir) {
86
101
  for (const name of ["opencode.jsonc", "opencode.json"]) {
87
102
  const candidate = join(configDir, name);
@@ -99,12 +114,21 @@ export function backupFile(file, now = new Date()) {
99
114
  }
100
115
 
101
116
  function run(command, args, options = {}) {
102
- return execFileSync(command, args, {
117
+ const result = spawn.sync(command, args, {
103
118
  encoding: "utf8",
104
119
  windowsHide: true,
105
120
  stdio: ["ignore", "pipe", "pipe"],
106
121
  ...options,
107
- }).trim();
122
+ });
123
+ if (result.error) throw result.error;
124
+ if (result.status !== 0) {
125
+ const error = new Error(`${command} exited with status ${result.status}.`);
126
+ error.status = result.status;
127
+ error.stdout = result.stdout;
128
+ error.stderr = result.stderr;
129
+ throw error;
130
+ }
131
+ return result.stdout.trim();
108
132
  }
109
133
 
110
134
  export function installedVersion(configDir) {
@@ -118,7 +142,7 @@ export function packageVersion(packageRoot) {
118
142
  }
119
143
 
120
144
  export function latestVersion() {
121
- return JSON.parse(run(NPM_COMMAND, [...NPM_PREFIX, "view", PACKAGE_NAME, "version", "--json", "--prefer-online"]));
145
+ return JSON.parse(run(NPM_COMMAND, ["view", PACKAGE_NAME, "version", "--json", "--prefer-online"]));
122
146
  }
123
147
 
124
148
  export function installPackage(configDir, version) {
@@ -126,7 +150,7 @@ export function installPackage(configDir, version) {
126
150
  const backups = ["package.json", "package-lock.json"]
127
151
  .map((name) => backupFile(join(configDir, name)))
128
152
  .filter(Boolean);
129
- run(NPM_COMMAND, [...NPM_PREFIX, "install", "--save-exact", `${PACKAGE_NAME}@${version}`], { cwd: configDir });
153
+ run(NPM_COMMAND, ["install", "--save-exact", `${PACKAGE_NAME}@${version}`], { cwd: configDir });
130
154
  return backups;
131
155
  }
132
156
 
@@ -135,17 +159,19 @@ export function uninstallPackage(configDir) {
135
159
  .map((name) => backupFile(join(configDir, name)))
136
160
  .filter(Boolean);
137
161
  try {
138
- run(NPM_COMMAND, [...NPM_PREFIX, "uninstall", PACKAGE_NAME], { cwd: configDir });
162
+ run(NPM_COMMAND, ["uninstall", PACKAGE_NAME], { cwd: configDir });
139
163
  } catch {}
140
164
  return backups;
141
165
  }
142
166
 
143
- export function writePluginConfig(configDir, remove = false) {
167
+ export function writePluginConfig(configDir, remove = false, models) {
144
168
  mkdirSync(configDir, { recursive: true });
145
169
  const file = findConfigFile(configDir);
146
170
  const existed = existsSync(file);
147
171
  const previous = existed ? readFileSync(file, "utf8") : '{\n "$schema": "https://opencode.ai/config.json"\n}\n';
148
- const next = updatePluginConfig(previous, packageEntry(configDir), remove);
172
+ const next = models && !remove
173
+ ? updateAgentModels(previous, packageEntry(configDir), models)
174
+ : updatePluginConfig(previous, packageEntry(configDir), remove);
149
175
  if (next === previous) return { file, backup: null, changed: false, existed };
150
176
  const backup = backupFile(file);
151
177
  writeFileSync(file, next, "utf8");