opencode-agenthub 0.1.1 → 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/dist/composer/builtin-assets.js +2 -0
- package/dist/composer/compose.js +30 -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 +340 -73
- package/dist/composer/platform.js +4 -1
- package/dist/composer/settings.js +103 -2
- package/dist/skills/agenthub-doctor/diagnose.js +21 -0
- package/dist/skills/agenthub-doctor/interactive.js +19 -0
- 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 +15 -1
- package/package.json +1 -1
|
@@ -7,10 +7,12 @@ const builtInSkillsRoot = path.resolve(currentDir, "..", "skills");
|
|
|
7
7
|
const hrSupportBinRoot = path.join(builtInSkillsRoot, "hr-support", "bin");
|
|
8
8
|
const codingLibraryFiles = [
|
|
9
9
|
"bundles/auto.json",
|
|
10
|
+
"bundles/explore.json",
|
|
10
11
|
"bundles/plan.json",
|
|
11
12
|
"bundles/build.json",
|
|
12
13
|
"profiles/auto.json",
|
|
13
14
|
"souls/auto.md",
|
|
15
|
+
"souls/explore.md",
|
|
14
16
|
"souls/plan.md",
|
|
15
17
|
"souls/build.md"
|
|
16
18
|
];
|
package/dist/composer/compose.js
CHANGED
|
@@ -75,6 +75,12 @@ const validateProfileDefaultAgent = ({
|
|
|
75
75
|
}
|
|
76
76
|
const matchedAgent = agentConfig[configuredDefaultAgent];
|
|
77
77
|
if (matchedAgent && !isDisabledAgentEntry(matchedAgent)) {
|
|
78
|
+
const matchedBundle = bundles.find((bundle) => bundle.agent.name === configuredDefaultAgent);
|
|
79
|
+
if (matchedBundle?.agent.mode !== "primary") {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`Profile '${profile.name}' defaultAgent '${configuredDefaultAgent}' must point to a primary agent, but the matched bundle uses mode '${matchedBundle?.agent.mode || "unknown"}'.`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
78
84
|
return configuredDefaultAgent;
|
|
79
85
|
}
|
|
80
86
|
const bundleHint = bundleMatch ? ` Bundle '${bundleMatch.name}' uses bundle agent.name '${bundleMatch.agent.name}'. Set defaultAgent to that value instead.` : "";
|
|
@@ -84,6 +90,21 @@ const validateProfileDefaultAgent = ({
|
|
|
84
90
|
`Profile '${profile.name}' sets defaultAgent '${configuredDefaultAgent}', but no enabled generated agent matches that name.${bundleHint}${disabledHint} Available agent names: ${availableAgents.join(", ") || "(none)"}.`
|
|
85
91
|
);
|
|
86
92
|
};
|
|
93
|
+
const validateTeamHasPrimaryAgent = ({
|
|
94
|
+
profile,
|
|
95
|
+
bundles,
|
|
96
|
+
nativeAgentPolicy
|
|
97
|
+
}) => {
|
|
98
|
+
const hasVisiblePrimary = bundles.some(
|
|
99
|
+
(bundle) => bundle.agent.mode === "primary" && bundle.agent.hidden !== true
|
|
100
|
+
);
|
|
101
|
+
if (hasVisiblePrimary) return;
|
|
102
|
+
if (nativeAgentPolicy === "team-only") {
|
|
103
|
+
throw new Error(
|
|
104
|
+
`Profile '${profile.name}' must include at least one primary, visible staged agent when nativeAgentPolicy is 'team-only'.`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
87
108
|
const workflowInjectionMatchesBundles = (workflowInjection, bundleNames = []) => {
|
|
88
109
|
if (!workflowInjection?.enabled) return false;
|
|
89
110
|
if (!workflowInjection.bundles || workflowInjection.bundles.length === 0) {
|
|
@@ -509,6 +530,10 @@ const composeWorkspace = async (workspace, profileName, configRoot, options = {}
|
|
|
509
530
|
const bundles = await Promise.all(
|
|
510
531
|
profile.bundles.map((bundleName) => loadBundle(libraryRoot, bundleName))
|
|
511
532
|
);
|
|
533
|
+
const nativeAgentPolicy = resolveNativeAgentPolicy(profile);
|
|
534
|
+
if (nativeAgentPolicy === "team-only" && !bundles.some((bundle) => bundle.agent.name === "explore")) {
|
|
535
|
+
bundles.push(await loadBundle(libraryRoot, "explore"));
|
|
536
|
+
}
|
|
512
537
|
const outputRoot = configRoot || path.join(workspace, ".opencode-agenthub", activeRuntimeDirName);
|
|
513
538
|
await resetWorkspaceRuntimeRoot(workspace, outputRoot);
|
|
514
539
|
await ensureDir(path.join(outputRoot, "xdg", "opencode"));
|
|
@@ -601,7 +626,6 @@ const composeWorkspace = async (workspace, profileName, configRoot, options = {}
|
|
|
601
626
|
const nativeConfig = await loadNativeOpenCodeConfig();
|
|
602
627
|
const nativePluginEntries = await readNativePluginEntries();
|
|
603
628
|
const nativeAgents = nativeConfig?.agent || {};
|
|
604
|
-
const nativeAgentPolicy = resolveNativeAgentPolicy(profile);
|
|
605
629
|
if (nativeAgentPolicy === "inherit") {
|
|
606
630
|
for (const [agentName, nativeAgent] of Object.entries(nativeAgents)) {
|
|
607
631
|
if (!nativeAgent || typeof nativeAgent !== "object") continue;
|
|
@@ -641,6 +665,11 @@ const composeWorkspace = async (workspace, profileName, configRoot, options = {}
|
|
|
641
665
|
agentConfig,
|
|
642
666
|
nativeAgentPolicy
|
|
643
667
|
});
|
|
668
|
+
validateTeamHasPrimaryAgent({
|
|
669
|
+
profile,
|
|
670
|
+
bundles,
|
|
671
|
+
nativeAgentPolicy
|
|
672
|
+
});
|
|
644
673
|
const omoBaseline = Object.keys(omoCategories).length > 0 ? await loadOmoBaseline() : null;
|
|
645
674
|
const omoConfig = Object.keys(omoCategories).length > 0 ? {
|
|
646
675
|
...omoBaseline || {},
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "explore",
|
|
3
|
+
"runtime": "native",
|
|
4
|
+
"soul": "explore",
|
|
5
|
+
"skills": [],
|
|
6
|
+
"mcp": [],
|
|
7
|
+
"guards": ["no_task", "no_omo"],
|
|
8
|
+
"agent": {
|
|
9
|
+
"name": "explore",
|
|
10
|
+
"mode": "subagent",
|
|
11
|
+
"hidden": true,
|
|
12
|
+
"model": "",
|
|
13
|
+
"description": "Read-only exploration subagent for repo scanning, search, and investigation",
|
|
14
|
+
"permission": {
|
|
15
|
+
"*": "allow",
|
|
16
|
+
"edit": "deny",
|
|
17
|
+
"write": "deny",
|
|
18
|
+
"task": "deny"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -15,6 +15,7 @@ You are part of the **HR profile**, not the host project runtime.
|
|
|
15
15
|
- GitHub source repos are discovery inputs only; curated reusable workers live under `$HR_HOME/inventory/`.
|
|
16
16
|
- Candidate review must stay read-only with respect to external source repos. Do not execute untrusted code.
|
|
17
17
|
- Model suggestions inside staffing plans are advisory metadata only.
|
|
18
|
+
- If the synced model catalog (`$HR_HOME/inventory/models/catalog.json` or `valid-model-ids.txt`) is empty or missing, no HR agent may propose, fill in, or confirm a concrete `provider/model` id. Use `<pending-catalog-sync>` or an explicit blocker until verified catalog data is available.
|
|
18
19
|
- Do not present a staged package as ready unless it passes non-interactive import-root and assemble-only validation.
|
|
19
20
|
|
|
20
21
|
## Delegation rule
|
|
@@ -4,7 +4,7 @@ This protocol is mandatory. It exists to prevent five common failures:
|
|
|
4
4
|
|
|
5
5
|
1. doing the worker jobs yourself instead of delegating
|
|
6
6
|
2. moving through stages without user feedback points
|
|
7
|
-
3.
|
|
7
|
+
3. turning Stage 1 into a fixed intake questionnaire before the user has seen staffing options
|
|
8
8
|
4. accepting poor agent/profile names without user confirmation
|
|
9
9
|
5. delivering a confusing final handoff that makes promote sound mandatory
|
|
10
10
|
|
|
@@ -32,18 +32,12 @@ If yes, delegate. Do not do it yourself.
|
|
|
32
32
|
|
|
33
33
|
### Stage 1 - REQUIREMENTS
|
|
34
34
|
|
|
35
|
-
Before leaving requirements, you must explicitly ask for and summarize
|
|
35
|
+
Before leaving requirements, you must explicitly ask for and summarize only the minimum needed to plan staffing options:
|
|
36
36
|
|
|
37
|
-
-
|
|
38
|
-
- team
|
|
39
|
-
- sourcing preferences or exclusions
|
|
40
|
-
- acceptance criteria / what “done” means
|
|
41
|
-
- **AI model preferences**
|
|
42
|
-
- ask whether the user has preferred providers, exact models, cost tiers, or variants
|
|
43
|
-
- if the user has no preference, record `no preference` explicitly
|
|
44
|
-
- workflow / risk preferences
|
|
37
|
+
- the user's primary use cases or scenarios
|
|
38
|
+
- whether they want a single agent, a team, or attachable skills
|
|
45
39
|
|
|
46
|
-
Do not
|
|
40
|
+
Do not turn Stage 1 into a fixed intake questionnaire.
|
|
47
41
|
|
|
48
42
|
### Stage 2 - STAFFING PLAN
|
|
49
43
|
|
|
@@ -64,14 +58,19 @@ After sourcing and review work returns, give the user a shortlist summary before
|
|
|
64
58
|
Before leaving architecture review, you must explicitly confirm:
|
|
65
59
|
|
|
66
60
|
- final composition
|
|
67
|
-
- per-agent model map
|
|
68
61
|
- final agent names
|
|
69
62
|
- final profile name
|
|
70
63
|
- whether default opencode agents stay visible or are hidden
|
|
71
|
-
- whether promote should set the new profile as the default personal profile
|
|
72
64
|
|
|
73
65
|
### Stage 5 - STAGING & CONFIRMATION
|
|
74
66
|
|
|
67
|
+
Before staging begins, you must explicitly confirm the AI model choice for the assembled team.
|
|
68
|
+
|
|
69
|
+
- if the user has no preference and the synced catalog contains verified entries, present a reasonable default recommendation from that verified catalog
|
|
70
|
+
- If the synced model catalog is empty or missing, do not invent model names. State that model selection is blocked pending catalog sync or ask the user to provide an exact verified `provider/model` id.
|
|
71
|
+
- if the user wants per-agent overrides, confirm them here
|
|
72
|
+
- validate exact `provider/model` ids against the synced catalog before staging
|
|
73
|
+
|
|
75
74
|
Do not make promote sound mandatory.
|
|
76
75
|
|
|
77
76
|
Your final human handoff must use this exact order:
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<!-- version: 1.0.0 -->
|
|
2
|
+
|
|
3
|
+
# You are Explore, a read-only investigation subagent.
|
|
4
|
+
|
|
5
|
+
Your job is to help the active team understand the codebase quickly and accurately.
|
|
6
|
+
|
|
7
|
+
## Focus
|
|
8
|
+
|
|
9
|
+
- inspect files and folder structure
|
|
10
|
+
- search for relevant symbols, patterns, and references
|
|
11
|
+
- summarize findings with concrete evidence
|
|
12
|
+
- help the caller narrow uncertainty before implementation
|
|
13
|
+
|
|
14
|
+
## Constraints
|
|
15
|
+
|
|
16
|
+
- stay read-only
|
|
17
|
+
- do not edit or write files
|
|
18
|
+
- do not create commits
|
|
19
|
+
- do not delegate further
|
|
20
|
+
- do not invent facts that are not grounded in the repo
|
|
21
|
+
|
|
22
|
+
## Output style
|
|
23
|
+
|
|
24
|
+
- concise
|
|
25
|
+
- evidence-first
|
|
26
|
+
- cite specific files, functions, or patterns when possible
|
|
@@ -51,6 +51,7 @@ Keep handoff output compact.
|
|
|
51
51
|
- If a staged profile sets `defaultAgent`, use the staged bundle `agent.name`, not the bundle filename. Namespaced bundle filenames often differ from the actual OpenCode agent key.
|
|
52
52
|
- If the operator does not want default opencode agents such as `general`, `explore`, `plan`, or `build`, stage the target profile with `"nativeAgentPolicy": "team-only"`. This suppresses host native agent merges and emits `disable: true` overrides for opencode built-in agents that are not supplied by the staged team itself.
|
|
53
53
|
- Before writing any staged `agent.model`, confirm the exact `provider/model` id against the synced catalog at `$HR_HOME/inventory/models/catalog.json` or `$HR_HOME/inventory/models/valid-model-ids.txt`. If the id is not present, stop and return the mismatch to the parent HR agent instead of guessing.
|
|
54
|
+
- If the synced model catalog is empty or missing, do not write any `agent.model` value. Stop and report that model assembly is blocked until catalog data is available.
|
|
54
55
|
- If the operator specifies model variants such as `xhigh`, `high`, or `thinking`, stage them canonically as separate fields: `agent.model: "provider/model"` and `agent.variant: "..."`.
|
|
55
56
|
- If the operator wants the promoted team to become the default for future bare `agenthub start`, record that in `handoff.json` as `promotion_preferences.set_default_profile: true`.
|
|
56
57
|
- The staged package must make it explicit that `agenthub hr <profile-name>` can be used in the current or another workspace before promote.
|
|
@@ -17,7 +17,7 @@ Pure-soul CTO-style reviewer for staffing architecture. You judge whether the pr
|
|
|
17
17
|
|
|
18
18
|
- latest staffing plan
|
|
19
19
|
- shortlisted local inventory workers and their worker cards
|
|
20
|
-
-
|
|
20
|
+
- model preferences if already supplied; otherwise, note where model choice should be finalized during staging
|
|
21
21
|
- synced model catalog if present at `$HR_HOME/inventory/models/catalog.json`
|
|
22
22
|
|
|
23
23
|
## Output Contract
|
|
@@ -43,6 +43,7 @@ recommendation: <1-3 sentences>
|
|
|
43
43
|
## Review Questions
|
|
44
44
|
|
|
45
45
|
- Is the team overstaffed or missing a role?
|
|
46
|
+
- Does the team have at least one primary-capable agent?
|
|
46
47
|
- Should a role be a primary-capable pure-soul agent or a narrower mixed subagent?
|
|
47
48
|
- Are model choices proportionate to the task?
|
|
48
49
|
- Which default model should each staged agent use, and where should the user explicitly choose or override that default?
|
|
@@ -53,5 +54,6 @@ recommendation: <1-3 sentences>
|
|
|
53
54
|
|
|
54
55
|
- Recommend simplification when possible.
|
|
55
56
|
- Treat user model preferences as advisory until they are validated against the synced model catalog. Do not invent or silently normalize unknown model ids.
|
|
57
|
+
- If model choices are still unresolved, keep architecture review focused on composition and naming, then hand model confirmation forward to staging.
|
|
56
58
|
- Do not begin architecture review until the parent HR agent confirms that `Stage 3 - CANDIDATE REVIEW` is complete and the shortlist has user approval.
|
|
57
59
|
- Never delegate further.
|
|
@@ -17,7 +17,7 @@ Mixed soul+skill staffing-planning worker. You build the staffing-plan deliverab
|
|
|
17
17
|
|
|
18
18
|
- confirmed requirements summary from the parent HR agent
|
|
19
19
|
- current HR inventory state under `$HR_HOME/inventory/`
|
|
20
|
-
- any source preferences, exclusions, naming preferences
|
|
20
|
+
- any source preferences, exclusions, or naming preferences already confirmed by the user
|
|
21
21
|
|
|
22
22
|
## Output Contract
|
|
23
23
|
|
|
@@ -34,7 +34,7 @@ Keep the return compact and field-based. Include:
|
|
|
34
34
|
- `alternatives`
|
|
35
35
|
- `composition`
|
|
36
36
|
- `draft_names`
|
|
37
|
-
- `
|
|
37
|
+
- `required_skills`
|
|
38
38
|
- `risks`
|
|
39
39
|
- `next_action`
|
|
40
40
|
|
|
@@ -43,6 +43,6 @@ Keep the return compact and field-based. Include:
|
|
|
43
43
|
- Use the `hr-staffing` skill protocol.
|
|
44
44
|
- Do not source repos directly; request sourcing gaps back to the parent HR agent.
|
|
45
45
|
- Treat agent names and profile names as draft proposals until the user confirms them.
|
|
46
|
-
-
|
|
46
|
+
- Do not require confirmed AI model preferences before producing staffing options. Focus on team size, composition, and required skills first.
|
|
47
47
|
- Do not begin until the parent HR agent confirms that `Stage 1 - REQUIREMENTS` is complete.
|
|
48
48
|
- Never delegate further.
|
|
@@ -59,6 +59,7 @@ Also determine:
|
|
|
59
59
|
4. Inspect new or changed worker cards in `$HR_HOME/inventory/workers/`.
|
|
60
60
|
5. Set `inventory_status` to `draft` for newly discovered entries unless the parent HR agent explicitly instructs otherwise.
|
|
61
61
|
6. Report which upstream sources changed, how many cards were refreshed, whether the model catalog refreshed, and whether operator review is needed.
|
|
62
|
+
7. If the model catalog sync produced zero validated model ids, report that as an explicit blocker: `Model catalog is empty — downstream model proposals are blocked until catalog data is available.`
|
|
62
63
|
|
|
63
64
|
## Rules
|
|
64
65
|
|
|
@@ -73,6 +73,8 @@ Every candidate agent must be classified as one of these two classes:
|
|
|
73
73
|
- usually better as a subagent
|
|
74
74
|
- the adapted description must preserve the narrow scope and specialized workflow
|
|
75
75
|
|
|
76
|
+
Every assembled team must include at least one agent with `deployment_role: primary-capable` that will be staged as a visible `mode: "primary"` agent. If the user wants `nativeAgentPolicy: "team-only"`, this is mandatory. Do not approve an all-subagent team unless native agents stay visible.
|
|
77
|
+
|
|
76
78
|
`skill` assets are not a third agent class. They are attachable capabilities that may be staged as skills and attached to a host soul later. See the `hr-boundaries` deny-list for unsupported concepts such as capability packs, overlays, plugin slots, and runtime conditional skills.
|
|
77
79
|
|
|
78
80
|
## HR Session Stages
|
|
@@ -83,11 +85,11 @@ Each stage must produce its required deliverable before the gate. If the deliver
|
|
|
83
85
|
|
|
84
86
|
| Stage | Name | Required deliverable | Compact shape |
|
|
85
87
|
|---|---|---|---|
|
|
86
|
-
| 1 | `REQUIREMENTS` | Confirmed requirements summary | `
|
|
87
|
-
| 2 | `STAFFING PLAN` | `$HR_HOME/state/staffing-plans/latest.json` and `latest.md` | `recommended:` `alternatives:` `composition:` `draft-names:` `
|
|
88
|
+
| 1 | `REQUIREMENTS` | Confirmed requirements summary | `use-cases:` `team-shape:` |
|
|
89
|
+
| 2 | `STAFFING PLAN` | `$HR_HOME/state/staffing-plans/latest.json` and `latest.md` | `recommended:` `alternatives:` `composition:` `draft-names:` `required-skills:` `risks:` |
|
|
88
90
|
| 3 | `CANDIDATE REVIEW` | Shortlist review shown to the user | Per candidate: `slug:` `fit:` `agent_class:` `deploy_role:` `gaps:` `risks:` |
|
|
89
|
-
| 4 | `ARCHITECTURE REVIEW` | Final composition review
|
|
90
|
-
| 5 | `STAGING & CONFIRMATION` | Staged package + final checklist | `package_id:` `contents:` `checklist:` `promote_cmd:` `default-profile:` |
|
|
91
|
+
| 4 | `ARCHITECTURE REVIEW` | Final composition review | `team:` `overlaps:` `simplifications:` `default-opencode-agents:` `unresolved:` |
|
|
92
|
+
| 5 | `STAGING & CONFIRMATION` | Staged package + final checklist | `package_id:` `contents:` `checklist:` `promote_cmd:` `default-profile:` `model-choice:` |
|
|
91
93
|
|
|
92
94
|
Always tell the user which stage you are in. Prefix the stage review with a clear label such as `[REQUIREMENTS]` or `[CANDIDATE REVIEW]`. Prefer compact stage reports over silent worker chaining.
|
|
93
95
|
|
|
@@ -97,11 +99,11 @@ A stage gate is satisfied only when the required deliverable has been produced,
|
|
|
97
99
|
|
|
98
100
|
Before Stage 1, present the fixed HR process in one compact block and confirm that the user wants to proceed with it.
|
|
99
101
|
|
|
100
|
-
- `REQUIREMENTS` ->
|
|
101
|
-
- `STAFFING PLAN` -> recommended team and alternatives
|
|
102
|
+
- `REQUIREMENTS` -> main use cases and basic team direction
|
|
103
|
+
- `STAFFING PLAN` -> recommended team size, composition, and alternatives
|
|
102
104
|
- `CANDIDATE REVIEW` -> shortlist with fit and risk
|
|
103
|
-
- `ARCHITECTURE REVIEW` -> final composition and
|
|
104
|
-
- `STAGING & CONFIRMATION` -> staged package and
|
|
105
|
+
- `ARCHITECTURE REVIEW` -> final composition, names, and assemble readiness
|
|
106
|
+
- `STAGING & CONFIRMATION` -> final model choice, staged package, and checklist
|
|
105
107
|
|
|
106
108
|
Use `question()` for the process confirmation. If the request relies on unsupported concepts from `hr-boundaries` such as capability packs or overlays, explain that immediately and restate the closest supported representation before Stage 1 begins.
|
|
107
109
|
|
|
@@ -110,19 +112,16 @@ Use `question()` for the process confirmation. If the request relies on unsuppor
|
|
|
110
112
|
### Stage 1 - REQUIREMENTS
|
|
111
113
|
|
|
112
114
|
1. Read the user's request carefully.
|
|
113
|
-
2. Before building a plan or dispatching any worker, ask clarifying questions
|
|
115
|
+
2. Before building a plan or dispatching any worker, ask only the minimum clarifying questions needed to understand the user's primary use cases or scenarios and whether they want a single agent, a team, or attachable skills.
|
|
114
116
|
3. Echo back a short structured requirements summary covering at least:
|
|
115
|
-
-
|
|
117
|
+
- the primary use cases or scenarios to support
|
|
116
118
|
- whether the user wants a single agent, a team, or attachable skills
|
|
117
|
-
- any sourcing preferences or exclusions
|
|
118
|
-
- any model, workflow, or risk preferences
|
|
119
|
-
- acceptance criteria and naming preferences when relevant
|
|
120
119
|
4. Stop and wait for the user to confirm or refine that summary.
|
|
121
120
|
|
|
122
121
|
### Stage 2 - STAFFING PLAN
|
|
123
122
|
|
|
124
123
|
5. Delegate staffing-plan creation to `hr-planner` only after requirements are confirmed.
|
|
125
|
-
6. Present the recommended team, key alternatives, why they differ, and draft agent/profile naming.
|
|
124
|
+
6. Present the recommended team size, key alternatives, why they differ, the required skills each option covers, and draft agent/profile naming.
|
|
126
125
|
7. Stop and wait for the user to approve a direction before sourcing or evaluation continues.
|
|
127
126
|
|
|
128
127
|
### Stage 3 - CANDIDATE REVIEW
|
|
@@ -135,20 +134,20 @@ Use `question()` for the process confirmation. If the request relies on unsuppor
|
|
|
135
134
|
### Stage 4 - ARCHITECTURE REVIEW
|
|
136
135
|
|
|
137
136
|
12. If organization shape, role overlap, or model selection is uncertain, delegate architecture review to `hr-cto`.
|
|
138
|
-
13. Present the architecture recommendation, including simplifications, swaps, unresolved tradeoffs, and
|
|
139
|
-
14.
|
|
140
|
-
15.
|
|
141
|
-
16.
|
|
142
|
-
17. Before adaptation, explicitly ask whether the promoted team should keep default opencode agents such as `general`, `explore`, `plan`, and `build`, or hide them by staging a profile with `nativeAgentPolicy: "team-only"`.
|
|
143
|
-
18. Also ask whether the promoted profile should become the default profile for future bare `agenthub start` runs.
|
|
144
|
-
19. Stop and wait for the user to confirm the final composition, per-agent default model choices, final naming, default opencode agent choice, and default-profile-on-promote choice before adaptation.
|
|
137
|
+
13. Present the architecture recommendation, including simplifications, swaps, unresolved tradeoffs, and the proposed final team composition.
|
|
138
|
+
14. Ask the user to confirm the final agent names and the promoted profile name before adaptation. If draft names are still weak or generic, propose better names first.
|
|
139
|
+
15. Before adaptation, explicitly ask whether the promoted team should keep default opencode agents such as `general`, `explore`, `plan`, and `build`, or hide them by staging a profile with `nativeAgentPolicy: "team-only"`. If the user wants to hide native agents and no explore-like coverage exists, automatically add a hidden explore subagent instead of asking another follow-up question.
|
|
140
|
+
16. Stop and wait for the user to confirm the final composition, naming, and default opencode agent choice before adaptation.
|
|
145
141
|
|
|
146
142
|
### Stage 5 - STAGING & CONFIRMATION
|
|
147
143
|
|
|
148
|
-
20.
|
|
149
|
-
21.
|
|
150
|
-
22.
|
|
151
|
-
23.
|
|
144
|
+
20. Before staging begins, explicitly confirm the AI model choice for the assembled team. Read the synced catalog at `$HR_HOME/inventory/models/catalog.json` or `$HR_HOME/inventory/models/valid-model-ids.txt` and validate every proposed `provider/model` name against it. If the user gives an inexact or unknown name, do not guess. Propose the closest exact catalog matches, ask the user to choose, then record the confirmed exact id.
|
|
145
|
+
21. If the synced catalog is empty or missing, do not suggest exact model ids from your own knowledge. Treat model choice as blocked until the catalog is synced, or ask the user to provide an exact verified `provider/model` id.
|
|
146
|
+
22. Also ask whether the promoted profile should become the default personal profile for future bare `agenthub start` runs.
|
|
147
|
+
23. When model choices and default-profile preference are confirmed, delegate adaptation to `hr-adapter`.
|
|
148
|
+
24. Run final readiness checks through `hr-verifier`.
|
|
149
|
+
25. Present the final human checklist and require explicit approval.
|
|
150
|
+
26. After approval, give the operator a structured handoff in this order:
|
|
152
151
|
- `BUILT` - the exact staging folder path under `$HR_HOME/staging/<package-id>/`
|
|
153
152
|
- `TEST HERE` - how to run `agenthub hr <profile-name>` in the current repo to test the staged team without modifying the personal home
|
|
154
153
|
- `USE ELSEWHERE` - say the same staged profile can be used in any other workspace by running `agenthub hr <profile-name>` there before promote
|
|
@@ -32,8 +32,32 @@ const pickModelSelection = (...sources) => {
|
|
|
32
32
|
...model && variant ? { variant } : {}
|
|
33
33
|
};
|
|
34
34
|
};
|
|
35
|
+
const validateModelIdentifier = (value) => {
|
|
36
|
+
const trimmed = normalizeOptionalString(value);
|
|
37
|
+
if (!trimmed) {
|
|
38
|
+
return { ok: false, reason: "blank", message: "Model id cannot be blank." };
|
|
39
|
+
}
|
|
40
|
+
if (!trimmed.includes("/")) {
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
reason: "format",
|
|
44
|
+
message: "Model id must use provider/model format."
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return { ok: true };
|
|
48
|
+
};
|
|
49
|
+
const validateModelAgainstCatalog = (value, knownModels) => {
|
|
50
|
+
if (!knownModels || knownModels.size === 0) return { ok: true };
|
|
51
|
+
return knownModels.has(value) ? { ok: true } : {
|
|
52
|
+
ok: false,
|
|
53
|
+
reason: "unknown",
|
|
54
|
+
message: `Model '${value}' was not found in the HR model catalog.`
|
|
55
|
+
};
|
|
56
|
+
};
|
|
35
57
|
export {
|
|
36
58
|
normalizeModelSelection,
|
|
37
59
|
parseModelString,
|
|
38
|
-
pickModelSelection
|
|
60
|
+
pickModelSelection,
|
|
61
|
+
validateModelAgainstCatalog,
|
|
62
|
+
validateModelIdentifier
|
|
39
63
|
};
|
|
@@ -37,19 +37,28 @@ import {
|
|
|
37
37
|
import { readPackageVersion } from "./package-version.js";
|
|
38
38
|
import {
|
|
39
39
|
mergeAgentHubSettingsDefaults,
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
hrAgentNames,
|
|
41
|
+
loadNativeOpenCodePreferences,
|
|
42
|
+
listAvailableOpencodeModels,
|
|
43
|
+
probeOpencodeModelAvailability,
|
|
44
|
+
readHrKnownModelIds,
|
|
42
45
|
recommendedHrBootstrapModel,
|
|
43
|
-
recommendedHrBootstrapVariant,
|
|
44
46
|
readAgentHubSettings,
|
|
47
|
+
resolveHrBootstrapAgentModels,
|
|
48
|
+
validateHrAgentModelConfiguration,
|
|
45
49
|
writeAgentHubSettings
|
|
46
50
|
} from "./settings.js";
|
|
51
|
+
import {
|
|
52
|
+
validateModelAgainstCatalog,
|
|
53
|
+
validateModelIdentifier
|
|
54
|
+
} from "./model-utils.js";
|
|
47
55
|
import {
|
|
48
56
|
displayHomeConfigPath,
|
|
49
57
|
interactivePromptResetSequence,
|
|
50
58
|
resolvePythonCommand,
|
|
51
59
|
shouldChmod,
|
|
52
60
|
shouldOfferEnvrc,
|
|
61
|
+
shouldUseReadlineTerminal,
|
|
53
62
|
spawnOptions,
|
|
54
63
|
stripTerminalControlInput,
|
|
55
64
|
windowsStartupNotice
|
|
@@ -146,8 +155,8 @@ FLAGS (hr)
|
|
|
146
155
|
hr <profile> Test an HR-home profile in the current workspace
|
|
147
156
|
hr last Reuse the last HR profile tested in this workspace
|
|
148
157
|
hr set <profile> Unsupported (use explicit '${cliCommand} hr <profile>' each time)
|
|
149
|
-
first bootstrap
|
|
150
|
-
|
|
158
|
+
first bootstrap HR first asks about your situation, inspects resources,
|
|
159
|
+
reports a recommendation, then lets you accept or override it
|
|
151
160
|
|
|
152
161
|
FLAGS (new / compose profile)
|
|
153
162
|
--from <profile> Seed bundles/plugins from an existing profile
|
|
@@ -711,6 +720,7 @@ const printRuntimeBanner = (label, root) => {
|
|
|
711
720
|
process.stdout.write(`[agenthub] Home: ${root}
|
|
712
721
|
`);
|
|
713
722
|
};
|
|
723
|
+
let suppressNextHrRuntimeBanner = false;
|
|
714
724
|
const listPromotablePackageIds = async (hrRoot = defaultHrHome()) => {
|
|
715
725
|
try {
|
|
716
726
|
const entries = await readdir(path.join(hrRoot, "staging"), { withFileTypes: true });
|
|
@@ -840,70 +850,285 @@ const ensureHomeReadyOrBootstrap = async (targetRoot = defaultAgentHubHome()) =>
|
|
|
840
850
|
process.stdout.write(`\u2713 First run \u2014 initialised coding system at ${targetRoot}
|
|
841
851
|
`);
|
|
842
852
|
};
|
|
843
|
-
const
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
853
|
+
const formatCountLabel = (count, singular, plural = `${singular}s`) => {
|
|
854
|
+
if (count === null) return `unknown ${plural}`;
|
|
855
|
+
return `${count} ${count === 1 ? singular : plural}`;
|
|
856
|
+
};
|
|
857
|
+
const inspectHrBootstrapResources = async (hrRoot) => {
|
|
858
|
+
const [configuredGithubSources, configuredModelCatalogSources, knownModels, availableModels, freeModels, native] = await Promise.all([
|
|
859
|
+
countConfiguredHrGithubSources(hrRoot),
|
|
860
|
+
countConfiguredHrModelCatalogSources(hrRoot),
|
|
861
|
+
readHrKnownModelIds(hrRoot),
|
|
862
|
+
listAvailableOpencodeModels(),
|
|
863
|
+
listOpencodeFreeModels(),
|
|
864
|
+
loadNativeOpenCodePreferences()
|
|
865
|
+
]);
|
|
866
|
+
const recommendedAvailability = await probeOpencodeModelAvailability(
|
|
867
|
+
recommendedHrBootstrapModel,
|
|
868
|
+
{ listModels: async () => availableModels }
|
|
869
|
+
);
|
|
870
|
+
return {
|
|
871
|
+
configuredGithubSources,
|
|
872
|
+
configuredModelCatalogSources,
|
|
873
|
+
knownModels,
|
|
874
|
+
availableModels,
|
|
875
|
+
freeModels,
|
|
876
|
+
nativeModel: native?.model,
|
|
877
|
+
recommendedAvailability
|
|
878
|
+
};
|
|
879
|
+
};
|
|
880
|
+
const recommendHrBootstrapSelection = (resources) => {
|
|
881
|
+
if (resources.recommendedAvailability.available) {
|
|
882
|
+
return {
|
|
883
|
+
strategy: "recommended",
|
|
884
|
+
summary: `I recommend starting with the recommended HR model (${recommendedHrBootstrapModel}).`,
|
|
885
|
+
reason: "It is available in this opencode environment and matches the built-in HR default."
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
if (resources.freeModels.length > 0) {
|
|
889
|
+
return {
|
|
890
|
+
strategy: "free",
|
|
891
|
+
summary: "I recommend starting with the best available free HR model.",
|
|
892
|
+
reason: `${resources.recommendedAvailability.message} A free fallback is available right now.`
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
const nativeModelSyntax = resources.nativeModel ? validateModelIdentifier(resources.nativeModel) : void 0;
|
|
896
|
+
if (resources.nativeModel && nativeModelSyntax?.ok) {
|
|
897
|
+
return {
|
|
898
|
+
strategy: "native",
|
|
899
|
+
summary: `I recommend reusing your native default model (${resources.nativeModel}).`,
|
|
900
|
+
reason: "No verified free fallback is visible, but your native opencode default looks usable."
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
return {
|
|
904
|
+
strategy: "custom",
|
|
905
|
+
summary: "I recommend entering a custom HR model now.",
|
|
906
|
+
reason: "The recommended preset is not currently verified and no safer automatic fallback was found."
|
|
907
|
+
};
|
|
908
|
+
};
|
|
909
|
+
const printHrBootstrapAssessment = (resources, recommendation) => {
|
|
910
|
+
void resources;
|
|
911
|
+
process.stdout.write(`
|
|
912
|
+
Recommended setup:
|
|
913
|
+
${recommendation.summary}
|
|
914
|
+
|
|
915
|
+
`);
|
|
916
|
+
};
|
|
917
|
+
const buildHrModelSelection = async (rl, hrRoot, strategy) => {
|
|
918
|
+
if (strategy === "recommended") {
|
|
848
919
|
process.stdout.write(
|
|
849
|
-
`
|
|
920
|
+
`[agenthub] Recommended HR preset requires OpenAI model access in your opencode environment.
|
|
850
921
|
`
|
|
851
922
|
);
|
|
852
|
-
|
|
853
|
-
|
|
923
|
+
return {
|
|
924
|
+
consoleModel: recommendedHrBootstrapModel,
|
|
925
|
+
subagentStrategy: "recommended",
|
|
926
|
+
sharedSubagentModel: recommendedHrBootstrapModel
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
if (strategy === "native") {
|
|
930
|
+
const native = await loadNativeOpenCodePreferences();
|
|
931
|
+
if (!native?.model) {
|
|
932
|
+
process.stdout.write("[agenthub] No native default model is configured. Choose another fallback.\n");
|
|
933
|
+
return buildHrModelSelection(rl, hrRoot, "free");
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
consoleModel: native.model,
|
|
937
|
+
subagentStrategy: "native",
|
|
938
|
+
sharedSubagentModel: native.model
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
if (strategy === "free") {
|
|
942
|
+
const freeModels = await listOpencodeFreeModels();
|
|
943
|
+
const fallbackFreeModel = freeModels.includes("opencode/minimax-m2.5-free") ? "opencode/minimax-m2.5-free" : freeModels[0] || "opencode/minimax-m2.5-free";
|
|
944
|
+
const choices = freeModels.length > 0 ? freeModels : [fallbackFreeModel];
|
|
945
|
+
process.stdout.write("Current opencode free models:\n");
|
|
946
|
+
const selected = choices.length === 1 ? (process.stdout.write(` 1. ${choices[0]}
|
|
947
|
+
`), choices[0]) : await promptIndexedChoice(
|
|
948
|
+
rl,
|
|
949
|
+
"Choose a free model for HR",
|
|
950
|
+
choices,
|
|
951
|
+
fallbackFreeModel
|
|
854
952
|
);
|
|
855
|
-
|
|
856
|
-
|
|
953
|
+
return {
|
|
954
|
+
consoleModel: selected,
|
|
955
|
+
subagentStrategy: "free",
|
|
956
|
+
sharedSubagentModel: selected
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
const custom = await promptRequired(rl, "Custom HR model", recommendedHrBootstrapModel);
|
|
960
|
+
return {
|
|
961
|
+
consoleModel: custom,
|
|
962
|
+
subagentStrategy: "custom",
|
|
963
|
+
sharedSubagentModel: custom
|
|
964
|
+
};
|
|
965
|
+
};
|
|
966
|
+
const checkHrBootstrapSelection = async (hrRoot, selection) => {
|
|
967
|
+
const model = selection.sharedSubagentModel || selection.consoleModel;
|
|
968
|
+
if (!model) {
|
|
969
|
+
return {
|
|
970
|
+
ok: false,
|
|
971
|
+
selection,
|
|
972
|
+
stage: "syntax",
|
|
973
|
+
message: "Model id cannot be blank."
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
const syntax = validateModelIdentifier(model);
|
|
977
|
+
if (!syntax.ok) {
|
|
978
|
+
return { ok: false, selection, stage: "syntax", message: syntax.message };
|
|
979
|
+
}
|
|
980
|
+
const knownModels = await readHrKnownModelIds(hrRoot);
|
|
981
|
+
const catalog = validateModelAgainstCatalog(model, knownModels);
|
|
982
|
+
if (!catalog.ok) {
|
|
983
|
+
return { ok: false, selection, stage: "catalog", message: catalog.message };
|
|
984
|
+
}
|
|
985
|
+
const availability = await probeOpencodeModelAvailability(model, {
|
|
986
|
+
listModels: listAvailableOpencodeModels
|
|
987
|
+
});
|
|
988
|
+
if (!availability.available) {
|
|
989
|
+
return {
|
|
990
|
+
ok: false,
|
|
991
|
+
selection,
|
|
992
|
+
stage: availability.reason === "probe_failed" ? "probe_failed" : "availability",
|
|
993
|
+
message: availability.message
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
return { ok: true, selection };
|
|
997
|
+
};
|
|
998
|
+
const promptValidatedHrModelSelection = async (rl, hrRoot, strategy) => {
|
|
999
|
+
let selection = await buildHrModelSelection(rl, hrRoot, strategy);
|
|
1000
|
+
while (true) {
|
|
1001
|
+
const check = await checkHrBootstrapSelection(hrRoot, selection);
|
|
1002
|
+
if (check.ok) return check.selection;
|
|
1003
|
+
process.stdout.write(`${check.message}
|
|
1004
|
+
`);
|
|
1005
|
+
if (check.stage === "syntax" && selection.subagentStrategy === "custom") {
|
|
1006
|
+
selection = await buildHrModelSelection(rl, hrRoot, "custom");
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
const action = await promptChoice(
|
|
857
1010
|
rl,
|
|
858
|
-
"
|
|
859
|
-
["
|
|
860
|
-
"
|
|
1011
|
+
check.stage === "probe_failed" ? "Model verification failed \u2014 continue or choose a fallback" : "Choose a fallback",
|
|
1012
|
+
["continue", "free", "native", "custom", "retry recommended"],
|
|
1013
|
+
check.stage === "probe_failed" ? "continue" : "free"
|
|
861
1014
|
);
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
} else if (subagentStrategy === "custom") {
|
|
880
|
-
sharedSubagentModel = await promptRequired(
|
|
1015
|
+
if (action === "continue") return selection;
|
|
1016
|
+
selection = await buildHrModelSelection(
|
|
1017
|
+
rl,
|
|
1018
|
+
hrRoot,
|
|
1019
|
+
action === "retry recommended" ? "recommended" : action
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
const promptHrBootstrapModelSelection = async (hrRoot) => {
|
|
1024
|
+
const rl = createPromptInterface();
|
|
1025
|
+
try {
|
|
1026
|
+
process.stdout.write("\nFirst-time HR Office setup\n");
|
|
1027
|
+
const resources = await inspectHrBootstrapResources(hrRoot);
|
|
1028
|
+
const recommendation = recommendHrBootstrapSelection(resources);
|
|
1029
|
+
printHrBootstrapAssessment(resources, recommendation);
|
|
1030
|
+
while (true) {
|
|
1031
|
+
const action = await promptChoice(
|
|
881
1032
|
rl,
|
|
882
|
-
"
|
|
883
|
-
|
|
1033
|
+
"Apply this recommendation now",
|
|
1034
|
+
["accept", "recommended", "free", "native", "custom"],
|
|
1035
|
+
"accept"
|
|
884
1036
|
);
|
|
885
|
-
|
|
1037
|
+
const strategy = action === "accept" ? recommendation.strategy : action;
|
|
1038
|
+
const validated = await promptValidatedHrModelSelection(rl, hrRoot, strategy);
|
|
1039
|
+
const finalModel = validated.sharedSubagentModel || validated.consoleModel;
|
|
1040
|
+
if (!finalModel) continue;
|
|
1041
|
+
const finalSyntax = validateModelIdentifier(finalModel);
|
|
1042
|
+
if (finalSyntax.ok) {
|
|
1043
|
+
return validated;
|
|
1044
|
+
}
|
|
886
1045
|
}
|
|
887
|
-
|
|
888
|
-
|
|
1046
|
+
} finally {
|
|
1047
|
+
rl.close();
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
const shouldUseInteractivePrompts = () => process.env.OPENCODE_AGENTHUB_FORCE_INTERACTIVE_PROMPTS === "1" || Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1051
|
+
const applyHrModelSelection = async (targetRoot, selection) => {
|
|
1052
|
+
await installHrOfficeHomeWithOptions({
|
|
1053
|
+
hrRoot: targetRoot,
|
|
1054
|
+
hrModelSelection: selection
|
|
1055
|
+
});
|
|
1056
|
+
};
|
|
1057
|
+
const repairHrModelConfigurationIfNeeded = async (targetRoot) => {
|
|
1058
|
+
const settings = await readAgentHubSettings(targetRoot);
|
|
1059
|
+
if (!shouldUseInteractivePrompts()) {
|
|
1060
|
+
for (const agentName of hrAgentNames) {
|
|
1061
|
+
const model = settings?.agents?.[agentName]?.model;
|
|
1062
|
+
if (typeof model !== "string" || model.trim().length === 0) continue;
|
|
1063
|
+
const syntax = validateModelIdentifier(model);
|
|
1064
|
+
if (!syntax.ok) {
|
|
1065
|
+
fail(`HR model configuration needs attention. Agent '${agentName}' model '${model}' is invalid: ${syntax.message}`);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
const status = await validateHrAgentModelConfiguration(targetRoot, settings);
|
|
1071
|
+
if (status.valid) return;
|
|
1072
|
+
const rl = createPromptInterface();
|
|
1073
|
+
try {
|
|
1074
|
+
process.stdout.write("[agenthub] HR model configuration needs attention.\n");
|
|
1075
|
+
if (status.message) process.stdout.write(`${status.message}
|
|
889
1076
|
`);
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1077
|
+
const repair = await promptBoolean(
|
|
1078
|
+
rl,
|
|
1079
|
+
"Reconfigure HR models now?",
|
|
1080
|
+
true
|
|
1081
|
+
);
|
|
1082
|
+
if (!repair) {
|
|
1083
|
+
fail("Aborted before repairing invalid HR model configuration.");
|
|
1084
|
+
}
|
|
1085
|
+
const fallback = await promptChoice(
|
|
1086
|
+
rl,
|
|
1087
|
+
"Choose a fallback",
|
|
1088
|
+
["free", "native", "custom", "retry recommended"],
|
|
1089
|
+
"free"
|
|
1090
|
+
);
|
|
1091
|
+
const validated = await promptValidatedHrModelSelection(
|
|
1092
|
+
rl,
|
|
1093
|
+
targetRoot,
|
|
1094
|
+
fallback === "retry recommended" ? "recommended" : fallback
|
|
1095
|
+
);
|
|
1096
|
+
const resolved = await resolveHrBootstrapAgentModels({
|
|
1097
|
+
targetRoot,
|
|
1098
|
+
selection: validated
|
|
1099
|
+
});
|
|
1100
|
+
const merged = mergeAgentHubSettingsDefaults(settings || {});
|
|
1101
|
+
merged.agents = merged.agents || {};
|
|
1102
|
+
for (const agentName of hrAgentNames) {
|
|
1103
|
+
const resolvedSelection = resolved.agentModels[agentName];
|
|
1104
|
+
merged.agents[agentName] = {
|
|
1105
|
+
...merged.agents[agentName] || {},
|
|
1106
|
+
model: resolvedSelection.model,
|
|
1107
|
+
...resolvedSelection.variant ? { variant: resolvedSelection.variant } : {}
|
|
1108
|
+
};
|
|
1109
|
+
if (!resolvedSelection.variant) delete merged.agents[agentName].variant;
|
|
1110
|
+
}
|
|
1111
|
+
merged.meta = {
|
|
1112
|
+
...merged.meta,
|
|
1113
|
+
onboarding: {
|
|
1114
|
+
...merged.meta?.onboarding,
|
|
1115
|
+
modelStrategy: resolved.strategy,
|
|
1116
|
+
mode: merged.meta?.onboarding?.mode || "hr-office",
|
|
1117
|
+
importedNativeBasics: merged.meta?.onboarding?.importedNativeBasics ?? true,
|
|
1118
|
+
importedNativeAgents: merged.meta?.onboarding?.importedNativeAgents ?? true,
|
|
1119
|
+
createdAt: merged.meta?.onboarding?.createdAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
1120
|
+
}
|
|
894
1121
|
};
|
|
1122
|
+
await writeAgentHubSettings(targetRoot, merged);
|
|
1123
|
+
process.stdout.write("[agenthub] Updated HR model configuration.\n");
|
|
895
1124
|
} finally {
|
|
896
1125
|
rl.close();
|
|
897
1126
|
}
|
|
898
1127
|
};
|
|
899
1128
|
const printHrModelOverrideHint = (targetRoot) => {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
`[agenthub] Change later with: ${cliCommand} doctor --target-root ${targetRoot} --agent ${hrPrimaryAgentName} --model <model>`,
|
|
904
|
-
`[agenthub] HR subagents: ${hrSubagentNames.join(", ")} (use the same doctor command with --agent <name>)`
|
|
905
|
-
].join("\n") + "\n"
|
|
906
|
-
);
|
|
1129
|
+
void targetRoot;
|
|
1130
|
+
process.stdout.write(`Tip: change HR models later with '${cliCommand} doctor'.
|
|
1131
|
+
`);
|
|
907
1132
|
};
|
|
908
1133
|
const countConfiguredHrGithubSources = async (targetRoot) => {
|
|
909
1134
|
try {
|
|
@@ -954,8 +1179,12 @@ const syncHrSourceInventoryOnFirstRun = async (targetRoot) => {
|
|
|
954
1179
|
);
|
|
955
1180
|
}
|
|
956
1181
|
const sourceLabel = sourceParts.length > 0 ? sourceParts.join(" + ") : "configured HR sources";
|
|
957
|
-
process.stdout.write(
|
|
958
|
-
`
|
|
1182
|
+
process.stdout.write(
|
|
1183
|
+
`
|
|
1184
|
+
Step 3/3 \xB7 Sync inventory
|
|
1185
|
+
Sync the HR sourcer inventory from ${sourceLabel} \u2014 this may take a moment, please wait...
|
|
1186
|
+
`
|
|
1187
|
+
);
|
|
959
1188
|
try {
|
|
960
1189
|
const pythonCommand = resolvePythonCommand();
|
|
961
1190
|
const scriptPath = path.join(targetRoot, "bin", "sync_sources.py");
|
|
@@ -982,12 +1211,10 @@ const syncHrSourceInventoryOnFirstRun = async (targetRoot) => {
|
|
|
982
1211
|
});
|
|
983
1212
|
const summary = stdout.trim();
|
|
984
1213
|
if (code === 0) {
|
|
985
|
-
|
|
1214
|
+
void summary;
|
|
1215
|
+
const repoSummary = configuredSourceCount && configuredSourceCount > 0 ? `${configuredSourceCount} repo${configuredSourceCount === 1 ? "" : "s"}` : "configured sources";
|
|
1216
|
+
process.stdout.write(`\u2713 HR sourcer inventory sync complete (${repoSummary}).
|
|
986
1217
|
`);
|
|
987
|
-
process.stdout.write(
|
|
988
|
-
`[agenthub] HR source status: ${path.join(targetRoot, "source-status.json")}
|
|
989
|
-
`
|
|
990
|
-
);
|
|
991
1218
|
return;
|
|
992
1219
|
}
|
|
993
1220
|
process.stderr.write(
|
|
@@ -1007,18 +1234,33 @@ const syncHrSourceInventoryOnFirstRun = async (targetRoot) => {
|
|
|
1007
1234
|
};
|
|
1008
1235
|
const ensureHrOfficeReadyOrBootstrap = async (targetRoot = defaultHrHome(), options = {}) => {
|
|
1009
1236
|
if (await hrHomeInitialized(targetRoot)) return;
|
|
1010
|
-
const shouldPrompt =
|
|
1237
|
+
const shouldPrompt = shouldUseInteractivePrompts();
|
|
1238
|
+
process.stdout.write("\nHR Office \u2014 first-time setup\n\n");
|
|
1239
|
+
process.stdout.write(
|
|
1240
|
+
"Heads up: a full HR assemble can take about 20\u201330 minutes because AI may need time to choose and evaluate the souls and skills your agents need.\n\n"
|
|
1241
|
+
);
|
|
1242
|
+
process.stdout.write("This will:\n");
|
|
1243
|
+
process.stdout.write("1. Choose an AI model for HR agents\n");
|
|
1244
|
+
process.stdout.write("2. Create the HR Office workspace\n");
|
|
1245
|
+
if (options.syncSourcesOnFirstRun ?? true) {
|
|
1246
|
+
process.stdout.write("3. Sync the HR sourcer inventory (this may take a little longer)\n\n");
|
|
1247
|
+
} else {
|
|
1248
|
+
process.stdout.write("3. Skip inventory sync for now because you are assembling only\n\n");
|
|
1249
|
+
}
|
|
1011
1250
|
const hrModelSelection = shouldPrompt ? await promptHrBootstrapModelSelection(targetRoot) : void 0;
|
|
1012
|
-
await
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
process.stdout.write(`\u2713 First run \u2014 initialised HR Office at ${targetRoot}
|
|
1251
|
+
await applyHrModelSelection(targetRoot, hrModelSelection || {});
|
|
1252
|
+
process.stdout.write(`
|
|
1253
|
+
Step 2/3 \xB7 Create workspace
|
|
1254
|
+
\u2713 First run \u2014 initialised HR Office at ${targetRoot}
|
|
1017
1255
|
`);
|
|
1018
1256
|
printHrModelOverrideHint(targetRoot);
|
|
1019
1257
|
if (options.syncSourcesOnFirstRun ?? true) {
|
|
1020
1258
|
await syncHrSourceInventoryOnFirstRun(targetRoot);
|
|
1021
1259
|
}
|
|
1260
|
+
process.stdout.write(`
|
|
1261
|
+
\u2713 HR Office is ready.
|
|
1262
|
+
`);
|
|
1263
|
+
suppressNextHrRuntimeBanner = true;
|
|
1022
1264
|
};
|
|
1023
1265
|
const isHrRuntimeSelection = (selection) => selection?.kind === "profile" && selection.profile === "hr";
|
|
1024
1266
|
const normalizeCsv = (value) => value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
@@ -1190,17 +1432,38 @@ const maybeConfigureEnvrc = async (workspace, configRoot) => {
|
|
|
1190
1432
|
};
|
|
1191
1433
|
const createPromptInterface = () => {
|
|
1192
1434
|
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
1193
|
-
|
|
1435
|
+
const terminal = interactive && shouldUseReadlineTerminal();
|
|
1436
|
+
if (terminal) {
|
|
1194
1437
|
const resetSequence = interactivePromptResetSequence();
|
|
1195
1438
|
if (resetSequence) process.stdout.write(resetSequence);
|
|
1196
1439
|
}
|
|
1197
1440
|
return readline.createInterface({
|
|
1198
1441
|
input: process.stdin,
|
|
1199
1442
|
output: process.stdout,
|
|
1200
|
-
terminal
|
|
1443
|
+
terminal
|
|
1201
1444
|
});
|
|
1202
1445
|
};
|
|
1203
|
-
const
|
|
1446
|
+
const scriptedPromptAnswers = (() => {
|
|
1447
|
+
const raw = process.env.OPENCODE_AGENTHUB_SCRIPTED_ANSWERS;
|
|
1448
|
+
if (!raw) return void 0;
|
|
1449
|
+
try {
|
|
1450
|
+
const parsed2 = JSON.parse(raw);
|
|
1451
|
+
return Array.isArray(parsed2) ? parsed2.map((value) => typeof value === "string" ? value : String(value)) : void 0;
|
|
1452
|
+
} catch {
|
|
1453
|
+
return raw.split("\n");
|
|
1454
|
+
}
|
|
1455
|
+
})();
|
|
1456
|
+
let scriptedPromptIndex = 0;
|
|
1457
|
+
const askPrompt = async (rl, question) => {
|
|
1458
|
+
if (scriptedPromptAnswers && scriptedPromptIndex < scriptedPromptAnswers.length) {
|
|
1459
|
+
const answer = scriptedPromptAnswers[scriptedPromptIndex++] || "";
|
|
1460
|
+
const sanitized = stripTerminalControlInput(answer);
|
|
1461
|
+
process.stdout.write(`${question}${sanitized}
|
|
1462
|
+
`);
|
|
1463
|
+
return sanitized;
|
|
1464
|
+
}
|
|
1465
|
+
return stripTerminalControlInput(await rl.question(question));
|
|
1466
|
+
};
|
|
1204
1467
|
const promptRequired = async (rl, question, defaultValue) => {
|
|
1205
1468
|
while (true) {
|
|
1206
1469
|
const answer = await askPrompt(
|
|
@@ -2260,6 +2523,7 @@ if (parsed.command === "hr") {
|
|
|
2260
2523
|
await ensureHrOfficeReadyOrBootstrap(resolveSelectedHomeRoot(parsed), {
|
|
2261
2524
|
syncSourcesOnFirstRun: !parsed.assembleOnly
|
|
2262
2525
|
});
|
|
2526
|
+
await repairHrModelConfigurationIfNeeded(resolveSelectedHomeRoot(parsed) || defaultHrHome());
|
|
2263
2527
|
if (parsed.hrIntent?.kind === "office") {
|
|
2264
2528
|
parsed.workspace = resolveSelectedHomeRoot(parsed) || defaultHrHome();
|
|
2265
2529
|
} else if (parsed.hrIntent?.kind === "compose") {
|
|
@@ -2284,10 +2548,13 @@ if (parsed.command === "run" || parsed.command === "start" || parsed.command ===
|
|
|
2284
2548
|
}
|
|
2285
2549
|
const finalConfigRoot = resolveConfigRoot(parsed);
|
|
2286
2550
|
const result = await composeSelection(parsed, finalConfigRoot);
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
)
|
|
2551
|
+
if (!(parsed.command === "hr" && suppressNextHrRuntimeBanner)) {
|
|
2552
|
+
printRuntimeBanner(
|
|
2553
|
+
parsed.command === "hr" ? "HR Office" : "My Team",
|
|
2554
|
+
resolveSelectedHomeRoot(parsed) || defaultAgentHubHome()
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2557
|
+
suppressNextHrRuntimeBanner = false;
|
|
2291
2558
|
if (shouldChmod()) {
|
|
2292
2559
|
await chmod(path.join(result.configRoot, "run.sh"), 493);
|
|
2293
2560
|
}
|
|
@@ -9,7 +9,8 @@ const csiSequencePattern = /\u001b\[[0-?]*[ -/]*[@-~]/g;
|
|
|
9
9
|
const oscSequencePattern = /\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)/g;
|
|
10
10
|
const singleEscapePattern = /\u001b[@-_]/g;
|
|
11
11
|
const controlCharacterPattern = /[\u0000-\u001f\u007f]/g;
|
|
12
|
-
const
|
|
12
|
+
const degradedLeadingCsiFragmentPattern = /^(?:(?:\d{1,3};){1,10}\d{1,3}m)+/;
|
|
13
|
+
const stripTerminalControlInput = (value) => value.replace(oscSequencePattern, "").replace(csiSequencePattern, "").replace(singleEscapePattern, "").replace(controlCharacterPattern, "").replace(degradedLeadingCsiFragmentPattern, "");
|
|
13
14
|
const interactivePromptResetSequence = (win = detectWindows()) => isWindows(win) ? [
|
|
14
15
|
"\x1B[?1000l",
|
|
15
16
|
"\x1B[?1001l",
|
|
@@ -19,6 +20,7 @@ const interactivePromptResetSequence = (win = detectWindows()) => isWindows(win)
|
|
|
19
20
|
"\x1B[?1006l",
|
|
20
21
|
"\x1B[?1015l"
|
|
21
22
|
].join("") : "";
|
|
23
|
+
const shouldUseReadlineTerminal = (win = detectWindows()) => !isWindows(win);
|
|
22
24
|
const generateRunScript = () => `#!/usr/bin/env bash
|
|
23
25
|
set -euo pipefail
|
|
24
26
|
|
|
@@ -57,6 +59,7 @@ export {
|
|
|
57
59
|
resolvePythonCommand,
|
|
58
60
|
shouldChmod,
|
|
59
61
|
shouldOfferEnvrc,
|
|
62
|
+
shouldUseReadlineTerminal,
|
|
60
63
|
spawnOptions,
|
|
61
64
|
stripTerminalControlInput,
|
|
62
65
|
symlinkType,
|
|
@@ -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 || {});
|
|
@@ -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,
|
|
@@ -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.
|
|
@@ -168,6 +168,7 @@ def validate_profile_default_agents(import_root: Path) -> None:
|
|
|
168
168
|
)
|
|
169
169
|
|
|
170
170
|
references: list[tuple[str, str]] = []
|
|
171
|
+
primary_agent_names: set[str] = set()
|
|
171
172
|
missing_bundles: list[str] = []
|
|
172
173
|
for raw_bundle_name in bundle_names:
|
|
173
174
|
if not isinstance(raw_bundle_name, str) or not raw_bundle_name.strip():
|
|
@@ -181,11 +182,16 @@ def validate_profile_default_agents(import_root: Path) -> None:
|
|
|
181
182
|
continue
|
|
182
183
|
agent = bundle.get("agent", {})
|
|
183
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
|
|
184
187
|
if not isinstance(agent_name, str) or not agent_name.strip():
|
|
185
188
|
raise SystemExit(
|
|
186
189
|
f"Bundle '{bundle_name}' is missing required agent.name; profile '{profile_name}' cannot use it."
|
|
187
190
|
)
|
|
188
|
-
|
|
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)
|
|
189
195
|
|
|
190
196
|
if missing_bundles:
|
|
191
197
|
detail = ", ".join(missing_bundles)
|
|
@@ -199,6 +205,10 @@ def validate_profile_default_agents(import_root: Path) -> None:
|
|
|
199
205
|
raise SystemExit(
|
|
200
206
|
f"Profile '{profile_name}' uses nativeAgentPolicy 'team-only' and must set defaultAgent explicitly."
|
|
201
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
|
+
)
|
|
202
212
|
if default_agent is None:
|
|
203
213
|
continue
|
|
204
214
|
if not isinstance(default_agent, str) or not default_agent.strip():
|
|
@@ -224,6 +234,10 @@ def validate_profile_default_agents(import_root: Path) -> None:
|
|
|
224
234
|
|
|
225
235
|
agent_names = {agent_name for _, agent_name in references}
|
|
226
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
|
+
)
|
|
227
241
|
continue
|
|
228
242
|
|
|
229
243
|
available = ", ".join(sorted(agent_names)) or "(none)"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-agenthub",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
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",
|