An agent is a role: a system prompt plus a skill set, living under ~/.shelbi/projects/<id>/agents/<name>/. Alongside its instructions.md, skills/, and (Claude-only) settings.json, each agent carries a runner-agnostic manifest at agent.yaml that declares its recommended runner and model. The consuming project overrides those recommendations to fit its own runner fleet.

shelbi init scaffolds a self-documenting agent.yaml for every default agent, with the identity active and every optional knob commented out beneath a pointer back to this page.

Where it lives

~/.shelbi/projects/<id>/agents/<name>/
  agent.yaml        # the manifest (this page)
  instructions.md   # system prompt
  skills/           # agent-scoped skills
  settings.json     # Claude Code hooks (Claude only)

A missing agent.yaml is non-fatal: resolution falls back to the built-in defaults. A present-but-malformed manifest is a hard error, so a typo can never silently launch on the wrong model.

agent.yaml

name: adversarial-reviewer          # package identity (stable across installs)
version: 1.2.0                      # manifest version (semver); advisory today
description: Adversarial code review before a human sees the branch
 
preferred_runner: claude            # which runners: block is the default (a KIND)
runners:                            # per-runner-kind recommendations
  claude:
    model: claude-opus-4-8          # logical model id, applied per adapter
    reasoning_effort: high          # low | medium | high | max
  codex:
    model: gpt-5
    reasoning_effort: high
 
permissions_mode: read-only         # a request; the project caps it (never up)
 
requires:                           # compatibility hints (advisory this milestone)
  runner_kinds: [claude, codex]     # kinds this agent can run on
  shelbi: ">=0.8"
FieldTypeRequiredDefaultDescription
namestringyesPackage identity, independent of the install directory. Must be non-empty. The directory name is how a project addresses the agent; name is how a marketplace would.
versionstringnoManifest version (semver). Advisory this milestone.
descriptionstringnoHuman/marketplace-facing summary. When set it is the preferred display label (falling back to the directory name).
preferred_runnerRunnerKindnoclaudeWhich runner kind the agent recommends by default. A recommendation only: the project's agents.<name>.runner wins.
runnersmap of RunnerKind{ model?, reasoning_effort? }no{}Per-kind model + effort the agent recommends. Both optional and resolved field by field.
permissions_modestringnoThe permission posture the agent requests. Clamped down to the project ceiling, never escalated (see permissions).
requires{ runner_kinds?, shelbi? }noCompatibility hints. runner_kinds lists the kinds the agent can run on; shelbi is a semver range for the host. Advisory this milestone; enforced at install in a future marketplace flow.

Validation runs at parse time: name must be non-empty, and when both preferred_runner and requires.runner_kinds are set the preferred kind must appear in that list (an agent cannot recommend a runner it declares it cannot run on).

The preferred_ prefix is deliberate and lives only on the agent side: the agent recommends (preferred_runner), the project decides (runner).

Runner kinds

preferred_runner, requires.runner_kinds, the project runners fleet, and agents.<name>.runner all name a canonical runner kind, never a project-local runner key. This keeps a distributable agent portable across projects that configure their fleets differently.

KindAdapter
claudeClaude Code. --model and --effort flags; --append-system-prompt wiring.
codexCodex. --model and -c model_reasoning_effort="…"; the max level maps to Codex xhigh.
genericAny other runner. No model/effort contract Shelbi drives.

A model like claude-opus-4-8 is a logical value; turning it into a launch flag is the resolved kind's adapter's job. Because model and effort live inside each runners: block, they travel together per kind: overriding an agent from Claude onto Codex selects the codex block (its own model + effort) rather than leaving a stale Claude model behind.

Project runners

The project's runner fleet is a single top-level runners: map keyed by runner kind. Each entry is the concrete launcher (the same fields as an agent_runners entry, the fleet's evolution) plus an optional house model / reasoning_effort.

FieldTypeRequiredDefaultDescription
commandstringyesExecutable to invoke.
flagslist of stringno[]Extra flags appended to every invocation.
dialog_signatureslist of objectnobuilt-in setBlocking-dialog signatures, as in agent_runners.
modelstringnoHouse model for every agent of this kind. When set it is authoritative; when omitted, each agent's manifest recommendation applies.
reasoning_effortlow | medium | high | maxnoHouse effort for every agent of this kind. Same field-level override rule as model.
# project.yaml
runners:
  claude:
    command: claude
    # model / reasoning_effort omitted -> each agent's manifest decides
  codex:
    command: codex
    model: gpt-5          # authoritative for every codex agent

Kind to concrete-runner is a direct lookup: exactly one entry per kind, no indirection. When a kind has no runners entry, resolution bridges to the legacy agent_runners map keyed by the kind's canonical name, so a project predating the fleet keeps working with no manual migration.

Per-agent pins

The agents: map pins which runner kind the project runs a given agent on, keyed by the agent's directory name. This is authoritative over the manifest's preferred_runner. There is no per-agent model block: per-agent differentiation comes from each agent's manifest, and a house model is set once per kind in runners.

# project.yaml
agents:
  orchestrator: { runner: codex }   # this role runs on the codex kind
  review:       { runner: claude }  # claude kind; model from review's manifest

Resolution and precedence

Two chains resolve per dispatch, both highest to lowest.

Runner kind (which kind an agent runs on):

project agents.<name>.runner  ->  agent.yaml preferred_runner  ->  built-in default (claude)

Model / effort (the values for the resolved kind, each field independent):

project runners.<kind>.{model,reasoning_effort}  ->  agent.yaml runners.<kind>.{model,reasoning_effort}  ->  built-in default (none)

Each field resolves independently: a project runners.claude block that sets model but omits reasoning_effort overrides only the model; effort still falls through to the agent's manifest. So to keep two Claude agents on different models (review on Opus, developer on Sonnet), leave runners.claude.model unset and let each manifest decide; to impose one house model on a kind, set it once at the fleet level.

Model-in-flags stays supported: an agent_runners entry that bakes --model into flags keeps working, and a resolved manifest model takes precedence, the stale flag being replaced so the launch line carries exactly one model.

Permissions are a ceiling

Unlike model (taste), permissions_mode is a security boundary, so it resolves with the opposite asymmetry: the project's workspace_permissions_mode is a ceiling. An agent may request equal-or-tighter and gets it; a request looser than the ceiling is clamped down, never granted. The plan's generic names map onto Claude's modes at launch: read-only becomes plan, full-access becomes bypassPermissions; auto / default / acceptEdits pass through unchanged. An unrecognized mode ranks as default, so a typo can never widen the ceiling.

Migration from Workspace.runner

Earlier versions selected a runner per workspace slot (workspaces[].runner). That selector is removed: a workspace no longer picks a runner, and runner/model resolution is owned by the agent and project layers described here. A legacy runner: key on a workspace is accepted-and-ignored on load and dropped on the next save, so an existing project migrates with no manual step. This milestone assumes a uniform fleet (every machine has every runner kind installed); a machine/workspace capability declaration for heterogeneous fleets is a filed follow-up.