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 +6 -1
- package/bin/orchestration.js +64 -6
- package/package.json +4 -8
- package/src/agents.js +3 -0
- package/src/installer.js +54 -28
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
|
|
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
|
}
|
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,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
|
|
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
|
+
"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": ">=
|
|
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 {
|
|
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 =
|
|
10
|
-
const
|
|
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
|
-
|
|
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
|
-
})
|
|
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, [
|
|
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, [
|
|
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, [
|
|
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 =
|
|
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");
|