@jmanuelcorral/openteam 0.9.3 → 0.10.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.
- package/README.es.md +1 -1
- package/README.md +1 -1
- package/dist/cli.js +362 -34
- package/dist/commands/dispatch.d.ts.map +1 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/rolesInit.d.ts +55 -0
- package/dist/commands/rolesInit.d.ts.map +1 -0
- package/dist/commands/setup.d.ts +17 -0
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/config/errors.d.ts.map +1 -1
- package/dist/config/schema.d.ts +2 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +193 -31
- package/dist/messages/commands.d.ts +36 -0
- package/dist/messages/commands.d.ts.map +1 -1
- package/dist/opencodeArtifacts/orchestratorAgent.d.ts.map +1 -1
- package/dist/orchestrator/roles.d.ts +20 -0
- package/dist/orchestrator/roles.d.ts.map +1 -1
- package/dist/orchestrator/roster.d.ts +10 -2
- package/dist/orchestrator/roster.d.ts.map +1 -1
- package/dist/orchestrator/rosterPersistence.d.ts +14 -13
- package/dist/orchestrator/rosterPersistence.d.ts.map +1 -1
- package/dist/orchestrator/runtimeSelection.d.ts.map +1 -1
- package/dist/plugin/registerCastTool.d.ts +52 -0
- package/dist/plugin/registerCastTool.d.ts.map +1 -0
- package/dist/router/chooseModel.d.ts.map +1 -1
- package/package.json +1 -1
package/README.es.md
CHANGED
|
@@ -819,7 +819,7 @@ Consulta [docs/compatibility.md](docs/compatibility.md). Resumen verificado el `
|
|
|
819
819
|
|
|
820
820
|
| Superficie | Versión / contrato |
|
|
821
821
|
| --- | --- |
|
|
822
|
-
| openteam | `0.
|
|
822
|
+
| openteam | `0.10.0`; versión actual del paquete |
|
|
823
823
|
| `@opencode-ai/plugin` | `1.18.19` |
|
|
824
824
|
| `@opencode-ai/sdk` | `1.18.19` |
|
|
825
825
|
| Hook de routing | `chat.message`; no `chat.params` para cambiar modelo |
|
package/README.md
CHANGED
|
@@ -700,7 +700,7 @@ See [docs/compatibility.md](docs/compatibility.md). Verified summary as of `2026
|
|
|
700
700
|
|
|
701
701
|
| Surface | Version / contract |
|
|
702
702
|
| --- | --- |
|
|
703
|
-
| openteam | `0.
|
|
703
|
+
| openteam | `0.10.0`; current package version |
|
|
704
704
|
| `@opencode-ai/plugin` | `1.18.19` |
|
|
705
705
|
| `@opencode-ai/sdk` | `1.18.19` |
|
|
706
706
|
| Routing hook | `chat.message`; not `chat.params` for model changes |
|
package/dist/cli.js
CHANGED
|
@@ -101,7 +101,8 @@ var AgentRoleProfileSchema = z.object({
|
|
|
101
101
|
preferredFrontierModels: z.array(z.union([modelSelectionSchema, z.literal("auto")])),
|
|
102
102
|
localFirst: z.boolean(),
|
|
103
103
|
requiresLocalRuntime: z.boolean().optional(),
|
|
104
|
-
localRuntimes: z.array(z.enum(["ollama", "lmstudio", "foundry-local", "lemonade"])).optional()
|
|
104
|
+
localRuntimes: z.array(z.enum(["ollama", "lmstudio", "foundry-local", "lemonade"])).optional(),
|
|
105
|
+
localModels: z.partialRecord(z.enum(["ollama", "lmstudio", "foundry-local", "lemonade"]), z.string().min(1)).optional()
|
|
105
106
|
}).strict();
|
|
106
107
|
var TEAM_ROLES = {
|
|
107
108
|
architect: {
|
|
@@ -392,6 +393,9 @@ var OrchestratorRolesSchema = z2.record(z2.string().min(1), z2.unknown()).defaul
|
|
|
392
393
|
if (parsed.data.localRuntimes !== undefined) {
|
|
393
394
|
profile.localRuntimes = parsed.data.localRuntimes;
|
|
394
395
|
}
|
|
396
|
+
if (parsed.data.localModels !== undefined) {
|
|
397
|
+
profile.localModels = parsed.data.localModels;
|
|
398
|
+
}
|
|
395
399
|
parsedRoles[roleID] = profile;
|
|
396
400
|
}
|
|
397
401
|
return parsedRoles;
|
|
@@ -403,6 +407,7 @@ var defaultFrontierModel = {
|
|
|
403
407
|
providerID: "anthropic",
|
|
404
408
|
modelID: "claude-sonnet-4-5"
|
|
405
409
|
};
|
|
410
|
+
var DEFAULT_LOCAL_MAX_CONCURRENCY = 4;
|
|
406
411
|
var LocalRuntimeSchema = z2.object({
|
|
407
412
|
id: z2.enum(["ollama", "lmstudio", "foundry-local", "lemonade"]),
|
|
408
413
|
enabled: z2.boolean().default(true),
|
|
@@ -473,13 +478,23 @@ var OpenTeamConfigObjectSchema = z2.object({
|
|
|
473
478
|
localDefault: ModelRefSchema.nullable().default(null),
|
|
474
479
|
trivialPromptMaxChars: z2.number().int().positive().default(280),
|
|
475
480
|
frontierPromptMinChars: z2.number().int().positive().default(2000),
|
|
476
|
-
frontierOnly: z2.boolean().default(false)
|
|
477
|
-
|
|
481
|
+
frontierOnly: z2.boolean().default(false),
|
|
482
|
+
localOnly: z2.boolean().default(false)
|
|
483
|
+
}).strict().superRefine((router, ctx) => {
|
|
484
|
+
if (router.frontierOnly && router.localOnly) {
|
|
485
|
+
ctx.addIssue({
|
|
486
|
+
code: "custom",
|
|
487
|
+
path: ["localOnly"],
|
|
488
|
+
message: "router.localOnly and router.frontierOnly cannot both be true"
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}).default({
|
|
478
492
|
mode: "balanced",
|
|
479
493
|
localDefault: null,
|
|
480
494
|
trivialPromptMaxChars: 280,
|
|
481
495
|
frontierPromptMinChars: 2000,
|
|
482
|
-
frontierOnly: false
|
|
496
|
+
frontierOnly: false,
|
|
497
|
+
localOnly: false
|
|
483
498
|
}),
|
|
484
499
|
local: z2.object({
|
|
485
500
|
runtimes: z2.array(LocalRuntimeSchema).default([])
|
|
@@ -863,6 +878,16 @@ function chooseModel(input) {
|
|
|
863
878
|
if (override === "alwaysFrontier") {
|
|
864
879
|
return finalizeDecision(input, tier, frontier, budgetAction, decision(frontier.model, "frontier", ["explicit-always-frontier"], []));
|
|
865
880
|
}
|
|
881
|
+
if (input.config.router.localOnly) {
|
|
882
|
+
if (local === null) {
|
|
883
|
+
return finalizeDecision(input, tier, frontier, budgetAction, blockedDecision(frontier.model, ["router-local-only", "forced-local-none-configured"], "forced-local-none-configured"));
|
|
884
|
+
}
|
|
885
|
+
return finalizeDecision(input, tier, frontier, budgetAction, resolveLocalPrimary(input, local, frontier.model, {
|
|
886
|
+
baseRationale: ["router-local-only"],
|
|
887
|
+
allowFrontier: false,
|
|
888
|
+
noLocalRationale: "forced-local-unavailable"
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
866
891
|
if (input.config.router.frontierOnly) {
|
|
867
892
|
return finalizeDecision(input, tier, frontier, budgetAction, decision(frontier.model, "frontier", ["router-frontier-only"], []));
|
|
868
893
|
}
|
|
@@ -1485,6 +1510,10 @@ function configuredRuntime(runtimeId, config) {
|
|
|
1485
1510
|
function runtimeIdOfModel(model, config) {
|
|
1486
1511
|
return config.local.runtimes.find((runtime) => sameModel(runtime.defaultModel, model))?.id;
|
|
1487
1512
|
}
|
|
1513
|
+
function roleLocalModel(role, runtimeId, fallback) {
|
|
1514
|
+
const pinned = role.localModels?.[runtimeId];
|
|
1515
|
+
return pinned === undefined ? fallback : { providerID: runtimeId, modelID: pinned };
|
|
1516
|
+
}
|
|
1488
1517
|
function selectDispatchRuntime(role, decision2, reachable, config) {
|
|
1489
1518
|
if (decision2.routeKind !== "local") {
|
|
1490
1519
|
return { runtimeId: undefined, decision: decision2 };
|
|
@@ -1499,15 +1528,25 @@ function selectDispatchRuntime(role, decision2, reachable, config) {
|
|
|
1499
1528
|
if (runtime === undefined) {
|
|
1500
1529
|
continue;
|
|
1501
1530
|
}
|
|
1502
|
-
const model = runtime.defaultModel;
|
|
1531
|
+
const model = roleLocalModel(role, runtimeId, runtime.defaultModel);
|
|
1503
1532
|
return {
|
|
1504
1533
|
runtimeId,
|
|
1505
1534
|
decision: { ...decision2, selected: model }
|
|
1506
1535
|
};
|
|
1507
1536
|
}
|
|
1508
1537
|
}
|
|
1538
|
+
const resolvedRuntimeId = runtimeIdOfModel(decision2.selected, config);
|
|
1539
|
+
if (resolvedRuntimeId !== undefined) {
|
|
1540
|
+
const pinned = roleLocalModel(role, resolvedRuntimeId, decision2.selected);
|
|
1541
|
+
if (pinned !== decision2.selected) {
|
|
1542
|
+
return {
|
|
1543
|
+
runtimeId: resolvedRuntimeId,
|
|
1544
|
+
decision: { ...decision2, selected: pinned }
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1509
1548
|
return {
|
|
1510
|
-
runtimeId:
|
|
1549
|
+
runtimeId: resolvedRuntimeId,
|
|
1511
1550
|
decision: decision2
|
|
1512
1551
|
};
|
|
1513
1552
|
}
|
|
@@ -2877,7 +2916,8 @@ var KNOWN_CONFIG_KEYS_BY_PATH = new Map([
|
|
|
2877
2916
|
"localDefault",
|
|
2878
2917
|
"trivialPromptMaxChars",
|
|
2879
2918
|
"frontierPromptMinChars",
|
|
2880
|
-
"frontierOnly"
|
|
2919
|
+
"frontierOnly",
|
|
2920
|
+
"localOnly"
|
|
2881
2921
|
]
|
|
2882
2922
|
],
|
|
2883
2923
|
["router.localDefault", MODEL_REF_KEYS],
|
|
@@ -8660,16 +8700,21 @@ function buildOrchestratorAgent(frontier, options = {}) {
|
|
|
8660
8700
|
" `routing-cost` (plus the guaranteed `orchestrator`, `guardian`,",
|
|
8661
8701
|
" `scribe`, `ralph`). The roleID space is open: use free-form project",
|
|
8662
8702
|
" roleIDs only when no curated role fits.",
|
|
8663
|
-
`5. **Register the cast**
|
|
8664
|
-
"
|
|
8665
|
-
"
|
|
8666
|
-
|
|
8703
|
+
`5. **Register the cast** by calling the \`openteam-register-cast\` tool`,
|
|
8704
|
+
" with `{ universe, entries: [{ roleID, agentName }] }`. The tool validates",
|
|
8705
|
+
" the payload, re-asserts any missing guaranteed role, and persists the",
|
|
8706
|
+
` roster to \`${OPENTEAM_ROSTER_PATH}\` — outside \`.opencode/agent/\`,`,
|
|
8707
|
+
" which would otherwise load it as a phantom agent. Never write that file",
|
|
8708
|
+
" by hand: an invalid draft is rejected naming the offending field and",
|
|
8709
|
+
" nothing is written. Always reuse the same cast; do not re-cast without",
|
|
8710
|
+
" reason.",
|
|
8667
8711
|
"",
|
|
8668
8712
|
"## Roster file format",
|
|
8669
8713
|
"",
|
|
8670
|
-
"
|
|
8671
|
-
"`{ universe, entries: [{ roleID, agentName }] }
|
|
8672
|
-
"
|
|
8714
|
+
"`openteam-register-cast` persists the roster as Markdown with exactly one",
|
|
8715
|
+
"fenced `json` block matching `{ universe, entries: [{ roleID, agentName }] }`,",
|
|
8716
|
+
"and preserves any prose already in the document. Never write a table: the",
|
|
8717
|
+
"parser reads only the fenced block. Minimal parseable example:",
|
|
8673
8718
|
"",
|
|
8674
8719
|
"# openteam roster",
|
|
8675
8720
|
"",
|
|
@@ -8727,8 +8772,8 @@ function buildOrchestratorAgent(frontier, options = {}) {
|
|
|
8727
8772
|
" frontier only for architecture, security or ambiguous debugging:",
|
|
8728
8773
|
" openteam already routes cheapest-capable automatically.",
|
|
8729
8774
|
" - A focused, actionable role prompt.",
|
|
8730
|
-
|
|
8731
|
-
" tasks** to the newly created team with the `task` tool.",
|
|
8775
|
+
"4. Register the cast with the `openteam-register-cast` tool and **hand out",
|
|
8776
|
+
" the tasks** to the newly created team with the `task` tool.",
|
|
8732
8777
|
"",
|
|
8733
8778
|
"## Standard team roles",
|
|
8734
8779
|
"",
|
|
@@ -10315,6 +10360,41 @@ var migrateMessages = {
|
|
|
10315
10360
|
manualWorktreeEntry: (label, from, to) => ` · ${label}: ${from} → ${to}`,
|
|
10316
10361
|
success: " ✓ machine-local runtime output relocated under .opencode/openteam-local/."
|
|
10317
10362
|
};
|
|
10363
|
+
var rolesInitMessages = {
|
|
10364
|
+
header: "openteam roles init:",
|
|
10365
|
+
title: "openteam roles init",
|
|
10366
|
+
noRoster: (path) => ` ✗ no roster at ${path}.`,
|
|
10367
|
+
noRosterRemedy: " Cast the team first: run `openteam setup`, then ask the orchestrator to register the cast.",
|
|
10368
|
+
unparseableRoster: (path, error) => ` ✗ ${path} is present but unparseable: ${error}`,
|
|
10369
|
+
unparseableRosterRemedy: " Run `openteam doctor` for the remedy, then re-run this command.",
|
|
10370
|
+
allProfiled: (count) => ` ✓ ${count} roster role(s) checked; no unprofiled roles — every role resolves a routing profile.`,
|
|
10371
|
+
whyTitle: "Why you are being asked",
|
|
10372
|
+
why: (count) => [
|
|
10373
|
+
`${count} roster role(s) resolve no routing profile.`,
|
|
10374
|
+
"Until a profile exists they are frontier-eligible: their work can escalate",
|
|
10375
|
+
"to a frontier model. A 'local only' note in the roster prose does not",
|
|
10376
|
+
"prevent this — prose outside the JSON fence is never read."
|
|
10377
|
+
].join(`
|
|
10378
|
+
`),
|
|
10379
|
+
policyQuestion: (roleID, agentName) => `Routing policy for "${roleID}" (${agentName})`,
|
|
10380
|
+
localOnlyLabel: (roleID, agentName) => `Local only — ${roleID} (${agentName}) never leaves this machine`,
|
|
10381
|
+
localOnlyHint: "sets requiresLocalRuntime; the role fails closed when no local runtime is reachable",
|
|
10382
|
+
frontierOkLabel: "Local first, frontier allowed",
|
|
10383
|
+
frontierOkHint: "records today's implicit behaviour: cheapest-capable frontier when the task warrants it",
|
|
10384
|
+
skipLabel: "Skip for now",
|
|
10385
|
+
skipHint: "leaves the role unprofiled, so it stays frontier-eligible and doctor keeps reporting it",
|
|
10386
|
+
runtimeQuestion: (roleID) => `Preferred local runtime(s) for "${roleID}" (in failover order)`,
|
|
10387
|
+
runtimeHint: "machine-local — written to the git-ignored overlay",
|
|
10388
|
+
cancelled: (reason) => ` ✗ cancelled (${reason}); nothing was written.`,
|
|
10389
|
+
invalidResult: (reason) => ` ✗ refusing to write: the resulting config is invalid — ${reason}`,
|
|
10390
|
+
nothingWritten: (skipped) => ` · skipped ${skipped}; nothing written.`,
|
|
10391
|
+
nothingToWriteOutro: "Nothing to write.",
|
|
10392
|
+
pinnedLocal: (roleIDs, path) => ` ✓ pinned local-only: ${roleIDs} → ${path} (tracked, travels with the roster)`,
|
|
10393
|
+
frontierAllowed: (roleIDs, path) => ` ✓ frontier allowed: ${roleIDs} → ${path} (model choice stays "auto": cheapest-capable per task)`,
|
|
10394
|
+
runtimeBinding: (path) => ` ✓ runtime binding: ${path} (git-ignored, machine-local)`,
|
|
10395
|
+
skippedSummary: (roleIDs) => ` · skipped: ${roleIDs} — still frontier-eligible; re-run this command to revisit.`,
|
|
10396
|
+
outro: "Role policy updated."
|
|
10397
|
+
};
|
|
10318
10398
|
|
|
10319
10399
|
// src/commands/baseline.ts
|
|
10320
10400
|
function formatRef(ref) {
|
|
@@ -10458,10 +10538,23 @@ function renderConsoleStatus(console_) {
|
|
|
10458
10538
|
}
|
|
10459
10539
|
|
|
10460
10540
|
// src/commands/doctor.ts
|
|
10461
|
-
function runtimeLine(snapshot) {
|
|
10541
|
+
function runtimeLine(snapshot, runtime) {
|
|
10462
10542
|
const mark = snapshot.reachable ? "✓" : "✗";
|
|
10463
10543
|
const detail = snapshot.reachable ? `${snapshot.models.length} model(s)` : snapshot.error ?? "unreachable";
|
|
10464
|
-
|
|
10544
|
+
const declared = runtime?.maxConcurrency;
|
|
10545
|
+
const slots = declared === undefined ? ` · ${DEFAULT_LOCAL_MAX_CONCURRENCY} slot(s) (default)` : ` · ${declared} slot(s) (declared)`;
|
|
10546
|
+
return ` ${mark} ${snapshot.id.padEnd(14)} ${snapshot.baseURL || "(no baseURL)"} — ${detail}${slots}`;
|
|
10547
|
+
}
|
|
10548
|
+
function isNetworkRuntime(baseURL) {
|
|
10549
|
+
if (baseURL.length === 0) {
|
|
10550
|
+
return false;
|
|
10551
|
+
}
|
|
10552
|
+
try {
|
|
10553
|
+
const host = new URL(baseURL).hostname.toLowerCase();
|
|
10554
|
+
return host !== "localhost" && host !== "127.0.0.1" && host !== "::1" && host !== "[::1]" && !host.endsWith(".localhost");
|
|
10555
|
+
} catch {
|
|
10556
|
+
return false;
|
|
10557
|
+
}
|
|
10465
10558
|
}
|
|
10466
10559
|
function failureReasonText(reason, searchedPaths) {
|
|
10467
10560
|
switch (reason) {
|
|
@@ -10617,7 +10710,8 @@ function rosterHealthSection(audit, loadError, rosterRoleCount) {
|
|
|
10617
10710
|
if (unprofiled.length > 0) {
|
|
10618
10711
|
const roleIDs = unprofiled.map((entry) => entry.roleID).join(", ");
|
|
10619
10712
|
lines.push(` · unprofiled roleID(s): ${roleIDs}`);
|
|
10620
|
-
lines.push("
|
|
10713
|
+
lines.push(" routing: no profile means frontier-eligible — work for these roles can escalate to a frontier model. A 'local only' note in the roster prose does NOT prevent this: prose outside the JSON fence is never read.");
|
|
10714
|
+
lines.push(' remedy: run `openteam roles init` to compose a profile interactively, or add one by hand under orchestrator.roles in .opencode/openteam.json with "requiresLocalRuntime": true to pin the role local; for curated routing, change roleID to a TEAM_ROLES key instead.');
|
|
10621
10715
|
}
|
|
10622
10716
|
return lines;
|
|
10623
10717
|
}
|
|
@@ -10652,9 +10746,17 @@ function renderDoctor(input) {
|
|
|
10652
10746
|
lines.push(" (no runtime enabled)");
|
|
10653
10747
|
} else {
|
|
10654
10748
|
for (const snapshot of input.snapshots) {
|
|
10655
|
-
lines.push(runtimeLine(snapshot));
|
|
10749
|
+
lines.push(runtimeLine(snapshot, enabledRuntimes.find((r) => r.id === snapshot.id)));
|
|
10656
10750
|
}
|
|
10657
10751
|
}
|
|
10752
|
+
for (const snapshot of input.snapshots) {
|
|
10753
|
+
if (!isNetworkRuntime(snapshot.baseURL)) {
|
|
10754
|
+
continue;
|
|
10755
|
+
}
|
|
10756
|
+
const runtime = enabledRuntimes.find((r) => r.id === snapshot.id);
|
|
10757
|
+
const cap = runtime?.maxConcurrency ?? DEFAULT_LOCAL_MAX_CONCURRENCY;
|
|
10758
|
+
lines.push(` ⚠ ${snapshot.id} is reached over the network: its ${cap}-slot cap is enforced per openteam process, so`, " two processes at once (an opencode session plus 'openteam console') can together exceed it.");
|
|
10759
|
+
}
|
|
10658
10760
|
lines.push(` telemetry: ${input.telemetryPath} — ${input.telemetryRecords} record(s)`);
|
|
10659
10761
|
if (input.diagnostics !== undefined) {
|
|
10660
10762
|
lines.push(...diagnosticsSection(input.diagnostics));
|
|
@@ -11157,7 +11259,8 @@ function buildOpenTeamConfig(answers) {
|
|
|
11157
11259
|
const runtime = {
|
|
11158
11260
|
id: choice.id,
|
|
11159
11261
|
enabled: choice.enabled,
|
|
11160
|
-
defaultModel: { providerID: choice.id, modelID: choice.defaultModelID }
|
|
11262
|
+
defaultModel: { providerID: choice.id, modelID: choice.defaultModelID },
|
|
11263
|
+
maxConcurrency: choice.maxConcurrency ?? DEFAULT_LOCAL_MAX_CONCURRENCY
|
|
11161
11264
|
};
|
|
11162
11265
|
if (choice.baseURL !== undefined) {
|
|
11163
11266
|
runtime.baseURL = choice.baseURL;
|
|
@@ -11168,6 +11271,7 @@ function buildOpenTeamConfig(answers) {
|
|
|
11168
11271
|
});
|
|
11169
11272
|
const primaryLocal = firstEnabled(answers.runtimes);
|
|
11170
11273
|
const localDefault2 = primaryLocal !== undefined ? { providerID: primaryLocal.id, modelID: primaryLocal.defaultModelID } : null;
|
|
11274
|
+
const localOnly = answers.localOnly === true && primaryLocal !== undefined;
|
|
11171
11275
|
return OpenTeamConfigSchema.parse({
|
|
11172
11276
|
baseline: {
|
|
11173
11277
|
mode: "auto",
|
|
@@ -11177,7 +11281,8 @@ function buildOpenTeamConfig(answers) {
|
|
|
11177
11281
|
router: {
|
|
11178
11282
|
mode: answers.routerMode,
|
|
11179
11283
|
localDefault: localDefault2,
|
|
11180
|
-
frontierOnly: primaryLocal === undefined
|
|
11284
|
+
frontierOnly: primaryLocal === undefined,
|
|
11285
|
+
localOnly
|
|
11181
11286
|
},
|
|
11182
11287
|
local: { runtimes },
|
|
11183
11288
|
privacyMode: answers.privacyMode,
|
|
@@ -11207,15 +11312,17 @@ function buildOpencodeConfig(answers) {
|
|
|
11207
11312
|
models
|
|
11208
11313
|
};
|
|
11209
11314
|
}
|
|
11315
|
+
const primaryLocal = firstEnabled(answers.runtimes);
|
|
11316
|
+
const localOnly = answers.localOnly === true && primaryLocal !== undefined;
|
|
11317
|
+
const mainModel = localOnly && primaryLocal !== undefined ? `${primaryLocal.id}/${primaryLocal.defaultModelID}` : `${answers.frontier.providerID}/${answers.frontier.modelID}`;
|
|
11210
11318
|
const config = {
|
|
11211
11319
|
$schema: "https://opencode.ai/config.json",
|
|
11212
11320
|
plugin: [OPENTEAM_PLUGIN_SPEC],
|
|
11213
|
-
model:
|
|
11321
|
+
model: mainModel
|
|
11214
11322
|
};
|
|
11215
11323
|
if (Object.keys(provider).length > 0) {
|
|
11216
11324
|
config.provider = provider;
|
|
11217
11325
|
}
|
|
11218
|
-
const primaryLocal = firstEnabled(answers.runtimes);
|
|
11219
11326
|
if (primaryLocal !== undefined) {
|
|
11220
11327
|
config.small_model = `${primaryLocal.id}/${primaryLocal.defaultModelID}`;
|
|
11221
11328
|
}
|
|
@@ -11276,6 +11383,13 @@ async function browseFrontier(prompt, profiles) {
|
|
|
11276
11383
|
const modelID = await prompt.select(opts);
|
|
11277
11384
|
return { providerID, modelID };
|
|
11278
11385
|
}
|
|
11386
|
+
function cheapestFrontier(profiles) {
|
|
11387
|
+
const first = sortFrontierByCost(profiles)[0];
|
|
11388
|
+
if (first === undefined) {
|
|
11389
|
+
throw new Error("no frontier profile available for the baseline");
|
|
11390
|
+
}
|
|
11391
|
+
return { providerID: first.ref.providerID, modelID: first.ref.modelID };
|
|
11392
|
+
}
|
|
11279
11393
|
async function chooseFrontier(prompt, profiles) {
|
|
11280
11394
|
const sorted = sortFrontierByCost(profiles);
|
|
11281
11395
|
const first = sorted[0];
|
|
@@ -11462,6 +11576,10 @@ async function runSetup(deps) {
|
|
|
11462
11576
|
runtimes.push(await resolveEnabledRuntime(deps, meta, info));
|
|
11463
11577
|
}
|
|
11464
11578
|
const hasLocalEnabled = enabled.size > 0;
|
|
11579
|
+
const localOnly = hasLocalEnabled ? await prompt.confirm({
|
|
11580
|
+
message: "Run without frontier models? (local runtimes only; openteam never escalates)",
|
|
11581
|
+
initial: false
|
|
11582
|
+
}) : false;
|
|
11465
11583
|
let frontierProfiles;
|
|
11466
11584
|
try {
|
|
11467
11585
|
const loaded = await deps.loadFrontierProfiles();
|
|
@@ -11469,7 +11587,18 @@ async function runSetup(deps) {
|
|
|
11469
11587
|
} catch {
|
|
11470
11588
|
frontierProfiles = [...CURATED_FRONTIER_PROFILES];
|
|
11471
11589
|
}
|
|
11472
|
-
const frontier = await chooseFrontier(prompt, frontierProfiles);
|
|
11590
|
+
const frontier = localOnly ? cheapestFrontier(frontierProfiles) : await chooseFrontier(prompt, frontierProfiles);
|
|
11591
|
+
if (localOnly) {
|
|
11592
|
+
prompt.note([
|
|
11593
|
+
"openteam will never escalate to a frontier model.",
|
|
11594
|
+
"opencode's main model is pointed at your local runtime too, so the",
|
|
11595
|
+
"session itself does not reach a frontier provider either.",
|
|
11596
|
+
"",
|
|
11597
|
+
`A baseline is still recorded (${frontier.providerID}/${frontier.modelID})`,
|
|
11598
|
+
"but stays unused. Set router.localOnly to false to re-enable it."
|
|
11599
|
+
].join(`
|
|
11600
|
+
`), "Local-only mode");
|
|
11601
|
+
}
|
|
11473
11602
|
const routerMode = await prompt.select({
|
|
11474
11603
|
message: "Routing mode",
|
|
11475
11604
|
choices: [
|
|
@@ -11514,7 +11643,8 @@ async function runSetup(deps) {
|
|
|
11514
11643
|
frontier,
|
|
11515
11644
|
routerMode,
|
|
11516
11645
|
privacyMode,
|
|
11517
|
-
yolo
|
|
11646
|
+
yolo,
|
|
11647
|
+
localOnly
|
|
11518
11648
|
};
|
|
11519
11649
|
const openTeamConfig = buildOpenTeamConfig(answers);
|
|
11520
11650
|
const opencodeConfig = buildOpencodeConfig(answers);
|
|
@@ -11582,14 +11712,14 @@ async function runSetup(deps) {
|
|
|
11582
11712
|
return `${runtime.id} (${runtime.defaultModelID})${remote}`;
|
|
11583
11713
|
}).join(", ");
|
|
11584
11714
|
prompt.note([
|
|
11585
|
-
`Baseline frontier: ${frontier.providerID}/${frontier.modelID}`,
|
|
11715
|
+
`Baseline frontier: ${localOnly ? `${frontier.providerID}/${frontier.modelID} (recorded but unused — local-only)` : `${frontier.providerID}/${frontier.modelID}`}`,
|
|
11586
11716
|
`Local runtimes: ${enabledSummary || "none (frontier-only)"}`,
|
|
11587
11717
|
`YOLO mode: ${yolo ? "enabled (auto-approves permissions)" : "disabled"}`,
|
|
11588
11718
|
"Web Console: launched separately from the CLI with 'openteam console' (multi-session, loopback)",
|
|
11589
11719
|
`Wrote: ${opencodePath}, ${DEFAULT_CONFIG_PATH}, ${ORCHESTRATOR_AGENT_PATH}, ${AGENT_DIR}/*.md (${roleAgentFiles.length} standard subagents), ${OPENTEAM_COMMAND_DIR}/*.md (${slashCommands.length} commands); ensured ${OPENCODE_GITIGNORE_PATH}`,
|
|
11590
11720
|
"",
|
|
11591
11721
|
"Next steps:",
|
|
11592
|
-
` 1. Authenticate the frontier provider: opencode auth login`,
|
|
11722
|
+
localOnly ? " 1. No frontier auth needed; openteam runs entirely on your local runtimes" : ` 1. Authenticate the frontier provider: opencode auth login`,
|
|
11593
11723
|
hasLocalEnabled ? " 2. Make sure your local runtime (local or on the network) is reachable; openteam routes local-first automatically" : " 2. Open opencode in this repo; openteam will use frontier models only",
|
|
11594
11724
|
" 3. Press Tab and pick the 'openteam' agent, or type / and pick an /openteam… command"
|
|
11595
11725
|
].join(`
|
|
@@ -12280,6 +12410,7 @@ var HELP = [
|
|
|
12280
12410
|
" openteam local off Use frontier models only (no local runtime)",
|
|
12281
12411
|
" openteam local on Re-enable local-first routing",
|
|
12282
12412
|
" openteam doctor Diagnose runtimes and config",
|
|
12413
|
+
" openteam roles init Compose per-role routing policy for unprofiled roster roles",
|
|
12283
12414
|
" openteam migrate Relocate pre-P15b artifacts to their split homes (idempotent)",
|
|
12284
12415
|
" openteam agents List agents and each one's LLM (local/frontier)",
|
|
12285
12416
|
" openteam console Launch the multi-session web Console (Ctrl+C to stop; --open opens the browser)",
|
|
@@ -13211,6 +13342,178 @@ ${HELP}`
|
|
|
13211
13342
|
}
|
|
13212
13343
|
}
|
|
13213
13344
|
|
|
13345
|
+
// src/commands/rolesInit.ts
|
|
13346
|
+
var ROLES_INIT_LOCAL_ONLY = "local-only";
|
|
13347
|
+
var ROLES_INIT_FRONTIER_OK = "frontier-ok";
|
|
13348
|
+
var ROLES_INIT_SKIP = "skip";
|
|
13349
|
+
function isJsonRecord2(value) {
|
|
13350
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13351
|
+
}
|
|
13352
|
+
function serialize(value) {
|
|
13353
|
+
return `${JSON.stringify(value, null, 2)}
|
|
13354
|
+
`;
|
|
13355
|
+
}
|
|
13356
|
+
async function readJsonFile(deps, path3) {
|
|
13357
|
+
const content = await deps.readFile(path3);
|
|
13358
|
+
if (content === undefined)
|
|
13359
|
+
return {};
|
|
13360
|
+
const raw = parseConfigFileJson(path3, content);
|
|
13361
|
+
return isJsonRecord2(raw) ? raw : {};
|
|
13362
|
+
}
|
|
13363
|
+
function existingRoles(config) {
|
|
13364
|
+
const orchestrator = config.orchestrator;
|
|
13365
|
+
if (!isJsonRecord2(orchestrator))
|
|
13366
|
+
return {};
|
|
13367
|
+
const roles = orchestrator.roles;
|
|
13368
|
+
return isJsonRecord2(roles) ? roles : {};
|
|
13369
|
+
}
|
|
13370
|
+
function withRole(config, roleID, profile) {
|
|
13371
|
+
const orchestrator = isJsonRecord2(config.orchestrator) ? config.orchestrator : {};
|
|
13372
|
+
const roles = isJsonRecord2(orchestrator.roles) ? orchestrator.roles : {};
|
|
13373
|
+
return {
|
|
13374
|
+
...config,
|
|
13375
|
+
orchestrator: {
|
|
13376
|
+
...orchestrator,
|
|
13377
|
+
roles: { ...roles, [roleID]: profile }
|
|
13378
|
+
}
|
|
13379
|
+
};
|
|
13380
|
+
}
|
|
13381
|
+
function localOnlyProfile(roleID) {
|
|
13382
|
+
const base = synthesiseFallbackProfile(roleID);
|
|
13383
|
+
return {
|
|
13384
|
+
...base,
|
|
13385
|
+
preferredFrontierModels: [],
|
|
13386
|
+
localFirst: true,
|
|
13387
|
+
requiresLocalRuntime: true
|
|
13388
|
+
};
|
|
13389
|
+
}
|
|
13390
|
+
function frontierEligibleProfile(roleID) {
|
|
13391
|
+
return { ...synthesiseFallbackProfile(roleID) };
|
|
13392
|
+
}
|
|
13393
|
+
function policyChoices(roleID, agentName) {
|
|
13394
|
+
return [
|
|
13395
|
+
{
|
|
13396
|
+
value: ROLES_INIT_LOCAL_ONLY,
|
|
13397
|
+
label: rolesInitMessages.localOnlyLabel(roleID, agentName),
|
|
13398
|
+
hint: rolesInitMessages.localOnlyHint
|
|
13399
|
+
},
|
|
13400
|
+
{
|
|
13401
|
+
value: ROLES_INIT_FRONTIER_OK,
|
|
13402
|
+
label: rolesInitMessages.frontierOkLabel,
|
|
13403
|
+
hint: rolesInitMessages.frontierOkHint
|
|
13404
|
+
},
|
|
13405
|
+
{
|
|
13406
|
+
value: ROLES_INIT_SKIP,
|
|
13407
|
+
label: rolesInitMessages.skipLabel,
|
|
13408
|
+
hint: rolesInitMessages.skipHint
|
|
13409
|
+
}
|
|
13410
|
+
];
|
|
13411
|
+
}
|
|
13412
|
+
async function runRolesInit(deps) {
|
|
13413
|
+
const lines = [rolesInitMessages.header];
|
|
13414
|
+
const rosterContent = await deps.readFile(OPENTEAM_ROSTER_PATH);
|
|
13415
|
+
if (rosterContent === undefined) {
|
|
13416
|
+
lines.push(rolesInitMessages.noRoster(OPENTEAM_ROSTER_PATH), rolesInitMessages.noRosterRemedy);
|
|
13417
|
+
return { stdout: lines.join(`
|
|
13418
|
+
`), exitCode: 1 };
|
|
13419
|
+
}
|
|
13420
|
+
const parsed = parseRosterResult(rosterContent);
|
|
13421
|
+
if (!parsed.ok) {
|
|
13422
|
+
lines.push(rolesInitMessages.unparseableRoster(OPENTEAM_ROSTER_PATH, parsed.error.message), rolesInitMessages.unparseableRosterRemedy);
|
|
13423
|
+
return { stdout: lines.join(`
|
|
13424
|
+
`), exitCode: 1 };
|
|
13425
|
+
}
|
|
13426
|
+
const baseConfig = await readJsonFile(deps, DEFAULT_CONFIG_PATH);
|
|
13427
|
+
const configuredRoles = existingRoles(baseConfig);
|
|
13428
|
+
const unprofiled = parsed.roster.entries.filter((entry) => getRoleProfile(entry.roleID, configuredRoles) === undefined && !isKnownNonWorkerRole(entry.roleID));
|
|
13429
|
+
if (unprofiled.length === 0) {
|
|
13430
|
+
lines.push(rolesInitMessages.allProfiled(parsed.roster.entries.length));
|
|
13431
|
+
return { stdout: lines.join(`
|
|
13432
|
+
`), exitCode: 0 };
|
|
13433
|
+
}
|
|
13434
|
+
deps.prompt.intro(rolesInitMessages.title);
|
|
13435
|
+
deps.prompt.note(rolesInitMessages.why(unprofiled.length), rolesInitMessages.whyTitle);
|
|
13436
|
+
const runtimes = (await deps.detectRuntimes?.())?.filter((runtime) => runtime.available);
|
|
13437
|
+
let nextConfig = baseConfig;
|
|
13438
|
+
let nextOverlay;
|
|
13439
|
+
const pinned = [];
|
|
13440
|
+
const relaxed = [];
|
|
13441
|
+
const skipped = [];
|
|
13442
|
+
try {
|
|
13443
|
+
for (const entry of unprofiled) {
|
|
13444
|
+
const choice = await deps.prompt.select({
|
|
13445
|
+
message: rolesInitMessages.policyQuestion(entry.roleID, entry.agentName),
|
|
13446
|
+
choices: policyChoices(entry.roleID, entry.agentName),
|
|
13447
|
+
initial: ROLES_INIT_LOCAL_ONLY
|
|
13448
|
+
});
|
|
13449
|
+
if (choice === ROLES_INIT_SKIP) {
|
|
13450
|
+
skipped.push(entry.roleID);
|
|
13451
|
+
continue;
|
|
13452
|
+
}
|
|
13453
|
+
if (choice === ROLES_INIT_FRONTIER_OK) {
|
|
13454
|
+
nextConfig = withRole(nextConfig, entry.roleID, frontierEligibleProfile(entry.roleID));
|
|
13455
|
+
relaxed.push(entry.roleID);
|
|
13456
|
+
continue;
|
|
13457
|
+
}
|
|
13458
|
+
nextConfig = withRole(nextConfig, entry.roleID, localOnlyProfile(entry.roleID));
|
|
13459
|
+
pinned.push(entry.roleID);
|
|
13460
|
+
if (runtimes !== undefined && runtimes.length > 0) {
|
|
13461
|
+
const selected = await deps.prompt.multiselect({
|
|
13462
|
+
message: rolesInitMessages.runtimeQuestion(entry.roleID),
|
|
13463
|
+
choices: runtimes.map((runtime) => ({
|
|
13464
|
+
value: runtime.id,
|
|
13465
|
+
label: runtime.id,
|
|
13466
|
+
hint: rolesInitMessages.runtimeHint
|
|
13467
|
+
}))
|
|
13468
|
+
});
|
|
13469
|
+
if (selected.length > 0) {
|
|
13470
|
+
nextOverlay = withRole(nextOverlay ?? {}, entry.roleID, {
|
|
13471
|
+
localRuntimes: selected
|
|
13472
|
+
});
|
|
13473
|
+
}
|
|
13474
|
+
}
|
|
13475
|
+
}
|
|
13476
|
+
} catch (error) {
|
|
13477
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13478
|
+
lines.push(rolesInitMessages.cancelled(message));
|
|
13479
|
+
return { stdout: lines.join(`
|
|
13480
|
+
`), exitCode: 1 };
|
|
13481
|
+
}
|
|
13482
|
+
if (pinned.length === 0 && relaxed.length === 0) {
|
|
13483
|
+
lines.push(rolesInitMessages.nothingWritten(skipped.join(", ")));
|
|
13484
|
+
deps.prompt.outro(rolesInitMessages.nothingToWriteOutro);
|
|
13485
|
+
return { stdout: lines.join(`
|
|
13486
|
+
`), exitCode: 0 };
|
|
13487
|
+
}
|
|
13488
|
+
const merged = mergeConfigInputs(nextConfig, nextOverlay);
|
|
13489
|
+
try {
|
|
13490
|
+
loadOpenTeamConfig(merged);
|
|
13491
|
+
} catch (error) {
|
|
13492
|
+
lines.push(rolesInitMessages.invalidResult(error instanceof Error ? error.message : String(error)));
|
|
13493
|
+
return { stdout: lines.join(`
|
|
13494
|
+
`), exitCode: 1 };
|
|
13495
|
+
}
|
|
13496
|
+
await deps.writeFile(DEFAULT_CONFIG_PATH, serialize(nextConfig));
|
|
13497
|
+
if (nextOverlay !== undefined) {
|
|
13498
|
+
await deps.writeFile(DEFAULT_LOCAL_OVERLAY_PATH, serialize(nextOverlay));
|
|
13499
|
+
}
|
|
13500
|
+
if (pinned.length > 0) {
|
|
13501
|
+
lines.push(rolesInitMessages.pinnedLocal(pinned.join(", "), DEFAULT_CONFIG_PATH));
|
|
13502
|
+
}
|
|
13503
|
+
if (relaxed.length > 0) {
|
|
13504
|
+
lines.push(rolesInitMessages.frontierAllowed(relaxed.join(", "), DEFAULT_CONFIG_PATH));
|
|
13505
|
+
}
|
|
13506
|
+
if (nextOverlay !== undefined) {
|
|
13507
|
+
lines.push(rolesInitMessages.runtimeBinding(DEFAULT_LOCAL_OVERLAY_PATH));
|
|
13508
|
+
}
|
|
13509
|
+
if (skipped.length > 0) {
|
|
13510
|
+
lines.push(rolesInitMessages.skippedSummary(skipped.join(", ")));
|
|
13511
|
+
}
|
|
13512
|
+
deps.prompt.outro(rolesInitMessages.outro);
|
|
13513
|
+
return { stdout: lines.join(`
|
|
13514
|
+
`), exitCode: 0 };
|
|
13515
|
+
}
|
|
13516
|
+
|
|
13214
13517
|
// src/config/legacyMigration.ts
|
|
13215
13518
|
async function relocateNode(from, to, port, files) {
|
|
13216
13519
|
if (!await port.exists(from)) {
|
|
@@ -14656,16 +14959,18 @@ async function* linesFromReadable(readable) {
|
|
|
14656
14959
|
}
|
|
14657
14960
|
|
|
14658
14961
|
// src/orchestrator/rosterPersistence.ts
|
|
14659
|
-
function
|
|
14962
|
+
function rosterFence(roster) {
|
|
14660
14963
|
const data = { universe: roster.universe, entries: roster.entries };
|
|
14661
|
-
|
|
14964
|
+
return `\`\`\`json
|
|
14965
|
+
${JSON.stringify(data, null, 2)}
|
|
14966
|
+
\`\`\``;
|
|
14967
|
+
}
|
|
14968
|
+
function serializeRoster(roster) {
|
|
14662
14969
|
return `# openteam roster
|
|
14663
14970
|
|
|
14664
14971
|
Universe: ${roster.universe}
|
|
14665
14972
|
|
|
14666
|
-
|
|
14667
|
-
${json}
|
|
14668
|
-
\`\`\`
|
|
14973
|
+
${rosterFence(roster)}
|
|
14669
14974
|
`;
|
|
14670
14975
|
}
|
|
14671
14976
|
async function persistRoster(storage, roster, path4 = OPENTEAM_ROSTER_PATH) {
|
|
@@ -14958,7 +15263,7 @@ async function buildMemoryRuntimeFromConfig(config, deps, options = {}) {
|
|
|
14958
15263
|
// package.json
|
|
14959
15264
|
var package_default = {
|
|
14960
15265
|
name: "@jmanuelcorral/openteam",
|
|
14961
|
-
version: "0.
|
|
15266
|
+
version: "0.10.0",
|
|
14962
15267
|
packageManager: "bun@1.3.14",
|
|
14963
15268
|
description: "Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.",
|
|
14964
15269
|
license: "MIT",
|
|
@@ -15375,6 +15680,20 @@ function createSetupDeps() {
|
|
|
15375
15680
|
}
|
|
15376
15681
|
};
|
|
15377
15682
|
}
|
|
15683
|
+
function createRolesInitDeps() {
|
|
15684
|
+
const detect = createDetect(nodeExec2);
|
|
15685
|
+
return {
|
|
15686
|
+
prompt: clackPrompter,
|
|
15687
|
+
readFile: (path4) => storage.read(path4),
|
|
15688
|
+
writeFile: async (path4, contents) => {
|
|
15689
|
+
await storage.write(path4, contents);
|
|
15690
|
+
},
|
|
15691
|
+
detectRuntimes: async () => (await detect()).map((runtime) => ({
|
|
15692
|
+
id: runtime.id,
|
|
15693
|
+
available: runtime.reachable
|
|
15694
|
+
}))
|
|
15695
|
+
};
|
|
15696
|
+
}
|
|
15378
15697
|
async function main() {
|
|
15379
15698
|
const argv = process.argv.slice(2);
|
|
15380
15699
|
const configSelection = parseConfigSelection(argv);
|
|
@@ -15382,6 +15701,15 @@ async function main() {
|
|
|
15382
15701
|
const result2 = await runSetup(createSetupDeps());
|
|
15383
15702
|
if (result2.stdout.length > 0) {
|
|
15384
15703
|
process.stdout.write(`${result2.stdout}
|
|
15704
|
+
`);
|
|
15705
|
+
}
|
|
15706
|
+
process.exitCode = result2.exitCode;
|
|
15707
|
+
return;
|
|
15708
|
+
}
|
|
15709
|
+
if (argv[0] === "roles" && argv[1] === "init") {
|
|
15710
|
+
const result2 = await runRolesInit(createRolesInitDeps());
|
|
15711
|
+
if (result2.stdout.length > 0) {
|
|
15712
|
+
process.stdout.write(`${result2.stdout}
|
|
15385
15713
|
`);
|
|
15386
15714
|
}
|
|
15387
15715
|
process.exitCode = result2.exitCode;
|