opencode-agenthub 0.1.0

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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +373 -0
  3. package/dist/composer/bootstrap.js +493 -0
  4. package/dist/composer/builtin-assets.js +139 -0
  5. package/dist/composer/capabilities.js +20 -0
  6. package/dist/composer/compose.js +824 -0
  7. package/dist/composer/defaults.js +10 -0
  8. package/dist/composer/home-transfer.js +288 -0
  9. package/dist/composer/install-home.js +5 -0
  10. package/dist/composer/library/README.md +93 -0
  11. package/dist/composer/library/bundles/auto.json +18 -0
  12. package/dist/composer/library/bundles/build.json +17 -0
  13. package/dist/composer/library/bundles/hr-adapter.json +26 -0
  14. package/dist/composer/library/bundles/hr-cto.json +24 -0
  15. package/dist/composer/library/bundles/hr-evaluator.json +26 -0
  16. package/dist/composer/library/bundles/hr-planner.json +26 -0
  17. package/dist/composer/library/bundles/hr-sourcer.json +24 -0
  18. package/dist/composer/library/bundles/hr-verifier.json +26 -0
  19. package/dist/composer/library/bundles/hr.json +35 -0
  20. package/dist/composer/library/bundles/plan.json +19 -0
  21. package/dist/composer/library/instructions/hr-boundaries.md +38 -0
  22. package/dist/composer/library/instructions/hr-protocol.md +102 -0
  23. package/dist/composer/library/profiles/auto.json +9 -0
  24. package/dist/composer/library/profiles/hr.json +9 -0
  25. package/dist/composer/library/souls/auto.md +29 -0
  26. package/dist/composer/library/souls/build.md +21 -0
  27. package/dist/composer/library/souls/hr-adapter.md +64 -0
  28. package/dist/composer/library/souls/hr-cto.md +57 -0
  29. package/dist/composer/library/souls/hr-evaluator.md +64 -0
  30. package/dist/composer/library/souls/hr-planner.md +48 -0
  31. package/dist/composer/library/souls/hr-sourcer.md +70 -0
  32. package/dist/composer/library/souls/hr-verifier.md +62 -0
  33. package/dist/composer/library/souls/hr.md +186 -0
  34. package/dist/composer/library/souls/plan.md +23 -0
  35. package/dist/composer/library/workflow/auto-mode.json +139 -0
  36. package/dist/composer/model-utils.js +39 -0
  37. package/dist/composer/opencode-profile.js +2299 -0
  38. package/dist/composer/package-manager.js +75 -0
  39. package/dist/composer/package-version.js +20 -0
  40. package/dist/composer/platform.js +48 -0
  41. package/dist/composer/query.js +133 -0
  42. package/dist/composer/settings.js +400 -0
  43. package/dist/plugins/opencode-agenthub.js +310 -0
  44. package/dist/plugins/opencode-question.js +223 -0
  45. package/dist/plugins/plan-guidance.js +263 -0
  46. package/dist/plugins/runtime-config.js +57 -0
  47. package/dist/skills/agenthub-doctor/SKILL.md +238 -0
  48. package/dist/skills/agenthub-doctor/diagnose.js +213 -0
  49. package/dist/skills/agenthub-doctor/fix.js +293 -0
  50. package/dist/skills/agenthub-doctor/index.js +30 -0
  51. package/dist/skills/agenthub-doctor/interactive.js +756 -0
  52. package/dist/skills/hr-assembly/SKILL.md +121 -0
  53. package/dist/skills/hr-final-check/SKILL.md +98 -0
  54. package/dist/skills/hr-review/SKILL.md +100 -0
  55. package/dist/skills/hr-staffing/SKILL.md +85 -0
  56. package/dist/skills/hr-support/bin/sync_sources.py +560 -0
  57. package/dist/skills/hr-support/bin/validate_staged_package.py +290 -0
  58. package/dist/skills/hr-support/bin/vendor_stage_mcps.py +234 -0
  59. package/dist/skills/hr-support/bin/vendor_stage_skills.py +104 -0
  60. package/dist/types.js +11 -0
  61. package/package.json +54 -0
@@ -0,0 +1,824 @@
1
+ import { existsSync } from "node:fs";
2
+ import {
3
+ lstat,
4
+ mkdir,
5
+ readdir,
6
+ readFile,
7
+ rm,
8
+ symlink,
9
+ writeFile
10
+ } from "node:fs/promises";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { fileURLToPath } from "node:url";
14
+ import {
15
+ normalizeModelSelection,
16
+ pickModelSelection
17
+ } from "./model-utils.js";
18
+ import {
19
+ loadNativeOpenCodeConfig,
20
+ readAgentHubSettings,
21
+ readNativePluginEntries,
22
+ readWorkflowInjectionConfig
23
+ } from "./settings.js";
24
+ import {
25
+ generateRunCmd,
26
+ generateRunScript,
27
+ resolveHomeConfigRoot,
28
+ symlinkType
29
+ } from "./platform.js";
30
+ const isPlainObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
31
+ const deepMergeRecords = (base, override) => {
32
+ const result = { ...base };
33
+ for (const [key, value] of Object.entries(override)) {
34
+ const existing = result[key];
35
+ result[key] = isPlainObject(existing) && isPlainObject(value) ? deepMergeRecords(existing, value) : value;
36
+ }
37
+ return result;
38
+ };
39
+ const activeRuntimeDirName = "current";
40
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
41
+ const repoSrcRoot = path.resolve(currentDir, "..");
42
+ const repoRoot = path.resolve(repoSrcRoot, "..");
43
+ const builtInLibraryRoot = path.join(currentDir, "library");
44
+ const templateRepoRoot = "${REPO_ROOT}";
45
+ const templateRepoSrcRoot = "${REPO_SRC_ROOT}";
46
+ const templateLibraryRoot = "${LIBRARY_ROOT}";
47
+ const OPENCODE_BUILTIN_AGENTS = ["general", "explore", "plan", "build"];
48
+ const unique = (values) => [...new Set(values)];
49
+ const resolveNativeAgentPolicy = (profile) => {
50
+ if (profile.nativeAgentPolicy) return profile.nativeAgentPolicy;
51
+ if (profile.inheritNativeAgents === false) return "override";
52
+ return "inherit";
53
+ };
54
+ const isDisabledAgentEntry = (value) => isPlainObject(value) && value.disable === true;
55
+ const validateProfileDefaultAgent = ({
56
+ profile,
57
+ bundles,
58
+ agentConfig,
59
+ nativeAgentPolicy
60
+ }) => {
61
+ if (nativeAgentPolicy === "team-only" && !profile.defaultAgent?.trim()) {
62
+ throw new Error(
63
+ `Team-only profile '${profile.name}' must set defaultAgent explicitly so the runtime has a stable primary agent when built-ins are disabled.`
64
+ );
65
+ }
66
+ if (!profile.defaultAgent) return void 0;
67
+ const configuredDefaultAgent = profile.defaultAgent.trim();
68
+ if (!configuredDefaultAgent) return void 0;
69
+ const bundleMatch = bundles.find((bundle) => bundle.name === configuredDefaultAgent);
70
+ if (bundleMatch && bundleMatch.agent.name !== configuredDefaultAgent) {
71
+ const availableAgents2 = Object.entries(agentConfig).filter(([, entry]) => !isDisabledAgentEntry(entry)).map(([name]) => name).sort();
72
+ throw new Error(
73
+ `Profile '${profile.name}' sets defaultAgent '${configuredDefaultAgent}', but that matches bundle '${bundleMatch.name}' instead of its bundle agent.name '${bundleMatch.agent.name}'. Set defaultAgent to '${bundleMatch.agent.name}'. Available agent names: ${availableAgents2.join(", ") || "(none)"}.`
74
+ );
75
+ }
76
+ const matchedAgent = agentConfig[configuredDefaultAgent];
77
+ if (matchedAgent && !isDisabledAgentEntry(matchedAgent)) {
78
+ return configuredDefaultAgent;
79
+ }
80
+ const bundleHint = bundleMatch ? ` Bundle '${bundleMatch.name}' uses bundle agent.name '${bundleMatch.agent.name}'. Set defaultAgent to that value instead.` : "";
81
+ const disabledHint = matchedAgent && isDisabledAgentEntry(matchedAgent) ? ` Agent '${configuredDefaultAgent}' exists in the composed runtime but is disabled under nativeAgentPolicy '${nativeAgentPolicy}'.` : "";
82
+ const availableAgents = Object.entries(agentConfig).filter(([, entry]) => !isDisabledAgentEntry(entry)).map(([name]) => name).sort();
83
+ throw new Error(
84
+ `Profile '${profile.name}' sets defaultAgent '${configuredDefaultAgent}', but no enabled generated agent matches that name.${bundleHint}${disabledHint} Available agent names: ${availableAgents.join(", ") || "(none)"}.`
85
+ );
86
+ };
87
+ const workflowInjectionMatchesBundles = (workflowInjection, bundleNames = []) => {
88
+ if (!workflowInjection?.enabled) return false;
89
+ if (!workflowInjection.bundles || workflowInjection.bundles.length === 0) {
90
+ return true;
91
+ }
92
+ return workflowInjection.bundles.some((bundleName) => bundleNames.includes(bundleName));
93
+ };
94
+ const resolveRuntimeInjectionConfig = ({
95
+ planDetection,
96
+ workflowInjection,
97
+ bundleNames = []
98
+ }) => {
99
+ if (workflowInjection) {
100
+ const matchedWorkflowInjection = workflowInjectionMatchesBundles(workflowInjection, bundleNames) ? workflowInjection : void 0;
101
+ return {
102
+ ...matchedWorkflowInjection ? { workflowInjection: matchedWorkflowInjection } : {},
103
+ ...matchedWorkflowInjection && planDetection?.enabled ? { planDetection } : {}
104
+ };
105
+ }
106
+ return {
107
+ planDetection: planDetection?.enabled ? planDetection : void 0
108
+ };
109
+ };
110
+ const readJson = async (filePath) => {
111
+ const content = await readFile(filePath, "utf-8");
112
+ return JSON.parse(content);
113
+ };
114
+ const ensureDir = async (dirPath) => {
115
+ await mkdir(dirPath, { recursive: true });
116
+ };
117
+ const pathExists = async (targetPath) => {
118
+ try {
119
+ await lstat(targetPath);
120
+ return true;
121
+ } catch {
122
+ return false;
123
+ }
124
+ };
125
+ const getAgentHubHome = () => process.env.OPENCODE_AGENTHUB_HOME || resolveHomeConfigRoot(os.homedir(), "opencode-agenthub");
126
+ const resolveLibraryRoot = (homeRoot = getAgentHubHome()) => {
127
+ const agentHubHome = homeRoot;
128
+ if (existsSync(path.join(agentHubHome, "profiles")) && existsSync(path.join(agentHubHome, "bundles"))) {
129
+ return agentHubHome;
130
+ }
131
+ return builtInLibraryRoot;
132
+ };
133
+ const resolveCandidateLibraryRoots = (libraryRoot) => unique([libraryRoot, builtInLibraryRoot]);
134
+ const firstExistingPath = async (paths) => {
135
+ for (const candidate of paths) {
136
+ if (await pathExists(candidate)) {
137
+ return candidate;
138
+ }
139
+ }
140
+ return null;
141
+ };
142
+ const loadProfile = async (libraryRoot, profileName) => {
143
+ for (const root of resolveCandidateLibraryRoots(libraryRoot)) {
144
+ const profilePath = path.join(root, "profiles", `${profileName}.json`);
145
+ const profile = await readJsonIfExists(profilePath);
146
+ if (profile) {
147
+ return profile;
148
+ }
149
+ }
150
+ throw new Error(
151
+ `Profile '${profileName}' not found in ${path.join(libraryRoot, "profiles")} or ${path.join(builtInLibraryRoot, "profiles")}. Create that profile or re-run bootstrap with a starter kit.`
152
+ );
153
+ };
154
+ const loadBundle = async (libraryRoot, bundleName) => {
155
+ let raw = null;
156
+ for (const root of resolveCandidateLibraryRoots(libraryRoot)) {
157
+ raw = await readJsonIfExists(
158
+ path.join(root, "bundles", `${bundleName}.json`)
159
+ );
160
+ if (raw) break;
161
+ }
162
+ if (!raw) {
163
+ throw new Error(
164
+ `Bundle '${bundleName}' not found in ${path.join(libraryRoot, "bundles")} or ${path.join(builtInLibraryRoot, "bundles")}.`
165
+ );
166
+ }
167
+ const soul = raw.soul || raw.prompt;
168
+ if (!soul) {
169
+ throw new Error(`Bundle '${bundleName}' is missing required 'soul' field.`);
170
+ }
171
+ return {
172
+ ...raw,
173
+ soul
174
+ };
175
+ };
176
+ const readJsonIfExists = async (filePath) => {
177
+ try {
178
+ return await readJson(filePath);
179
+ } catch (error) {
180
+ if (error.code === "ENOENT") {
181
+ return null;
182
+ }
183
+ throw error;
184
+ }
185
+ };
186
+ const replaceTemplateTokens = (value, libraryRoot) => value.replaceAll(templateRepoRoot, repoRoot).replaceAll(templateRepoSrcRoot, repoSrcRoot).replaceAll(templateLibraryRoot, libraryRoot);
187
+ const resolveTemplateValue = (value, libraryRoot) => {
188
+ if (typeof value === "string") {
189
+ return replaceTemplateTokens(value, libraryRoot);
190
+ }
191
+ if (Array.isArray(value)) {
192
+ return value.map((item) => resolveTemplateValue(item, libraryRoot));
193
+ }
194
+ if (value && typeof value === "object") {
195
+ return Object.fromEntries(
196
+ Object.entries(value).map(
197
+ ([key, entryValue]) => [
198
+ key,
199
+ resolveTemplateValue(entryValue, libraryRoot)
200
+ ]
201
+ )
202
+ );
203
+ }
204
+ return value;
205
+ };
206
+ const loadMcpEntry = async (libraryRoot, mcpName) => {
207
+ const candidateRoots = unique([libraryRoot, builtInLibraryRoot]);
208
+ for (const root of candidateRoots) {
209
+ const entry = await readJsonIfExists(
210
+ path.join(root, "mcp", `${mcpName}.json`)
211
+ );
212
+ if (entry) {
213
+ return resolveTemplateValue(entry, root);
214
+ }
215
+ }
216
+ throw new Error(
217
+ `Unknown MCP entry '${mcpName}'. Add ${mcpName}.json under ${path.join(libraryRoot, "mcp")} or ${path.join(builtInLibraryRoot, "mcp")}.`
218
+ );
219
+ };
220
+ const loadAllMcpEntries = async (libraryRoot) => {
221
+ const mcpNames = unique(
222
+ (await Promise.all(
223
+ resolveCandidateLibraryRoots(libraryRoot).map(async (root) => {
224
+ const mcpDir = path.join(root, "mcp");
225
+ if (!await pathExists(mcpDir)) return [];
226
+ const entries = await readdir(mcpDir, { withFileTypes: true });
227
+ return entries.filter((entry) => entry.isFile() && path.extname(entry.name) === ".json").map((entry) => path.basename(entry.name, ".json"));
228
+ })
229
+ )).flat()
230
+ ).sort();
231
+ if (mcpNames.length === 0) return {};
232
+ return Object.fromEntries(
233
+ await Promise.all(
234
+ mcpNames.map(async (name) => [
235
+ name,
236
+ await loadMcpEntry(libraryRoot, name)
237
+ ])
238
+ )
239
+ );
240
+ };
241
+ const resetGeneratedDir = async (dirPath) => {
242
+ await rm(dirPath, { recursive: true, force: true });
243
+ await ensureDir(dirPath);
244
+ };
245
+ const listAgentHubSoulNames = async (libraryRoot) => {
246
+ return unique(
247
+ (await Promise.all(
248
+ resolveCandidateLibraryRoots(libraryRoot).map(async (root) => {
249
+ const soulsRoot = path.join(root, "souls");
250
+ if (!await pathExists(soulsRoot)) return [];
251
+ const entries = await readdir(soulsRoot, { withFileTypes: true });
252
+ return entries.filter((entry) => entry.isFile() && path.extname(entry.name) === ".md").map((entry) => path.basename(entry.name, ".md"));
253
+ })
254
+ )).flat()
255
+ ).sort();
256
+ };
257
+ const listAgentHubSkillNames = async (libraryRoot) => {
258
+ return unique(
259
+ (await Promise.all(
260
+ resolveCandidateLibraryRoots(libraryRoot).map(async (root) => {
261
+ const skillsRoot = path.join(root, "skills");
262
+ if (!await pathExists(skillsRoot)) return [];
263
+ const entries = await readdir(skillsRoot, { withFileTypes: true });
264
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
265
+ })
266
+ )).flat()
267
+ ).sort();
268
+ };
269
+ const readWorkflowInjectionConfigWithFallback = async (libraryRoot, fallbackRoot) => {
270
+ for (const root of unique([libraryRoot, fallbackRoot, builtInLibraryRoot]).filter(Boolean)) {
271
+ const config = await readWorkflowInjectionConfig(root);
272
+ if (config) {
273
+ return config;
274
+ }
275
+ }
276
+ return null;
277
+ };
278
+ const resetWorkspaceRuntimeRoot = async (workspace, outputRoot) => {
279
+ const workspaceRuntimeRoot = path.join(workspace, ".opencode-agenthub");
280
+ if (path.resolve(path.dirname(outputRoot)) !== path.resolve(workspaceRuntimeRoot)) {
281
+ await ensureDir(outputRoot);
282
+ return;
283
+ }
284
+ await rm(workspaceRuntimeRoot, { recursive: true, force: true });
285
+ await ensureDir(outputRoot);
286
+ await writeFile(
287
+ path.join(workspaceRuntimeRoot, "active-runtime.json"),
288
+ `${JSON.stringify({ active: path.basename(outputRoot) }, null, 2)}
289
+ `,
290
+ "utf-8"
291
+ );
292
+ };
293
+ const resolveSoulSource = async (libraryRoot, soulName) => {
294
+ const source = await firstExistingPath([
295
+ path.join(libraryRoot, "souls", `${soulName}.md`),
296
+ path.join(libraryRoot, "agents", `${soulName}.md`),
297
+ path.join(builtInLibraryRoot, "souls", `${soulName}.md`),
298
+ path.join(builtInLibraryRoot, "agents", `${soulName}.md`),
299
+ path.join(repoSrcRoot, "agents", `${soulName}.md`)
300
+ ]);
301
+ if (!source) {
302
+ throw new Error(
303
+ `Unknown soul '${soulName}'. Add ${soulName}.md under ${path.join(libraryRoot, "souls")} or provide a repo source file.`
304
+ );
305
+ }
306
+ return source;
307
+ };
308
+ const resolveSkillSource = async (libraryRoot, skillName) => {
309
+ const source = await firstExistingPath([
310
+ path.join(libraryRoot, "skills", skillName),
311
+ path.join(builtInLibraryRoot, "skills", skillName),
312
+ path.join(repoSrcRoot, "skills", skillName)
313
+ ]);
314
+ if (!source) {
315
+ throw new Error(
316
+ `Unknown skill '${skillName}'. Add ${skillName} under ${path.join(libraryRoot, "skills")} or provide a repo source directory.`
317
+ );
318
+ }
319
+ return source;
320
+ };
321
+ const resolveInstructionSource = async (libraryRoot, instructionName) => {
322
+ const source = await firstExistingPath([
323
+ path.join(libraryRoot, "instructions", `${instructionName}.md`),
324
+ path.join(builtInLibraryRoot, "instructions", `${instructionName}.md`),
325
+ path.join(repoSrcRoot, "instructions", `${instructionName}.md`)
326
+ ]);
327
+ if (!source) {
328
+ throw new Error(
329
+ `Unknown instruction '${instructionName}'. Add ${instructionName}.md under ${path.join(libraryRoot, "instructions")} or provide a repo source file.`
330
+ );
331
+ }
332
+ return source;
333
+ };
334
+ const mergeAgentPrompt = async (libraryRoot, bundle) => {
335
+ const soulSource = await resolveSoulSource(libraryRoot, bundle.soul);
336
+ const soulText = await readFile(soulSource, "utf-8");
337
+ const instructionTexts = await Promise.all(
338
+ (bundle.instructions || []).map(async (instructionName) => {
339
+ const source = await resolveInstructionSource(libraryRoot, instructionName);
340
+ const content = await readFile(source, "utf-8");
341
+ return {
342
+ instructionName,
343
+ content: content.trim()
344
+ };
345
+ })
346
+ );
347
+ const sections = [soulText.trim()];
348
+ for (const instruction of instructionTexts) {
349
+ if (!instruction.content) continue;
350
+ sections.push(`## Attached Instruction: ${instruction.instructionName}
351
+
352
+ ${instruction.content}`);
353
+ }
354
+ return `${sections.filter(Boolean).join("\n\n")}
355
+ `;
356
+ };
357
+ const mountBundleArtifacts = async (outputRoot, libraryRoot, bundles) => {
358
+ const agentsDir = path.join(outputRoot, "agents");
359
+ const skillsDir = path.join(outputRoot, "skills");
360
+ await Promise.all([
361
+ resetGeneratedDir(agentsDir),
362
+ resetGeneratedDir(skillsDir)
363
+ ]);
364
+ const mountedAgentNames = /* @__PURE__ */ new Set();
365
+ for (const bundle of bundles) {
366
+ if (mountedAgentNames.has(bundle.agent.name)) continue;
367
+ const mergedPrompt = await mergeAgentPrompt(libraryRoot, bundle);
368
+ await writeFile(path.join(agentsDir, `${bundle.agent.name}.md`), mergedPrompt, "utf-8");
369
+ mountedAgentNames.add(bundle.agent.name);
370
+ }
371
+ const skillNames = unique(bundles.flatMap((bundle) => bundle.skills));
372
+ for (const skillName of skillNames) {
373
+ const source = await resolveSkillSource(libraryRoot, skillName);
374
+ await symlink(source, path.join(skillsDir, skillName), symlinkType());
375
+ }
376
+ };
377
+ const toGeneratedHeader = (source) => `// GENERATED BY opencode-agenthub. DO NOT EDIT.
378
+ // Source: ${source}
379
+ `;
380
+ const phaseDescription = (category) => `${category} phase worker generated from workflow bundle`;
381
+ const loadOmoBaseline = async () => {
382
+ const baselinePath = path.join(
383
+ resolveHomeConfigRoot(os.homedir(), "opencode"),
384
+ "oh-my-opencode.json"
385
+ );
386
+ return readJsonIfExists(baselinePath);
387
+ };
388
+ const writeGeneratedRuntimeFiles = async ({
389
+ outputRoot,
390
+ source,
391
+ opencodeConfig,
392
+ lock,
393
+ omoConfig,
394
+ planDetection,
395
+ workflowInjection
396
+ }) => {
397
+ const opencodeConfigText = `${toGeneratedHeader(source)}${JSON.stringify(opencodeConfig, null, 2)}
398
+ `;
399
+ await writeFile(
400
+ path.join(outputRoot, "opencode.jsonc"),
401
+ opencodeConfigText,
402
+ "utf-8"
403
+ );
404
+ if (omoConfig && Object.keys(omoConfig).length > 0) {
405
+ const omoConfigText = `${toGeneratedHeader(source)}${JSON.stringify(omoConfig, null, 2)}
406
+ `;
407
+ await writeFile(
408
+ path.join(outputRoot, "oh-my-opencode.json"),
409
+ omoConfigText,
410
+ "utf-8"
411
+ );
412
+ }
413
+ const xdgConfigText = `${toGeneratedHeader(source)}${JSON.stringify({
414
+ $schema: "https://opencode.ai/config.json",
415
+ plugin: Array.isArray(opencodeConfig.plugin) ? opencodeConfig.plugin : []
416
+ }, null, 2)}
417
+ `;
418
+ await writeFile(
419
+ path.join(outputRoot, "xdg", "opencode", "opencode.json"),
420
+ xdgConfigText,
421
+ "utf-8"
422
+ );
423
+ await writeFile(
424
+ path.join(outputRoot, "agenthub-lock.json"),
425
+ `${toGeneratedHeader(source)}${JSON.stringify(lock, null, 2)}
426
+ `,
427
+ "utf-8"
428
+ );
429
+ if (lock.runtimeInfo) {
430
+ const runtimeConfig = {
431
+ generated: (/* @__PURE__ */ new Date()).toISOString(),
432
+ agents: lock.runtimeInfo,
433
+ ...planDetection?.enabled ? { planDetection } : {},
434
+ ...workflowInjection?.enabled ? { workflowInjection } : {}
435
+ };
436
+ await writeFile(
437
+ path.join(outputRoot, "agenthub-runtime.json"),
438
+ `${toGeneratedHeader(source)}${JSON.stringify(runtimeConfig, null, 2)}
439
+ `,
440
+ "utf-8"
441
+ );
442
+ }
443
+ await writeFile(path.join(outputRoot, "run.sh"), generateRunScript(), "utf-8");
444
+ await writeFile(path.join(outputRoot, "run.cmd"), generateRunCmd(), "utf-8");
445
+ };
446
+ const resolveGuard = (guardName, guardRegistry, visited = /* @__PURE__ */ new Set()) => {
447
+ if (visited.has(guardName)) {
448
+ const chain = Array.from(visited).concat(guardName);
449
+ const error = new Error(
450
+ `Circular guard dependency detected: ${chain.join(" -> ")}`
451
+ );
452
+ error.name = "GuardResolutionError";
453
+ error.guardName = guardName;
454
+ error.chain = chain;
455
+ throw error;
456
+ }
457
+ const guardDef = guardRegistry[guardName];
458
+ if (!guardDef) {
459
+ console.warn(`\u26A0\uFE0F Warning: Guard '${guardName}' not found in registry. Skipping.`);
460
+ return { permission: {}, blockedTools: [] };
461
+ }
462
+ const newVisited = new Set(visited);
463
+ newVisited.add(guardName);
464
+ const parentGuards = [];
465
+ if (guardDef.extends && guardDef.extends.length > 0) {
466
+ for (const parentName of guardDef.extends) {
467
+ parentGuards.push(resolveGuard(parentName, guardRegistry, newVisited));
468
+ }
469
+ }
470
+ const mergedPermission = {};
471
+ const mergedBlockedTools = [];
472
+ for (const parent of parentGuards) {
473
+ Object.assign(mergedPermission, parent.permission);
474
+ mergedBlockedTools.push(...parent.blockedTools);
475
+ }
476
+ if (guardDef.permission) {
477
+ Object.assign(mergedPermission, guardDef.permission);
478
+ }
479
+ if (guardDef.blockedTools) {
480
+ mergedBlockedTools.push(...guardDef.blockedTools);
481
+ }
482
+ return {
483
+ permission: mergedPermission,
484
+ blockedTools: unique(mergedBlockedTools)
485
+ };
486
+ };
487
+ const resolveGuards = (guardNames, guardRegistry) => {
488
+ if (!guardNames || guardNames.length === 0) {
489
+ return { permission: {}, blockedTools: [] };
490
+ }
491
+ const resolvedGuards = guardNames.map(
492
+ (name) => resolveGuard(name, guardRegistry)
493
+ );
494
+ const mergedPermission = {};
495
+ const mergedBlockedTools = [];
496
+ for (const resolved of resolvedGuards) {
497
+ Object.assign(mergedPermission, resolved.permission);
498
+ mergedBlockedTools.push(...resolved.blockedTools);
499
+ }
500
+ return {
501
+ permission: mergedPermission,
502
+ blockedTools: unique(mergedBlockedTools)
503
+ };
504
+ };
505
+ const composeWorkspace = async (workspace, profileName, configRoot, options = {}) => {
506
+ const libraryRoot = resolveLibraryRoot(options.homeRoot);
507
+ const settingsRoot = options.settingsRoot || libraryRoot;
508
+ const profile = await loadProfile(libraryRoot, profileName);
509
+ const bundles = await Promise.all(
510
+ profile.bundles.map((bundleName) => loadBundle(libraryRoot, bundleName))
511
+ );
512
+ const outputRoot = configRoot || path.join(workspace, ".opencode-agenthub", activeRuntimeDirName);
513
+ await resetWorkspaceRuntimeRoot(workspace, outputRoot);
514
+ await ensureDir(path.join(outputRoot, "xdg", "opencode"));
515
+ await mountBundleArtifacts(outputRoot, libraryRoot, bundles);
516
+ const allMcpNames = unique(bundles.flatMap((bundle) => bundle.mcp || []));
517
+ const mcpEntries = Object.fromEntries(
518
+ await Promise.all(
519
+ allMcpNames.map(async (name) => [
520
+ name,
521
+ await loadMcpEntry(libraryRoot, name)
522
+ ])
523
+ )
524
+ );
525
+ const settings = await readAgentHubSettings(libraryRoot) || (settingsRoot !== libraryRoot ? await readAgentHubSettings(settingsRoot) : null);
526
+ const workflowInjectionConfig = await readWorkflowInjectionConfigWithFallback(
527
+ libraryRoot,
528
+ settingsRoot
529
+ );
530
+ const runtimeInjectionConfig = resolveRuntimeInjectionConfig({
531
+ planDetection: settings?.planDetection,
532
+ workflowInjection: workflowInjectionConfig,
533
+ bundleNames: bundles.map((bundle) => bundle.name)
534
+ });
535
+ const agentConfig = {};
536
+ const omoCategories = {};
537
+ const runtimeInfo = {};
538
+ const guardRegistry = settings?.guards || {};
539
+ const hasOmoBundle = bundles.some((b) => b.runtime === "omo");
540
+ const nativeBundles = bundles.filter((b) => b.runtime === "native" || !b.runtime);
541
+ if (hasOmoBundle && nativeBundles.length > 0) {
542
+ for (const bundle of nativeBundles) {
543
+ if (!bundle.guards?.includes("no_omo")) {
544
+ bundle.guards = [...bundle.guards || [], "no_omo"];
545
+ }
546
+ }
547
+ }
548
+ for (const bundle of bundles) {
549
+ const bundleGuards = bundle.guards || [];
550
+ const settingsGuards = settings?.agents?.[bundle.agent.name]?.guards || [];
551
+ const allGuards = unique([...bundleGuards, ...settingsGuards]);
552
+ const resolvedGuard = resolveGuards(allGuards, guardRegistry);
553
+ const settingsPermission = settings?.agents?.[bundle.agent.name]?.permission;
554
+ const permission = {
555
+ ...bundle.agent.permission || {},
556
+ ...resolvedGuard.permission,
557
+ // Apply resolved guard permissions
558
+ ...settingsPermission || {}
559
+ // Settings override everything
560
+ };
561
+ const blockedTools = [...resolvedGuard.blockedTools];
562
+ const finalSkills = unique(bundle.skills || []);
563
+ if (bundle.runtime === "omo" && bundle.categories) {
564
+ permission.task = { "*": "deny" };
565
+ for (const category of Object.keys(bundle.categories)) {
566
+ permission.task[category] = "allow";
567
+ omoCategories[category] = { model: bundle.categories[category] };
568
+ agentConfig[category] = {
569
+ mode: "subagent",
570
+ hidden: true,
571
+ description: phaseDescription(category)
572
+ };
573
+ }
574
+ permission.question = "allow";
575
+ }
576
+ runtimeInfo[bundle.agent.name] = {
577
+ runtime: bundle.runtime,
578
+ blockedTools,
579
+ guards: allGuards,
580
+ skills: finalSkills
581
+ };
582
+ const resolvedModel = pickModelSelection(
583
+ normalizeModelSelection(
584
+ settings?.agents?.[bundle.agent.name]?.model,
585
+ settings?.agents?.[bundle.agent.name]?.variant
586
+ ),
587
+ normalizeModelSelection(bundle.agent.model, bundle.agent.variant),
588
+ normalizeModelSelection(settings?.opencode?.model)
589
+ );
590
+ agentConfig[bundle.agent.name] = {
591
+ mode: bundle.agent.mode,
592
+ hidden: bundle.agent.hidden,
593
+ ...resolvedModel.model ? { model: resolvedModel.model } : {},
594
+ ...resolvedModel.variant ? { variant: resolvedModel.variant } : {},
595
+ ...typeof bundle.agent.steps === "number" ? { steps: bundle.agent.steps } : {},
596
+ description: bundle.agent.description,
597
+ ...finalSkills.length > 0 ? { skills: finalSkills } : {},
598
+ ...Object.keys(permission).length > 0 ? { permission } : {}
599
+ };
600
+ }
601
+ const nativeConfig = await loadNativeOpenCodeConfig();
602
+ const nativePluginEntries = await readNativePluginEntries();
603
+ const nativeAgents = nativeConfig?.agent || {};
604
+ const nativeAgentPolicy = resolveNativeAgentPolicy(profile);
605
+ if (nativeAgentPolicy === "inherit") {
606
+ for (const [agentName, nativeAgent] of Object.entries(nativeAgents)) {
607
+ if (!nativeAgent || typeof nativeAgent !== "object") continue;
608
+ if (agentConfig[agentName]) continue;
609
+ const settingsAgent = settings?.agents?.[agentName];
610
+ const resolvedModel = pickModelSelection(
611
+ normalizeModelSelection(settingsAgent?.model, settingsAgent?.variant),
612
+ normalizeModelSelection(
613
+ typeof nativeAgent.model === "string" ? nativeAgent.model : void 0,
614
+ typeof nativeAgent.variant === "string" ? nativeAgent.variant : void 0
615
+ )
616
+ );
617
+ const nativePermission = nativeAgent.permission && typeof nativeAgent.permission === "object" ? nativeAgent.permission : void 0;
618
+ const mergedPermission = deepMergeRecords(
619
+ nativePermission || {},
620
+ settingsAgent?.permission || {}
621
+ );
622
+ agentConfig[agentName] = {
623
+ ...nativeAgent,
624
+ ...resolvedModel.model ? { model: resolvedModel.model } : {},
625
+ ...resolvedModel.variant ? { variant: resolvedModel.variant } : {},
626
+ ...settingsAgent?.prompt ? { prompt: settingsAgent.prompt } : {},
627
+ ...Object.keys(mergedPermission).length > 0 ? { permission: mergedPermission } : {}
628
+ };
629
+ }
630
+ }
631
+ if (nativeAgentPolicy === "team-only") {
632
+ for (const builtInName of OPENCODE_BUILTIN_AGENTS) {
633
+ if (!agentConfig[builtInName]) {
634
+ agentConfig[builtInName] = { disable: true };
635
+ }
636
+ }
637
+ }
638
+ const resolvedDefaultAgent = validateProfileDefaultAgent({
639
+ profile,
640
+ bundles,
641
+ agentConfig,
642
+ nativeAgentPolicy
643
+ });
644
+ const omoBaseline = Object.keys(omoCategories).length > 0 ? await loadOmoBaseline() : null;
645
+ const omoConfig = Object.keys(omoCategories).length > 0 ? {
646
+ ...omoBaseline || {},
647
+ categories: {
648
+ ...omoBaseline?.categories || {},
649
+ ...omoCategories
650
+ }
651
+ } : void 0;
652
+ const resolvedGlobalModel = normalizeModelSelection(settings?.opencode?.model);
653
+ const opencodeConfig = {
654
+ $schema: "https://opencode.ai/config.json",
655
+ ...settings?.opencode?.provider ? { provider: settings.opencode.provider } : {},
656
+ ...resolvedGlobalModel.model ? { model: resolvedGlobalModel.model } : {},
657
+ ...settings?.opencode?.small_model ? { small_model: settings.opencode.small_model } : {},
658
+ plugin: unique([...profile.plugins, ...nativePluginEntries]),
659
+ ...Object.keys(mcpEntries).length > 0 ? { mcp: mcpEntries } : {},
660
+ ...resolvedDefaultAgent ? { default_agent: resolvedDefaultAgent } : {},
661
+ agent: agentConfig
662
+ };
663
+ const lock = {
664
+ profile: profile.name,
665
+ nativeAgentPolicy,
666
+ libraryRoot,
667
+ ...settingsRoot !== libraryRoot ? { settingsRoot } : {},
668
+ workspace,
669
+ configRoot: outputRoot,
670
+ bundles: bundles.map((bundle) => ({
671
+ name: bundle.name,
672
+ runtime: bundle.runtime,
673
+ soul: bundle.soul,
674
+ instructions: bundle.instructions || [],
675
+ skills: bundle.skills,
676
+ mcp: bundle.mcp || [],
677
+ guards: bundle.guards || []
678
+ })),
679
+ runtimeInfo
680
+ // Add runtime info for plugin
681
+ };
682
+ await writeGeneratedRuntimeFiles({
683
+ outputRoot,
684
+ source: `profile:${profile.name}`,
685
+ opencodeConfig,
686
+ lock,
687
+ omoConfig,
688
+ planDetection: runtimeInjectionConfig.planDetection,
689
+ workflowInjection: runtimeInjectionConfig.workflowInjection
690
+ });
691
+ return { workspace, configRoot: outputRoot, profile, bundles };
692
+ };
693
+ const composeToolInjection = async (workspace, configRoot, options = {}) => {
694
+ const libraryRoot = resolveLibraryRoot(options.homeRoot);
695
+ const settingsRoot = options.settingsRoot || libraryRoot;
696
+ const outputRoot = configRoot || path.join(workspace, ".opencode-agenthub", activeRuntimeDirName);
697
+ await resetWorkspaceRuntimeRoot(workspace, outputRoot);
698
+ await ensureDir(path.join(outputRoot, "xdg", "opencode"));
699
+ await resetGeneratedDir(path.join(outputRoot, "agents"));
700
+ await resetGeneratedDir(path.join(outputRoot, "skills"));
701
+ const mcpEntries = await loadAllMcpEntries(libraryRoot);
702
+ const opencodeConfig = {
703
+ $schema: "https://opencode.ai/config.json",
704
+ ...Object.keys(mcpEntries).length > 0 ? { mcp: mcpEntries } : {}
705
+ };
706
+ const lock = {
707
+ mode: "tool-injection",
708
+ libraryRoot,
709
+ workspace,
710
+ configRoot: outputRoot,
711
+ mcp: Object.keys(mcpEntries)
712
+ };
713
+ const workflowInjectionConfig = await readWorkflowInjectionConfigWithFallback(
714
+ libraryRoot,
715
+ settingsRoot
716
+ );
717
+ await writeGeneratedRuntimeFiles({
718
+ outputRoot,
719
+ source: "mode:tool-injection",
720
+ opencodeConfig,
721
+ lock,
722
+ workflowInjection: workflowInjectionMatchesBundles(workflowInjectionConfig) ? workflowInjectionConfig : void 0
723
+ });
724
+ return {
725
+ workspace,
726
+ configRoot: outputRoot,
727
+ mcpNames: Object.keys(mcpEntries)
728
+ };
729
+ };
730
+ const composeCustomizedAgent = async (workspace, configRoot, options = {}) => {
731
+ const libraryRoot = resolveLibraryRoot(options.homeRoot);
732
+ const settingsRoot = options.settingsRoot || libraryRoot;
733
+ const outputRoot = configRoot || path.join(workspace, ".opencode-agenthub", activeRuntimeDirName);
734
+ const souls = await listAgentHubSoulNames(libraryRoot);
735
+ if (souls.length === 0) {
736
+ throw new Error(
737
+ `customized-agent mode requires at least one soul in ${path.join(libraryRoot, "souls")}`
738
+ );
739
+ }
740
+ const selectedSoul = souls[0];
741
+ const skills = await listAgentHubSkillNames(libraryRoot);
742
+ const mcpEntries = await loadAllMcpEntries(libraryRoot);
743
+ const mcpNames = Object.keys(mcpEntries).sort();
744
+ const agentName = selectedSoul;
745
+ await resetWorkspaceRuntimeRoot(workspace, outputRoot);
746
+ await ensureDir(path.join(outputRoot, "xdg", "opencode"));
747
+ await mountBundleArtifacts(outputRoot, libraryRoot, [
748
+ {
749
+ name: "customized-agent",
750
+ runtime: "native",
751
+ soul: selectedSoul,
752
+ instructions: [],
753
+ skills,
754
+ mcp: mcpNames,
755
+ guards: ["no_task"],
756
+ agent: {
757
+ name: agentName,
758
+ mode: "primary",
759
+ hidden: false,
760
+ model: "github-copilot/claude-haiku-4.5",
761
+ description: "Auto-generated native agent from imported Agent Hub assets"
762
+ }
763
+ }
764
+ ]);
765
+ const opencodeConfig = {
766
+ $schema: "https://opencode.ai/config.json",
767
+ plugin: ["opencode-agenthub"],
768
+ ...mcpNames.length > 0 ? { mcp: mcpEntries } : {},
769
+ agent: {
770
+ [agentName]: {
771
+ mode: "primary",
772
+ hidden: false,
773
+ model: "github-copilot/claude-haiku-4.5",
774
+ description: "Auto-generated native agent from imported Agent Hub assets",
775
+ permission: {
776
+ task: { "*": "deny" }
777
+ }
778
+ }
779
+ }
780
+ };
781
+ const lock = {
782
+ mode: "customized-agent",
783
+ libraryRoot,
784
+ workspace,
785
+ configRoot: outputRoot,
786
+ soul: selectedSoul,
787
+ skills,
788
+ mcp: mcpNames
789
+ };
790
+ const workflowInjectionConfig = await readWorkflowInjectionConfigWithFallback(
791
+ libraryRoot,
792
+ settingsRoot
793
+ );
794
+ await writeGeneratedRuntimeFiles({
795
+ outputRoot,
796
+ source: "mode:customized-agent",
797
+ opencodeConfig,
798
+ lock,
799
+ workflowInjection: workflowInjectionMatchesBundles(workflowInjectionConfig) ? workflowInjectionConfig : void 0
800
+ });
801
+ return {
802
+ workspace,
803
+ configRoot: outputRoot,
804
+ soul: selectedSoul,
805
+ skills,
806
+ mcpNames
807
+ };
808
+ };
809
+ const getDefaultConfigRoot = (workspace, _profileName) => path.join(workspace, ".opencode-agenthub", activeRuntimeDirName);
810
+ const getWorkspaceRuntimeRoot = (workspace) => path.join(workspace, ".opencode-agenthub", activeRuntimeDirName);
811
+ const getAgentHubPaths = () => ({
812
+ repoRoot,
813
+ repoSrcRoot,
814
+ builtInLibraryRoot,
815
+ agentHubHome: getAgentHubHome()
816
+ });
817
+ export {
818
+ composeCustomizedAgent,
819
+ composeToolInjection,
820
+ composeWorkspace,
821
+ getAgentHubPaths,
822
+ getDefaultConfigRoot,
823
+ getWorkspaceRuntimeRoot
824
+ };