Skip to content

Concepts

Prompt Architecture

Brute’s default system prompt is not one string — it is a stack of small prompt modules selected per provider and rendered at turn time. Brute::Middleware::SystemPrompt prepends the result as the :system message (unless one already exists in the log).

Brute::SystemPrompt.build stores a block; it runs only when prepare receives a runtime context:

sp = Brute::SystemPrompt.build do |prompt, ctx|
prompt << Brute::Prompts::Identity.call(ctx)
prompt << Brute::Prompts::Environment.call(ctx)
end
sp.prepare(provider_name: "anthropic", model_name: "claude-...", cwd: Dir.pwd)
# => #<SystemPrompt::Result> — .to_s (joined) or .sections (one per module)

Deferral matters because modules adapt to the context: which model family is running, what the working directory holds, which skills were discovered. Nothing is frozen at agent-construction time.

Brute::SystemPrompt.default picks an ordered module list from STACKS by provider — "anthropic", "openai", "google", with "default" as the fallback. Gateway providers (opencode_*) infer the upstream model family from the model name, so a claude-* model behind a gateway gets the Anthropic stack.

The Anthropic stack, for illustration: Identity, ToneAndStyle, Objectivity, TaskManagement, DoingTasks, ToolUsage, Conventions, GitSafety, CodeReferences, Environment, Skills, Instructions. Other providers get leaner variants; several modules also carry per-provider text templates under lib/brute/prompts/text/ (e.g. identity/anthropic.txt, identity/google.txt) so even shared sections speak each model’s dialect.

After the base stack, the default builder appends reminders based on runtime state:

Condition Module
ctx[:agent] == "plan" Prompts::PlanReminder
ctx[:agent_switched] == "build" Prompts::BuildSwitch
ctx[:max_steps_reached] Prompts::MaxSteps

A module is just .call(ctx) -> String-or-nil

Section titled “A module is just .call(ctx) -> String-or-nil”

Every prompt module answers one class method. Prompts::Skills is typical — its ERB template renders only each skill’s name, description, and location into <available_skills> (the body loads later via the skill tool), reading skills from ctx[:skills] or scanning ctx[:cwd].

Pass your own to the middleware — built from any mix of Brute modules and your own text:

sp = Brute::SystemPrompt.build do |prompt, ctx|
prompt << "You are a release engineer for #{ctx[:cwd]}."
prompt << Brute::Prompts::GitSafety.call(ctx)
prompt << Brute::Prompts::Skills.call(ctx) # scans cwd when no ctx[:skills]
end
Brute.agent.use(Brute::Middleware::SystemPrompt, system_prompt: sp)