opencode-agenthub 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +373 -0
  3. package/dist/composer/bootstrap.js +493 -0
  4. package/dist/composer/builtin-assets.js +139 -0
  5. package/dist/composer/capabilities.js +20 -0
  6. package/dist/composer/compose.js +824 -0
  7. package/dist/composer/defaults.js +10 -0
  8. package/dist/composer/home-transfer.js +288 -0
  9. package/dist/composer/install-home.js +5 -0
  10. package/dist/composer/library/README.md +93 -0
  11. package/dist/composer/library/bundles/auto.json +18 -0
  12. package/dist/composer/library/bundles/build.json +17 -0
  13. package/dist/composer/library/bundles/hr-adapter.json +26 -0
  14. package/dist/composer/library/bundles/hr-cto.json +24 -0
  15. package/dist/composer/library/bundles/hr-evaluator.json +26 -0
  16. package/dist/composer/library/bundles/hr-planner.json +26 -0
  17. package/dist/composer/library/bundles/hr-sourcer.json +24 -0
  18. package/dist/composer/library/bundles/hr-verifier.json +26 -0
  19. package/dist/composer/library/bundles/hr.json +35 -0
  20. package/dist/composer/library/bundles/plan.json +19 -0
  21. package/dist/composer/library/instructions/hr-boundaries.md +38 -0
  22. package/dist/composer/library/instructions/hr-protocol.md +102 -0
  23. package/dist/composer/library/profiles/auto.json +9 -0
  24. package/dist/composer/library/profiles/hr.json +9 -0
  25. package/dist/composer/library/souls/auto.md +29 -0
  26. package/dist/composer/library/souls/build.md +21 -0
  27. package/dist/composer/library/souls/hr-adapter.md +64 -0
  28. package/dist/composer/library/souls/hr-cto.md +57 -0
  29. package/dist/composer/library/souls/hr-evaluator.md +64 -0
  30. package/dist/composer/library/souls/hr-planner.md +48 -0
  31. package/dist/composer/library/souls/hr-sourcer.md +70 -0
  32. package/dist/composer/library/souls/hr-verifier.md +62 -0
  33. package/dist/composer/library/souls/hr.md +186 -0
  34. package/dist/composer/library/souls/plan.md +23 -0
  35. package/dist/composer/library/workflow/auto-mode.json +139 -0
  36. package/dist/composer/model-utils.js +39 -0
  37. package/dist/composer/opencode-profile.js +2299 -0
  38. package/dist/composer/package-manager.js +75 -0
  39. package/dist/composer/package-version.js +20 -0
  40. package/dist/composer/platform.js +48 -0
  41. package/dist/composer/query.js +133 -0
  42. package/dist/composer/settings.js +400 -0
  43. package/dist/plugins/opencode-agenthub.js +310 -0
  44. package/dist/plugins/opencode-question.js +223 -0
  45. package/dist/plugins/plan-guidance.js +263 -0
  46. package/dist/plugins/runtime-config.js +57 -0
  47. package/dist/skills/agenthub-doctor/SKILL.md +238 -0
  48. package/dist/skills/agenthub-doctor/diagnose.js +213 -0
  49. package/dist/skills/agenthub-doctor/fix.js +293 -0
  50. package/dist/skills/agenthub-doctor/index.js +30 -0
  51. package/dist/skills/agenthub-doctor/interactive.js +756 -0
  52. package/dist/skills/hr-assembly/SKILL.md +121 -0
  53. package/dist/skills/hr-final-check/SKILL.md +98 -0
  54. package/dist/skills/hr-review/SKILL.md +100 -0
  55. package/dist/skills/hr-staffing/SKILL.md +85 -0
  56. package/dist/skills/hr-support/bin/sync_sources.py +560 -0
  57. package/dist/skills/hr-support/bin/validate_staged_package.py +290 -0
  58. package/dist/skills/hr-support/bin/vendor_stage_mcps.py +234 -0
  59. package/dist/skills/hr-support/bin/vendor_stage_skills.py +104 -0
  60. package/dist/types.js +11 -0
  61. package/package.json +54 -0
@@ -0,0 +1,2299 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { chmod, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import readline from "node:readline/promises";
6
+ import {
7
+ agentHubHomeInitialized,
8
+ defaultAgentHubHome,
9
+ defaultHrHome,
10
+ hrHomeInitialized,
11
+ installAgentHubHome,
12
+ installHrOfficeHomeWithOptions,
13
+ promptHubInitAnswers,
14
+ syncBuiltInAgentHubAssets
15
+ } from "./bootstrap.js";
16
+ import {
17
+ getBuiltInManifestKeysForMode,
18
+ listBuiltInAssetNames
19
+ } from "./builtin-assets.js";
20
+ import {
21
+ composeCustomizedAgent,
22
+ composeToolInjection,
23
+ composeWorkspace,
24
+ getDefaultConfigRoot,
25
+ getWorkspaceRuntimeRoot
26
+ } from "./compose.js";
27
+ import { expandProfileAddSelections, listProfileAddCapabilityNames } from "./capabilities.js";
28
+ import {
29
+ exportAgentHubHome,
30
+ importAgentHubHome
31
+ } from "./home-transfer.js";
32
+ import {
33
+ inspectRuntimeConfig,
34
+ resolvePluginConfigRoot,
35
+ summarizeRuntimeFeatureState
36
+ } from "../plugins/runtime-config.js";
37
+ import { readPackageVersion } from "./package-version.js";
38
+ import {
39
+ mergeAgentHubSettingsDefaults,
40
+ hrPrimaryAgentName,
41
+ hrSubagentNames,
42
+ recommendedHrBootstrapModel,
43
+ recommendedHrBootstrapVariant,
44
+ readAgentHubSettings,
45
+ writeAgentHubSettings
46
+ } from "./settings.js";
47
+ import {
48
+ displayHomeConfigPath,
49
+ resolvePythonCommand,
50
+ shouldChmod,
51
+ shouldOfferEnvrc,
52
+ spawnOptions,
53
+ windowsStartupNotice
54
+ } from "./platform.js";
55
+ const cliCommand = "agenthub";
56
+ const compatibilityCliCommand = "opencode-agenthub";
57
+ const printHelp = () => {
58
+ const agentHubHomePath = displayHomeConfigPath("opencode-agenthub");
59
+ const hrHomePath = displayHomeConfigPath("opencode-agenthub-hr");
60
+ const hrSettingsPath = displayHomeConfigPath("opencode-agenthub-hr", ["settings.json"]);
61
+ const hrStagingPath = displayHomeConfigPath("opencode-agenthub-hr", ["staging"]);
62
+ process.stdout.write(`${cliCommand} \u2014 Agent Hub for opencode (requires Node \u2265 18.0.0)
63
+
64
+ USAGE
65
+ ${cliCommand} <command> [options]
66
+
67
+ ALIAS
68
+ ${compatibilityCliCommand} <command> [options]
69
+
70
+ COMMANDS (everyday)
71
+ start Start My Team (default profile > last profile > auto)
72
+ hr [profile] Enter HR Office or test an HR profile in this workspace
73
+ promote Promote an approved staged HR package into My Team
74
+
75
+ COMMANDS (maintenance)
76
+ backup Back up My Team (personal home only)
77
+ restore Restore My Team from a backup bundle
78
+ upgrade Preview or sync built-in managed assets
79
+
80
+ COMMANDS (advanced)
81
+ setup Initialize the Agent Hub home directory
82
+ list List installed assets (souls, bundles, profiles, skills, more)
83
+ new Create new souls, skills, bundles, profiles, and advanced assets
84
+ plugin Inspect plugin-only runtime health
85
+ doctor Inspect and fix Agent Hub home assets
86
+ compose Create a new profile or bundle scaffold (alias for new profile/bundle)
87
+
88
+ COMMANDS (compatibility aliases)
89
+ run Alias for 'start'
90
+ hub-doctor Alias for 'doctor'
91
+ hub-export Advanced: export a chosen Agent Hub home to a portable directory
92
+ hub-import Advanced: import a previously-exported Agent Hub home
93
+
94
+ BUILT-IN PROFILES (for use with 'start' / 'run')
95
+ auto Default coding agent (Auto + Plan + Build)
96
+ hr HR console with staffing subagents
97
+
98
+ EXAMPLES
99
+ ${cliCommand} start
100
+ ${cliCommand} start last
101
+ ${cliCommand} start set reviewer-team
102
+ ${cliCommand} hr
103
+ ${cliCommand} hr last
104
+ ${cliCommand} hr recruiter-team
105
+ ${cliCommand} promote
106
+ ${cliCommand} backup --output ./my-team-backup
107
+ ${cliCommand} restore --source ./my-team-backup
108
+ ${cliCommand} start auto --workspace /path/to/project
109
+ ${cliCommand} list
110
+ ${cliCommand} list bundles
111
+ ${cliCommand} new soul reviewer
112
+ ${cliCommand} new profile my-team --from auto --add hr-suite
113
+ ${cliCommand} upgrade
114
+ ${cliCommand} upgrade --force
115
+ ${cliCommand} plugin doctor
116
+ ${cliCommand} doctor --fix-all
117
+ ${cliCommand} hub-export --output ./agenthub-backup
118
+ ${cliCommand} hub-import --source ./agenthub-backup
119
+
120
+ FLAGS (global)
121
+ --help, -h Show this help message
122
+ --version, -v Print version
123
+
124
+ FLAGS (setup)
125
+ setup [auto|minimal] Setup mode (default: auto)
126
+ --target-root <path> Override Agent Hub home location
127
+ --import-souls <path> Import existing soul/agent prompt folder
128
+ --import-instructions <path> Import existing instructions folder
129
+ --import-skills <path> Import existing skills folder
130
+ --import-mcp-servers <path> Import existing MCP server folder
131
+
132
+ FLAGS (start / run)
133
+ --workspace <path> Target workspace (default: cwd)
134
+ --config-root <path> Override .opencode config directory
135
+ --assemble-only Write config files but do not launch opencode
136
+ --mode <tools-only|customized-agent>
137
+ Advanced: launch with a built-in bundle mode instead of a profile
138
+ start last Reuse the last profile used in this workspace (fallback: auto)
139
+ start set <profile> Save the default personal profile for future bare 'start'
140
+ -- <args> Pass remaining args to opencode
141
+
142
+ FLAGS (hr)
143
+ hr Enter the isolated HR Office (bootstraps it on first use)
144
+ hr <profile> Test an HR-home profile in the current workspace
145
+ hr last Reuse the last HR profile tested in this workspace
146
+ hr set <profile> Unsupported (use explicit '${cliCommand} hr <profile>' each time)
147
+ first bootstrap Interactive terminals can choose recommended / free / custom
148
+ defaults to openai/gpt-5.4-mini when left blank
149
+
150
+ FLAGS (new / compose profile)
151
+ --from <profile> Seed bundles/plugins from an existing profile
152
+ --add <bundle|cap> Add bundle(s) or capability shorthand (repeatable)
153
+ --reserved-ok Allow names that collide with built-in asset names
154
+
155
+ FLAGS (upgrade)
156
+ --target-root <path> Agent Hub home to inspect/sync
157
+ --dry-run Preview managed file changes (default)
158
+ --force Overwrite built-in managed files
159
+
160
+ FLAGS (plugin doctor)
161
+ --config-root <path> Inspect a specific opencode config root
162
+
163
+ FLAGS (doctor / hub-doctor)
164
+ --target-root <path> Agent Hub home to inspect (default: ${agentHubHomePath})
165
+ --fix-all Apply all safe automatic fixes
166
+ --dry-run Preview fixes without writing
167
+ --agent <name> Target a specific agent
168
+ --model <model> Override the agent's model
169
+ --clear-model Remove the agent's model override
170
+ --prompt-file <path> Set the agent's soul/prompt from a file
171
+ --clear-prompt Remove the agent's soul/prompt override
172
+
173
+ FLAGS (hub-export)
174
+ --output <path> Destination directory (required)
175
+ --source-root <path> Agent Hub home to export (default: ${agentHubHomePath})
176
+
177
+ FLAGS (hub-import)
178
+ --source <path> Exported Agent Hub directory to import (required)
179
+ --target-root <path> Destination Agent Hub home (default: ${agentHubHomePath})
180
+ --overwrite Overwrite matching entries
181
+ --settings <preserve|replace> How to handle settings (default: preserve)
182
+
183
+ FLAGS (backup)
184
+ --output <path> Destination directory (required; personal home only)
185
+
186
+ FLAGS (restore)
187
+ --source <path> Backup directory to restore from (required; personal home only)
188
+ --overwrite Overwrite matching entries
189
+ --settings <preserve|replace> How to handle settings (default: preserve)
190
+
191
+ FLAGS (promote)
192
+ [package-id] Staged package id under ${hrStagingPath}
193
+
194
+ PLUGIN-ONLY MODE
195
+ Agent Hub ships as both a CLI and an opencode plugin. When loaded as
196
+ a plugin without a configured hub runtime, it runs in degraded mode:
197
+ - Tool blocking (blockedTools) is disabled
198
+ - The call_omo_agent tool is blocked as a safety fallback
199
+ Run '${cliCommand} setup' once to initialize the hub runtime and exit
200
+ degraded mode. Features that require the hub runtime: profile composition,
201
+ agent souls, skill injection, and plan detection.
202
+
203
+ HR HOME
204
+ HR live state is stored separately at ${hrHomePath}
205
+ Override with OPENCODE_AGENTHUB_HR_HOME environment variable.
206
+ Use '${cliCommand} hr' to enter the HR Office. If HR is not installed yet, Agent Hub bootstraps it automatically.
207
+ Use '${cliCommand} hr <profile>' to test an HR-home profile or a staged profile in a workspace before promote.
208
+ HR model overrides live in ${hrSettingsPath}.
209
+
210
+ NOTE
211
+ This package requires Node on PATH.
212
+ Windows users should use WSL 2 for the best experience; native Windows remains best-effort in alpha.
213
+ `);
214
+ };
215
+ const fail = (message) => {
216
+ process.stderr.write(`${message}
217
+ `);
218
+ process.exit(1);
219
+ };
220
+ const formatWorkspaceAccessError = (workspace) => {
221
+ const quotedWorkspace = `'${workspace}'`;
222
+ const macHint = process.platform === "darwin" ? "\n\nOn macOS this usually means Terminal/Bun/opencode does not currently have permission to read that folder. Check Privacy & Security access for Documents/Desktop/iCloud-backed folders, then retry." : "";
223
+ return [
224
+ `Workspace ${quotedWorkspace} is not readable.`,
225
+ "Agent Hub can assemble the runtime, but opencode will fail to launch if it cannot scan the workspace.",
226
+ `Try: ls ${quotedWorkspace}`
227
+ ].join("\n") + macHint;
228
+ };
229
+ const ensureWorkspaceReadable = async (workspace) => {
230
+ try {
231
+ await readdir(workspace);
232
+ } catch (error) {
233
+ if (error && typeof error === "object" && "code" in error && (error.code === "EACCES" || error.code === "EPERM")) {
234
+ fail(formatWorkspaceAccessError(workspace));
235
+ }
236
+ throw error;
237
+ }
238
+ };
239
+ const parseSetupMode = (value) => {
240
+ if (!value) return void 0;
241
+ const normalized = value.trim().toLowerCase();
242
+ if (normalized === "minimal" || normalized === "auto") {
243
+ return normalized;
244
+ }
245
+ return fail(`Invalid setup mode '${value}'. Use 'auto' or 'minimal'.`);
246
+ };
247
+ const normalizeRuntimeProfileName = (value) => {
248
+ return value;
249
+ };
250
+ const parseBundleName = (value) => {
251
+ const normalized = value.trim().toLowerCase();
252
+ if (normalized === "tools-only" || normalized === "customized-agent") {
253
+ return normalized;
254
+ }
255
+ return fail(
256
+ `Invalid bundle '${value}'. Use 'tools-only' or 'customized-agent'.`
257
+ );
258
+ };
259
+ const parseSettingsMode = (value) => {
260
+ if (!value || value === "preserve" || value === "replace")
261
+ return value || "preserve";
262
+ return fail(`Invalid settings mode '${value}'. Use 'preserve' or 'replace'.`);
263
+ };
264
+ const parseArgs = (argv) => {
265
+ let command = "run";
266
+ let runtimeSelection;
267
+ let composeSelection2;
268
+ let listTarget;
269
+ let pluginSubcommand;
270
+ let workspace = process.cwd();
271
+ let configRoot;
272
+ let assembleOnly = false;
273
+ const opencodeArgs = [];
274
+ const bootstrapOptions = {};
275
+ const profileCreateOptions = {
276
+ addBundles: [],
277
+ reservedOk: false
278
+ };
279
+ const upgradeOptions = {
280
+ force: false,
281
+ dryRun: true
282
+ };
283
+ const transferOptions = {
284
+ overwrite: false,
285
+ settingsMode: "preserve"
286
+ };
287
+ const doctorOptions = {
288
+ fixAll: false,
289
+ dryRun: false,
290
+ clearModel: false,
291
+ clearPrompt: false
292
+ };
293
+ let promotePackageId;
294
+ let startIntent;
295
+ let hrIntent;
296
+ let index = 0;
297
+ const maybeCommand = argv[0];
298
+ if (maybeCommand === "setup" || maybeCommand === "hr" || maybeCommand === "backup" || maybeCommand === "restore" || maybeCommand === "promote" || maybeCommand === "new" || maybeCommand === "upgrade" || maybeCommand === "plugin" || maybeCommand === "hub-export" || maybeCommand === "hub-import" || maybeCommand === "compose" || maybeCommand === "run" || maybeCommand === "start" || maybeCommand === "list" || maybeCommand === "doctor" || maybeCommand === "hub-doctor") {
299
+ command = maybeCommand;
300
+ index = 1;
301
+ const targetType = argv[index];
302
+ const targetName = argv[index + 1];
303
+ if (command === "hr") {
304
+ if (targetType === "set") {
305
+ fail(`'hr set <profile>' is not supported. Use '${cliCommand} hr <profile>' to test a temporary HR profile in this workspace.`);
306
+ }
307
+ if (targetType === "last") {
308
+ hrIntent = { kind: "compose", source: "last" };
309
+ index = 2;
310
+ } else {
311
+ const hrProfileArg = targetType && !targetType.startsWith("-") ? targetType : void 0;
312
+ if (hrProfileArg) {
313
+ hrIntent = {
314
+ kind: "compose",
315
+ profile: normalizeRuntimeProfileName(hrProfileArg),
316
+ source: "explicit"
317
+ };
318
+ runtimeSelection = {
319
+ kind: "profile",
320
+ profile: normalizeRuntimeProfileName(hrProfileArg)
321
+ };
322
+ index = 2;
323
+ } else {
324
+ hrIntent = { kind: "office" };
325
+ runtimeSelection = {
326
+ kind: "profile",
327
+ profile: "hr"
328
+ };
329
+ }
330
+ }
331
+ } else if (command === "promote" && targetType && !targetType.startsWith("-")) {
332
+ promotePackageId = targetType;
333
+ index = 2;
334
+ }
335
+ if ((command === "compose" || command === "new") && targetType === "profile") {
336
+ const name = targetName?.trim();
337
+ if (!name) {
338
+ fail(`'${command} profile' requires a profile name.`);
339
+ }
340
+ composeSelection2 = { kind: "profile", name };
341
+ index = 3;
342
+ } else if ((command === "compose" || command === "new") && targetType === "bundle") {
343
+ const name = targetName?.trim();
344
+ if (!name) {
345
+ fail(`'${command} bundle' requires a bundle name.`);
346
+ }
347
+ composeSelection2 = { kind: "bundle", name };
348
+ index = 3;
349
+ } else if (command === "new" && targetType === "soul") {
350
+ const name = targetName?.trim();
351
+ if (!name) fail("'new soul' requires a name.");
352
+ composeSelection2 = { kind: "soul", name };
353
+ index = 3;
354
+ } else if (command === "new" && targetType === "skill") {
355
+ const name = targetName?.trim();
356
+ if (!name) fail("'new skill' requires a name.");
357
+ composeSelection2 = { kind: "skill", name };
358
+ index = 3;
359
+ } else if (command === "new" && targetType === "instruction") {
360
+ const name = targetName?.trim();
361
+ if (!name) fail("'new instruction' requires a name.");
362
+ composeSelection2 = { kind: "instruction", name };
363
+ index = 3;
364
+ } else if (command === "plugin" && targetType === "doctor") {
365
+ pluginSubcommand = "doctor";
366
+ index = 2;
367
+ } else if (command === "setup" && targetType && !targetType.startsWith("-")) {
368
+ bootstrapOptions.mode = parseSetupMode(targetType);
369
+ index = 2;
370
+ } else if (command === "run" && targetType === "profile") {
371
+ startIntent = {
372
+ kind: "compose",
373
+ profile: normalizeRuntimeProfileName(targetName || "auto"),
374
+ source: "explicit"
375
+ };
376
+ runtimeSelection = {
377
+ kind: "profile",
378
+ profile: normalizeRuntimeProfileName(targetName || "auto")
379
+ };
380
+ index = 3;
381
+ } else if (command === "run" && targetType === "bundle") {
382
+ runtimeSelection = { kind: parseBundleName(targetName || "") };
383
+ index = 3;
384
+ } else if (command === "run" && targetType === "last") {
385
+ startIntent = { kind: "compose", source: "last" };
386
+ index = 2;
387
+ } else if (command === "run" && targetType === "set") {
388
+ const profile = normalizeRuntimeProfileName(targetName || "");
389
+ if (!profile) {
390
+ fail("'run set <profile>' requires a profile name.");
391
+ }
392
+ startIntent = { kind: "set-default", profile };
393
+ index = 3;
394
+ } else if (command === "run" && targetType && !targetType.startsWith("-")) {
395
+ startIntent = {
396
+ kind: "compose",
397
+ profile: normalizeRuntimeProfileName(targetType),
398
+ source: "explicit"
399
+ };
400
+ runtimeSelection = {
401
+ kind: "profile",
402
+ profile: normalizeRuntimeProfileName(targetType)
403
+ };
404
+ index = 2;
405
+ } else if (command === "start" && targetType === "profile") {
406
+ startIntent = {
407
+ kind: "compose",
408
+ profile: normalizeRuntimeProfileName(targetName || "auto"),
409
+ source: "explicit"
410
+ };
411
+ runtimeSelection = {
412
+ kind: "profile",
413
+ profile: normalizeRuntimeProfileName(targetName || "auto")
414
+ };
415
+ index = 3;
416
+ } else if (command === "start" && targetType === "bundle") {
417
+ runtimeSelection = { kind: parseBundleName(targetName || "") };
418
+ index = 3;
419
+ } else if (command === "start" && targetType === "last") {
420
+ startIntent = { kind: "compose", source: "last" };
421
+ index = 2;
422
+ } else if (command === "start" && targetType === "set") {
423
+ const profile = normalizeRuntimeProfileName(targetName || "");
424
+ if (!profile) {
425
+ fail("'start set <profile>' requires a profile name.");
426
+ }
427
+ startIntent = { kind: "set-default", profile };
428
+ index = 3;
429
+ } else if (command === "start" && targetType && !targetType.startsWith("-")) {
430
+ startIntent = {
431
+ kind: "compose",
432
+ profile: normalizeRuntimeProfileName(targetType),
433
+ source: "explicit"
434
+ };
435
+ runtimeSelection = {
436
+ kind: "profile",
437
+ profile: normalizeRuntimeProfileName(targetType)
438
+ };
439
+ index = 2;
440
+ } else if (command === "list" && targetType && !targetType.startsWith("-")) {
441
+ listTarget = targetType;
442
+ index = 2;
443
+ }
444
+ }
445
+ if (maybeCommand && !maybeCommand.startsWith("-") && command === "run" && maybeCommand !== "run" && index === 0) {
446
+ process.stderr.write(
447
+ `Unknown command: '${maybeCommand}'
448
+
449
+ Run '${cliCommand} --help' to see available commands.
450
+ `
451
+ );
452
+ process.exit(1);
453
+ }
454
+ for (; index < argv.length; index += 1) {
455
+ const arg = argv[index];
456
+ if (arg === "--help" || arg === "-h") {
457
+ printHelp();
458
+ process.exit(0);
459
+ }
460
+ if (arg === "--version" || arg === "-v") {
461
+ process.stdout.write(`${readPackageVersion()}
462
+ `);
463
+ process.exit(0);
464
+ }
465
+ if (arg === "--profile") {
466
+ runtimeSelection = {
467
+ kind: "profile",
468
+ profile: normalizeRuntimeProfileName(argv[index + 1] || "auto")
469
+ };
470
+ index += 1;
471
+ continue;
472
+ }
473
+ if (arg === "--mode") {
474
+ runtimeSelection = { kind: parseBundleName(argv[index + 1] || "") };
475
+ index += 1;
476
+ continue;
477
+ }
478
+ if (arg === "--from") {
479
+ profileCreateOptions.fromProfile = argv[index + 1];
480
+ index += 1;
481
+ continue;
482
+ }
483
+ if (arg === "--add") {
484
+ profileCreateOptions.addBundles.push(argv[index + 1] || "");
485
+ index += 1;
486
+ continue;
487
+ }
488
+ if (arg === "--reserved-ok") {
489
+ profileCreateOptions.reservedOk = true;
490
+ continue;
491
+ }
492
+ if (arg === "--workspace") {
493
+ workspace = path.resolve(argv[index + 1] || workspace);
494
+ index += 1;
495
+ continue;
496
+ }
497
+ if (arg === "--config-root") {
498
+ configRoot = path.resolve(argv[index + 1] || workspace);
499
+ index += 1;
500
+ continue;
501
+ }
502
+ if (arg === "--assemble-only") {
503
+ assembleOnly = true;
504
+ continue;
505
+ }
506
+ if (arg === "--target-root") {
507
+ const resolved = path.resolve(argv[index + 1] || defaultAgentHubHome());
508
+ bootstrapOptions.targetRoot = resolved;
509
+ transferOptions.targetRoot = resolved;
510
+ index += 1;
511
+ continue;
512
+ }
513
+ if (arg === "--source-root" || arg === "--source") {
514
+ transferOptions.sourceRoot = path.resolve(argv[index + 1] || workspace);
515
+ index += 1;
516
+ continue;
517
+ }
518
+ if (arg === "--output") {
519
+ transferOptions.outputRoot = path.resolve(argv[index + 1] || workspace);
520
+ index += 1;
521
+ continue;
522
+ }
523
+ if (arg === "--overwrite") {
524
+ transferOptions.overwrite = true;
525
+ continue;
526
+ }
527
+ if (arg === "--force") {
528
+ upgradeOptions.force = true;
529
+ continue;
530
+ }
531
+ if (arg === "--settings") {
532
+ transferOptions.settingsMode = parseSettingsMode(argv[index + 1]);
533
+ index += 1;
534
+ continue;
535
+ }
536
+ if (arg === "--fix-all") {
537
+ doctorOptions.fixAll = true;
538
+ continue;
539
+ }
540
+ if (arg === "--dry-run") {
541
+ doctorOptions.dryRun = true;
542
+ upgradeOptions.dryRun = true;
543
+ continue;
544
+ }
545
+ if (arg === "--agent") {
546
+ doctorOptions.agent = argv[index + 1];
547
+ index += 1;
548
+ continue;
549
+ }
550
+ if (arg === "--model") {
551
+ doctorOptions.model = argv[index + 1];
552
+ index += 1;
553
+ continue;
554
+ }
555
+ if (arg === "--clear-model") {
556
+ doctorOptions.clearModel = true;
557
+ continue;
558
+ }
559
+ if (arg === "--prompt-file") {
560
+ doctorOptions.promptFile = path.resolve(argv[index + 1] || workspace);
561
+ index += 1;
562
+ continue;
563
+ }
564
+ if (arg === "--clear-prompt") {
565
+ doctorOptions.clearPrompt = true;
566
+ continue;
567
+ }
568
+ if (arg === "--import-souls") {
569
+ bootstrapOptions.importSoulsPath = path.resolve(
570
+ argv[index + 1] || workspace
571
+ );
572
+ index += 1;
573
+ continue;
574
+ }
575
+ if (arg === "--import-instructions") {
576
+ bootstrapOptions.importInstructionsPath = path.resolve(
577
+ argv[index + 1] || workspace
578
+ );
579
+ index += 1;
580
+ continue;
581
+ }
582
+ if (arg === "--import-skills") {
583
+ bootstrapOptions.importSkillsPath = path.resolve(
584
+ argv[index + 1] || workspace
585
+ );
586
+ index += 1;
587
+ continue;
588
+ }
589
+ if (arg === "--import-mcp-servers") {
590
+ bootstrapOptions.importMcpServersPath = path.resolve(
591
+ argv[index + 1] || workspace
592
+ );
593
+ index += 1;
594
+ continue;
595
+ }
596
+ if (arg === "--") {
597
+ opencodeArgs.push(...argv.slice(index + 1));
598
+ break;
599
+ }
600
+ if (command === "promote" && !arg.startsWith("-") && !promotePackageId) {
601
+ promotePackageId = arg;
602
+ continue;
603
+ }
604
+ opencodeArgs.push(arg);
605
+ }
606
+ if (command === "start" && !startIntent) {
607
+ startIntent = { kind: "compose" };
608
+ }
609
+ if (command === "start" && startIntent?.kind === "compose" && !runtimeSelection) {
610
+ runtimeSelection = { kind: "profile", profile: startIntent.profile || "auto" };
611
+ }
612
+ if (runtimeSelection && runtimeSelection.kind !== "profile" && command === "setup") {
613
+ fail(
614
+ "'setup' does not accept '--mode'. Use 'setup minimal' for a minimal home."
615
+ );
616
+ }
617
+ if ((command === "compose" || command === "new") && !composeSelection2) {
618
+ if (command === "compose") {
619
+ fail("Use 'compose profile <name>' or 'compose bundle <name>'.");
620
+ }
621
+ fail(
622
+ "Use 'new soul <name>', 'new skill <name>', 'new instruction <name>', 'new bundle <name>', or 'new profile <name>'."
623
+ );
624
+ }
625
+ if ((profileCreateOptions.fromProfile || profileCreateOptions.addBundles.length > 0) && composeSelection2?.kind !== "profile") {
626
+ fail("'--from' and '--add' can only be used with 'compose profile' or 'new profile'.");
627
+ }
628
+ if (command === "doctor" || command === "hub-doctor") {
629
+ const modelActionCount = Number(Boolean(doctorOptions.model)) + Number(doctorOptions.clearModel);
630
+ const promptActionCount = Number(Boolean(doctorOptions.promptFile)) + Number(doctorOptions.clearPrompt);
631
+ const totalActionCount = modelActionCount + promptActionCount;
632
+ if (totalActionCount > 1) {
633
+ fail(
634
+ "Use exactly one of '--model <value>', '--clear-model', '--prompt-file <path>', or '--clear-prompt' with '--agent'."
635
+ );
636
+ }
637
+ if (totalActionCount > 0 && !doctorOptions.agent) {
638
+ fail("'doctor' prompt/model override commands require '--agent <name>'.");
639
+ }
640
+ if (doctorOptions.agent && totalActionCount === 0) {
641
+ fail(
642
+ "'doctor --agent <name>' requires one of '--model <value>', '--clear-model', '--prompt-file <path>', or '--clear-prompt'."
643
+ );
644
+ }
645
+ }
646
+ if (command === "hub-export" && !transferOptions.outputRoot) {
647
+ fail("'hub-export' requires '--output <path>'.");
648
+ }
649
+ if (command === "hub-import" && !transferOptions.sourceRoot) {
650
+ fail("'hub-import' requires '--source <path>'.");
651
+ }
652
+ if (command === "upgrade") {
653
+ doctorOptions.dryRun = false;
654
+ if (upgradeOptions.force) {
655
+ upgradeOptions.dryRun = false;
656
+ }
657
+ }
658
+ return {
659
+ command,
660
+ runtimeSelection,
661
+ composeSelection: composeSelection2,
662
+ listTarget,
663
+ pluginSubcommand,
664
+ workspace,
665
+ configRoot,
666
+ assembleOnly,
667
+ opencodeArgs,
668
+ bootstrapOptions,
669
+ profileCreateOptions,
670
+ upgradeOptions,
671
+ transferOptions,
672
+ doctorOptions,
673
+ promotePackageId,
674
+ startIntent,
675
+ hrIntent
676
+ };
677
+ };
678
+ const printTransferReport = (action, report) => {
679
+ process.stdout.write(`${action} complete
680
+ `);
681
+ process.stdout.write(`- source: ${report.sourceRoot}
682
+ `);
683
+ if (report.sourceKind) {
684
+ process.stdout.write(`- source kind: ${report.sourceKind}
685
+ `);
686
+ }
687
+ process.stdout.write(`- target: ${report.targetRoot}
688
+ `);
689
+ process.stdout.write(`- copied: ${report.copied.length}
690
+ `);
691
+ process.stdout.write(`- skipped: ${report.skipped.length}
692
+ `);
693
+ process.stdout.write(`- overwritten: ${report.overwritten.length}
694
+ `);
695
+ process.stdout.write(`- settings: ${report.settingsAction}
696
+ `);
697
+ if (report.warnings.length > 0) {
698
+ process.stdout.write(`Warnings:
699
+ `);
700
+ for (const warning of report.warnings) {
701
+ process.stdout.write(`- ${warning}
702
+ `);
703
+ }
704
+ }
705
+ };
706
+ const printRuntimeBanner = (label, root) => {
707
+ process.stdout.write(`[agenthub] Environment: ${label}
708
+ `);
709
+ process.stdout.write(`[agenthub] Home: ${root}
710
+ `);
711
+ };
712
+ const listPromotablePackageIds = async (hrRoot = defaultHrHome()) => {
713
+ try {
714
+ const entries = await readdir(path.join(hrRoot, "staging"), { withFileTypes: true });
715
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
716
+ } catch {
717
+ return [];
718
+ }
719
+ };
720
+ const readPromoteHandoff = async (sourceRoot) => {
721
+ try {
722
+ return JSON.parse(
723
+ await readFile(path.join(path.dirname(sourceRoot), "handoff.json"), "utf-8")
724
+ );
725
+ } catch {
726
+ return null;
727
+ }
728
+ };
729
+ const resolvePromoteDefaultProfile = (handoff) => {
730
+ if (!handoff?.promotion_preferences?.set_default_profile) return void 0;
731
+ const targetProfile = handoff.target_profile?.trim();
732
+ if (targetProfile) return targetProfile;
733
+ const proposedProfile = handoff.proposed_profile?.trim();
734
+ return proposedProfile || void 0;
735
+ };
736
+ const resolvePromoteSourceRoot = async (packageId, hrRoot = defaultHrHome()) => {
737
+ if (packageId) {
738
+ return path.join(hrRoot, "staging", packageId, "agenthub-home");
739
+ }
740
+ const packageIds = await listPromotablePackageIds(hrRoot);
741
+ if (packageIds.length === 0) {
742
+ fail(`No staged HR packages found in ${path.join(hrRoot, "staging")}.`);
743
+ }
744
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
745
+ fail(`'promote' requires a package id in non-interactive mode. Available: ${packageIds.join(", ")}`);
746
+ }
747
+ const rl = createPromptInterface();
748
+ try {
749
+ process.stdout.write(`Available HR packages: ${packageIds.join(", ")}
750
+ `);
751
+ const selected = await promptRequired(rl, "Package to promote", packageIds[0]);
752
+ if (!packageIds.includes(selected)) {
753
+ fail(`Unknown staged HR package '${selected}'. Available: ${packageIds.join(", ")}`);
754
+ }
755
+ return path.join(hrRoot, "staging", selected, "agenthub-home");
756
+ } finally {
757
+ rl.close();
758
+ }
759
+ };
760
+ const validatePromoteSourceRoot = async (sourceRoot, hrRoot = defaultHrHome()) => {
761
+ const resolvedSource = path.resolve(sourceRoot);
762
+ const stagingRoot = path.resolve(path.join(hrRoot, "staging"));
763
+ if (!resolvedSource.startsWith(`${stagingRoot}${path.sep}`)) {
764
+ fail(`Promote source must be inside ${stagingRoot}`);
765
+ }
766
+ const finalChecklistPath = path.join(path.dirname(resolvedSource), "final-checklist.md");
767
+ let finalChecklist;
768
+ try {
769
+ finalChecklist = await readFile(finalChecklistPath, "utf-8");
770
+ } catch {
771
+ finalChecklist = void 0;
772
+ }
773
+ if (!finalChecklist || !finalChecklist.includes("READY FOR HUMAN CONFIRMATION")) {
774
+ process.stderr.write(
775
+ `[agenthub] Warning: ${finalChecklistPath} is missing or not marked READY FOR HUMAN CONFIRMATION. Continuing with manual promote.
776
+ `
777
+ );
778
+ }
779
+ const handoffPath = path.join(path.dirname(resolvedSource), "handoff.json");
780
+ let handoff = null;
781
+ try {
782
+ handoff = JSON.parse(await readFile(handoffPath, "utf-8"));
783
+ } catch {
784
+ process.stderr.write(`[agenthub] Warning: ${handoffPath} is missing.
785
+ `);
786
+ return;
787
+ }
788
+ const instructions = handoff.operator_instructions;
789
+ if (!instructions?.test_current_workspace) {
790
+ process.stderr.write(
791
+ `[agenthub] Warning: ${handoffPath} is missing operator_instructions.test_current_workspace. Operators may not realize staged profiles can be tested before promote.
792
+ `
793
+ );
794
+ }
795
+ if (!instructions?.use_in_another_workspace) {
796
+ process.stderr.write(
797
+ `[agenthub] Warning: ${handoffPath} is missing operator_instructions.use_in_another_workspace. Operators may not realize staged profiles can be used in another workspace before promote.
798
+ `
799
+ );
800
+ }
801
+ if (!instructions?.promote) {
802
+ process.stderr.write(
803
+ `[agenthub] Warning: ${handoffPath} is missing operator_instructions.promote.
804
+ `
805
+ );
806
+ }
807
+ if (!instructions?.advanced_import) {
808
+ process.stderr.write(
809
+ `[agenthub] Warning: ${handoffPath} is missing operator_instructions.advanced_import.
810
+ `
811
+ );
812
+ }
813
+ const hostRequirements = handoff.host_requirements;
814
+ if (hostRequirements?.mcp_servers_bundled === false) {
815
+ const missing = hostRequirements.missing?.join(", ") || "MCP server artifacts";
816
+ fail(
817
+ `Staged package cannot be promoted yet because required MCP server artifacts are missing: ${missing}. Re-stage the package with bundled MCP servers first.`
818
+ );
819
+ }
820
+ };
821
+ const ensureHomeReadyOrFail = async (targetRoot = defaultAgentHubHome()) => {
822
+ if (await agentHubHomeInitialized(targetRoot)) return;
823
+ fail(`Agent Hub home is not initialized. Run:
824
+ ${cliCommand} setup`);
825
+ };
826
+ const ensureSelectedHomeReadyOrFail = async (parsed2) => {
827
+ const targetRoot = resolveSelectedHomeRoot(parsed2);
828
+ if (parsed2.command === "hr") {
829
+ if (await hrHomeInitialized(targetRoot || defaultHrHome())) return;
830
+ fail(`HR Office is not initialized. Run:
831
+ ${cliCommand} hr`);
832
+ }
833
+ await ensureHomeReadyOrFail(targetRoot);
834
+ };
835
+ const ensureHomeReadyOrBootstrap = async (targetRoot = defaultAgentHubHome()) => {
836
+ if (await agentHubHomeInitialized(targetRoot)) return;
837
+ await installAgentHubHome({ targetRoot, mode: "auto" });
838
+ process.stdout.write(`\u2713 First run \u2014 initialised coding system at ${targetRoot}
839
+ `);
840
+ };
841
+ const promptHrBootstrapModelSelection = async (hrRoot) => {
842
+ const rl = createPromptInterface();
843
+ try {
844
+ process.stdout.write("\nFirst-time HR Office setup\n");
845
+ process.stdout.write("Choose an HR model preset:\n");
846
+ process.stdout.write(
847
+ ` recommended Use ${recommendedHrBootstrapModel} (${recommendedHrBootstrapVariant})
848
+ `
849
+ );
850
+ process.stdout.write(
851
+ " free Pick from current opencode free models (quality may drop)\n"
852
+ );
853
+ process.stdout.write(" custom Enter any model id yourself\n\n");
854
+ const subagentStrategy = await promptChoice(
855
+ rl,
856
+ "HR model preset",
857
+ ["recommended", "free", "custom"],
858
+ "recommended"
859
+ );
860
+ let sharedSubagentModel;
861
+ let consoleModel;
862
+ if (subagentStrategy === "recommended") {
863
+ consoleModel = recommendedHrBootstrapModel;
864
+ sharedSubagentModel = recommendedHrBootstrapModel;
865
+ }
866
+ if (subagentStrategy === "free") {
867
+ const freeModels = await listOpencodeFreeModels();
868
+ const fallbackFreeModel = freeModels.includes("opencode/minimax-m2.5-free") ? "opencode/minimax-m2.5-free" : freeModels[0] || "opencode/minimax-m2.5-free";
869
+ process.stdout.write("Current opencode free models:\n");
870
+ sharedSubagentModel = await promptIndexedChoice(
871
+ rl,
872
+ "Choose a free model for HR",
873
+ freeModels.length > 0 ? freeModels : [fallbackFreeModel],
874
+ fallbackFreeModel
875
+ );
876
+ consoleModel = sharedSubagentModel;
877
+ } else if (subagentStrategy === "custom") {
878
+ sharedSubagentModel = await promptRequired(
879
+ rl,
880
+ "Custom HR model",
881
+ recommendedHrBootstrapModel
882
+ );
883
+ consoleModel = sharedSubagentModel;
884
+ }
885
+ process.stdout.write(`
886
+ [agenthub] HR settings will be written to ${path.join(hrRoot, "settings.json")}
887
+ `);
888
+ return {
889
+ consoleModel,
890
+ subagentStrategy,
891
+ sharedSubagentModel
892
+ };
893
+ } finally {
894
+ rl.close();
895
+ }
896
+ };
897
+ const printHrModelOverrideHint = (targetRoot) => {
898
+ process.stdout.write(
899
+ [
900
+ `[agenthub] HR model settings: ${path.join(targetRoot, "settings.json")}`,
901
+ `[agenthub] Change later with: ${cliCommand} doctor --target-root ${targetRoot} --agent ${hrPrimaryAgentName} --model <model>`,
902
+ `[agenthub] HR subagents: ${hrSubagentNames.join(", ")} (use the same doctor command with --agent <name>)`
903
+ ].join("\n") + "\n"
904
+ );
905
+ };
906
+ const countConfiguredHrGithubSources = async (targetRoot) => {
907
+ try {
908
+ const raw = JSON.parse(
909
+ await readFile(path.join(targetRoot, "hr-config.json"), "utf-8")
910
+ );
911
+ const githubSources = [];
912
+ if (Array.isArray(raw.sources)) {
913
+ githubSources.push(...raw.sources);
914
+ } else if (raw.sources && typeof raw.sources === "object") {
915
+ const nestedGithub = raw.sources.github;
916
+ if (Array.isArray(nestedGithub)) githubSources.push(...nestedGithub);
917
+ }
918
+ if (Array.isArray(raw.github)) githubSources.push(...raw.github);
919
+ return githubSources.length;
920
+ } catch {
921
+ return null;
922
+ }
923
+ };
924
+ const countConfiguredHrModelCatalogSources = async (targetRoot) => {
925
+ try {
926
+ const raw = JSON.parse(
927
+ await readFile(path.join(targetRoot, "hr-config.json"), "utf-8")
928
+ );
929
+ const modelSources = [];
930
+ if (raw.sources && typeof raw.sources === "object") {
931
+ const nestedModels = raw.sources.models;
932
+ if (Array.isArray(nestedModels)) modelSources.push(...nestedModels);
933
+ }
934
+ if (Array.isArray(raw.models)) modelSources.push(...raw.models);
935
+ return modelSources.length;
936
+ } catch {
937
+ return null;
938
+ }
939
+ };
940
+ const syncHrSourceInventoryOnFirstRun = async (targetRoot) => {
941
+ const configuredSourceCount = await countConfiguredHrGithubSources(targetRoot);
942
+ const configuredModelSourceCount = await countConfiguredHrModelCatalogSources(targetRoot);
943
+ const sourceParts = [];
944
+ if (configuredSourceCount && configuredSourceCount > 0) {
945
+ sourceParts.push(
946
+ `${configuredSourceCount} GitHub repo${configuredSourceCount === 1 ? "" : "s"}`
947
+ );
948
+ }
949
+ if (configuredModelSourceCount && configuredModelSourceCount > 0) {
950
+ sourceParts.push(
951
+ `${configuredModelSourceCount} model catalog${configuredModelSourceCount === 1 ? "" : "s"}`
952
+ );
953
+ }
954
+ const sourceLabel = sourceParts.length > 0 ? sourceParts.join(" + ") : "configured HR sources";
955
+ process.stdout.write(`[agenthub] First run \u2014 syncing HR inventory from ${sourceLabel}...
956
+ `);
957
+ try {
958
+ const pythonCommand = resolvePythonCommand();
959
+ const scriptPath = path.join(targetRoot, "bin", "sync_sources.py");
960
+ const child = spawn(pythonCommand, [scriptPath], {
961
+ cwd: targetRoot,
962
+ env: {
963
+ ...process.env,
964
+ OPENCODE_AGENTHUB_HR_HOME: targetRoot
965
+ },
966
+ stdio: ["ignore", "pipe", "pipe"],
967
+ ...spawnOptions()
968
+ });
969
+ let stdout = "";
970
+ let stderr = "";
971
+ child.stdout.on("data", (chunk) => {
972
+ stdout += chunk.toString();
973
+ });
974
+ child.stderr.on("data", (chunk) => {
975
+ stderr += chunk.toString();
976
+ });
977
+ const code = await new Promise((resolve, reject) => {
978
+ child.on("error", reject);
979
+ child.on("close", (exitCode) => resolve(exitCode ?? 1));
980
+ });
981
+ const summary = stdout.trim();
982
+ if (code === 0) {
983
+ if (summary) process.stdout.write(`${summary}
984
+ `);
985
+ process.stdout.write(
986
+ `[agenthub] HR source status: ${path.join(targetRoot, "source-status.json")}
987
+ `
988
+ );
989
+ return;
990
+ }
991
+ process.stderr.write(
992
+ `[agenthub] Warning: first-run HR source sync did not complete. Continue using HR and retry later with '${pythonCommand} ${scriptPath}'.
993
+ `
994
+ );
995
+ if (stderr.trim()) process.stderr.write(`${stderr.trim()}
996
+ `);
997
+ } catch (error) {
998
+ const reason = error instanceof Error ? error.message : String(error);
999
+ const pythonCommand = resolvePythonCommand();
1000
+ process.stderr.write(
1001
+ `[agenthub] Warning: failed to launch first-run HR source sync (${reason}). Retry later with '${pythonCommand} ${path.join(targetRoot, "bin", "sync_sources.py")}'.
1002
+ `
1003
+ );
1004
+ }
1005
+ };
1006
+ const ensureHrOfficeReadyOrBootstrap = async (targetRoot = defaultHrHome(), options = {}) => {
1007
+ if (await hrHomeInitialized(targetRoot)) return;
1008
+ const shouldPrompt = process.stdin.isTTY && process.stdout.isTTY;
1009
+ const hrModelSelection = shouldPrompt ? await promptHrBootstrapModelSelection(targetRoot) : void 0;
1010
+ await installHrOfficeHomeWithOptions({
1011
+ hrRoot: targetRoot,
1012
+ hrModelSelection
1013
+ });
1014
+ process.stdout.write(`\u2713 First run \u2014 initialised HR Office at ${targetRoot}
1015
+ `);
1016
+ printHrModelOverrideHint(targetRoot);
1017
+ if (options.syncSourcesOnFirstRun ?? true) {
1018
+ await syncHrSourceInventoryOnFirstRun(targetRoot);
1019
+ }
1020
+ };
1021
+ const isHrRuntimeSelection = (selection) => selection?.kind === "profile" && selection.profile === "hr";
1022
+ const normalizeCsv = (value) => value.split(",").map((item) => item.trim()).filter(Boolean);
1023
+ const uniqueValues = (values) => [...new Set(values)];
1024
+ const normalizeOptional = (value) => {
1025
+ const trimmed = value.trim();
1026
+ return trimmed || void 0;
1027
+ };
1028
+ const toJsonFile = (root, directory, name) => path.join(root, directory, `${name}.json`);
1029
+ const listNamesByExt = async (dirPath, ext) => {
1030
+ try {
1031
+ const entries = await readdir(dirPath, { withFileTypes: true });
1032
+ return entries.filter((entry) => entry.isFile() && entry.name.endsWith(ext)).map((entry) => entry.name.slice(0, -ext.length)).sort();
1033
+ } catch {
1034
+ return [];
1035
+ }
1036
+ };
1037
+ const writeJsonFile = async (filePath, payload) => {
1038
+ await mkdir(path.dirname(filePath), { recursive: true });
1039
+ await writeFile(filePath, `${JSON.stringify(payload, null, 2)}
1040
+ `, "utf-8");
1041
+ };
1042
+ const workspacePreferencesPath = (workspace) => path.join(workspace, ".opencode-agenthub.user.json");
1043
+ const readJsonIfExists = async (filePath) => {
1044
+ try {
1045
+ const content = await readFile(filePath, "utf-8");
1046
+ const normalized = content.split("\n").filter((line) => !line.trim().startsWith("//")).join("\n");
1047
+ return JSON.parse(normalized);
1048
+ } catch (e) {
1049
+ const code = e.code;
1050
+ if (code === "ENOENT" || code === "EISDIR") return void 0;
1051
+ throw e;
1052
+ }
1053
+ };
1054
+ const loadWorkspacePreferences = async (workspace) => {
1055
+ const raw = await readJsonIfExists(
1056
+ workspacePreferencesPath(workspace)
1057
+ ) || {};
1058
+ return {
1059
+ _version: 1,
1060
+ ...raw
1061
+ };
1062
+ };
1063
+ const saveWorkspacePreferences = async (workspace, preferences) => {
1064
+ await writeJsonFile(workspacePreferencesPath(workspace), {
1065
+ _version: 1,
1066
+ ...preferences
1067
+ });
1068
+ };
1069
+ const updateWorkspacePreferences = async (workspace, updater) => {
1070
+ const current = await loadWorkspacePreferences(workspace);
1071
+ await saveWorkspacePreferences(workspace, updater(current));
1072
+ };
1073
+ const readStartDefaultProfile = async (targetRoot = defaultAgentHubHome()) => {
1074
+ const settings = await readAgentHubSettings(targetRoot);
1075
+ return settings?.preferences?.defaultProfile?.trim() || void 0;
1076
+ };
1077
+ const setStartDefaultProfile = async (profile, targetRoot = defaultAgentHubHome()) => {
1078
+ const existingSettings = await readAgentHubSettings(targetRoot) || {};
1079
+ const mergedSettings = mergeAgentHubSettingsDefaults(existingSettings);
1080
+ await writeAgentHubSettings(targetRoot, {
1081
+ ...mergedSettings,
1082
+ preferences: {
1083
+ ...mergedSettings.preferences || {},
1084
+ defaultProfile: profile
1085
+ }
1086
+ });
1087
+ };
1088
+ const resolveStartProfilePreference = async (workspace, targetRoot = defaultAgentHubHome()) => {
1089
+ const defaultProfile = await readStartDefaultProfile(targetRoot);
1090
+ if (defaultProfile) {
1091
+ return { profile: defaultProfile, source: "default" };
1092
+ }
1093
+ const preferences = await loadWorkspacePreferences(workspace);
1094
+ const lastProfile = preferences.start?.lastProfile?.trim();
1095
+ if (lastProfile) {
1096
+ return { profile: lastProfile, source: "last" };
1097
+ }
1098
+ return { profile: "auto", source: "fallback" };
1099
+ };
1100
+ const resolveHrLastProfilePreference = async (workspace) => {
1101
+ const preferences = await loadWorkspacePreferences(workspace);
1102
+ return preferences.hr?.lastProfile?.trim() || void 0;
1103
+ };
1104
+ const resolveStartLastProfilePreference = async (workspace) => {
1105
+ const preferences = await loadWorkspacePreferences(workspace);
1106
+ const lastProfile = preferences.start?.lastProfile?.trim();
1107
+ if (lastProfile) {
1108
+ return { profile: lastProfile, source: "last" };
1109
+ }
1110
+ return { profile: "auto", source: "fallback" };
1111
+ };
1112
+ const noteProfileResolution = (command, source, profile) => {
1113
+ if (source === "explicit") return;
1114
+ if (command === "start" && source === "default") {
1115
+ process.stderr.write(`[agenthub] start -> using personal default profile '${profile}'.
1116
+ `);
1117
+ return;
1118
+ }
1119
+ if (source === "last") {
1120
+ process.stderr.write(`[agenthub] ${command} -> using last profile '${profile}'.
1121
+ `);
1122
+ return;
1123
+ }
1124
+ if (command === "start" && source === "fallback") {
1125
+ process.stderr.write(`[agenthub] start -> no default or previous profile found; using 'auto'.
1126
+ `);
1127
+ }
1128
+ };
1129
+ const warnIfWorkspaceRuntimeWillBeReplaced = async (workspace, label) => {
1130
+ const lockPath = path.join(getWorkspaceRuntimeRoot(workspace), "agenthub-lock.json");
1131
+ if (!await readJsonIfExists(lockPath)) return;
1132
+ process.stderr.write(
1133
+ `[agenthub] Replacing the current workspace runtime with ${label}. Plain 'opencode' in this folder will use the new runtime after compose.
1134
+ `
1135
+ );
1136
+ };
1137
+ const toWorkspaceEnvrc = (workspace, configRoot) => {
1138
+ const resolvedConfigRoot = path.resolve(configRoot);
1139
+ const relativeConfigRoot = path.relative(workspace, resolvedConfigRoot);
1140
+ const configRootRef = relativeConfigRoot && !relativeConfigRoot.startsWith("..") ? `$PWD/${relativeConfigRoot}` : resolvedConfigRoot;
1141
+ return `# Generated by opencode-agenthub. Remove this file to disable auto-activation.
1142
+ export XDG_CONFIG_HOME="${configRootRef}/xdg"
1143
+ export OPENCODE_DISABLE_PROJECT_CONFIG=true
1144
+ export OPENCODE_CONFIG_DIR="${configRootRef}"
1145
+ `;
1146
+ };
1147
+ const maybeConfigureEnvrc = async (workspace, configRoot) => {
1148
+ if (!shouldOfferEnvrc()) return;
1149
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return;
1150
+ const preferences = await loadWorkspacePreferences(workspace);
1151
+ const envrcPath = path.join(workspace, ".envrc");
1152
+ const envrcExists = await stat(envrcPath).then((s) => s.isFile() || s.isFIFO()).catch((e) => e.code === "ENOENT" ? false : Promise.reject(e));
1153
+ if (envrcExists) {
1154
+ if (!preferences.envrc?.enabled || !preferences.envrc?.prompted) {
1155
+ await saveWorkspacePreferences(workspace, {
1156
+ ...preferences,
1157
+ envrc: { prompted: true, enabled: true }
1158
+ });
1159
+ }
1160
+ return;
1161
+ }
1162
+ if (preferences.envrc?.prompted) return;
1163
+ const rl = createPromptInterface();
1164
+ try {
1165
+ const enableEnvrc = await promptBoolean(
1166
+ rl,
1167
+ "Enable Agent Hub auto-activation with .envrc so plain 'opencode' works here?",
1168
+ false
1169
+ );
1170
+ if (enableEnvrc) {
1171
+ await writeFile(
1172
+ envrcPath,
1173
+ toWorkspaceEnvrc(workspace, configRoot),
1174
+ "utf-8"
1175
+ );
1176
+ process.stdout.write(
1177
+ `Wrote ${envrcPath}. Run 'direnv allow' in this workspace to enable plain 'opencode'.
1178
+ `
1179
+ );
1180
+ }
1181
+ await saveWorkspacePreferences(workspace, {
1182
+ ...preferences,
1183
+ envrc: { prompted: true, enabled: enableEnvrc }
1184
+ });
1185
+ } finally {
1186
+ rl.close();
1187
+ }
1188
+ };
1189
+ const createPromptInterface = () => readline.createInterface({ input: process.stdin, output: process.stdout });
1190
+ const promptRequired = async (rl, question, defaultValue) => {
1191
+ while (true) {
1192
+ const answer = await rl.question(
1193
+ defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `
1194
+ );
1195
+ const value = normalizeOptional(answer) || defaultValue;
1196
+ if (value) return value;
1197
+ process.stdout.write("This field is required.\n");
1198
+ }
1199
+ };
1200
+ const promptOptional = async (rl, question, defaultValue) => {
1201
+ const answer = await rl.question(
1202
+ defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `
1203
+ );
1204
+ return normalizeOptional(answer) || defaultValue;
1205
+ };
1206
+ const promptCsv = async (rl, question, defaultValues = []) => {
1207
+ const defaultValue = defaultValues.join(", ");
1208
+ const answer = await rl.question(
1209
+ defaultValue ? `${question} [${defaultValue}]: ` : `${question}: `
1210
+ );
1211
+ return normalizeCsv(answer || defaultValue);
1212
+ };
1213
+ const promptBoolean = async (rl, question, defaultValue) => {
1214
+ const suffix = defaultValue ? "[Y/n]" : "[y/N]";
1215
+ while (true) {
1216
+ const answer = (await rl.question(`${question} ${suffix}: `)).trim().toLowerCase();
1217
+ if (!answer) return defaultValue;
1218
+ if (answer === "y" || answer === "yes") return true;
1219
+ if (answer === "n" || answer === "no") return false;
1220
+ process.stdout.write("Please answer y or n.\n");
1221
+ }
1222
+ };
1223
+ const promptChoice = async (rl, question, choices, defaultValue) => {
1224
+ const label = `${question} [${choices.join("/")}] (${defaultValue})`;
1225
+ while (true) {
1226
+ const answer = (await rl.question(`${label}: `)).trim().toLowerCase();
1227
+ if (!answer) return defaultValue;
1228
+ const match = choices.find((choice) => choice === answer);
1229
+ if (match) return match;
1230
+ process.stdout.write(`Choose one of: ${choices.join(", ")}
1231
+ `);
1232
+ }
1233
+ };
1234
+ const promptIndexedChoice = async (rl, question, choices, defaultValue) => {
1235
+ choices.forEach((choice, index) => {
1236
+ process.stdout.write(` ${index + 1}. ${choice}
1237
+ `);
1238
+ });
1239
+ const defaultIndex = Math.max(choices.indexOf(defaultValue), 0) + 1;
1240
+ while (true) {
1241
+ const answer = (await rl.question(`${question} [1-${choices.length}] (${defaultIndex}): `)).trim().toLowerCase();
1242
+ if (!answer) return defaultValue;
1243
+ const numeric = Number(answer);
1244
+ if (Number.isInteger(numeric) && numeric >= 1 && numeric <= choices.length) {
1245
+ return choices[numeric - 1] || defaultValue;
1246
+ }
1247
+ const exactMatch = choices.find((choice) => choice.toLowerCase() === answer);
1248
+ if (exactMatch) return exactMatch;
1249
+ process.stdout.write("Choose a listed number or exact model id.\n");
1250
+ }
1251
+ };
1252
+ const listOpencodeFreeModels = async () => new Promise((resolve) => {
1253
+ const child = spawn("opencode", ["models", "opencode"], {
1254
+ stdio: ["ignore", "pipe", "ignore"],
1255
+ ...spawnOptions()
1256
+ });
1257
+ let stdout = "";
1258
+ child.stdout.on("data", (chunk) => {
1259
+ stdout += chunk.toString();
1260
+ });
1261
+ child.on("error", () => resolve([]));
1262
+ child.on("close", () => {
1263
+ const models = stdout.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("opencode/") && line.includes("free"));
1264
+ resolve([...new Set(models)].sort());
1265
+ });
1266
+ });
1267
+ const promptOptionalCsvSelection = async (rl, question, available, defaultValues = []) => {
1268
+ const include = await promptBoolean(rl, question, false);
1269
+ if (!include) return [];
1270
+ if (available.length > 0) {
1271
+ process.stdout.write(`Available: ${available.join(", ")}
1272
+ `);
1273
+ }
1274
+ return promptCsv(rl, "Enter names (comma-separated)", defaultValues);
1275
+ };
1276
+ const listSkillNames = async (skillsDir) => {
1277
+ try {
1278
+ const entries = await readdir(skillsDir, { withFileTypes: true });
1279
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
1280
+ } catch {
1281
+ return [];
1282
+ }
1283
+ };
1284
+ const assertNameNotReserved = async (kind, name, reservedOk) => {
1285
+ if (reservedOk) return;
1286
+ const builtIns = await listBuiltInAssetNames(kind);
1287
+ if (!builtIns.has(name)) return;
1288
+ fail(
1289
+ `'${name}' is a reserved built-in ${kind} name. Use a different name or pass '--reserved-ok' to override.`
1290
+ );
1291
+ };
1292
+ const detectSetupModeForHome = async (targetRoot) => {
1293
+ const settings = await readAgentHubSettings(targetRoot);
1294
+ const mode = settings?.meta?.onboarding?.mode;
1295
+ if (mode === "auto" || mode === "minimal") {
1296
+ return mode;
1297
+ }
1298
+ const legacyStarter = settings?.meta?.onboarding?.starter;
1299
+ if (legacyStarter === "none" || legacyStarter === "framework") {
1300
+ return "minimal";
1301
+ }
1302
+ return "auto";
1303
+ };
1304
+ const detectInstallModeForHome = async (targetRoot, isHrHome = false) => {
1305
+ if (isHrHome) return "hr-office";
1306
+ return detectSetupModeForHome(targetRoot);
1307
+ };
1308
+ const warnIfBuiltInsDrifted = async (targetRoot, mode) => {
1309
+ const settings = await readAgentHubSettings(targetRoot);
1310
+ const builtinVersion = settings?.meta?.builtinVersion;
1311
+ if (!builtinVersion || Object.keys(builtinVersion).length === 0) return;
1312
+ const effectiveMode = mode ?? await detectInstallModeForHome(targetRoot);
1313
+ const allowedKeys = new Set(getBuiltInManifestKeysForMode(effectiveMode));
1314
+ const currentVersion = readPackageVersion();
1315
+ const staleAssets = Object.entries(builtinVersion).filter(([asset]) => allowedKeys.has(asset)).filter(([, installedVersion]) => installedVersion !== currentVersion).map(([asset]) => asset).sort();
1316
+ if (staleAssets.length === 0) return;
1317
+ process.stderr.write(
1318
+ [
1319
+ `[agenthub] Built-in assets may be stale (${staleAssets.length} item${staleAssets.length === 1 ? "" : "s"}) relative to package ${currentVersion}.`,
1320
+ `Run '${cliCommand} upgrade${targetRoot !== defaultAgentHubHome() ? ` --target-root ${targetRoot}` : ""}' to preview or sync updates.`
1321
+ ].join("\n") + "\n"
1322
+ );
1323
+ };
1324
+ const hrLegacyAssetNames = [
1325
+ "hr",
1326
+ "hr-sourcer",
1327
+ "hr-evaluator",
1328
+ "hr-cto",
1329
+ "hr-adapter",
1330
+ "hr-verifier"
1331
+ ];
1332
+ const warnAboutLegacyHrAssets = async (personalRoot = defaultAgentHubHome()) => {
1333
+ const hrBundleDir = path.join(personalRoot, "bundles");
1334
+ const found = await Promise.all(
1335
+ hrLegacyAssetNames.map(async (name) => ({
1336
+ name,
1337
+ exists: await readFile(path.join(hrBundleDir, `${name}.json`), "utf-8").then(() => true).catch(() => false)
1338
+ }))
1339
+ );
1340
+ const present = found.filter((entry) => entry.exists).map((entry) => entry.name);
1341
+ if (present.length === 0) return;
1342
+ process.stderr.write(
1343
+ [
1344
+ `[agenthub] Notice: legacy HR assets were found in your personal home (${present.join(", ")}).`,
1345
+ `[agenthub] They are left in place for compatibility. Use '${cliCommand} hr' for new isolated HR work.`
1346
+ ].join("\n") + "\n"
1347
+ );
1348
+ };
1349
+ const profileExistsInHome = async (targetRoot, profile) => {
1350
+ return Boolean(
1351
+ await readJsonIfExists(
1352
+ path.join(targetRoot, "profiles", `${profile}.json`)
1353
+ )
1354
+ );
1355
+ };
1356
+ const listStagedHrProfileMatches = async (profile, hrRoot = defaultHrHome()) => {
1357
+ try {
1358
+ const stagingRoot = path.join(hrRoot, "staging");
1359
+ const entries = await readdir(stagingRoot, { withFileTypes: true });
1360
+ const matches = await Promise.all(
1361
+ entries.filter((entry) => entry.isDirectory()).map(async (entry) => {
1362
+ const libraryRoot = path.join(stagingRoot, entry.name, "agenthub-home");
1363
+ const profilePath = path.join(libraryRoot, "profiles", `${profile}.json`);
1364
+ const profileStat = await stat(profilePath).catch(() => null);
1365
+ if (!profileStat?.isFile()) return null;
1366
+ return {
1367
+ packageId: entry.name,
1368
+ profile,
1369
+ libraryRoot,
1370
+ profilePath,
1371
+ modifiedAtMs: profileStat.mtimeMs
1372
+ };
1373
+ })
1374
+ );
1375
+ return matches.filter((match) => Boolean(match)).sort((a, b) => b.modifiedAtMs - a.modifiedAtMs || a.packageId.localeCompare(b.packageId));
1376
+ } catch {
1377
+ return [];
1378
+ }
1379
+ };
1380
+ const resolveHrProfileSource = async (profile, hrRoot = defaultHrHome()) => {
1381
+ if (await profileExistsInHome(hrRoot, profile)) {
1382
+ return {
1383
+ libraryRoot: hrRoot,
1384
+ settingsRoot: hrRoot,
1385
+ kind: "home"
1386
+ };
1387
+ }
1388
+ const stagedMatches = await listStagedHrProfileMatches(profile, hrRoot);
1389
+ if (stagedMatches.length > 0) {
1390
+ return {
1391
+ libraryRoot: stagedMatches[0].libraryRoot,
1392
+ settingsRoot: hrRoot,
1393
+ kind: "staged",
1394
+ match: stagedMatches[0]
1395
+ };
1396
+ }
1397
+ return {
1398
+ libraryRoot: hrRoot,
1399
+ settingsRoot: hrRoot,
1400
+ kind: "home"
1401
+ };
1402
+ };
1403
+ const printHrStagedProfileResolution = async (profile, match, hrRoot = defaultHrHome()) => {
1404
+ const stagedMatches = await listStagedHrProfileMatches(profile, hrRoot);
1405
+ process.stderr.write(
1406
+ `[agenthub] Staging test -> using profile '${profile}' from staged package '${match.packageId}'.
1407
+ `
1408
+ );
1409
+ process.stderr.write(`[agenthub] Source: ${match.libraryRoot}
1410
+ `);
1411
+ if (stagedMatches.length > 1) {
1412
+ const alternates = stagedMatches.slice(1).map((item) => item.packageId).join(", ");
1413
+ process.stderr.write(
1414
+ `[agenthub] Warning: profile '${profile}' also exists in other staged packages: ${alternates}. Using the most recently updated match.
1415
+ `
1416
+ );
1417
+ }
1418
+ };
1419
+ const printUpgradeReport = (targetRoot, report, options) => {
1420
+ const verb = options.dryRun ? "Would" : "Did";
1421
+ process.stdout.write(`Built-in asset sync ${options.dryRun ? "preview" : "complete"}
1422
+ `);
1423
+ process.stdout.write(`- target: ${targetRoot}
1424
+ `);
1425
+ process.stdout.write(`- mode: ${options.dryRun ? "dry-run" : options.force ? "force" : "safe"}
1426
+ `);
1427
+ process.stdout.write(`- add: ${report.added.length}
1428
+ `);
1429
+ process.stdout.write(`- update: ${report.updated.length}
1430
+ `);
1431
+ process.stdout.write(`- skip: ${report.skipped.length}
1432
+ `);
1433
+ for (const [label, entries] of [
1434
+ [`${verb} add`, report.added],
1435
+ [`${verb} update`, report.updated],
1436
+ [`${verb} skip`, report.skipped]
1437
+ ]) {
1438
+ if (entries.length === 0) continue;
1439
+ process.stdout.write(`${label}:
1440
+ `);
1441
+ for (const entry of entries) {
1442
+ process.stdout.write(`- ${entry}
1443
+ `);
1444
+ }
1445
+ }
1446
+ if (options.dryRun) {
1447
+ process.stdout.write("Run again with '--force' to overwrite managed built-in files.\n");
1448
+ } else if (!options.force) {
1449
+ process.stdout.write("Existing managed files were left in place; re-run with '--force' to overwrite them.\n");
1450
+ }
1451
+ };
1452
+ const createSoulDefinition = async (root, name, reservedOk = false) => {
1453
+ await assertNameNotReserved("soul", name, reservedOk);
1454
+ const filePath = path.join(root, "souls", `${name}.md`);
1455
+ const rl = createPromptInterface();
1456
+ try {
1457
+ await maybeOverwrite(rl, filePath);
1458
+ const content = [
1459
+ `# ${name}`,
1460
+ "",
1461
+ "## Description",
1462
+ "Describe this soul's purpose and when to use it.",
1463
+ "",
1464
+ "## Behavior",
1465
+ "- Primary goals",
1466
+ "- Constraints",
1467
+ "- Expected output style",
1468
+ ""
1469
+ ].join("\n");
1470
+ await mkdir(path.dirname(filePath), { recursive: true });
1471
+ await writeFile(filePath, content, "utf-8");
1472
+ return filePath;
1473
+ } finally {
1474
+ rl.close();
1475
+ }
1476
+ };
1477
+ const createInstructionDefinition = async (root, name, reservedOk = false) => {
1478
+ await assertNameNotReserved("instruction", name, reservedOk);
1479
+ const filePath = path.join(root, "instructions", `${name}.md`);
1480
+ const rl = createPromptInterface();
1481
+ try {
1482
+ await maybeOverwrite(rl, filePath);
1483
+ const content = [
1484
+ `# ${name}`,
1485
+ "",
1486
+ "## Purpose",
1487
+ "State the instruction this file adds to an agent.",
1488
+ "",
1489
+ "## Rules",
1490
+ "- Add concrete rules here",
1491
+ ""
1492
+ ].join("\n");
1493
+ await mkdir(path.dirname(filePath), { recursive: true });
1494
+ await writeFile(filePath, content, "utf-8");
1495
+ return filePath;
1496
+ } finally {
1497
+ rl.close();
1498
+ }
1499
+ };
1500
+ const createSkillDefinition = async (root, name, reservedOk = false) => {
1501
+ await assertNameNotReserved("skill", name, reservedOk);
1502
+ const filePath = path.join(root, "skills", name, "SKILL.md");
1503
+ const rl = createPromptInterface();
1504
+ try {
1505
+ await maybeOverwrite(rl, filePath);
1506
+ const content = [
1507
+ `# ${name}`,
1508
+ "",
1509
+ "## When to use",
1510
+ "Describe when this skill should be loaded.",
1511
+ "",
1512
+ "## Instructions",
1513
+ "- Add the concrete workflow here",
1514
+ ""
1515
+ ].join("\n");
1516
+ await mkdir(path.dirname(filePath), { recursive: true });
1517
+ await writeFile(filePath, content, "utf-8");
1518
+ return filePath;
1519
+ } finally {
1520
+ rl.close();
1521
+ }
1522
+ };
1523
+ const readProfileDefinition = async (root, name) => readJsonIfExists(path.join(root, "profiles", `${name}.json`));
1524
+ const promptRecord = async (rl, question) => {
1525
+ const answer = await rl.question(
1526
+ `${question} (comma-separated key=value, blank to skip): `
1527
+ );
1528
+ const entries = normalizeCsv(answer);
1529
+ if (entries.length === 0) return void 0;
1530
+ const record = {};
1531
+ for (const entry of entries) {
1532
+ const separator = entry.indexOf("=");
1533
+ if (separator === -1) {
1534
+ fail(`Invalid entry '${entry}'. Use key=value format.`);
1535
+ }
1536
+ const key = entry.slice(0, separator).trim();
1537
+ const value = entry.slice(separator + 1).trim();
1538
+ if (!key || !value) {
1539
+ fail(`Invalid entry '${entry}'. Use key=value format.`);
1540
+ }
1541
+ record[key] = value;
1542
+ }
1543
+ return Object.keys(record).length > 0 ? record : void 0;
1544
+ };
1545
+ const maybeOverwrite = async (rl, filePath) => {
1546
+ try {
1547
+ await readFile(filePath, "utf-8");
1548
+ } catch {
1549
+ return;
1550
+ }
1551
+ const overwrite = await promptBoolean(
1552
+ rl,
1553
+ `${path.basename(filePath)} already exists. Overwrite it?`,
1554
+ false
1555
+ );
1556
+ if (!overwrite) {
1557
+ fail(`Aborted without changing ${filePath}`);
1558
+ }
1559
+ };
1560
+ const createProfileDefinition = async (root, name, options = { addBundles: [], reservedOk: false }) => {
1561
+ await assertNameNotReserved("profile", name, options.reservedOk);
1562
+ const filePath = toJsonFile(root, "profiles", name);
1563
+ const availableBundles = await listNamesByExt(path.join(root, "bundles"), ".json");
1564
+ const addCapabilities = listProfileAddCapabilityNames();
1565
+ const seededProfile = options.fromProfile ? await readProfileDefinition(root, options.fromProfile) : void 0;
1566
+ if (options.fromProfile && !seededProfile) {
1567
+ const availableProfiles = await listNamesByExt(path.join(root, "profiles"), ".json");
1568
+ fail(
1569
+ `Profile '${options.fromProfile}' was not found. Available profiles: ${availableProfiles.join(", ") || "(none)"}`
1570
+ );
1571
+ }
1572
+ const expandedAdds = expandProfileAddSelections(options.addBundles.filter(Boolean));
1573
+ const seededBundles = uniqueValues([
1574
+ ...seededProfile?.bundles || [],
1575
+ ...expandedAdds
1576
+ ]);
1577
+ const invalidBundles = seededBundles.filter((bundle) => !availableBundles.includes(bundle));
1578
+ if (invalidBundles.length > 0) {
1579
+ fail(
1580
+ `Unknown bundle(s): ${invalidBundles.join(", ")}. Available bundles: ${availableBundles.join(", ") || "(none)"}. Capability shorthands: ${addCapabilities.join(", ") || "(none)"}`
1581
+ );
1582
+ }
1583
+ const rl = createPromptInterface();
1584
+ try {
1585
+ await maybeOverwrite(rl, filePath);
1586
+ if (availableBundles.length > 0) {
1587
+ process.stdout.write(
1588
+ `Available bundles: ${availableBundles.join(", ")}
1589
+ `
1590
+ );
1591
+ }
1592
+ if (addCapabilities.length > 0) {
1593
+ process.stdout.write(
1594
+ `Capability shorthands for --add: ${addCapabilities.join(", ")}
1595
+ `
1596
+ );
1597
+ }
1598
+ const description = await promptOptional(rl, "Profile description");
1599
+ const bundles = await promptCsv(
1600
+ rl,
1601
+ "Bundles to include",
1602
+ seededBundles
1603
+ );
1604
+ const defaultAgent = await promptOptional(
1605
+ rl,
1606
+ "Default agent (leave blank to let runtime decide)"
1607
+ );
1608
+ const plugins = await promptCsv(
1609
+ rl,
1610
+ "Plugins to enable (comma-separated package names or paths)",
1611
+ seededProfile?.plugins || []
1612
+ );
1613
+ const payload = {
1614
+ name,
1615
+ bundles,
1616
+ plugins
1617
+ };
1618
+ if (description) payload.description = description;
1619
+ if (defaultAgent) payload.defaultAgent = defaultAgent;
1620
+ await writeJsonFile(filePath, payload);
1621
+ return filePath;
1622
+ } finally {
1623
+ rl.close();
1624
+ }
1625
+ };
1626
+ const createBundleDefinition = async (root, name, reservedOk = false) => {
1627
+ await assertNameNotReserved("bundle", name, reservedOk);
1628
+ const filePath = toJsonFile(root, "bundles", name);
1629
+ const availableSouls = await listNamesByExt(path.join(root, "souls"), ".md");
1630
+ const availableInstructions = await listNamesByExt(
1631
+ path.join(root, "instructions"),
1632
+ ".md"
1633
+ );
1634
+ const availableSkills = await listSkillNames(path.join(root, "skills"));
1635
+ const availableMcp = await listNamesByExt(path.join(root, "mcp"), ".json");
1636
+ const rl = createPromptInterface();
1637
+ try {
1638
+ await maybeOverwrite(rl, filePath);
1639
+ if (availableSouls.length > 0) {
1640
+ process.stdout.write(`Available souls: ${availableSouls.join(", ")}
1641
+ `);
1642
+ }
1643
+ if (availableInstructions.length > 0) {
1644
+ process.stdout.write(`Available instructions: ${availableInstructions.join(", ")}
1645
+ `);
1646
+ }
1647
+ if (availableSkills.length > 0) {
1648
+ process.stdout.write(`Available skills: ${availableSkills.join(", ")}
1649
+ `);
1650
+ }
1651
+ const runtime = await promptChoice(
1652
+ rl,
1653
+ "Runtime",
1654
+ ["native", "omo"],
1655
+ "native"
1656
+ );
1657
+ const readOnly = runtime === "native" ? await promptBoolean(
1658
+ rl,
1659
+ "Read-only agent? (Y = plan-like, N = build-like)",
1660
+ true
1661
+ ) : false;
1662
+ const soul = await promptRequired(rl, "Soul name", availableSouls[0]);
1663
+ const instructions = await promptOptionalCsvSelection(
1664
+ rl,
1665
+ "Attach instructions from instructions/?",
1666
+ availableInstructions
1667
+ );
1668
+ const skills = await promptOptionalCsvSelection(
1669
+ rl,
1670
+ "Attach skills from skills/?",
1671
+ availableSkills
1672
+ );
1673
+ const mcp = await promptOptionalCsvSelection(
1674
+ rl,
1675
+ "Attach custom MCP tools? (basic opencode tools stay available)",
1676
+ availableMcp
1677
+ );
1678
+ const guards = await promptCsv(rl, "Extra guards (comma-separated)");
1679
+ const categories = runtime === "omo" ? await promptRecord(rl, "Category model mapping") : void 0;
1680
+ const agentName = await promptRequired(rl, "Agent name", name);
1681
+ const agentMode = await promptChoice(
1682
+ rl,
1683
+ "Agent mode",
1684
+ ["primary", "subagent"],
1685
+ "primary"
1686
+ );
1687
+ const hidden = await promptBoolean(
1688
+ rl,
1689
+ "Hide this agent from normal listings?",
1690
+ false
1691
+ );
1692
+ const model = await promptRequired(rl, "Agent model");
1693
+ const description = await promptOptional(rl, "Agent description");
1694
+ const payload = {
1695
+ name,
1696
+ runtime,
1697
+ soul,
1698
+ ...instructions.length > 0 ? { instructions } : {},
1699
+ skills,
1700
+ agent: {
1701
+ name: agentName,
1702
+ mode: agentMode,
1703
+ hidden,
1704
+ model
1705
+ }
1706
+ };
1707
+ if (readOnly) {
1708
+ payload.guards = uniqueValues([...payload.guards || [], "no_task"]);
1709
+ }
1710
+ if (mcp.length > 0) payload.mcp = mcp;
1711
+ if (guards.length > 0) {
1712
+ payload.guards = uniqueValues([...payload.guards || [], ...guards]);
1713
+ }
1714
+ if (description) payload.agent.description = description;
1715
+ if (categories && Object.keys(categories).length > 0) {
1716
+ payload.categories = categories;
1717
+ const enableSpawn = await promptBoolean(
1718
+ rl,
1719
+ "Generate category-family spawn config from those categories?",
1720
+ true
1721
+ );
1722
+ if (enableSpawn) {
1723
+ payload.spawn = {
1724
+ strategy: "category-family",
1725
+ source: "categories",
1726
+ shared: {
1727
+ soul,
1728
+ skills
1729
+ }
1730
+ };
1731
+ }
1732
+ }
1733
+ await writeJsonFile(filePath, payload);
1734
+ return filePath;
1735
+ } finally {
1736
+ rl.close();
1737
+ }
1738
+ };
1739
+ const resolveConfigRoot = (parsed2) => {
1740
+ if (parsed2.configRoot) return parsed2.configRoot;
1741
+ if (!parsed2.runtimeSelection) {
1742
+ return parsed2.workspace;
1743
+ }
1744
+ if (parsed2.runtimeSelection.kind === "tools-only") {
1745
+ return getWorkspaceRuntimeRoot(parsed2.workspace);
1746
+ }
1747
+ if (parsed2.runtimeSelection.kind === "customized-agent") {
1748
+ return getWorkspaceRuntimeRoot(parsed2.workspace);
1749
+ }
1750
+ return getDefaultConfigRoot(
1751
+ parsed2.workspace,
1752
+ parsed2.runtimeSelection.profile
1753
+ );
1754
+ };
1755
+ const resolveSelectedHomeRoot = (parsed2) => {
1756
+ if (parsed2.bootstrapOptions.targetRoot) {
1757
+ return parsed2.bootstrapOptions.targetRoot;
1758
+ }
1759
+ if (parsed2.command === "hr") {
1760
+ return defaultHrHome();
1761
+ }
1762
+ return void 0;
1763
+ };
1764
+ const composeSelection = async (parsed2, configRoot) => {
1765
+ let homeRoot = resolveSelectedHomeRoot(parsed2);
1766
+ let settingsRoot = homeRoot;
1767
+ if (!parsed2.runtimeSelection) {
1768
+ return { workspace: parsed2.workspace, configRoot };
1769
+ }
1770
+ if (parsed2.command === "hr" && parsed2.hrIntent?.kind === "compose" && parsed2.runtimeSelection.kind === "profile") {
1771
+ const resolved = await resolveHrProfileSource(
1772
+ parsed2.runtimeSelection.profile,
1773
+ defaultHrHome()
1774
+ );
1775
+ homeRoot = resolved.libraryRoot;
1776
+ settingsRoot = resolved.settingsRoot;
1777
+ if (resolved.kind === "staged" && resolved.match) {
1778
+ await printHrStagedProfileResolution(
1779
+ parsed2.runtimeSelection.profile,
1780
+ resolved.match,
1781
+ defaultHrHome()
1782
+ );
1783
+ }
1784
+ }
1785
+ if (parsed2.runtimeSelection.kind === "tools-only") {
1786
+ return composeToolInjection(parsed2.workspace, configRoot, { homeRoot, settingsRoot });
1787
+ }
1788
+ if (parsed2.runtimeSelection.kind === "customized-agent") {
1789
+ return composeCustomizedAgent(parsed2.workspace, configRoot, { homeRoot, settingsRoot });
1790
+ }
1791
+ return composeWorkspace(
1792
+ parsed2.workspace,
1793
+ parsed2.runtimeSelection.profile,
1794
+ configRoot,
1795
+ { homeRoot, settingsRoot }
1796
+ );
1797
+ };
1798
+ const runOpencode = async (workspace, configRoot, opencodeArgs) => {
1799
+ const env = configRoot ? {
1800
+ ...process.env,
1801
+ XDG_CONFIG_HOME: path.join(configRoot, "xdg"),
1802
+ OPENCODE_DISABLE_PROJECT_CONFIG: "true",
1803
+ OPENCODE_CONFIG_DIR: configRoot
1804
+ } : process.env;
1805
+ const child = spawn("opencode", opencodeArgs, {
1806
+ cwd: workspace,
1807
+ stdio: "inherit",
1808
+ env,
1809
+ ...spawnOptions()
1810
+ });
1811
+ child.on("exit", (code, signal) => {
1812
+ if (signal) {
1813
+ process.kill(process.pid, signal);
1814
+ return;
1815
+ }
1816
+ process.exit(code ?? 0);
1817
+ });
1818
+ };
1819
+ const parsed = parseArgs(process.argv.slice(2));
1820
+ const startupNotice = windowsStartupNotice();
1821
+ if (startupNotice) {
1822
+ process.stderr.write(`${startupNotice}
1823
+ `);
1824
+ }
1825
+ if (parsed.command === "start" || parsed.command === "run") {
1826
+ if (parsed.runtimeSelection?.kind === "profile" && parsed.runtimeSelection.profile === "hr") {
1827
+ fail(
1828
+ `'start hr' and 'run hr' are no longer supported. Use '${cliCommand} hr' for HR Office or '${cliCommand} hr <profile>' to test an HR profile in this workspace.`
1829
+ );
1830
+ }
1831
+ if (parsed.startIntent?.kind === "set-default") {
1832
+ const targetRoot = resolveSelectedHomeRoot(parsed) || defaultAgentHubHome();
1833
+ await ensureHomeReadyOrFail(targetRoot);
1834
+ if (!await profileExistsInHome(targetRoot, parsed.startIntent.profile)) {
1835
+ fail(
1836
+ `Profile '${parsed.startIntent.profile}' not found in ${path.join(targetRoot, "profiles")}.`
1837
+ );
1838
+ }
1839
+ await setStartDefaultProfile(parsed.startIntent.profile, targetRoot);
1840
+ process.stdout.write(
1841
+ `Set default start profile to '${parsed.startIntent.profile}' for ${targetRoot}
1842
+ `
1843
+ );
1844
+ process.exit(0);
1845
+ }
1846
+ if (parsed.startIntent?.kind === "compose" && !parsed.startIntent.profile) {
1847
+ const resolved = parsed.startIntent.source === "last" ? await resolveStartLastProfilePreference(parsed.workspace) : await resolveStartProfilePreference(
1848
+ parsed.workspace,
1849
+ resolveSelectedHomeRoot(parsed) || defaultAgentHubHome()
1850
+ );
1851
+ parsed.startIntent = {
1852
+ kind: "compose",
1853
+ profile: resolved.profile,
1854
+ source: resolved.source
1855
+ };
1856
+ parsed.runtimeSelection = {
1857
+ kind: "profile",
1858
+ profile: resolved.profile
1859
+ };
1860
+ noteProfileResolution("start", resolved.source, resolved.profile);
1861
+ }
1862
+ }
1863
+ if (parsed.command === "hr" && parsed.hrIntent?.kind === "compose" && parsed.hrIntent.source === "last") {
1864
+ const lastProfile = await resolveHrLastProfilePreference(parsed.workspace);
1865
+ if (!lastProfile) {
1866
+ fail(`No previous HR workspace profile for this folder. Use: ${cliCommand} hr <profile>`);
1867
+ }
1868
+ parsed.hrIntent = { kind: "compose", profile: lastProfile, source: "last" };
1869
+ parsed.runtimeSelection = {
1870
+ kind: "profile",
1871
+ profile: lastProfile
1872
+ };
1873
+ noteProfileResolution("hr", "last", lastProfile);
1874
+ }
1875
+ if (parsed.command === "setup") {
1876
+ const options = Object.keys(parsed.bootstrapOptions).length ? parsed.bootstrapOptions : await promptHubInitAnswers();
1877
+ const targetRoot = await installAgentHubHome(options);
1878
+ const mode = options.mode ?? "auto";
1879
+ if (mode === "auto") {
1880
+ process.stdout.write(`\u2713 Coding system ready. Run: ${cliCommand} start
1881
+ `);
1882
+ } else {
1883
+ process.stdout.write("\u2713 Minimal Agent Hub structure ready. Add your own assets anytime.\n");
1884
+ }
1885
+ process.stdout.write(`${targetRoot}
1886
+ `);
1887
+ process.exit(0);
1888
+ }
1889
+ if (parsed.command === "backup") {
1890
+ const outputRoot = parsed.transferOptions.outputRoot;
1891
+ if (!outputRoot) {
1892
+ fail("'backup' requires '--output <path>'.");
1893
+ }
1894
+ const report = await exportAgentHubHome({
1895
+ sourceRoot: defaultAgentHubHome(),
1896
+ outputRoot,
1897
+ pluginVersion: readPackageVersion()
1898
+ });
1899
+ printTransferReport("Backup", report);
1900
+ process.exit(0);
1901
+ }
1902
+ if (parsed.command === "restore") {
1903
+ const importSourceRoot = parsed.transferOptions.sourceRoot;
1904
+ if (!importSourceRoot) {
1905
+ fail("'restore' requires '--source <path>'.");
1906
+ }
1907
+ const report = await importAgentHubHome({
1908
+ sourceRoot: importSourceRoot,
1909
+ targetRoot: defaultAgentHubHome(),
1910
+ overwrite: parsed.transferOptions.overwrite,
1911
+ settingsMode: parsed.transferOptions.settingsMode
1912
+ });
1913
+ printTransferReport("Restore", report);
1914
+ process.exit(0);
1915
+ }
1916
+ if (parsed.command === "promote") {
1917
+ const hrRoot = defaultHrHome();
1918
+ const sourceRoot = await resolvePromoteSourceRoot(parsed.promotePackageId, hrRoot);
1919
+ const handoff = await readPromoteHandoff(sourceRoot);
1920
+ await validatePromoteSourceRoot(sourceRoot, hrRoot);
1921
+ const report = await importAgentHubHome({
1922
+ sourceRoot,
1923
+ targetRoot: defaultAgentHubHome(),
1924
+ overwrite: false,
1925
+ settingsMode: "preserve"
1926
+ });
1927
+ const defaultProfile = resolvePromoteDefaultProfile(handoff);
1928
+ if (defaultProfile) {
1929
+ await setStartDefaultProfile(defaultProfile);
1930
+ }
1931
+ printTransferReport("Promote", report);
1932
+ if (defaultProfile) {
1933
+ process.stdout.write(`- default profile updated: ${defaultProfile}
1934
+ `);
1935
+ }
1936
+ process.exit(0);
1937
+ }
1938
+ if (parsed.command === "hub-export") {
1939
+ const outputRoot = parsed.transferOptions.outputRoot;
1940
+ if (!outputRoot) {
1941
+ fail("'hub-export' requires '--output <path>'.");
1942
+ }
1943
+ const report = await exportAgentHubHome({
1944
+ sourceRoot: parsed.transferOptions.sourceRoot,
1945
+ outputRoot,
1946
+ pluginVersion: readPackageVersion()
1947
+ });
1948
+ printTransferReport("Export", report);
1949
+ process.exit(0);
1950
+ }
1951
+ if (parsed.command === "hub-import") {
1952
+ const importSourceRoot = parsed.transferOptions.sourceRoot;
1953
+ if (!importSourceRoot) {
1954
+ fail("'hub-import' requires '--source <path>'.");
1955
+ }
1956
+ const report = await importAgentHubHome({
1957
+ sourceRoot: importSourceRoot,
1958
+ targetRoot: parsed.transferOptions.targetRoot,
1959
+ overwrite: parsed.transferOptions.overwrite,
1960
+ settingsMode: parsed.transferOptions.settingsMode
1961
+ });
1962
+ printTransferReport("Import", report);
1963
+ process.exit(0);
1964
+ }
1965
+ if (parsed.command === "compose" || parsed.command === "new") {
1966
+ const agentHubHome = parsed.bootstrapOptions.targetRoot || defaultAgentHubHome();
1967
+ await ensureHomeReadyOrFail(agentHubHome);
1968
+ const composeSelection2 = parsed.composeSelection;
1969
+ if (!composeSelection2) {
1970
+ fail(
1971
+ parsed.command === "compose" ? "Use 'compose profile <name>' or 'compose bundle <name>'." : "Use 'new soul <name>', 'new skill <name>', 'new instruction <name>', 'new bundle <name>', or 'new profile <name>'."
1972
+ );
1973
+ }
1974
+ const filePath = composeSelection2.kind === "profile" ? await createProfileDefinition(
1975
+ agentHubHome,
1976
+ composeSelection2.name,
1977
+ parsed.profileCreateOptions
1978
+ ) : composeSelection2.kind === "bundle" ? await createBundleDefinition(
1979
+ agentHubHome,
1980
+ composeSelection2.name,
1981
+ parsed.profileCreateOptions.reservedOk
1982
+ ) : composeSelection2.kind === "soul" ? await createSoulDefinition(
1983
+ agentHubHome,
1984
+ composeSelection2.name,
1985
+ parsed.profileCreateOptions.reservedOk
1986
+ ) : composeSelection2.kind === "skill" ? await createSkillDefinition(
1987
+ agentHubHome,
1988
+ composeSelection2.name,
1989
+ parsed.profileCreateOptions.reservedOk
1990
+ ) : await createInstructionDefinition(
1991
+ agentHubHome,
1992
+ composeSelection2.name,
1993
+ parsed.profileCreateOptions.reservedOk
1994
+ );
1995
+ await warnIfBuiltInsDrifted(agentHubHome, await detectSetupModeForHome(agentHubHome));
1996
+ process.stdout.write(`${filePath}
1997
+ `);
1998
+ process.exit(0);
1999
+ }
2000
+ if (parsed.command === "plugin") {
2001
+ if (parsed.pluginSubcommand !== "doctor") {
2002
+ fail("Use 'plugin doctor'.");
2003
+ }
2004
+ const configRoot = resolvePluginConfigRoot(parsed.configRoot);
2005
+ const runtimeInspection = await inspectRuntimeConfig(configRoot);
2006
+ process.stdout.write(`Plugin doctor
2007
+ `);
2008
+ process.stdout.write(`- config root: ${configRoot}
2009
+ `);
2010
+ process.stdout.write(`- runtime file: ${runtimeInspection.runtimeConfigPath}
2011
+ `);
2012
+ if (!runtimeInspection.ok) {
2013
+ process.stdout.write(`- runtime config: missing or unreadable
2014
+ `);
2015
+ process.stdout.write(`- active mode: degraded
2016
+ `);
2017
+ process.stdout.write(`- blocked tools: call_omo_agent (safety fallback)
2018
+ `);
2019
+ process.stdout.write(`- plan detection: disabled
2020
+ `);
2021
+ process.stdout.write(`Next: run '${cliCommand} start auto' or compose a profile to generate agenthub-runtime.json.
2022
+ `);
2023
+ } else {
2024
+ const summary = summarizeRuntimeFeatureState(runtimeInspection.config);
2025
+ process.stdout.write(`- runtime config: ok
2026
+ `);
2027
+ process.stdout.write(`- active mode: composed runtime
2028
+ `);
2029
+ process.stdout.write(`- blocked tools: ${Array.from(summary.blockedTools).sort().join(", ") || "(none)"}
2030
+ `);
2031
+ process.stdout.write(`- plan detection: ${summary.planDetection?.enabled ? "enabled" : "disabled"}
2032
+ `);
2033
+ }
2034
+ const pluginConfigPath = path.join(configRoot, "opencode.jsonc");
2035
+ const xdgPluginConfigPath = path.join(configRoot, "xdg", "opencode", "opencode.json");
2036
+ let pluginRegistered = false;
2037
+ for (const filePath of [pluginConfigPath, xdgPluginConfigPath]) {
2038
+ const config = await readJsonIfExists(filePath);
2039
+ if (Array.isArray(config?.plugin) && config.plugin.includes("opencode-agenthub")) {
2040
+ pluginRegistered = true;
2041
+ break;
2042
+ }
2043
+ }
2044
+ process.stdout.write(`- plugin registered: ${pluginRegistered ? "yes" : "no"}
2045
+ `);
2046
+ if (!pluginRegistered) {
2047
+ process.stdout.write(`Next: compose a profile so opencode.jsonc includes 'opencode-agenthub'.
2048
+ `);
2049
+ }
2050
+ process.exit(0);
2051
+ }
2052
+ if (parsed.command === "upgrade") {
2053
+ const targetRoot = resolveSelectedHomeRoot(parsed) || defaultAgentHubHome();
2054
+ await ensureHomeReadyOrFail(targetRoot);
2055
+ const mode = await detectSetupModeForHome(targetRoot);
2056
+ const report = await syncBuiltInAgentHubAssets({
2057
+ targetRoot,
2058
+ mode,
2059
+ force: parsed.upgradeOptions.force,
2060
+ dryRun: parsed.upgradeOptions.dryRun
2061
+ });
2062
+ printUpgradeReport(targetRoot, report, parsed.upgradeOptions);
2063
+ process.exit(0);
2064
+ }
2065
+ if ((parsed.command === "run" || parsed.command === "start") && !parsed.runtimeSelection) {
2066
+ if (parsed.assembleOnly) {
2067
+ fail("'run'/'start' without a profile cannot be used with '--assemble-only'.");
2068
+ }
2069
+ if (process.stderr.isTTY) {
2070
+ process.stderr.write(
2071
+ "\u26A0 No profile selected \u2014 launching plain opencode (hub runtime is inactive)\n"
2072
+ );
2073
+ }
2074
+ await ensureWorkspaceReadable(parsed.workspace);
2075
+ await runOpencode(parsed.workspace, void 0, parsed.opencodeArgs);
2076
+ process.exit(0);
2077
+ }
2078
+ if (parsed.command === "doctor" || parsed.command === "hub-doctor") {
2079
+ const targetRoot = parsed.bootstrapOptions.targetRoot || defaultAgentHubHome();
2080
+ const {
2081
+ runDiagnostics,
2082
+ interactiveAssembly,
2083
+ interactiveDoctor,
2084
+ updateAgentModelOverride,
2085
+ updateAgentPromptOverride,
2086
+ fixMissingGuards,
2087
+ createBundleForSoul,
2088
+ createProfile,
2089
+ fixOmoMixedProfile,
2090
+ getAvailableBundles
2091
+ } = await import("../skills/agenthub-doctor/index.js");
2092
+ if (parsed.doctorOptions.agent) {
2093
+ const promptFilePath = parsed.doctorOptions.promptFile;
2094
+ const message = parsed.doctorOptions.model || parsed.doctorOptions.clearModel ? await updateAgentModelOverride(
2095
+ targetRoot,
2096
+ parsed.doctorOptions.agent,
2097
+ parsed.doctorOptions.clearModel ? "" : parsed.doctorOptions.model || ""
2098
+ ) : await updateAgentPromptOverride(
2099
+ targetRoot,
2100
+ parsed.doctorOptions.agent,
2101
+ parsed.doctorOptions.clearPrompt ? "" : promptFilePath ? await readFile(promptFilePath, "utf-8") : fail(
2102
+ "'doctor --prompt-file <path>' requires a prompt file path."
2103
+ )
2104
+ );
2105
+ process.stdout.write(`${message}
2106
+ `);
2107
+ process.exit(0);
2108
+ }
2109
+ process.stdout.write("\u{1F50D} Running Agent Hub diagnostics...\n\n");
2110
+ const report = await runDiagnostics(targetRoot);
2111
+ if (report.healthy.length > 0) {
2112
+ process.stdout.write("\u2705 Healthy:\n");
2113
+ for (const item of report.healthy) {
2114
+ process.stdout.write(` - ${item}
2115
+ `);
2116
+ }
2117
+ process.stdout.write("\n");
2118
+ }
2119
+ if (report.issues.length === 0) {
2120
+ process.stdout.write("\u2705 No issues found! Agent Hub is ready to use.\n");
2121
+ if (parsed.doctorOptions.fixAll || parsed.doctorOptions.dryRun || !process.stdin.isTTY) {
2122
+ process.exit(0);
2123
+ }
2124
+ await interactiveDoctor(targetRoot, report);
2125
+ process.exit(0);
2126
+ }
2127
+ process.stdout.write("\u26A0\uFE0F Issues Found:\n");
2128
+ for (const issue of report.issues) {
2129
+ const icon = issue.severity === "error" ? "\u274C" : issue.severity === "warning" ? "\u26A0\uFE0F " : "\u2139\uFE0F ";
2130
+ process.stdout.write(` ${icon} ${issue.message}
2131
+ `);
2132
+ }
2133
+ process.stdout.write("\n");
2134
+ if (parsed.doctorOptions.dryRun) {
2135
+ process.stdout.write("(Dry run - no fixes applied)\n");
2136
+ process.exit(0);
2137
+ }
2138
+ if (parsed.doctorOptions.fixAll) {
2139
+ process.stdout.write("\u{1F527} Applying fixes...\n\n");
2140
+ const missingGuardsIssue = report.issues.find(
2141
+ (i) => i.type === "missing_guards"
2142
+ );
2143
+ if (missingGuardsIssue) {
2144
+ const guards = missingGuardsIssue.details.guards;
2145
+ const result2 = await fixMissingGuards(targetRoot, guards);
2146
+ process.stdout.write(
2147
+ ` ${result2.success ? "\u2713" : "\u2717"} ${result2.message}
2148
+ `
2149
+ );
2150
+ }
2151
+ const orphanedSoulsIssue = report.issues.find(
2152
+ (i) => i.type === "orphaned_souls"
2153
+ );
2154
+ if (orphanedSoulsIssue) {
2155
+ const souls = orphanedSoulsIssue.details.souls;
2156
+ for (const soul of souls) {
2157
+ const result2 = await createBundleForSoul(targetRoot, soul);
2158
+ process.stdout.write(
2159
+ ` ${result2.success ? "\u2713" : "\u2717"} ${result2.message}
2160
+ `
2161
+ );
2162
+ }
2163
+ }
2164
+ const noProfilesIssue = report.issues.find((i) => i.type === "no_profiles");
2165
+ if (noProfilesIssue) {
2166
+ const bundles = await getAvailableBundles(targetRoot);
2167
+ if (bundles.length > 0) {
2168
+ const result2 = await createProfile(targetRoot, "imported", {
2169
+ bundleNames: bundles
2170
+ });
2171
+ process.stdout.write(
2172
+ ` ${result2.success ? "\u2713" : "\u2717"} ${result2.message}
2173
+ `
2174
+ );
2175
+ }
2176
+ }
2177
+ const omoIssue = report.issues.find((i) => i.type === "omo_mixed_profile");
2178
+ if (omoIssue) {
2179
+ const result2 = await fixOmoMixedProfile(
2180
+ targetRoot,
2181
+ omoIssue.details
2182
+ );
2183
+ process.stdout.write(
2184
+ ` ${result2.success ? "\u2713" : "\u2717"} ${result2.message}
2185
+ `
2186
+ );
2187
+ }
2188
+ process.stdout.write("\n\u2705 All fixes applied!\n");
2189
+ process.exit(0);
2190
+ }
2191
+ await interactiveAssembly(targetRoot, report);
2192
+ process.exit(0);
2193
+ }
2194
+ if (parsed.command === "list") {
2195
+ const {
2196
+ labelSouls,
2197
+ labelBundles,
2198
+ labelProfiles,
2199
+ labelSkills,
2200
+ labelInstructions
2201
+ } = await import("./query.js");
2202
+ const hubHome = parsed.bootstrapOptions.targetRoot || defaultAgentHubHome();
2203
+ const target = parsed.listTarget;
2204
+ const printSection = (title, items) => {
2205
+ process.stdout.write(`
2206
+ ${title} (${items.length}):
2207
+ `);
2208
+ if (items.length === 0) {
2209
+ process.stdout.write(" (none)\n");
2210
+ } else {
2211
+ for (const item of items) {
2212
+ process.stdout.write(` ${item.name} [${item.source}]
2213
+ `);
2214
+ }
2215
+ }
2216
+ };
2217
+ if (!target || target === "souls") {
2218
+ printSection("Souls", await labelSouls(hubHome));
2219
+ }
2220
+ if (!target || target === "bundles") {
2221
+ printSection("Bundles", await labelBundles(hubHome));
2222
+ }
2223
+ if (!target || target === "profiles") {
2224
+ printSection("Profiles", await labelProfiles(hubHome));
2225
+ }
2226
+ if (!target || target === "skills") {
2227
+ printSection("Skills", await labelSkills(hubHome));
2228
+ }
2229
+ if (!target || target === "instructions") {
2230
+ printSection("Instructions", await labelInstructions(hubHome));
2231
+ }
2232
+ if (target && !["souls", "bundles", "profiles", "skills", "instructions"].includes(target)) {
2233
+ fail(`Unknown list target '${target}'. Use: souls, bundles, profiles, skills, instructions`);
2234
+ }
2235
+ process.stdout.write("\n");
2236
+ process.exit(0);
2237
+ }
2238
+ if (parsed.command === "hr") {
2239
+ await ensureHrOfficeReadyOrBootstrap(resolveSelectedHomeRoot(parsed), {
2240
+ syncSourcesOnFirstRun: !parsed.assembleOnly
2241
+ });
2242
+ if (parsed.hrIntent?.kind === "office") {
2243
+ parsed.workspace = resolveSelectedHomeRoot(parsed) || defaultHrHome();
2244
+ } else if (parsed.hrIntent?.kind === "compose") {
2245
+ await warnIfWorkspaceRuntimeWillBeReplaced(
2246
+ parsed.workspace,
2247
+ `HR profile '${parsed.hrIntent.profile}'`
2248
+ );
2249
+ }
2250
+ } else if (parsed.command === "start" || parsed.command === "run") {
2251
+ await ensureHomeReadyOrBootstrap(resolveSelectedHomeRoot(parsed));
2252
+ await warnAboutLegacyHrAssets(resolveSelectedHomeRoot(parsed) || defaultAgentHubHome());
2253
+ } else {
2254
+ await ensureSelectedHomeReadyOrFail(parsed);
2255
+ }
2256
+ {
2257
+ const selectedHome = resolveSelectedHomeRoot(parsed) || defaultAgentHubHome();
2258
+ const selectedMode = await detectInstallModeForHome(selectedHome, parsed.command === "hr");
2259
+ await warnIfBuiltInsDrifted(selectedHome, selectedMode);
2260
+ }
2261
+ if (parsed.command === "run" || parsed.command === "start" || parsed.command === "hr") {
2262
+ await ensureWorkspaceReadable(parsed.workspace);
2263
+ }
2264
+ const finalConfigRoot = resolveConfigRoot(parsed);
2265
+ const result = await composeSelection(parsed, finalConfigRoot);
2266
+ printRuntimeBanner(
2267
+ parsed.command === "hr" ? "HR Office" : "My Team",
2268
+ resolveSelectedHomeRoot(parsed) || defaultAgentHubHome()
2269
+ );
2270
+ if (shouldChmod()) {
2271
+ await chmod(path.join(result.configRoot, "run.sh"), 493);
2272
+ }
2273
+ if ((parsed.command === "start" || parsed.command === "run") && parsed.runtimeSelection?.kind === "profile") {
2274
+ await updateWorkspacePreferences(parsed.workspace, (current) => ({
2275
+ ...current,
2276
+ start: {
2277
+ ...current.start || {},
2278
+ lastProfile: parsed.runtimeSelection?.kind === "profile" ? parsed.runtimeSelection.profile : void 0
2279
+ }
2280
+ }));
2281
+ }
2282
+ if (parsed.command === "hr" && parsed.hrIntent?.kind === "compose" && parsed.runtimeSelection?.kind === "profile") {
2283
+ await updateWorkspacePreferences(parsed.workspace, (current) => ({
2284
+ ...current,
2285
+ hr: {
2286
+ ...current.hr || {},
2287
+ lastProfile: parsed.runtimeSelection?.kind === "profile" ? parsed.runtimeSelection.profile : void 0
2288
+ }
2289
+ }));
2290
+ }
2291
+ if ((parsed.command === "run" || parsed.command === "start" || parsed.command === "hr" && parsed.hrIntent?.kind === "compose") && parsed.runtimeSelection?.kind === "profile" && !parsed.assembleOnly) {
2292
+ await maybeConfigureEnvrc(parsed.workspace, result.configRoot);
2293
+ }
2294
+ if (parsed.command === "compose" || parsed.assembleOnly) {
2295
+ process.stdout.write(`${result.configRoot}
2296
+ `);
2297
+ process.exit(0);
2298
+ }
2299
+ await runOpencode(parsed.workspace, result.configRoot, parsed.opencodeArgs);