Agents
View as markdownAn agent is a role: a system prompt plus a skill set, living under
<config-root>/agents/<name>/ (<config-root> is ~/.shelbi/projects/<id>/
in global mode and <repo>/.shelbi/ in in-repo
mode). 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. An agent you create with
shelbi agent new gets only instructions.md and an empty
skills/: it has no agent.yaml until you add one, so it resolves through the
built-in defaults.
Where it lives
<config-root>/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"| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | yes | — | Package 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. |
version | string | no | — | Manifest version (semver). Advisory this milestone. |
description | string | no | — | Human/marketplace-facing summary. When set it is the preferred display label (falling back to the directory name). |
preferred_runner | RunnerKind | no | claude | Which runner kind the agent recommends by default. A recommendation only: the project's agents.<name>.runner wins. |
runners | map of RunnerKind → { model?, reasoning_effort? } | no | {} | Per-kind model + effort the agent recommends. Both optional and resolved field by field. |
permissions_mode | string | no | — | The permission posture the agent requests. Clamped down to the project ceiling, never escalated (see permissions). |
requires | { runner_kinds?, shelbi? } | no | — | Compatibility 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.
| Kind | Adapter |
|---|---|
claude | Claude Code. --model and --effort flags; --append-system-prompt wiring. |
codex | Codex. --model and -c model_reasoning_effort="…"; the max level maps to Codex xhigh. |
generic | Any 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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
command | string | yes | — | Executable to invoke. |
flags | list of string | no | [] | Extra flags appended to every invocation. |
dialog_signatures | list of object | no | built-in set | Blocking-dialog signatures, as in agent_runners. |
prompt_injection | { kind, flag? } | no | built-in per executable | How the initial prompt reaches the runner, as in agent_runners. |
integration | claude | codex | generic | no | detected from command | Pins the adapter for a wrapper executable whose basename Shelbi wouldn't recognize, as in agent_runners. |
model | string | no | — | House model for every agent of this kind. When set it is authoritative; when omitted, each agent's manifest recommendation applies. |
reasoning_effort | low | medium | high | max | no | — | House 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 agentKind 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 manifestResolution and precedence
Two chains resolve per dispatch, both highest to lowest. The top tier is the
task itself: a launch: block in the task's frontmatter (runner, model,
effort, permission_mode, each optional) overrides the project and the
manifest for that one task.
Runner kind (which kind an agent runs on):
task launch.runner -> project agents.<name>.runner -> agent.yaml preferred_runner -> built-in default (claude)Model / effort (the values for the resolved kind, each field independent):
task launch.{model,effort} -> project runners.<kind>.{model,reasoning_effort} -> agent.yaml runners.<kind>.{model,reasoning_effort} -> built-in default (none)# a task's frontmatter: run just this task on Opus at max effort
launch:
model: claude-opus-4-8
effort: maxEach 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 default, full-access becomes
bypassPermissions; auto / default / plan / acceptEdits pass through unchanged.
An unrecognized mode ranks as default, so a typo can never widen the ceiling.
A task's launch.permission_mode is a request too: it wins over the manifest's
permissions_mode, and both are clamped by the same ceiling, so a task can only
tighten the project's mode, never loosen it.
Editing from Settings
You don't have to edit these files by hand. Settings ▸ Agents, in the desktop
app (⌘6 while Settings is open) and in the TUI (press , in the sidebar, then
6), lists the project's agents with the workflow statuses each one runs. From
there you can create, duplicate, rename, and delete an agent, and edit one's
description, instructions, preferred runner, model, reasoning effort,
permissions mode, and skills, plus the shared instructions every agent's prompt
starts with. agent.yaml is saved through a patch that keeps its comments and
unknown keys, and Reset to default restores a shipped agent. If a file changed
on disk since the page read it, nothing is written.
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.