@jmanuelcorral/openteam
v0.29.1
Published
Cost-aware, local-first routing plugin for opencode with cheapest-capable frontier fallback and multi-agent orchestration.
Maintainers
Readme
openteam
🌐 Language / Idioma: English (this document) · Español
openteam — operator-controlled local, frontier, and mixed model execution for opencode, with cheapest-capable automatic selection and on-demand multi-agent orchestration.
📖 Documentation site: https://jmanuelcorral.github.io/openteam/ (also available in Español)
Table of contents
- What it is and why it exists
- Features
- Requirements
- Installation
- Quickstart:
openteam setup - The
openteamagent - Try it on another machine
- Updating openteam
- Configuration
- Usage
- Runtime commands
- Safe publishing
- Privacy
- Quality
- Compatibility
- Contributing
- Roadmap
- License
- Acknowledgements
What it is and why it exists
openteam adds a routing and orchestration layer to opencode. The operator chooses
a global local, frontier, or mixed execution domain; primary and
worker policies can select an exact model or use cheapest-capable "auto"
selection inside that domain.
openteam does not inspect prompt content to infer sensitivity before a
frontier call. Frontier egress is controlled explicitly: use local globally,
or narrow an agent to local under global mixed, when frontier is prohibited.
Features
- Per-turn routing before inference: legacy
chat.messageor the native 2.x prompt/model boundary. - Registration of OpenAI-compatible local runtimes: Ollama, LM Studio, Lemonade Server, llama-swap and Foundry Local.
- Capability profiles from Models.dev, with cache and a curated fallback when the catalog is unavailable.
- Cheapest-capable automatic selection by cost, availability, tool support, and context requirements inside the allowed domain.
- Budgets per session, per month and per frontier tokens per session.
- Hash-only
CostRecordJSONL telemetry: storespromptHash, cost/decision metrics — never raw prompts. - Per-agent exact pins or
"auto", optional ordered fallbacks, and bounded cross-model handoff for an incomplete node. - Multi-agent orchestration with genuinely parented workers, explicit model selection and bounded completion/cancellation.
- Lazy local/provider inventory before routing; session-event refreshes are awaited before selecting resident models.
- A metadata-only agent timeline with durable coordinator/attempt history, historical cursor inspection, and live invalidation hints.
- A local web Console aggregating every opencode session (cost, tokens, tool calls, routing, decisions) — redacted, loopback-only.
- A read-only MCP server exposing aggregated team memory, plus opt-in semantic recall.
Requirements
- opencode 1.18.19 (legacy) or stable
>=2.0.5 <3.0.0(native adapter). Other 2.x versions are not assumed compatible. See the host compatibility boundary. - Bun
1.3.14for reproducible installs, development, and tests. - Node.js
^22.22.2 || ^24.15.0 || >=26.0.0for the published CLI/plugin. - Optional local runtimes:
- Ollama at
http://localhost:11434/v1. - LM Studio at
http://localhost:1234/v1. - Lemonade Server at
http://localhost:13305/api/v1. - llama-swap at
http://localhost:8080/v1. - Foundry Local with a dynamic port discovered via CLI on Windows.
- Ollama at
Local runtimes are optional. You can run openteam frontier-only, or point it at an Ollama / LM Studio / Lemonade Server / llama-swap running on another machine on your LAN.
Installation
Option A: npm package
The package is published as @jmanuelcorral/openteam. These are the supported installation paths:
| Need | Supported method |
| --- | --- |
| Load the plugin in opencode | Add @jmanuelcorral/openteam to the plugin array in opencode.json. opencode resolves the package; no global CLI install is required. |
| Run setup once | bunx --package @jmanuelcorral/openteam@latest openteam setup |
| Keep the openteam CLI on PATH | bun add --global @jmanuelcorral/openteam@latest, then openteam setup |
Global installation with npm is not supported. Bun is the only supported package installer. Node.js runs the published CLI/plugin within the declared engine range; npm is limited to maintainer-only pack, Trusted Publishing, and link:local operations.
GitHub Packages route: releases are published to both npmjs and private GitHub Packages. See docs/guide/install-github-packages.md for the authenticated GitHub Packages install procedure, PATH setup on zsh, and Lemonade provider verification.
For direct opencode loading, reference the package name:
{
"$schema": "https://opencode.ai/config.json",
"model": "anthropic/claude-sonnet-4-5",
"small_model": "ollama/qwen3:8b",
"plugin": ["@jmanuelcorral/openteam"]
}Those model identifiers must exist in your configured opencode providers; replace them with available equivalents when needed.
Keep operator routing policy in .opencode/openteam.json, not in the plugin
declaration. The fastest way to create both files is the interactive
openteam setup command below.
Option B: development path
During development you can load a local plugin from .opencode/plugins/openteam.ts:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["./.opencode/plugins/openteam.ts"]
}Development-path loading does not change the configuration boundary: keep
operator policy in .opencode/openteam.json.
Quickstart: openteam setup
openteam setup detects the installed local runtimes (Ollama, LM Studio,
Lemonade Server, llama-swap, Foundry Local — all optional), lets you pick local models and
the frontier baseline (sorted cheapest-capable first), and writes the
project configuration and generated agent/command artifacts:
.opencode/opencode.jsonfor a new project, or the existing.opencode/opencode.json/ rootopencode.json: declares the plugin, local providers, frontiermodel, and localsmall_model..opencode/openteam.json: openteam's validated runtime configuration (execution domain, local runtimes, primary/role model policies, budgets, and bounded retries/handoffs)..opencode/agent/openteam.md: the primaryopenteamagent, visible in the Tab switcher..opencode/agent/<role>.mdfor the standard dispatch targets, plus generated.opencode/command/*.mdentries and the local.opencode/.gitignore.
# One-off, without a global install
bunx --package @jmanuelcorral/openteam@latest openteam setup
# Or, after the supported global Bun install
openteam setupThe old
openteam initcommand still works as a deprecated alias ofopenteam setup.
Lemonade uses active models only. Downloaded-but-unloaded, transitioning or unverified models are not selectable, and an empty active list does not allow a guessed model ID. openteam does not load or unload models to make a selection work. See the policy and server-side limits.
llama-swap also requires active models. The initial integration accepts
canonical local model IDs with verified ready state and no active routing
profile, on an unauthenticated, network-restricted server. Declared context
and output ceilings feed setup's opencode compaction limits; missing tool
support is not guessed. See the llama-swap guide
for configuration and the non-atomic server-side boundary.
The wizard:
- Probes local runtimes, then first asks which ones to enable. Choosing
None selects global
frontierdirectly; it does not ask a redundant three-way mode question and proceeds to frontier model selection. - For each enabled runtime, choose this machine or another LAN host, its default/thinking/coder models, and concurrency. Multiple instances retain distinct runtime and provider identities.
- With at least one local runtime, choose the team execution mode (default
mixed).localpins the primary to the first enabled runtime's thinking model or default;frontierasks for a frontier model;mixedasks whether the primary uses that local model or the selected frontier model. Frontier choices come from Models.dev cheapest-capable ordering, with browse/custom and curated-offline fallbacks. - Setup writes an exact primary policy with
fallbacks: [], persists each worker as the compact partial policy{ "model": "auto" }, and writes the2/2retry/model limits. Primary"auto"and primary fallbacks are supported by the schema and manual config, but setup does not prompt for them. - Choose whether to enable YOLO mode. Existing config files require overwrite confirmation.
After setup, authenticate a frontier provider if the configured policies allow
frontier, make sure each configured local runtime is reachable, and open
opencode in the repo. Re-tune the baseline later with
openteam baseline set <provider/model> or openteam baseline auto.
Upgrade note:
privacyModeis deprecated, ignored metadata and no longer prevents frontier execution. Convert any required restriction torouter.executionMode: "local"or an agentexecutionMode: "local"under globalmixed.
The openteam agent
openteam setup generates a single primary agent named openteam. Press
Tab in opencode to select it: you explicitly know you are in openteam mode.
Its model follows the resolved global and primary-agent policy; the prompt does
not select a domain by being judged sensitive or complex.
The generated primary frontmatter and opencode.json.model use the same exact
bootstrap model. Generated workers intentionally omit frontmatter model; their
policies in .opencode/openteam.json are authoritative. Guardian, scribe, and
ralph are not forced local by name.
Setup creates no worker agents, generic or project-specific. Built-in role
profiles are execution defaults, not additional .md agents. For each
substantive request the primary compares the required capabilities with the
roster and existing subagents. It reuses capable members and hires only missing
specialists, preserving the existing cast. If a member merely lacks an
execution profile, registration configures that same member with model: "auto"
instead of hiring a duplicate or sending you to a terminal wizard.
Existing policies remain unchanged; new automatic policies inherit the global
execution domain. Model placement comes from these policies, not from
automatic sensitivity classification. The unified router may still derive
capability and complexity requirements inside the permitted domain. (This
on-demand, themed-team model is inspired by
Squad — see
Acknowledgements.)
The team has a themed identity: the orchestrator picks (or asks for) a universe and names each agent after a character, recording the cast in .opencode/openteam/roster.md so names persist across sessions. Besides project-specific roles, it plans three standard roles created on demand:
- scribe — silent memory: logs decisions and learnings to
.opencode/openteam/decisions.mdwithout touching code. - ralph — automation and triage: monitors the backlog, prioritizes, coordinates execution and escalates to the human on blockers or approvals.
- guardian — security review and operator-policy advice. It is not an
automatic pre-frontier content gate and has no built-in local requirement.
Configure
executionMode: "local"explicitly if the role must stay local.
Before delegating, the orchestrator plans dependencies and parallelizes: it splits the request into tasks and, since opencode runs all task calls emitted in a single turn in parallel (Vercel AI SDK), it launches independent tasks together and sequences only the dependent ones into waves. Work that collides on the same files is separated into distinct waves to avoid conflicts. In the todo list each entry shows the responsible agent via a name prefix, e.g. [@Basher] Implement the router.
Execute the whole plan
Use /execute for the agreed plan in the conversation, or
/execute docs/mvp-plan.md for a project plan. The command prepares a local
manifest and hands all dependency waves to a code-driven supervisor, including
bounded retries, authorized model handoffs, independent acceptance and
targeted corrections after a rejected review. It does not ask for another
"continue" after each successful batch.
The existing limits still apply; execution is not unlimited. Progress is
available through /execute status <planID>, and a safely stopped plan can
continue through /execute resume <planID> in a new user turn. Crashed
in-flight executions are not silently duplicated. See
commands and stop/resume semantics.
Existing installations need the updated plugin and setup-generated command,
followed by an opencode reload.
Try it on another machine
To install and try openteam on another machine that already has opencode and Bun:
Run or install the CLI. Pick one of the supported paths:
# Global option (leaves the `openteam` command on the PATH) bun add --global @jmanuelcorral/openteam@latest # One-off option (no global install) bunx --package @jmanuelcorral/openteam@latest openteam setupInitialize the repository you will work in. It generates
opencode.json,.opencode/openteam.jsonand.opencode/agent/openteam.md:cd C:\path\to\your\repo openteam setup # global # The one-off option already ran setup in the previous step.The wizard detects Ollama / LM Studio / Lemonade Server / llama-swap / Foundry Local, lets you pick the local models (local or on another machine on the LAN) or none to run frontier-only, and the frontier baseline, then writes the configuration.
Authenticate the frontier provider you chose:
opencode auth loginStart your local runtime if you enabled one and it was not already up (e.g.
ollama serve, or open LM Studio / Foundry Local). If you pointed at a runtime on another machine on the LAN, make sure it is reachable from this machine (host exposed and port open); for Ollama this usually means starting it withOLLAMA_HOST=0.0.0.0 ollama serveon the remote machine. If you configured frontier-only, skip this step.Open opencode in the repo, press Tab and pick the
openteamagent. Give it the first prompt: it creates the subagent team on demand and runs each agent within the configured execution domain.
Check the status with openteam doctor (runtimes + config) and the savings with openteam report.
Updating openteam
openteam is published to npm when a GitHub Release is published. To pull the latest version:
# Global install
bun add --global @jmanuelcorral/openteam@latestCheck versions:
bun pm ls --global # installed global packagesThe most recent published version is shown at https://www.npmjs.com/package/@jmanuelcorral/openteam.
Notes when updating:
- You do not need to re-run
openteam setupto update the plugin: opencode resolves the@jmanuelcorral/openteampackage declared inopencode.jsonto the installed version. Restart opencode so it reloads the plugin. - Re-run
openteam setupto install new generated commands such as/create_rosteror regenerate configuration (runtimes, models or baseline). Review its confirmation before overwritingopencode.json,.opencode/openteam.jsonor.opencode/agent/openteam.md; it does not overwrite or remove existing worker files, including generic files from older installations. - One-off adjustments without a re-init:
openteam baseline set <provider/model>oropenteam baseline auto.
Configuration
The canonical runtime configuration lives in .opencode/openteam.json; this
repo ships .opencode/openteam.example.json as a complete minimal v2 example,
not as a byte-for-byte contract for every setup choice. Canonical v2
serialization is compact: it omits default legacy heuristic values, retains
non-default heuristic overrides for policy-less compatibility, and compacts
resolved role profiles to partial policies relative to their built-in or
synthesized baselines. The Zod-validated shape is in src/config/schema.ts.
telemetry is a plugin option read by src/index.ts, not part of the persisted
OpenTeamConfig.
Config files are strict: unknown keys fail closed in both the CLI and plugin
instead of being silently ignored. Errors name each rejected path (for example
router.executionMod) and include a nearest-key hint when available.
This strict validation applies to the config file on both paths; programmatic
plugin options are a separate surface and are not yet strictly validated (#102).
Existing configs without configVersion remain valid because openteam migrates
them to the current version before validation.
opencode config file resolution
openteam reads the opencode project config from two candidate paths and merges them — the same way opencode itself handles multiple config files:
| Priority | Path |
| --- | --- |
| Higher (overrides) | .opencode/opencode.json |
| Base | opencode.json (project root) |
Non-conflicting keys from both files survive. When the same key appears in
both, .opencode/opencode.json wins. This is verified against opencode 1.18.18:
a provider declared in either location appears in opencode models --pure.
JSONC comments (// line comments and /* … */ block comments) are
tolerated in both files — opencode itself accepts them, and openteam strips them
before parsing, so a commented config file works identically to a plain JSON one.
Malformed config — if one of the candidate files is present but cannot be
parsed (invalid JSON, schema violation), doctor and agents emit a warning
that names the offending file and continue rather than aborting. A bad config
file is not treated as a fatal error.
"Provider not configured" messages name the two paths that were searched, so you immediately know whether the file was not found or the provider is genuinely absent from both locations.
openteam setup write location — when setting up a fresh project (no
existing opencode config file), setup now writes to .opencode/opencode.json.
When an existing file is detected it is updated in place — only one file is
written, never both. Upgrading users: a re-run of setup will land in
.opencode/ if no root opencode.json is present.
No upward traversal — openteam reads only the two cwd-relative candidates
above. It does not walk up to the git root. Commands are designed to be run from
the project root; if you run them from a subdirectory you will see "provider not
configured" and should cd to the project root first.
| Key | Type | Default | Description |
| --- | --- | --- | --- |
| configVersion | 2, optional input | canonical 2 | Missing means v1 and migrates before validation. Non-integer or below 1 is unknown; above 2 is unsupported. |
| baseline.mode | "auto" \| "pinned" | "auto" | In auto, uses cheapest-capable frontier profiles when passed to the router; in pinned, uses baseline.pinnedModel if set. |
| baseline.pinnedModel | ModelRef \| null | null | Explicitly pinned frontier model. ModelRef is { "providerID": string, "modelID": string }. |
| baseline.hardDefault | ModelRef | { "providerID": "google", "modelID": "gemini-3.5-flash" } | Operator-configurable offline/curated fallback when no usable live or catalog profile drives automatic selection. It is not preferred over supplied profiles; setup may write the selected frontier model, and explicit per-agent pins remain allowed. |
| router.executionMode | "local" \| "frontier" \| "mixed", optional | effective "mixed" | Hard global execution-domain upper bound. Canonical serialization writes "mixed" when omitted. Agent policy may narrow mixed, never broaden local or frontier. |
| router.mode | "economy" \| "balanced" \| "quality" | "balanced" | Canonical legacy heuristic setting for callers with no v2 agent policy. It is not per-agent model selection and never overrides executionMode or adds sensitivity routing. |
| router.trivialPromptMaxChars | positive integer | 280 | Legacy policy-less mixed-routing threshold. Retained for compatible callers that supply no v2 agent policy; generated v2 policies do not rely on it. |
| router.frontierPromptMinChars | positive integer | 2000 | Legacy policy-less mixed-routing threshold. Retained for compatible callers that supply no v2 agent policy; generated v2 policies do not rely on it. |
| router.localDefault | ModelRef \| null | null | Primary local model. Not set by default — no local model is routed until you configure one via openteam setup or by hand. |
| local.runtimes | LocalRuntime[] | [] | Local runtimes to probe. Empty by default — no runtime is configured until you run openteam setup or add an entry by hand. |
| local.runtimes[].id | string | required | Instance identity, unique within the array. Free-form (lmstudio-a, lemonade-lan) so two machines of the same kind are distinct; when kind is omitted it must itself be a runtime kind (back-compat). |
| local.runtimes[].kind | "ollama" \| "lmstudio" \| "foundry-local" \| "lemonade" \| "llama-swap" optional | migrates from id | Runtime kind selecting the adapter. Optional: when absent, id is treated as the kind, so pre-existing configs keep working unchanged. |
| local.runtimes[].enabled | boolean | true | Enable or disable that runtime. |
| local.runtimes[].baseURL | optional URL | per runtime/config | OpenAI-compatible endpoint (/v1 for Ollama/LM Studio/llama-swap, /api/v1 for Lemonade); Foundry Local may omit it when using discovery. |
| local.runtimes[].discovery | "cli" \| "sdk" \| "manual" optional | none per item | Foundry Local discovery; the registry uses CLI when configured and not manual. |
| local.runtimes[].defaultModel | ModelRef | required | Default model for that runtime. |
| local.runtimes[].maxConcurrency | positive integer, optional | 4 | Concurrent-generation cap for this instance (its real slot count). Applied per instance. |
| local.runtimes[].modelConcurrency | Record<modelID, positive integer>, optional | none | Per-model slot counts, because slots belong to the loaded model rather than to the server: a single Lemonade host can serve one model at --parallel 3 and another at --parallel 1 simultaneously. Each declared model gets its own queue; every other model shares the instance-wide maxConcurrency pool. When left unset, openteam auto-detects each model's slots from the runtime's live health probe at dispatch time and applies them until you declare your own (explicit config always wins); openteam setup writes the map so the split survives restarts. |
| budgets.sessionUSD | positive number, optional | unlimited | Frontier budget per session. |
| budgets.monthlyUSD | positive number, optional | unlimited | Monthly frontier budget. |
| budgets.frontierTokensPerSession | positive integer, optional | unlimited | Frontier token limit per session. |
| budgets.hardStopOnBudgetExhaustion | boolean | false | Controls the configured budget action. It never authorizes leaving the execution domain. |
| orchestrator | object, optional | absent | Holds primary/role policy and pass limits. |
| orchestrator.maxRetries | non-negative integer, optional | effective 2 | Upper bound of extra openteam passes per authorized model; effective 2 permits up to three passes. The default is applied at runtime, not materialized by the schema. |
| orchestrator.maxModelsPerNode | positive integer, optional | effective 2 | Total model candidates used by one node: the selected model plus at most one fallback by default. The runtime default is not materialized; with retry defaults it permits at most six attempts. |
| orchestrator.taskBudget.maxBatches | integer 1..1000, optional | 8 | Cumulative batches for one trusted root user turn, including nested workers. |
| orchestrator.taskBudget.maxAttempts | integer 1..10000, optional | 32 | Cumulative admitted attempts, including retries and model handoffs. |
| orchestrator.taskBudget.maxDurationMs | integer 1000..86400000, optional | 7200000 | Task deadline from the root user turn; bounds queueing, creation and prompting, with separate bounded stop-confirmation grace. |
| orchestrator.taskBudget.maxDepth | integer 0..32, optional | 3 | Maximum nested batch depth; root is depth zero. |
| orchestrator.primary | partial ExecutionPolicy, optional | absent | Policy for the primary coordinator. Every field is optional. |
| orchestrator.roles | raw Record<string, partial AgentRoleProfile> | {} when orchestrator exists | Raw entries may be partial. Loaded entries are fully merged and validated: built-in profile first, otherwise a synthesized profile, then the raw override. |
| orchestrator.roles.<role>.opencodeAgent | non-empty string, optional | role ID | opencode agent name to dispatch for that role. |
| orchestrator.primary.executionMode / orchestrator.roles.<role>.executionMode | "local" \| "frontier" \| "mixed", optional | inherited | May narrow global mixed; cannot contradict global local or frontier. |
| orchestrator.primary.model / orchestrator.roles.<role>.model | ModelRef \| "auto", optional | omitted means "auto" | Exact selection or deterministic capable/available in-domain automatic selection. Mixed mode is local-first where capable, then cheapest-capable when catalog cost exists. Static unconfigured, out-of-domain, or known-incapable pins fail validation; transiently offline candidates are skipped only at runtime. Missing costs are not treated as zero. |
| orchestrator.primary.fallbacks / orchestrator.roles.<role>.fallbacks | ModelRef[], optional | derived for auto; none for an exact pin | An explicit list replaces derived auto fallbacks and retains declared order after de-duplication and skipping the selected model. Every entry receives static identity/domain/capability validation before capping. A temporarily offline backup does not invalidate a healthy primary; it is skipped in favor of the next authorized candidate. |
| privacyMode | legacy/deprecated | none | Accepted only as ignored compatibility metadata and removed on canonical persistence. Loading it emits one privacy-mode-ignored migration warning explaining frontier-egress risk and the executionMode: "local" remedy; the notice never changes routing. |
| telemetry.enabled | boolean (plugin option) | true | Enable/disable JSONL writing. |
| telemetry.path | string (plugin option) | .opencode/openteam-local/telemetry.jsonl | Path of the CostRecord JSONL log. |
Legacy router.localOnly and router.frontierOnly remain optional parse
aliases so old files load; migration and canonical writes replace them with
executionMode. TaskSignals.privacySensitive? likewise remains an optional
historical caller field, but classification, scoring, rationale, and routing
ignore it.
Example explicit coordinator and worker policies:
Exact policy models are always objects such as
{ "providerID": "anthropic", "modelID": "claude-sonnet-4-5" }, never a
"provider/model" scalar. The slash/CLI baseline command and opencode's own
model field use the scalar notation instead. Every example model must already
exist in the operator's configured providers. A runtime instance ID such as
lmstudio-b and a provider ID such as lmstudio are distinct concepts; use
the provider identity configured for opencode.
{
"configVersion": 2,
"router": { "executionMode": "mixed" },
"orchestrator": {
"maxRetries": 2,
"maxModelsPerNode": 2,
"primary": {
"executionMode": "frontier",
"model": "auto"
},
"roles": {
"scribe": {
"executionMode": "local",
"model": { "providerID": "ollama", "modelID": "qwen3:8b" }
},
"integration": {
"executionMode": "frontier",
"model": { "providerID": "anthropic", "modelID": "claude-sonnet-4-5" },
"fallbacks": [
{ "providerID": "google", "modelID": "gemini-3.5-flash" }
]
}
}
}
}The resolved routeKind is always concrete local, frontier, or blocked,
never mixed. Only the selected model and returned fallback chain may execute.
A blocked decision does not dispatch; its selected identity is a cost/reference
value only. Budget narrowing cannot substitute an unapproved local model: if no
authorized candidate remains, routing blocks.
Usage
On each turn, openteam resolves the global and agent execution policy, filters
available models by domain, tool support and context, ranks "auto" candidates
by cost against the unified router's capability/complexity requirements, and
assigns output.message.model. Prompt sensitivity and Graph privacy metadata
are not routing inputs.
Multi-agent orchestration uses static roles and opencode subsessions:
For coordinator-owned work, the role's defaultTier and capability minima are
floors. Ordinary task capability/complexity classification may raise the
effective requirement for harder work, but it cannot weaken the role
requirement. The effective tier drives routing and telemetry. Sensitivity
vocabulary and Graph privacy metadata have no routing effect.
| Role | opencode agent | Typical policy |
| --- | --- | --- | --- |
| rusty | architect | Configure "auto" or an exact in-domain model. |
| livingston | integration | Usually requires tool support; domain is explicit. |
| yen | local-runtime | May be narrowed to local under global mixed. |
| basher | routing-cost | Auto selection may optimize cost inside the domain. |
| scribe | scribe | No built-in local restriction; set one explicitly if required. |
| linus | tester | Configure models with the required tools/context. |
runRoleTask runs a single subsession; runRoleTasks runs several with a
batchID. Direct and batched attempts use the same stable logical report path
and must prove a current, changed-when-required, substantive durable report
before success. Permission and review policy remain separate from model
placement.
SDK child dispatch uses a plugin-scoped, one-shot model authorization keyed by
the exact child session, agent alias, and model. When the public SDK prompt
re-enters chat.message, only that matching claim preserves the
coordinator-authorized worker model; the authorization is released when the
prompt settles. Unmatched and user-initiated messages still follow normal
routing. This mechanism cannot broaden an execution domain or authorize a
model outside selected plus fallbackChain.
To inspect actual model use, handoff reasons, and cost/savings estimates when
available, read the telemetry JSONL. Key fields include promptHash,
promptChars, routeKind, selected, rationale, estimatedCostUSD,
baselineCostUSD, estimatedSavingsUSD, budgetAction, tokensIn, and
tokensOut. Unknown model costs remain null/unknown rather than becoming
free; savings are unknown when either required estimate is unavailable. For
coordinator auto routing, the effective capability requirement—the role floor
plus any upward task classification—determines both candidate capability and
the capable frontier baseline used for cost comparison.
Runtime commands
openteam exposes commands to inspect and change routing without editing JSON by hand, over three equivalent surfaces.
1. In-conversation tool
The plugin registers the openteam tool. Ask the agent to use it, or install the /openteam command. Actions:
show— show the effective baseline.set(withmodelasprovider/model) — pin the baseline (pinnedmode).auto— return to cheapest-capable (automode).doctor— diagnostics for execution policy, model availability, and telemetry.report— available cost and savings estimates from telemetry; unknown values remain unknown.
set / auto changes are persisted to .opencode/openteam.json and apply on the next plugin start.
2. Slash command /openteam
openteam setup automatically generates .opencode/command/openteam.md, so /openteam appears in opencode with no manual steps:
/openteam baseline show
/openteam baseline set anthropic/claude-sonnet-4-5
/openteam baseline auto
/openteam doctor
/openteam agents
/openteam console
/openteam reportDon't see
/openteam? Make sure.opencode/command/openteam.mdexists (created byopenteam setup) and restart/reload opencode to discover it.
Compose the team without starting work
/create_roster
/create_roster Build a booking API with a web dashboard and integration testsSetup also installs .opencode/command/create_roster.md, explicitly bound to
the openteam primary. With a description it composes from that goal; without
one it inspects the existing codebase. If there is no meaningful source, it
asks what you want to build and waits before writing anything.
The workflow reuses or extends the existing cast without overwriting customized
agents, registers .opencode/openteam/roster.md through the validated tool,
and creates missing .opencode/agent/<agentName>.md subagents. Registration
also initializes missing custom worker profiles in .opencode/openteam.json
with model: "auto"; it preserves existing profiles, the primary policy,
machine-local overrides and permissions. Worker filenames follow the roster's
aliases: scribe -> c3po needs c3po.md, not an additional scribe.md.
It verifies the persisted team without
starting implementation or distributing tasks. Ordinary work requests use the
same preparation, then continue with the requested task.
openteam roles init remains an optional compatibility command for legacy
unprofiled workers, not a required onboarding step. Rerun setup after updating
and reload opencode to discover the command; see the
commands guide.
Every team creation or extension, including automatic creation, must show the
registration tool's Agent / Role / Model table for the whole team before
work starts. It includes the primary and guaranteed roles. Exact model policies
show provider/model and any configured fallbacks; auto explicitly means the
concrete model will be selected when a task is dispatched, not a guessed model.
After the successful /create_roster summary, the primary asks whether you want
to reload opencode to discover the agents. Only an explicit affirmative answer
requests a project-instance reload; declining or not answering leaves the
prepared team in place without reloading. Host permissions still apply.
The request has a 30-second limit, including permission approval, and waits
for the confirming turn and other active sessions to finish. A new turn in any
project session cancels it. It does not restart the process, delete saved
conversation history, or authorize implementation work.
3. CLI (bin)
The package exposes the openteam binary. With a global install:
bun add --global @jmanuelcorral/openteam@latest
openteam baseline show
openteam baseline set anthropic/claude-sonnet-4-5
openteam doctor --config .opencode/openteam.json
openteam agents
openteam console
openteam report --telemetry .opencode/openteam-local/telemetry.jsonl
openteam yolo status
openteam local status
openteam local only
openteam local off
openteam local on
openteam upgrade
openteam upgrade --check
openteam upgrade --version <x.y.z>
openteam clear-cache
openteam clear-cache --deleteFlags: --config <path> and --telemetry <path> override the default paths; --opencode <path> points the yolo command at a specific opencode.json. An explicit --config may be relative or absolute and must be readable; only the default .opencode/openteam.json lookup falls back to built-in defaults when absent.
See each agent's LLM: openteam agents
Lists the agents in .opencode/agent/, showing their configured model and
provider. Agents without their own model inherit the opencode.json default.
That inherited/default model is not permission to escape the resolved
execution domain; effective primary/role policies in .opencode/openteam.json
remain authoritative.
YOLO mode (auto-approve permissions)
YOLO mode makes opencode auto-approve all permissions (like the native opencode --auto flag, but persistent). It writes the global rule permission: { "*": "allow" } to opencode.json and regenerates the openteam agent with the same wildcard:
openteam yolo status # is it active?
openteam yolo on # enable YOLO
openteam yolo off # disable YOLOThe agent permission overrides the global one. opencode merges each agent's frontmatter permission after the global one and the last matching rule wins, so
openteam yolo on/offregenerates.opencode/agent/openteam.mdto keep itspermissionin sync. After changing mode, restart or reload opencode.
YOLO does not change the execution domain. It only affects opencode permission approvals, and it does not add a sensitivity screen. In
frontierormixed, prompt content may be sent to frontier. Use it only in trusted environments.
Routing mode: openteam local
Use openteam local to switch between the three global execution domains without
re-running setup:
openteam local status # mixed, local, or frontier? + configured runtimes
openteam local off # frontier
openteam local only # local-only (never escalate to frontier)
openteam local on # mixedThe commands persist router.executionMode. local only is a hard upper
bound: no primary/worker pin or fallback can reach frontier, and the command is
rejected without an enabled local runtime. local off needs no local runtime.
local on allows both domains, and each agent may narrow itself to one of them.
Before any mode change is written, exact primary, role, and fallback policies
are validated against the proposed domain; a conflict rejects the command and
leaves config unchanged.
Every write removes legacy localOnly, frontierOnly, and root privacyMode;
local status shows the effective mode and enabled runtime identities.
Upgrading the plugin pin: openteam upgrade
openteam setup now writes an exact version pin derived from the installed
CLI version (e.g. @jmanuelcorral/openteam@<version>).
Because opencode's cache is keyed by the full spec string, each distinct pin
creates a new cache directory — the next opencode restart installs fresh
rather than serving a stale entry.
Use openteam upgrade to advance that pin without re-running setup:
openteam upgrade # update to latest published stable
openteam upgrade --check # read-only: show what would change
openteam upgrade --version <x.y.z> # pin to a specific published versionAfter running, restart opencode for the new pin to take effect. Clearing
stale cache entries with openteam clear-cache --delete is optional — new
pins create a new cache entry and the old one is simply unused.
upgrade does not delete cache entries, install or update the global CLI,
restart opencode, rewrite generated agent files, or change provider limits.
See docs/guide/update-plugin.md for the full workflow.
Updating the plugin and clearing the cache: openteam clear-cache
opencode caches each plugin installation and never refreshes an existing entry — there is no version comparison, TTL, or upstream eviction command. Publishing a new version of the plugin has no effect on a machine that already has a cache entry. Deleting the cache directory is the only eviction path.
openteam clear-cache # report only — prints the table, deletes nothing
openteam clear-cache --delete # delete all matched cache entries
openteam clear-cache --cache-root <path> # override the auto-resolved cache rootSlash command alias inside opencode: /openteam-clear-cache
The default (no flags) prints a table with columns spec dir, spec-pinned,
installed and mtime for every @jmanuelcorral/openteam* cache entry found.
Only entries in that scope are ever touched. Symlinks and Windows reparse points
are listed as [SKIP — reparse point] and never deleted. On Windows, close
opencode before running --delete to avoid [LOCKED] errors.
See docs/guide/update-plugin.md for the full explanation of the freeze mechanism and a step-by-step update procedure.
Iterative work under openteam-orchestrate 🎯
Unattended, backlog-style work is driven entirely from orchestration. The
current model gets orchestrator.maxRetries extra passes (effective runtime
default 2, so up to three passes per model). Persistent incompleteness uses
that model's pass budget before handing the same node to the next authorized
model. A recognized retry-safe prompt transport/capacity failure
(prompt-timeout, prompt-rate-limited, prompt-connection,
prompt-server, or prompt-context-length) is retried on that same model
within its pass allowance; create-stage retry recognizes the same transport
classes except context length. maxRetries is an upper bound, not a required
repeat floor: after confirmed stop, an eligible transport failure may consume
the remaining same-model allowance or hand off earlier to the next authorized
candidate. Auth,
content-filter, validation/invalid-model, generic rejection, and missing-session
or unclassified failures are terminal. A candidate found unavailable before
dispatch may be skipped because no attempt started. maxModelsPerNode has the
effective runtime default 2; together the defaults cap a node at up to six
openteam passes. Neither default is materialized by the schema. This is not a
guaranteed pass/HTTP-call count or a statement about provider-internal SDK
retries.
The batch, objective, report path, and durable work state are preserved. An
exact pin without fallbacks authorizes no second model.
Related and nested batches also share a persisted task allowance: by default
8 batches, 32 attempts, 2 hours and nesting depth 3. Configure the optional
orchestrator.taskBudget block and restart opencode to change future-task
limits. Another model-authored call or plugin reload cannot reset the task.
A new root user turn starts a new task; old workers keep their old allowance.
Exhaustion is explicit, not a successful completion.
Workers prefer openteam-submit-report with status and bounded,
substantive findings. The coordinator assigns its only writable report path
and preserves operator permission checks. Same-model report repair continues
in the same worker session without another full implementation restart.
Confirmed length-ended invalid native tool input requests smaller fresh calls
within existing limits; malformed JSON is never repaired and executed.
See the reliability decision.
Every pass, including a single pass with maxRetries: 0, needs a standalone
openteam-status: done declaration and a durable report with at least 40
substantive body characters after actual declaration lines are removed. The
last standalone declaration outside fenced code wins; quoted or fenced examples
remain body text and cannot declare completion. A marker alone is not
completion. The coordinator reads the report text only to parse that status and
test substance; it does not copy the body into role results, routing telemetry,
diagnostics, or the reference-only projection.
After a timed-out or otherwise thrown/uncertain prompt dispatch, the prior SDK
request must settle and server stop must be confirmed before any same-model
retry or model handoff. After a successful abort, bounded polling confirms stop
when the exact session is idle or is absent from a valid sparse status
snapshot; malformed/missing status payloads and sessions that remain busy or
retry block continuation. Status evidence alone does not replace request
settlement. Exhaustion is terminal failure/incomplete, and dependents do not
run. See
docs/guide/commands.md.
Multi-session web Console: openteam console
openteam exposes a local web Console you launch from the CLI with openteam console. It aggregates all opencode sessions at once (one tab per session, plus "All"), because each plugin writes redacted events to per-session JSONL logs that the Console watches (fs.watch) and aggregates.
It shows live: recorded token usage and cost estimates per message (input/output/reasoning/cache), tool calls with duration and ok/fail, recent routing (cost/savings and chosen model — prompt hashes only, never text), agent decisions and meetings, orchestration progress, the team, and commits. These controls and estimates are finite/auditable, but they are not provider invoice accounting.
Agent tree includes the operation graph, real chronology/replay, filters, canvas controls and a default-readable inspector. Missing/unverified usage is shown as unavailable / partial, not confirmed zero. Unknown tool duration stays unknown, and routing estimates are distinct from measured usage.
Conversations are visible by default. Active Agent tree and Logs views
automatically show operator prompts, visible agent responses and recorded tool
interactions, with live updates. Reads use opencode's current session history,
not prompt copies in telemetry. No diagnostic flag or per-agent reveal is
required; explicit console.sessionHistory.contentEnabled: false keeps a
metadata-only view. Content access does not enable terminal input or a PTY.
Each session tab also includes a "Session" view: a console where you can watch the live output (SSE) and send prompts or answer permissions for that opencode session, authenticated with an ephemeral token per launch. Optionally, with the terminal.pty flag, a "Terminal" view opens a real shell in the workspace via opencode's PTY API (no node-pty).
openteam console # print the URL and serve the multi-session Console
openteam console --open # also open the browser
openteam console --status # print the config only (don't launch the server)See the Console guide for the full reference.
Sessions are read from:
.opencode/openteam-local/sessions/*.jsonlYou can tune host/port and the terminal in .opencode/openteam.json (console
block):
{
"console": {
"host": "127.0.0.1",
"port": 4599,
"autoPortFallback": true,
"refreshMs": 2000,
"recentRoutes": 50,
"openBrowser": false,
"terminal": { "enabled": true, "pty": false }
}
}Agent tree and timeline. console.graphView.enabled defaults to true.
Native lifecycle history is composed independently of Graph execution
authority; the separate Graph DAG follows the effective graph.mode. See
Agent timeline and
Graph observability web.
Security: the server listens on loopback only (
127.0.0.1/localhost, Zod-validated). Conversation reads require the ephemeral token and exact-workspace validation, mask known credential patterns and useno-store. Telemetry/lifecycle/state remain reference-only; conversation text is not copied into them. Session input and PTY retain their separate permissions. If the port is busy andautoPortFallbackistrue, the Console tries the next free one. Remote access viaopenteam tunnelwas removed from the MVP (2026-08-31); there is no built-in remote-exposure path.
Interface stability. The Console's web surface is pre-1.0: its UI, endpoints, console config block and event format may still change. The routing, budgets, telemetry and memory core are stable.
Team memory over MCP (read-only): openteam mcp
openteam mcp starts a read-only MCP server (JSON-RPC 2.0 over stdio, no dependencies) that exposes the team memory aggregated from per-session events. It uses the same StorageProvider as the Console and never exposes raw prompts. Tools: list_decisions (filterable by agent/tag), list_meetings, list_sessions and cost_summary (recorded usage and available cost/savings estimates against the all-frontier baseline, with unknown costs retained as unknown; not an invoice).
Register it in opencode.json like any local MCP server:
{
"mcp": {
"openteam-memory": { "type": "local", "command": ["openteam", "mcp"] }
}
}The plugin's openteam-memory write tool is separate from this read-only
MCP server and from the general scribe role's model policy. Its extraction
transport is explicitly local-only and requires a reachable configured local
runtime. Global frontier execution refuses the memory-model call; the tool
does not add a frontier memory path or silently fall back to frontier.
Semantic recall (opt-in): recall_facts, recall_preferences, similar_tasks
Beyond team memory, openteam mcp can expose semantic recall of the long-term knowledge memory (facts, preferences, similar tasks). It is disabled by default; enable it with the memory.semantic block in .opencode/openteam.json:
{
"memory": {
"semantic": { "enabled": true }
}
}When enabled, and if there is a local runtime with a baseURL, the server adds three tools: recall_facts, recall_preferences and similar_tasks. The query is embedded locally (never leaves for frontier) and records are redacted at the source (storeContent=false by default), so the tools never return raw prompts. If bun:sqlite is missing or the index cannot be opened, openteam mcp degrades safely and keeps exposing team memory only.
Consolidation / forgetting: openteam memory consolidate
Long-term memory forgets by decay: each record's confidence decays over time and, once it falls below the threshold, it is marked soft-invalidated (without deleting: the history is kept for "as-of" auditing). Run the forgetting pass with:
openteam memory consolidateIt appends tombstones to the memory log and reports how many records were forgotten. It is idempotent and non-destructive.
Automatic push to context (opt-in)
With injection enabled, openteam prepends relevant memory to the prompt on each turn (chat.message), respecting a token budget:
{
"memory": {
"semantic": {
"enabled": true,
"injection": { "enabled": true, "maxChars": 1200, "maxItems": 8 }
}
}
}It is disabled by default. Injection happens after the model is chosen, so it does not alter routing or telemetry, and it never breaks the turn if recall fails. It only injects what is already in the records (redacted at the source if storeContent=false).
Ollama
ollama serve
ollama pull qwen3:8b
Invoke-RestMethod -Uri "http://localhost:11434/v1/models" -Method GetLM Studio
- Open LM Studio.
- Load a compatible model.
- Enable the local OpenAI-compatible server on port
1234.
Invoke-RestMethod -Uri "http://localhost:1234/v1/models" -Method GetLemonade Server
Invoke-RestMethod -Uri "http://localhost:13305/api/v1/models" -Method GetFoundry Local
foundry service statusopenteam extracts the port from the CLI output and normalizes the endpoint to http://localhost:<PORT>/v1.
llama-swap
Prepare the intended model with llama-swap before setup. These calls only read the catalog, readiness and profile state:
Invoke-RestMethod -Uri "http://localhost:8080/v1/models" -Method Get
Invoke-RestMethod -Uri "http://localhost:8080/running" -Method Get
Invoke-RestMethod -Uri "http://localhost:8080/api/profiles" -Method GetSee the llama-swap guide for ready-only selection, declared context/output limits and supported transport boundaries.
Telemetry
openteam writes a hash-only CostRecord JSONL log (never raw prompts). Tail the most recent entries:
Get-Content -Path ".opencode/openteam-local/telemetry.jsonl" -Tail 5Local verification
bun test
bun run typecheck
bun run lint
bun run format:check
bun run buildLocal development in dev mode (no registry)
To make the global openteam CLI and opencode use this repo's local build instead
of the published version (without going through any registry), the package is
linked with a global symlink:
bun run link:local # = bun run build && npm linknpm link creates a global link (junction/symlink) to this repo, so after each
bun run build the global CLI reflects the local code.
A versioned pre-push hook (.githooks/pre-push) can run bun run link:local
before each git push, keeping the local install current. Installing
dependencies does not modify Git. Enable the hook explicitly, once per clone:
bun run hooks:installThe hook warns if the build or the link fails, but it does not block the push.
Safe publishing
The release workflow builds and verifies a single tarball with no write
permissions and no OIDC. Another job publishes exactly that artifact, after
validating its integrity, via npm Trusted Publishing; a final job with only
contents: write attaches it to the GitHub Release. There is no fallback to
persistent credentials, and normal CI needs no npm configuration.
Before the first real release, the owner must register an npm Trusted Publisher
for @jmanuelcorral/openteam with owner jmanuelcorral, repository openteam,
workflow release.yml, allowed action npm publish and no environment. Until
that one-time registration is complete, publishing fails closed. After the first
successful OIDC publish, enforce token-less 2FA, revoke the old automation token
and remove its inherited GitHub Actions secret.
Privacy
openteam does not detect prompt sensitivity or promise to intercept sensitive
content before a frontier call. privacyMode is deprecated and has no routing
authority. frontier and mixed deliberately permit frontier egress under the
configured agent policy. To prohibit it, configure
router.executionMode: "local" globally, or narrow the relevant agent to
local under global mixed.
Credential redaction, prompt-hash telemetry, reference-only shared storage,
loopback/access controls, and finite budget/model/pass limits remain in force.
Telemetry uses hashPrompt (currently fnv1a32:<hash>) and never serializes the
full prompt. See SECURITY.md for vulnerability reporting.
Quality
Non-negotiable gates (see docs/testing-and-quality.md):
bun test- Global coverage: lines/functions
>= 85%. - Coverage of
src/router/**andsrc/memory/**:100%lines and functions (enforced bybun run coverage:check). bun run typecheckbiome check .(orbun run lint+bun run format:check)- Router determinism test.
- Contract tests when opencode hooks, agents, commands, providers or the SDK change.
Compatibility
See docs/compatibility.md. Verified summary as of 2026-09-22:
| Surface | Version / contract |
| --- | --- |
| openteam | 0.29.1; current package version |
| opencode host | 1.18.19 legacy; stable >=2.0.5 <3.0.0 native support |
| @opencode-ai/plugin | 1.18.19 |
| @opencode-ai/sdk | 1.18.19 |
| Routing hook | Legacy chat.message; native prompt + session.switchModel; never chat.params |
| Child sessions | Legacy SDK or native scoped Session service; actual parenting and explicit model |
Contributing
Contributions are welcome. Please read CONTRIBUTING.md for the development setup (Bun), the non-negotiable quality gates, the module boundaries (router/ is pure — no I/O, clock, randomness or logging) and the pull-request process. By participating you agree to abide by our Code of Conduct.
Roadmap
| Phase | Status | Deliverables |
| --- | --- | --- |
| 0. Scaffold / PoC | Done | TS/Bun, minimal plugin, chat.message hook. |
| 1. Local registry + router | Done | Ollama/LM Studio/Lemonade/Foundry, probes and deterministic chooseModel. |
| 2. Profiles + budget + telemetry | Done | ModelCapabilityProfile, budgets and CostRecord. |
| 3. Multi-agent orchestration | Done | Coordinator, roles, child sessions and permissions. |
| 4. npm packaging + docs | Done | Exportable package, README, Windows examples and compatibility matrix. |
| 5. Runtime commands | Done | baseline show/set/auto, doctor and report via tool, /openteam and CLI bin. |
| 6. Web Console + MCP memory | Done | The multi-session Console is loopback-only and gate-covered; read-only MCP server and semantic recall are stable. |
| Agent-stack MVP | In progress | Config v2 execution domains, per-agent model policies, bounded model handoff, removal of sensitivity routing, and retirement of duplicate Graph-local policy resolution. Unified router capability/complexity routing remains. LangGraph migration issues remain separate and pending. |
License
MIT © Jose Manuel Corral (@jmanuelcorral).
Acknowledgements
openteam stands on the shoulders of prior work. These projects shaped its design — we took inspiration from them, adapted the ideas to opencode and reimplemented them from scratch in TypeScript:
- Squad (bradygaster/squad) — the on-demand, themed multi-agent team model: a single orchestrator that casts specialist subagents as the work requires them. It inspired openteam's
openteamorchestrator agent and the team/roster convention. - Superpowers by Jesse Vincent — a composable, skill-driven methodology for coding agents. It inspired openteam's skill-based agent conventions and the spec-first / subagent-driven workflow.
- agent-memory-dotnet by José Luis Latorre — a semantic long-term memory model for agents. It inspired openteam's memory subsystem (extract → rank → consolidate → recall) with local-first, hash-redacted storage.
All trademarks and project names belong to their respective owners; the links above are provided for attribution and are not endorsements.
