opencode-herdr-orchestration 0.1.2 → 0.1.4
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 +53 -1
- package/bin/orchestration.js +57 -9
- package/package.json +8 -9
- package/src/installer.js +199 -0
package/README.md
CHANGED
|
@@ -4,10 +4,45 @@ 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
|
|
|
11
|
+
Install or update the plugin with the cross-platform npm CLI:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx -y opencode-herdr-orchestration@latest install
|
|
15
|
+
npx -y opencode-herdr-orchestration@latest update
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The installer:
|
|
19
|
+
|
|
20
|
+
- locates the global OpenCode config directory;
|
|
21
|
+
- installs an exact package version there;
|
|
22
|
+
- preserves existing JSONC comments, trailing commas, plugins, and tuple options;
|
|
23
|
+
- adds the stable file URL required by current OpenCode npm plugin loading;
|
|
24
|
+
- creates timestamped backups of changed config and npm manifest files;
|
|
25
|
+
- validates `shepherd-build` through a short-lived OpenCode debug process;
|
|
26
|
+
- never restarts or signals a running OpenCode process.
|
|
27
|
+
|
|
28
|
+
The shared Git push policy remains opt-in:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npx -y opencode-herdr-orchestration@latest install --with-hooks
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Inspect or remove the installation:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx -y opencode-herdr-orchestration@latest status
|
|
38
|
+
npx -y opencode-herdr-orchestration@latest uninstall
|
|
39
|
+
npx -y opencode-herdr-orchestration@latest uninstall --with-hooks
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
After installation or update, quit and restart OpenCode intentionally when ready. Existing processes keep their already-loaded configuration.
|
|
43
|
+
|
|
44
|
+
## Manual Installation
|
|
45
|
+
|
|
11
46
|
Add the published package to the global OpenCode configuration at `~/.config/opencode/opencode.json` or `~/.config/opencode/opencode.jsonc`:
|
|
12
47
|
|
|
13
48
|
```jsonc
|
|
@@ -19,6 +54,23 @@ Add the published package to the global OpenCode configuration at `~/.config/ope
|
|
|
19
54
|
|
|
20
55
|
If `plugin` already contains entries, append `"opencode-herdr-orchestration"` instead of replacing them. OpenCode installs npm plugins automatically when it starts.
|
|
21
56
|
|
|
57
|
+
Install the package into that config directory so the stable file URL can resolve it:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
cd ~/.config/opencode
|
|
61
|
+
npm install --save-exact opencode-herdr-orchestration@latest
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Current OpenCode releases may record but not execute npm plugins referenced only by package name. If that occurs, use the installed entry path in the plugin tuple instead:
|
|
65
|
+
|
|
66
|
+
```jsonc
|
|
67
|
+
{
|
|
68
|
+
"plugin": [
|
|
69
|
+
"file:///ABSOLUTE/PATH/TO/.config/opencode/node_modules/opencode-herdr-orchestration/src/plugin.js"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
22
74
|
Optionally install the shared Git push policy for all current and future repositories:
|
|
23
75
|
|
|
24
76
|
```bash
|
package/bin/orchestration.js
CHANGED
|
@@ -5,6 +5,18 @@ 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 {
|
|
9
|
+
configDirectory,
|
|
10
|
+
installPackage,
|
|
11
|
+
latestVersion,
|
|
12
|
+
PACKAGE_NAME,
|
|
13
|
+
packageVersion,
|
|
14
|
+
restoreBackup,
|
|
15
|
+
status as installationStatus,
|
|
16
|
+
uninstallPackage,
|
|
17
|
+
validateOpenCode,
|
|
18
|
+
writePluginConfig,
|
|
19
|
+
} from "../src/installer.js";
|
|
8
20
|
|
|
9
21
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
10
22
|
const hooksPath = join(homedir(), ".config", "opencode-herdr-orchestration", "hooks");
|
|
@@ -24,7 +36,7 @@ function currentHooksPath() {
|
|
|
24
36
|
}
|
|
25
37
|
}
|
|
26
38
|
|
|
27
|
-
function
|
|
39
|
+
function installHooks() {
|
|
28
40
|
const existing = currentHooksPath();
|
|
29
41
|
const force = flags.includes("--force");
|
|
30
42
|
if (existing && existing !== hooksPath && !force) {
|
|
@@ -37,7 +49,7 @@ function install() {
|
|
|
37
49
|
process.stdout.write(`Installed pre-push hook and set global core.hooksPath=${hooksPath}\n`);
|
|
38
50
|
}
|
|
39
51
|
|
|
40
|
-
function
|
|
52
|
+
function uninstallHooks() {
|
|
41
53
|
if (currentHooksPath() === hooksPath) {
|
|
42
54
|
git(["config", "--global", "--unset", "core.hooksPath"]);
|
|
43
55
|
rmSync(hooksPath, { recursive: true, force: true });
|
|
@@ -47,21 +59,57 @@ function uninstall() {
|
|
|
47
59
|
}
|
|
48
60
|
}
|
|
49
61
|
|
|
50
|
-
function
|
|
51
|
-
|
|
62
|
+
function installOrUpdate(useLatest) {
|
|
63
|
+
const configDir = configDirectory();
|
|
64
|
+
const version = useLatest ? latestVersion() : packageVersion(packageRoot);
|
|
65
|
+
const packageBackups = installPackage(configDir, version);
|
|
66
|
+
const result = writePluginConfig(configDir);
|
|
67
|
+
try {
|
|
68
|
+
validateOpenCode(configDir);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
restoreBackup(result.file, result.backup, result.existed);
|
|
71
|
+
throw new Error(`OpenCode validation failed; restored the previous config. ${error.message}`);
|
|
72
|
+
}
|
|
73
|
+
if (flags.includes("--with-hooks")) installHooks();
|
|
74
|
+
process.stdout.write(`Configured ${PACKAGE_NAME}@${version} in ${result.file}\n`);
|
|
75
|
+
if (result.backup) process.stdout.write(`Backup: ${result.backup}\n`);
|
|
76
|
+
for (const backup of packageBackups) process.stdout.write(`Backup: ${backup}\n`);
|
|
77
|
+
process.stdout.write("Restart OpenCode intentionally to load the new configuration.\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function uninstallOrchestration() {
|
|
81
|
+
const configDir = configDirectory();
|
|
82
|
+
const result = writePluginConfig(configDir, true);
|
|
83
|
+
const packageBackups = uninstallPackage(configDir);
|
|
84
|
+
if (flags.includes("--with-hooks")) uninstallHooks();
|
|
85
|
+
process.stdout.write(`Removed orchestration plugin configuration from ${result.file}\n`);
|
|
86
|
+
if (result.backup) process.stdout.write(`Backup: ${result.backup}\n`);
|
|
87
|
+
for (const backup of packageBackups) process.stdout.write(`Backup: ${backup}\n`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function fullStatus() {
|
|
91
|
+
const result = { ...installationStatus(configDirectory(), packageRoot), hooks: JSON.parse(captureHookStatus()) };
|
|
92
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function captureHookStatus() {
|
|
96
|
+
return JSON.stringify({
|
|
52
97
|
configuredHooksPath: currentHooksPath() || null,
|
|
53
98
|
expectedHooksPath: hooksPath,
|
|
54
99
|
hookInstalled: existsSync(installedHook),
|
|
55
100
|
active: currentHooksPath() === hooksPath && existsSync(installedHook),
|
|
56
|
-
}
|
|
101
|
+
});
|
|
57
102
|
}
|
|
58
103
|
|
|
59
104
|
try {
|
|
60
|
-
if (command === "install
|
|
61
|
-
else if (command === "
|
|
62
|
-
else if (command === "
|
|
105
|
+
if (command === "install") installOrUpdate(false);
|
|
106
|
+
else if (command === "update") installOrUpdate(true);
|
|
107
|
+
else if (command === "uninstall") uninstallOrchestration();
|
|
108
|
+
else if (command === "install-hooks") installHooks();
|
|
109
|
+
else if (command === "uninstall-hooks") uninstallHooks();
|
|
110
|
+
else if (command === "status") fullStatus();
|
|
63
111
|
else {
|
|
64
|
-
process.stderr.write("Usage: opencode-herdr-orchestration <install-hooks [--force]
|
|
112
|
+
process.stderr.write("Usage: opencode-herdr-orchestration <install|update|status|uninstall|install-hooks|uninstall-hooks> [--with-hooks] [--force]\n");
|
|
65
113
|
process.exitCode = 2;
|
|
66
114
|
}
|
|
67
115
|
} 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.4",
|
|
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",
|
|
@@ -45,5 +39,10 @@
|
|
|
45
39
|
"orchestration",
|
|
46
40
|
"agents"
|
|
47
41
|
],
|
|
48
|
-
"license": "MIT"
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@opencode-ai/plugin": "1.3.14",
|
|
45
|
+
"cross-spawn": "7.0.6",
|
|
46
|
+
"jsonc-parser": "3.3.1"
|
|
47
|
+
}
|
|
49
48
|
}
|
package/src/installer.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
5
|
+
import spawn from "cross-spawn";
|
|
6
|
+
import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
|
|
7
|
+
|
|
8
|
+
export const PACKAGE_NAME = "opencode-herdr-orchestration";
|
|
9
|
+
const NPM_COMMAND = "npm";
|
|
10
|
+
const OPENCODE_COMMAND = "opencode";
|
|
11
|
+
export const AGENT_NAMES = [
|
|
12
|
+
"shepherd-plan",
|
|
13
|
+
"shepherd-build",
|
|
14
|
+
"sheep-plan",
|
|
15
|
+
"sheep-build",
|
|
16
|
+
"shearer-review-low",
|
|
17
|
+
"shearer-review-medium",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function configDirectory(env = process.env) {
|
|
21
|
+
if (env.OPENCODE_CONFIG_DIR) return resolve(env.OPENCODE_CONFIG_DIR);
|
|
22
|
+
const home = env.HOME || env.USERPROFILE || homedir();
|
|
23
|
+
return join(home, ".config", "opencode");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function packageEntry(configDir) {
|
|
27
|
+
return pathToFileURL(join(configDir, "node_modules", PACKAGE_NAME, "src", "plugin.js")).href;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isOrchestrationPlugin(entry) {
|
|
31
|
+
const spec = Array.isArray(entry) ? entry[0] : entry;
|
|
32
|
+
return (
|
|
33
|
+
typeof spec === "string" &&
|
|
34
|
+
(spec === PACKAGE_NAME || spec.startsWith(`${PACKAGE_NAME}@`) || spec.includes(`/node_modules/${PACKAGE_NAME}/`))
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function updatePluginConfig(text, entry, remove = false) {
|
|
39
|
+
const errors = [];
|
|
40
|
+
const config = parse(text || "{}", errors, { allowTrailingComma: true, disallowComments: false });
|
|
41
|
+
if (errors.length) {
|
|
42
|
+
const first = errors[0];
|
|
43
|
+
throw new Error(`Invalid OpenCode JSONC at offset ${first.offset}: ${printParseErrorCode(first.error)}.`);
|
|
44
|
+
}
|
|
45
|
+
const formattingOptions = { insertSpaces: true, tabSize: 2, eol: text.includes("\r\n") ? "\r\n" : "\n" };
|
|
46
|
+
const plugins = Array.isArray(config?.plugin) ? config.plugin : [];
|
|
47
|
+
const matches = plugins.map((candidate, index) => isOrchestrationPlugin(candidate) ? index : -1).filter((index) => index >= 0);
|
|
48
|
+
const existing = matches.length ? plugins[matches[0]] : undefined;
|
|
49
|
+
const source = text || "{}";
|
|
50
|
+
|
|
51
|
+
if (matches.length === 1 && !remove) {
|
|
52
|
+
const replacement = Array.isArray(existing) ? [entry, existing[1] ?? {}] : entry;
|
|
53
|
+
return applyEdits(source, modify(source, ["plugin", matches[0]], replacement, { formattingOptions }));
|
|
54
|
+
}
|
|
55
|
+
if (matches.length && (remove || matches.length > 1)) {
|
|
56
|
+
const remaining = plugins.filter((candidate) => !isOrchestrationPlugin(candidate));
|
|
57
|
+
if (!remove) remaining.push(Array.isArray(existing) ? [entry, existing[1] ?? {}] : entry);
|
|
58
|
+
return applyEdits(source, modify(source, ["plugin"], remaining, { formattingOptions }));
|
|
59
|
+
}
|
|
60
|
+
if (!remove) {
|
|
61
|
+
const path = Array.isArray(config?.plugin) ? ["plugin", -1] : ["plugin"];
|
|
62
|
+
const value = Array.isArray(config?.plugin) ? entry : [entry];
|
|
63
|
+
return applyEdits(source, modify(source, path, value, { formattingOptions }));
|
|
64
|
+
}
|
|
65
|
+
return source;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function findConfigFile(configDir) {
|
|
69
|
+
for (const name of ["opencode.jsonc", "opencode.json"]) {
|
|
70
|
+
const candidate = join(configDir, name);
|
|
71
|
+
if (existsSync(candidate)) return candidate;
|
|
72
|
+
}
|
|
73
|
+
return join(configDir, "opencode.jsonc");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function backupFile(file, now = new Date()) {
|
|
77
|
+
if (!existsSync(file)) return null;
|
|
78
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
79
|
+
const backup = `${file}.backup-${stamp}`;
|
|
80
|
+
copyFileSync(file, backup);
|
|
81
|
+
return backup;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function run(command, args, options = {}) {
|
|
85
|
+
const result = spawn.sync(command, args, {
|
|
86
|
+
encoding: "utf8",
|
|
87
|
+
windowsHide: true,
|
|
88
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
89
|
+
...options,
|
|
90
|
+
});
|
|
91
|
+
if (result.error) throw result.error;
|
|
92
|
+
if (result.status !== 0) {
|
|
93
|
+
const error = new Error(`${command} exited with status ${result.status}.`);
|
|
94
|
+
error.status = result.status;
|
|
95
|
+
error.stdout = result.stdout;
|
|
96
|
+
error.stderr = result.stderr;
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
return result.stdout.trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function installedVersion(configDir) {
|
|
103
|
+
const manifest = join(configDir, "node_modules", PACKAGE_NAME, "package.json");
|
|
104
|
+
if (!existsSync(manifest)) return null;
|
|
105
|
+
return JSON.parse(readFileSync(manifest, "utf8")).version;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function packageVersion(packageRoot) {
|
|
109
|
+
return JSON.parse(readFileSync(join(packageRoot, "package.json"), "utf8")).version;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function latestVersion() {
|
|
113
|
+
return JSON.parse(run(NPM_COMMAND, ["view", PACKAGE_NAME, "version", "--json", "--prefer-online"]));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function installPackage(configDir, version) {
|
|
117
|
+
mkdirSync(configDir, { recursive: true });
|
|
118
|
+
const backups = ["package.json", "package-lock.json"]
|
|
119
|
+
.map((name) => backupFile(join(configDir, name)))
|
|
120
|
+
.filter(Boolean);
|
|
121
|
+
run(NPM_COMMAND, ["install", "--save-exact", `${PACKAGE_NAME}@${version}`], { cwd: configDir });
|
|
122
|
+
return backups;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function uninstallPackage(configDir) {
|
|
126
|
+
const backups = ["package.json", "package-lock.json"]
|
|
127
|
+
.map((name) => backupFile(join(configDir, name)))
|
|
128
|
+
.filter(Boolean);
|
|
129
|
+
try {
|
|
130
|
+
run(NPM_COMMAND, ["uninstall", PACKAGE_NAME], { cwd: configDir });
|
|
131
|
+
} catch {}
|
|
132
|
+
return backups;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function writePluginConfig(configDir, remove = false) {
|
|
136
|
+
mkdirSync(configDir, { recursive: true });
|
|
137
|
+
const file = findConfigFile(configDir);
|
|
138
|
+
const existed = existsSync(file);
|
|
139
|
+
const previous = existed ? readFileSync(file, "utf8") : '{\n "$schema": "https://opencode.ai/config.json"\n}\n';
|
|
140
|
+
const next = updatePluginConfig(previous, packageEntry(configDir), remove);
|
|
141
|
+
if (next === previous) return { file, backup: null, changed: false, existed };
|
|
142
|
+
const backup = backupFile(file);
|
|
143
|
+
writeFileSync(file, next, "utf8");
|
|
144
|
+
return { file, backup, changed: true, existed };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function restoreBackup(file, backup, existed = true) {
|
|
148
|
+
if (backup && existsSync(backup)) copyFileSync(backup, file);
|
|
149
|
+
else if (!existed) rmSync(file, { force: true });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function validateOpenCode(configDir) {
|
|
153
|
+
const output = run(OPENCODE_COMMAND, ["debug", "agent", "shepherd-build"], {
|
|
154
|
+
cwd: configDir,
|
|
155
|
+
env: { ...process.env, OPENCODE_DISABLE_PROJECT_CONFIG: "1" },
|
|
156
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
157
|
+
});
|
|
158
|
+
const parsed = JSON.parse(output);
|
|
159
|
+
if (parsed.name !== "shepherd-build") throw new Error("OpenCode did not resolve shepherd-build after installation.");
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function status(configDir, packageRoot) {
|
|
164
|
+
const file = findConfigFile(configDir);
|
|
165
|
+
let configured = false;
|
|
166
|
+
if (existsSync(file)) {
|
|
167
|
+
const config = parse(readFileSync(file, "utf8"), [], { allowTrailingComma: true, disallowComments: false });
|
|
168
|
+
configured = Array.isArray(config?.plugin) && config.plugin.some(isOrchestrationPlugin);
|
|
169
|
+
}
|
|
170
|
+
const installed = installedVersion(configDir);
|
|
171
|
+
let detectedAgents = [];
|
|
172
|
+
try {
|
|
173
|
+
const output = run(OPENCODE_COMMAND, ["agent", "list"], {
|
|
174
|
+
cwd: configDir,
|
|
175
|
+
env: { ...process.env, OPENCODE_DISABLE_PROJECT_CONFIG: "1" },
|
|
176
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
177
|
+
});
|
|
178
|
+
detectedAgents = AGENT_NAMES.filter((name) => output.includes(`${name} (primary)`));
|
|
179
|
+
} catch {}
|
|
180
|
+
let latest = null;
|
|
181
|
+
let latestVersionError = null;
|
|
182
|
+
try {
|
|
183
|
+
latest = latestVersion();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
latestVersionError = String(error?.stderr ?? error?.message ?? error).replace(/\s+/g, " ").trim().slice(0, 500);
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
package: PACKAGE_NAME,
|
|
189
|
+
cliVersion: packageVersion(packageRoot),
|
|
190
|
+
installedVersion: installed,
|
|
191
|
+
latestVersion: latest,
|
|
192
|
+
latestVersionError,
|
|
193
|
+
updateAvailable: Boolean(installed && latest && installed !== latest),
|
|
194
|
+
configFile: file,
|
|
195
|
+
pluginConfigured: configured,
|
|
196
|
+
detectedAgents,
|
|
197
|
+
agentsReady: detectedAgents.length === AGENT_NAMES.length,
|
|
198
|
+
};
|
|
199
|
+
}
|