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.
@@ -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
  ];
@@ -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. skipping explicit AI model preference collection
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 all of the following:
35
+ Before leaving requirements, you must explicitly ask for and summarize only the minimum needed to plan staffing options:
36
36
 
37
- - target domain / stack
38
- - team shape or deployment shape
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 infer model preferences silently.
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
- - any model preferences supplied by the user
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, and model preferences already confirmed by the user
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
- - `proposed_agent_models`
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
- - Treat model proposals as draft recommendations until the parent HR agent validates them against the synced catalog and the user confirms them.
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 | `domain:` `team-shape:` `sourcing:` `constraints:` `model-prefs:` `acceptance:` |
87
- | 2 | `STAFFING PLAN` | `$HR_HOME/state/staffing-plans/latest.json` and `latest.md` | `recommended:` `alternatives:` `composition:` `draft-names:` `proposed-agent-models:` `risks:` |
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 + model map | `team:` `overlaps:` `simplifications:` `per-agent-models:` `default-opencode-agents:` `set-default-on-promote:` `unresolved:` |
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` -> confirmed requirements summary
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 per-agent model map
104
- - `STAGING & CONFIRMATION` -> staged package and final checklist
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 with `question()` when the goal, team shape, constraints, acceptance criteria, model preferences, or sourcing constraints are still unclear.
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
- - target work domain or stack
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 a proposed default model for each staged agent.
139
- 14. Before confirming models, 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.
140
- 15. Ask the user to confirm or edit the per-agent default model map. Do not assume one shared model if the user wants role-specific defaults. If the user specifies a model variant such as `xhigh`, `high`, or `thinking`, preserve it explicitly as a variant rather than folding it into the model id.
141
- 16. 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.
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. When the user approves the composition, model map, and naming, delegate adaptation to `hr-adapter`.
149
- 21. Run final readiness checks through `hr-verifier`.
150
- 22. Present the final human checklist and require explicit approval.
151
- 23. After approval, give the operator a structured handoff in this order:
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
- hrPrimaryAgentName,
41
- hrSubagentNames,
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 Interactive terminals can choose recommended / free / custom
150
- defaults to openai/gpt-5.4-mini when left blank
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 promptHrBootstrapModelSelection = async (hrRoot) => {
844
- const rl = createPromptInterface();
845
- try {
846
- process.stdout.write("\nFirst-time HR Office setup\n");
847
- process.stdout.write("Choose an HR model preset:\n");
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
- ` recommended Use ${recommendedHrBootstrapModel} (${recommendedHrBootstrapVariant})
920
+ `[agenthub] Recommended HR preset requires OpenAI model access in your opencode environment.
850
921
  `
851
922
  );
852
- process.stdout.write(
853
- " free Pick from current opencode free models (quality may drop)\n"
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
- process.stdout.write(" custom Enter any model id yourself\n\n");
856
- const subagentStrategy = await promptChoice(
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
- "HR model preset",
859
- ["recommended", "free", "custom"],
860
- "recommended"
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
- let sharedSubagentModel;
863
- let consoleModel;
864
- if (subagentStrategy === "recommended") {
865
- consoleModel = recommendedHrBootstrapModel;
866
- sharedSubagentModel = recommendedHrBootstrapModel;
867
- }
868
- if (subagentStrategy === "free") {
869
- const freeModels = await listOpencodeFreeModels();
870
- const fallbackFreeModel = freeModels.includes("opencode/minimax-m2.5-free") ? "opencode/minimax-m2.5-free" : freeModels[0] || "opencode/minimax-m2.5-free";
871
- process.stdout.write("Current opencode free models:\n");
872
- sharedSubagentModel = await promptIndexedChoice(
873
- rl,
874
- "Choose a free model for HR",
875
- freeModels.length > 0 ? freeModels : [fallbackFreeModel],
876
- fallbackFreeModel
877
- );
878
- consoleModel = sharedSubagentModel;
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
- "Custom HR model",
883
- recommendedHrBootstrapModel
1033
+ "Apply this recommendation now",
1034
+ ["accept", "recommended", "free", "native", "custom"],
1035
+ "accept"
884
1036
  );
885
- consoleModel = sharedSubagentModel;
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
- process.stdout.write(`
888
- [agenthub] HR settings will be written to ${path.join(hrRoot, "settings.json")}
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
- return {
891
- consoleModel,
892
- subagentStrategy,
893
- sharedSubagentModel
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
- process.stdout.write(
901
- [
902
- `[agenthub] HR model settings: ${path.join(targetRoot, "settings.json")}`,
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(`[agenthub] First run \u2014 syncing HR inventory from ${sourceLabel}...
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
- if (summary) process.stdout.write(`${summary}
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 = process.stdin.isTTY && process.stdout.isTTY;
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 installHrOfficeHomeWithOptions({
1013
- hrRoot: targetRoot,
1014
- hrModelSelection
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
- if (interactive) {
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: interactive
1443
+ terminal
1201
1444
  });
1202
1445
  };
1203
- const askPrompt = async (rl, question) => stripTerminalControlInput(await rl.question(question));
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
- printRuntimeBanner(
2288
- parsed.command === "hr" ? "HR Office" : "My Team",
2289
- resolveSelectedHomeRoot(parsed) || defaultAgentHubHome()
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 stripTerminalControlInput = (value) => value.replace(oscSequencePattern, "").replace(csiSequencePattern, "").replace(singleEscapePattern, "").replace(controlCharacterPattern, "");
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 { normalizeModelSelection, pickModelSelection } from "./model-utils.js";
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 explicitly collected | pass/fail |
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
- proposed_agent_models:
61
- - agent: ... | model: ...
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
- references.append((bundle_name, agent_name.strip()))
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.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",