opencode-agenthub 0.1.0 → 0.1.2
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 +5 -2
- package/dist/composer/builtin-assets.js +2 -0
- package/dist/composer/compose.js +30 -1
- package/dist/composer/library/bundles/auto.json +1 -1
- package/dist/composer/library/bundles/explore.json +21 -0
- package/dist/composer/library/instructions/hr-boundaries.md +1 -0
- package/dist/composer/library/instructions/hr-protocol.md +12 -13
- package/dist/composer/library/souls/explore.md +26 -0
- package/dist/composer/library/souls/hr-adapter.md +1 -0
- package/dist/composer/library/souls/hr-cto.md +3 -1
- package/dist/composer/library/souls/hr-planner.md +3 -3
- package/dist/composer/library/souls/hr-sourcer.md +1 -0
- package/dist/composer/library/souls/hr.md +24 -25
- package/dist/composer/model-utils.js +25 -1
- package/dist/composer/opencode-profile.js +366 -78
- package/dist/composer/platform.js +19 -0
- package/dist/composer/settings.js +103 -2
- package/dist/skills/agenthub-doctor/diagnose.js +21 -0
- package/dist/skills/agenthub-doctor/fix.js +1 -1
- package/dist/skills/agenthub-doctor/interactive.js +21 -2
- package/dist/skills/hr-assembly/SKILL.md +3 -0
- package/dist/skills/hr-final-check/SKILL.md +3 -1
- package/dist/skills/hr-staffing/SKILL.md +4 -4
- package/dist/skills/hr-support/bin/validate_staged_package.py +43 -11
- package/package.json +3 -3
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import {
|
|
6
|
+
normalizeModelSelection,
|
|
7
|
+
pickModelSelection,
|
|
8
|
+
validateModelAgainstCatalog,
|
|
9
|
+
validateModelIdentifier
|
|
10
|
+
} from "./model-utils.js";
|
|
5
11
|
import { buildBuiltinVersionManifest } from "./builtin-assets.js";
|
|
6
12
|
import { getDefaultProfilePlugins } from "./defaults.js";
|
|
7
13
|
import { readPackageVersion } from "./package-version.js";
|
|
8
|
-
import { resolveHomeConfigRoot } from "./platform.js";
|
|
14
|
+
import { resolveHomeConfigRoot, spawnOptions } from "./platform.js";
|
|
9
15
|
const hrPrimaryAgentName = "hr";
|
|
10
16
|
const recommendedHrBootstrapModel = "openai/gpt-5.4-mini";
|
|
11
17
|
const recommendedHrBootstrapVariant = "high";
|
|
@@ -253,6 +259,97 @@ const normalizeConfiguredModel = (value) => {
|
|
|
253
259
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
254
260
|
};
|
|
255
261
|
const fallbackInstalledModel = (installedModels, nativeModel) => installedModels[hrPrimaryAgentName]?.model || installedModels.auto?.model || Object.values(installedModels)[0]?.model || nativeModel;
|
|
262
|
+
const readHrKnownModelIds = async (targetRoot) => {
|
|
263
|
+
const filePath = path.join(targetRoot, "inventory", "models", "valid-model-ids.txt");
|
|
264
|
+
try {
|
|
265
|
+
const raw = await readFile(filePath, "utf8");
|
|
266
|
+
const values = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
267
|
+
return values.length > 0 ? new Set(values) : void 0;
|
|
268
|
+
} catch {
|
|
269
|
+
return void 0;
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
const listAvailableOpencodeModels = async () => new Promise((resolve) => {
|
|
273
|
+
const child = spawn("opencode", ["models"], {
|
|
274
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
275
|
+
...spawnOptions()
|
|
276
|
+
});
|
|
277
|
+
const timeout = setTimeout(() => {
|
|
278
|
+
child.kill();
|
|
279
|
+
resolve(void 0);
|
|
280
|
+
}, 15e3);
|
|
281
|
+
let stdout = "";
|
|
282
|
+
child.stdout?.on("data", (chunk) => {
|
|
283
|
+
stdout += chunk.toString();
|
|
284
|
+
});
|
|
285
|
+
child.on("error", () => {
|
|
286
|
+
clearTimeout(timeout);
|
|
287
|
+
resolve(void 0);
|
|
288
|
+
});
|
|
289
|
+
child.on("close", () => {
|
|
290
|
+
clearTimeout(timeout);
|
|
291
|
+
const models = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
292
|
+
resolve(models.length > 0 ? [...new Set(models)].sort() : void 0);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
const probeOpencodeModelAvailability = async (model, options = {}) => {
|
|
296
|
+
const models = await (options.listModels ?? listAvailableOpencodeModels)();
|
|
297
|
+
if (!models) {
|
|
298
|
+
return {
|
|
299
|
+
available: false,
|
|
300
|
+
reason: "probe_failed",
|
|
301
|
+
message: "Unable to verify model availability from opencode. Continue only if you know this model works in your environment."
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return models.includes(model) ? { available: true } : {
|
|
305
|
+
available: false,
|
|
306
|
+
reason: "unavailable",
|
|
307
|
+
message: `Model '${model}' is not available in the current opencode environment.`
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
const agentModelValidationMessage = (agentName, model, status) => `Agent '${agentName}' model '${model}' is invalid: ${status.message}`;
|
|
311
|
+
const validateHrAgentModelConfiguration = async (targetRoot, settings, options = {}) => {
|
|
312
|
+
const currentSettings = settings ?? await readAgentHubSettings(targetRoot);
|
|
313
|
+
if (!currentSettings?.agents) return { valid: true };
|
|
314
|
+
const knownModels = await readHrKnownModelIds(targetRoot);
|
|
315
|
+
const availabilityCache = /* @__PURE__ */ new Map();
|
|
316
|
+
for (const agentName of hrAgentNames) {
|
|
317
|
+
const model = currentSettings.agents[agentName]?.model;
|
|
318
|
+
if (typeof model !== "string" || model.trim().length === 0) continue;
|
|
319
|
+
const syntax = validateModelIdentifier(model);
|
|
320
|
+
if (!syntax.ok) {
|
|
321
|
+
return {
|
|
322
|
+
valid: false,
|
|
323
|
+
agentName,
|
|
324
|
+
model,
|
|
325
|
+
syntax,
|
|
326
|
+
message: agentModelValidationMessage(agentName, model, syntax)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const catalog = validateModelAgainstCatalog(model, knownModels);
|
|
330
|
+
if (!catalog.ok) {
|
|
331
|
+
return {
|
|
332
|
+
valid: false,
|
|
333
|
+
agentName,
|
|
334
|
+
model,
|
|
335
|
+
catalog,
|
|
336
|
+
message: agentModelValidationMessage(agentName, model, catalog)
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const availability = availabilityCache.get(model) || await probeOpencodeModelAvailability(model, options);
|
|
340
|
+
availabilityCache.set(model, availability);
|
|
341
|
+
if (!availability.available && availability.reason !== "probe_failed") {
|
|
342
|
+
return {
|
|
343
|
+
valid: false,
|
|
344
|
+
agentName,
|
|
345
|
+
model,
|
|
346
|
+
availability,
|
|
347
|
+
message: agentModelValidationMessage(agentName, model, availability)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return { valid: true };
|
|
352
|
+
};
|
|
256
353
|
const resolveHrBootstrapAgentModels = async ({
|
|
257
354
|
targetRoot,
|
|
258
355
|
selection
|
|
@@ -383,11 +480,14 @@ export {
|
|
|
383
480
|
hrAgentNames,
|
|
384
481
|
hrPrimaryAgentName,
|
|
385
482
|
hrSubagentNames,
|
|
483
|
+
listAvailableOpencodeModels,
|
|
386
484
|
loadNativeOpenCodeConfig,
|
|
387
485
|
loadNativeOpenCodePreferences,
|
|
388
486
|
mergeAgentHubSettingsDefaults,
|
|
389
487
|
nativeOpenCodeConfigPath,
|
|
488
|
+
probeOpencodeModelAvailability,
|
|
390
489
|
readAgentHubSettings,
|
|
490
|
+
readHrKnownModelIds,
|
|
391
491
|
readNativeAgentOverrides,
|
|
392
492
|
readNativePluginEntries,
|
|
393
493
|
readWorkflowInjectionConfig,
|
|
@@ -395,6 +495,7 @@ export {
|
|
|
395
495
|
recommendedHrBootstrapVariant,
|
|
396
496
|
resolveHrBootstrapAgentModels,
|
|
397
497
|
settingsPathForRoot,
|
|
498
|
+
validateHrAgentModelConfiguration,
|
|
398
499
|
workflowInjectionPathForRoot,
|
|
399
500
|
writeAgentHubSettings
|
|
400
501
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, access } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { readAgentHubSettings } from "../../composer/settings.js";
|
|
4
|
+
import { validateModelIdentifier } from "../../composer/model-utils.js";
|
|
4
5
|
const readJson = async (filePath) => {
|
|
5
6
|
const content = await readFile(filePath, "utf-8");
|
|
6
7
|
return JSON.parse(content);
|
|
@@ -98,8 +99,28 @@ async function runDiagnostics(targetRoot) {
|
|
|
98
99
|
if (omoIssue) {
|
|
99
100
|
report.issues.push(omoIssue);
|
|
100
101
|
}
|
|
102
|
+
for (const issue of diagnoseInvalidModelSyntax(settings)) {
|
|
103
|
+
report.issues.push(issue);
|
|
104
|
+
}
|
|
101
105
|
return report;
|
|
102
106
|
}
|
|
107
|
+
const diagnoseInvalidModelSyntax = (settings) => {
|
|
108
|
+
if (!settings?.agents) return [];
|
|
109
|
+
const issues = [];
|
|
110
|
+
for (const [agentName, agent] of Object.entries(settings.agents)) {
|
|
111
|
+
if (typeof agent.model !== "string" || agent.model.trim().length === 0) continue;
|
|
112
|
+
const syntax = validateModelIdentifier(agent.model);
|
|
113
|
+
if (!syntax.ok) {
|
|
114
|
+
issues.push({
|
|
115
|
+
type: "model_invalid_syntax",
|
|
116
|
+
severity: "error",
|
|
117
|
+
message: `Agent '${agentName}' has an invalid model override: ${syntax.message}`,
|
|
118
|
+
details: { agentName, model: agent.model }
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return issues;
|
|
123
|
+
};
|
|
103
124
|
async function diagnoseMissingGuards(settings) {
|
|
104
125
|
if (!settings) return REQUIRED_GUARDS;
|
|
105
126
|
const existingGuards = Object.keys(settings.guards || {});
|
|
@@ -132,7 +132,7 @@ async function createBundleForSoul(targetRoot, soulName, options = {}) {
|
|
|
132
132
|
agent: {
|
|
133
133
|
name: options.agentName ?? soulName,
|
|
134
134
|
mode: options.mode ?? "primary",
|
|
135
|
-
model: options.model
|
|
135
|
+
model: options.model ?? "",
|
|
136
136
|
description: "Auto-generated bundle for imported soul"
|
|
137
137
|
}
|
|
138
138
|
};
|
|
@@ -3,9 +3,15 @@ import { readdir, readFile, access, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import {
|
|
5
5
|
loadNativeOpenCodeConfig,
|
|
6
|
+
probeOpencodeModelAvailability,
|
|
7
|
+
readHrKnownModelIds,
|
|
6
8
|
readAgentHubSettings,
|
|
7
9
|
writeAgentHubSettings
|
|
8
10
|
} from "../../composer/settings.js";
|
|
11
|
+
import {
|
|
12
|
+
validateModelAgainstCatalog,
|
|
13
|
+
validateModelIdentifier
|
|
14
|
+
} from "../../composer/model-utils.js";
|
|
9
15
|
import {
|
|
10
16
|
fixMissingGuards,
|
|
11
17
|
createBundleForSoul,
|
|
@@ -149,9 +155,9 @@ async function createBundleFlow(rl, targetRoot) {
|
|
|
149
155
|
const modeInput = await rl.question("Mode [primary/subagent, default: primary]: ");
|
|
150
156
|
const mode = modeInput.trim() === "subagent" ? "subagent" : "primary";
|
|
151
157
|
const modelInput = await rl.question(
|
|
152
|
-
"Model [default:
|
|
158
|
+
"Model [default: none]: "
|
|
153
159
|
);
|
|
154
|
-
const model = modelInput.trim()
|
|
160
|
+
const model = modelInput.trim();
|
|
155
161
|
const result = await createBundleForSoul(targetRoot, soulName.trim(), {
|
|
156
162
|
agentName: finalAgentName,
|
|
157
163
|
mode,
|
|
@@ -686,6 +692,19 @@ async function updateAgentModelOverride(targetRoot, agentName, model) {
|
|
|
686
692
|
await writeAgentHubSettings(targetRoot, settings);
|
|
687
693
|
return `Cleared model override for '${agentName}'.`;
|
|
688
694
|
}
|
|
695
|
+
const syntax = validateModelIdentifier(trimmed);
|
|
696
|
+
if (!syntax.ok) {
|
|
697
|
+
return `${syntax.message} Use provider/model format or leave blank to clear the override.`;
|
|
698
|
+
}
|
|
699
|
+
const knownModels = await readHrKnownModelIds(targetRoot);
|
|
700
|
+
const catalog = validateModelAgainstCatalog(trimmed, knownModels);
|
|
701
|
+
if (!catalog.ok) {
|
|
702
|
+
return `${catalog.message} Sync HR sources or choose a listed model, then try again.`;
|
|
703
|
+
}
|
|
704
|
+
const availability = await probeOpencodeModelAvailability(trimmed);
|
|
705
|
+
if (!availability.available) {
|
|
706
|
+
return `${availability.message} Pick another model or clear the override.`;
|
|
707
|
+
}
|
|
689
708
|
settings.agents[agentName] = {
|
|
690
709
|
...existing,
|
|
691
710
|
model: trimmed
|
|
@@ -41,7 +41,10 @@ $HR_HOME/staging/<package-id>/
|
|
|
41
41
|
- Prefer namespaced bundle/soul/profile names for adapted teams or rewrites so the package does not overwrite shared starter assets like `plan`, `build`, or `explore` unless the human explicitly requests replacement.
|
|
42
42
|
- If a staged profile sets `defaultAgent`, it must use the staged bundle's `agent.name` value, not the bundle filename. This matters when bundle filenames are namespaced but `agent.name` is shorter.
|
|
43
43
|
- Before final assembly, confirm whether the operator wants to keep default opencode agents such as `general`, `explore`, `plan`, and `build`. If not, the staged profile must set `"nativeAgentPolicy": "team-only"`. This suppresses host native agent merges and emits `disable: true` overrides for default opencode agents that are not supplied by the staged team itself.
|
|
44
|
+
- If `nativeAgentPolicy` is `team-only` and the staged bundle set does not already provide `explore`, automatically include the built-in hidden `explore` subagent so the team retains investigation coverage without another user prompt.
|
|
45
|
+
- Before final assembly, MUST verify the staged bundle set includes at least one `agent.mode: "primary"` agent that is not hidden. If all sourced candidates are subagent-style, either add/create a primary host agent or keep native agents visible. Never stage an all-subagent team as `team-only`.
|
|
44
46
|
- Before final assembly, confirm whether the promoted profile should become the default personal profile for future bare `agenthub start` runs. Record that choice in `handoff.json` under `promotion_preferences.set_default_profile`.
|
|
47
|
+
- If AI models are still unresolved when final assembly begins, stop and confirm the exact model choice here before writing staged agent defaults.
|
|
45
48
|
- If the operator specifies a model variant such as `xhigh`, `high`, or `thinking`, store it canonically as `agent.model: "provider/model"` plus `agent.variant: "..."`. For backward compatibility, combined strings like `"provider/model xhigh"` may still be accepted on read, but staged output should prefer the split form.
|
|
46
49
|
- The package must be promotable by:
|
|
47
50
|
|
|
@@ -69,11 +69,12 @@ Keep `final-checklist.md` compact and explicit. Use this shape:
|
|
|
69
69
|
| descriptions are operator-readable | pass/fail |
|
|
70
70
|
| MCP registrations resolve to staged servers or blocker | pass/fail |
|
|
71
71
|
| handoff clearly separates test/use/promote | pass/fail |
|
|
72
|
-
| model preferences were
|
|
72
|
+
| model preferences were confirmed before assembly | pass/fail |
|
|
73
73
|
| final names were user-confirmed | pass/fail |
|
|
74
74
|
| specialized work was delegated | pass/fail |
|
|
75
75
|
| staged model ids validated against synced catalog | pass/fail |
|
|
76
76
|
| profile defaultAgent matches bundle agent.name | pass/fail |
|
|
77
|
+
| team includes at least one primary, non-hidden agent | pass/fail |
|
|
77
78
|
| default opencode agent policy confirmed | pass/fail |
|
|
78
79
|
| default profile on promote confirmed | pass/fail |
|
|
79
80
|
| no host project mutations | pass/fail |
|
|
@@ -94,5 +95,6 @@ The package cannot be marked ready unless the verifier confirms:
|
|
|
94
95
|
7. if any bundle references MCP tools, the staged package includes the referenced `mcp/*.json` files, the required `mcp-servers/` implementation files, and `mcp-servers/package.json` when runtime dependencies are needed
|
|
95
96
|
8. the handoff clearly shows how to test/use the staged profile in a workspace before promote, and promote is not described as mandatory for workspace use
|
|
96
97
|
9. if a profile sets `defaultAgent`, that value exactly matches one of the staged bundles' `agent.name` values (not just the bundle filename)
|
|
98
|
+
10. the staged team includes at least one non-hidden primary agent, and any `team-only` profile keeps at least one such primary agent available to the operator
|
|
97
99
|
|
|
98
100
|
If bundle metadata contains fake runtime keys such as `optional_skills` or `runtime_conditional_skills`, mark the package `NOT READY` until they are removed or rewritten as plain documentation outside runtime bundle semantics.
|
|
@@ -37,8 +37,6 @@ Every staffing plan must include:
|
|
|
37
37
|
- `composition`
|
|
38
38
|
- `required_skills`
|
|
39
39
|
- `required_tools`
|
|
40
|
-
- `suggested_model_provider`
|
|
41
|
-
- `proposed_agent_models` (initial proposal only; final per-agent defaults require user confirmation later)
|
|
42
40
|
- `draft_names` (draft agent names and draft profile name for user review)
|
|
43
41
|
- `risks`
|
|
44
42
|
- `next_action`
|
|
@@ -57,8 +55,8 @@ composition:
|
|
|
57
55
|
draft_names:
|
|
58
56
|
- seat: ... | proposed_agent_name: ... | reason: ...
|
|
59
57
|
- profile: ... | reason: ...
|
|
60
|
-
|
|
61
|
-
-
|
|
58
|
+
required_skills:
|
|
59
|
+
- skill: ... | why: ...
|
|
62
60
|
risks:
|
|
63
61
|
- ...
|
|
64
62
|
next_action: ...
|
|
@@ -75,7 +73,9 @@ For each recommended entry, specify:
|
|
|
75
73
|
|
|
76
74
|
## Decision Rules
|
|
77
75
|
|
|
76
|
+
- At least one staffing-plan entry must have `deployment_role: primary-capable`.
|
|
78
77
|
- Prefer the smallest team that can cover planning, sourcing/exploration, implementation, audit, verification, and documentation.
|
|
78
|
+
- When multiple candidates can cover the same seat, prefer a pure-soul agent with attached skills over a mixed soul+skill agent. Use a mixed soul+skill agent only when the specialized workflow cannot be cleanly separated, the source is tightly fused, or the user explicitly wants that mixed form.
|
|
79
79
|
- Prefer local worker cards from `$HR_HOME/inventory/workers/` with `inventory_status = available`.
|
|
80
80
|
- Treat `draft` worker cards as sourcing inputs that still need review or explicit operator acceptance.
|
|
81
81
|
- Exclude `retired` worker cards from recommended staffing compositions.
|
|
@@ -9,12 +9,22 @@ import tempfile
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def
|
|
12
|
+
def wrap_executable(command: str) -> list[str]:
|
|
13
|
+
path = Path(command)
|
|
14
|
+
suffix = path.suffix.lower()
|
|
15
|
+
if suffix == ".js":
|
|
16
|
+
return ["node", command]
|
|
17
|
+
if os.name == "nt" and suffix in {".cmd", ".bat"}:
|
|
18
|
+
return ["cmd", "/c", command]
|
|
19
|
+
return [command]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_agenthub_command() -> list[str]:
|
|
13
23
|
if os.getenv("OPENCODE_AGENTHUB_BIN"):
|
|
14
|
-
return os.environ["OPENCODE_AGENTHUB_BIN"]
|
|
24
|
+
return wrap_executable(os.environ["OPENCODE_AGENTHUB_BIN"])
|
|
15
25
|
found = shutil.which("opencode-agenthub")
|
|
16
26
|
if found:
|
|
17
|
-
return found
|
|
27
|
+
return wrap_executable(found)
|
|
18
28
|
# Portable repo-local fallback: when running from src/skills/hr-support/bin/
|
|
19
29
|
# inside the source tree, try <repo-root>/bin/opencode-agenthub
|
|
20
30
|
this_file = Path(__file__).resolve()
|
|
@@ -28,13 +38,19 @@ def resolve_agenthub_bin() -> str:
|
|
|
28
38
|
and parts[-4] == "skills"
|
|
29
39
|
and parts[-5] == "src"
|
|
30
40
|
):
|
|
41
|
+
repo_bin_cmd = this_file.parents[4] / "bin" / "opencode-agenthub.cmd"
|
|
42
|
+
if os.name == "nt" and repo_bin_cmd.exists():
|
|
43
|
+
return ["cmd", "/c", str(repo_bin_cmd)]
|
|
31
44
|
repo_bin = this_file.parents[4] / "bin" / "opencode-agenthub"
|
|
32
|
-
if repo_bin.exists():
|
|
33
|
-
return str(repo_bin)
|
|
45
|
+
if os.name != "nt" and repo_bin.exists():
|
|
46
|
+
return [str(repo_bin)]
|
|
47
|
+
repo_dist = this_file.parents[4] / "dist" / "composer" / "opencode-profile.js"
|
|
48
|
+
if repo_dist.exists():
|
|
49
|
+
return ["node", str(repo_dist)]
|
|
34
50
|
raise SystemExit(
|
|
35
51
|
"Could not locate opencode-agenthub.\n"
|
|
36
52
|
" Set OPENCODE_AGENTHUB_BIN to the full path, or add opencode-agenthub to PATH.\n"
|
|
37
|
-
" When running from source, ensure bin/opencode-agenthub exists in the repo root."
|
|
53
|
+
" When running from source, ensure bin/opencode-agenthub (or .cmd on Windows) exists, or build dist/composer/opencode-profile.js in the repo root."
|
|
38
54
|
)
|
|
39
55
|
|
|
40
56
|
|
|
@@ -152,6 +168,7 @@ def validate_profile_default_agents(import_root: Path) -> None:
|
|
|
152
168
|
)
|
|
153
169
|
|
|
154
170
|
references: list[tuple[str, str]] = []
|
|
171
|
+
primary_agent_names: set[str] = set()
|
|
155
172
|
missing_bundles: list[str] = []
|
|
156
173
|
for raw_bundle_name in bundle_names:
|
|
157
174
|
if not isinstance(raw_bundle_name, str) or not raw_bundle_name.strip():
|
|
@@ -165,11 +182,16 @@ def validate_profile_default_agents(import_root: Path) -> None:
|
|
|
165
182
|
continue
|
|
166
183
|
agent = bundle.get("agent", {})
|
|
167
184
|
agent_name = agent.get("name") if isinstance(agent, dict) else None
|
|
185
|
+
agent_mode = agent.get("mode") if isinstance(agent, dict) else None
|
|
186
|
+
agent_hidden = agent.get("hidden") if isinstance(agent, dict) else None
|
|
168
187
|
if not isinstance(agent_name, str) or not agent_name.strip():
|
|
169
188
|
raise SystemExit(
|
|
170
189
|
f"Bundle '{bundle_name}' is missing required agent.name; profile '{profile_name}' cannot use it."
|
|
171
190
|
)
|
|
172
|
-
|
|
191
|
+
normalized_agent_name = agent_name.strip()
|
|
192
|
+
references.append((bundle_name, normalized_agent_name))
|
|
193
|
+
if agent_mode == "primary" and agent_hidden is not True:
|
|
194
|
+
primary_agent_names.add(normalized_agent_name)
|
|
173
195
|
|
|
174
196
|
if missing_bundles:
|
|
175
197
|
detail = ", ".join(missing_bundles)
|
|
@@ -183,6 +205,10 @@ def validate_profile_default_agents(import_root: Path) -> None:
|
|
|
183
205
|
raise SystemExit(
|
|
184
206
|
f"Profile '{profile_name}' uses nativeAgentPolicy 'team-only' and must set defaultAgent explicitly."
|
|
185
207
|
)
|
|
208
|
+
if native_agent_policy == "team-only" and not primary_agent_names:
|
|
209
|
+
raise SystemExit(
|
|
210
|
+
f"Profile '{profile_name}' uses nativeAgentPolicy 'team-only' but does not include at least one primary, non-hidden agent."
|
|
211
|
+
)
|
|
186
212
|
if default_agent is None:
|
|
187
213
|
continue
|
|
188
214
|
if not isinstance(default_agent, str) or not default_agent.strip():
|
|
@@ -208,6 +234,10 @@ def validate_profile_default_agents(import_root: Path) -> None:
|
|
|
208
234
|
|
|
209
235
|
agent_names = {agent_name for _, agent_name in references}
|
|
210
236
|
if normalized_default_agent in agent_names:
|
|
237
|
+
if normalized_default_agent not in primary_agent_names:
|
|
238
|
+
raise SystemExit(
|
|
239
|
+
f"Profile '{profile_name}' defaultAgent '{normalized_default_agent}' must point to a primary, non-hidden agent."
|
|
240
|
+
)
|
|
211
241
|
continue
|
|
212
242
|
|
|
213
243
|
available = ", ".join(sorted(agent_names)) or "(none)"
|
|
@@ -223,7 +253,6 @@ def main() -> int:
|
|
|
223
253
|
"Usage: validate_staged_package.py <stage-package-root|agenthub-home-root>"
|
|
224
254
|
)
|
|
225
255
|
|
|
226
|
-
agenthub_bin = resolve_agenthub_bin()
|
|
227
256
|
import_root = resolve_import_root(sys.argv[1])
|
|
228
257
|
workspace_root = Path.cwd()
|
|
229
258
|
validate_bundle_metadata(import_root)
|
|
@@ -235,6 +264,9 @@ def main() -> int:
|
|
|
235
264
|
if not profiles:
|
|
236
265
|
raise SystemExit("No profiles found in staged import root.")
|
|
237
266
|
|
|
267
|
+
# Integration smoke phase below requires a runnable agenthub binary.
|
|
268
|
+
agenthub_cmd = resolve_agenthub_command()
|
|
269
|
+
|
|
238
270
|
with tempfile.TemporaryDirectory(prefix="agenthub-stage-validate-") as temp_dir:
|
|
239
271
|
temp_root = Path(temp_dir)
|
|
240
272
|
temp_home = temp_root / "home"
|
|
@@ -243,7 +275,7 @@ def main() -> int:
|
|
|
243
275
|
|
|
244
276
|
run(
|
|
245
277
|
[
|
|
246
|
-
|
|
278
|
+
*agenthub_cmd,
|
|
247
279
|
"setup",
|
|
248
280
|
"minimal",
|
|
249
281
|
"--target-root",
|
|
@@ -252,7 +284,7 @@ def main() -> int:
|
|
|
252
284
|
)
|
|
253
285
|
run(
|
|
254
286
|
[
|
|
255
|
-
|
|
287
|
+
*agenthub_cmd,
|
|
256
288
|
"hub-import",
|
|
257
289
|
"--source",
|
|
258
290
|
str(import_root),
|
|
@@ -268,7 +300,7 @@ def main() -> int:
|
|
|
268
300
|
for profile in profiles:
|
|
269
301
|
run(
|
|
270
302
|
[
|
|
271
|
-
|
|
303
|
+
*agenthub_cmd,
|
|
272
304
|
"run",
|
|
273
305
|
profile,
|
|
274
306
|
"--workspace",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-agenthub",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "A control plane for organizing, composing, and activating OpenCode agents, skills, profiles, and bundles.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/plugins/opencode-agenthub.js",
|
|
7
7
|
"exports": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "node scripts/build.mjs",
|
|
17
17
|
"test:smoke": "bun test test/smoke-*.test.ts",
|
|
18
|
-
"
|
|
18
|
+
"prepack": "npm run build"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"LICENSE",
|