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 +6 -0
- package/bin/orchestration.js +67 -6
- package/package.json +1 -1
- package/src/agents.js +6 -0
- package/src/installer.js +36 -2
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
|
]
|
package/bin/orchestration.js
CHANGED
|
@@ -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
|
|
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
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 =
|
|
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");
|