brute 0.1.8 → 0.2.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.
- checksums.yaml +4 -4
- data/lib/brute/agent_stream.rb +4 -1
- data/lib/brute/middleware/message_tracking.rb +15 -1
- data/lib/brute/orchestrator.rb +20 -6
- data/lib/brute/prompts/autonomy.rb +21 -0
- data/lib/brute/prompts/base.rb +23 -0
- data/lib/brute/prompts/build_switch.rb +19 -0
- data/lib/brute/prompts/code_references.rb +21 -0
- data/lib/brute/prompts/code_style.rb +16 -0
- data/lib/brute/prompts/conventions.rb +20 -0
- data/lib/brute/prompts/doing_tasks.rb +11 -0
- data/lib/brute/prompts/editing_approach.rb +20 -0
- data/lib/brute/prompts/editing_constraints.rb +24 -0
- data/lib/brute/prompts/environment.rb +25 -0
- data/lib/brute/prompts/frontend_tasks.rb +21 -0
- data/lib/brute/prompts/git_safety.rb +19 -0
- data/lib/brute/prompts/identity.rb +11 -0
- data/lib/brute/prompts/instructions.rb +18 -0
- data/lib/brute/prompts/max_steps.rb +30 -0
- data/lib/brute/prompts/objectivity.rb +16 -0
- data/lib/brute/prompts/plan_reminder.rb +40 -0
- data/lib/brute/prompts/proactiveness.rb +19 -0
- data/lib/brute/prompts/security_and_safety.rb +17 -0
- data/lib/brute/prompts/skills.rb +22 -0
- data/lib/brute/prompts/task_management.rb +59 -0
- data/lib/brute/prompts/text/agents/compaction.txt +15 -0
- data/lib/brute/prompts/text/agents/explore.txt +17 -0
- data/lib/brute/prompts/text/agents/summary.txt +11 -0
- data/lib/brute/prompts/text/agents/title.txt +40 -0
- data/lib/brute/prompts/text/doing_tasks/anthropic.txt +11 -0
- data/lib/brute/prompts/text/doing_tasks/default.txt +6 -0
- data/lib/brute/prompts/text/doing_tasks/google.txt +9 -0
- data/lib/brute/prompts/text/identity/anthropic.txt +5 -0
- data/lib/brute/prompts/text/identity/default.txt +3 -0
- data/lib/brute/prompts/text/identity/google.txt +1 -0
- data/lib/brute/prompts/text/identity/openai.txt +3 -0
- data/lib/brute/prompts/text/tone_and_style/anthropic.txt +5 -0
- data/lib/brute/prompts/text/tone_and_style/default.txt +9 -0
- data/lib/brute/prompts/text/tone_and_style/google.txt +6 -0
- data/lib/brute/prompts/text/tone_and_style/openai.txt +17 -0
- data/lib/brute/prompts/text/tool_usage/anthropic.txt +16 -0
- data/lib/brute/prompts/text/tool_usage/default.txt +4 -0
- data/lib/brute/prompts/text/tool_usage/google.txt +4 -0
- data/lib/brute/prompts/tone_and_style.rb +11 -0
- data/lib/brute/prompts/tool_usage.rb +11 -0
- data/lib/brute/providers/models_dev.rb +111 -0
- data/lib/brute/providers/opencode_go.rb +38 -0
- data/lib/brute/providers/opencode_zen.rb +82 -0
- data/lib/brute/providers/shell.rb +108 -0
- data/lib/brute/providers/shell_response.rb +100 -0
- data/lib/brute/skill.rb +118 -0
- data/lib/brute/system_prompt.rb +141 -63
- data/lib/brute/tools/delegate.rb +25 -1
- data/lib/brute/tools/question.rb +59 -0
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +83 -3
- metadata +49 -1
data/lib/brute/skill.rb
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Brute
|
|
6
|
+
# Discovers and loads SKILL.md files from standard directories.
|
|
7
|
+
#
|
|
8
|
+
# A skill is a markdown file with YAML frontmatter:
|
|
9
|
+
#
|
|
10
|
+
# ---
|
|
11
|
+
# name: debugging
|
|
12
|
+
# description: Systematic debugging workflow for isolating and fixing bugs
|
|
13
|
+
# ---
|
|
14
|
+
#
|
|
15
|
+
# When debugging, follow these steps...
|
|
16
|
+
#
|
|
17
|
+
# Skills are scanned from (in order):
|
|
18
|
+
# 1. .brute/skills/**/SKILL.md (project-local)
|
|
19
|
+
# 2. ~/.config/brute/skills/**/SKILL.md (global)
|
|
20
|
+
#
|
|
21
|
+
# The directory name containing SKILL.md becomes the skill name if frontmatter
|
|
22
|
+
# doesn't specify one.
|
|
23
|
+
#
|
|
24
|
+
module Skill
|
|
25
|
+
Info = Struct.new(:name, :description, :location, :content, keyword_init: true)
|
|
26
|
+
|
|
27
|
+
FILENAME = "SKILL.md"
|
|
28
|
+
|
|
29
|
+
# Scan all skill directories and return an array of Info structs.
|
|
30
|
+
def self.all(cwd: Dir.pwd)
|
|
31
|
+
skills = {}
|
|
32
|
+
|
|
33
|
+
scan_dirs(cwd).each do |dir|
|
|
34
|
+
Dir.glob(File.join(dir, "**", FILENAME)).sort.each do |path|
|
|
35
|
+
info = load(path)
|
|
36
|
+
next unless info
|
|
37
|
+
# First found wins (project-local overrides global)
|
|
38
|
+
skills[info.name] ||= info
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
skills.values.sort_by(&:name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Get a single skill by name.
|
|
46
|
+
def self.get(name, cwd: Dir.pwd)
|
|
47
|
+
all(cwd: cwd).detect { |s| s.name == name }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Format skills as XML for the system prompt.
|
|
51
|
+
def self.fmt(skills)
|
|
52
|
+
return nil if skills.empty?
|
|
53
|
+
|
|
54
|
+
lines = ["<available_skills>"]
|
|
55
|
+
skills.each do |skill|
|
|
56
|
+
lines << " <skill>"
|
|
57
|
+
lines << " <name>#{skill.name}</name>"
|
|
58
|
+
lines << " <description>#{skill.description}</description>"
|
|
59
|
+
lines << " </skill>"
|
|
60
|
+
end
|
|
61
|
+
lines << "</available_skills>"
|
|
62
|
+
lines.join("\n")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Parse a SKILL.md file into an Info struct.
|
|
66
|
+
# Returns nil if the file is invalid or missing required fields.
|
|
67
|
+
def self.load(path)
|
|
68
|
+
raw = File.read(path)
|
|
69
|
+
frontmatter, content = parse_frontmatter(raw)
|
|
70
|
+
return nil unless frontmatter
|
|
71
|
+
|
|
72
|
+
name = frontmatter["name"] || File.basename(File.dirname(path))
|
|
73
|
+
description = frontmatter["description"]
|
|
74
|
+
return nil unless description && !description.strip.empty?
|
|
75
|
+
|
|
76
|
+
Info.new(
|
|
77
|
+
name: name.to_s.strip,
|
|
78
|
+
description: description.to_s.strip,
|
|
79
|
+
location: path,
|
|
80
|
+
content: content.to_s.strip,
|
|
81
|
+
)
|
|
82
|
+
rescue => e
|
|
83
|
+
warn "Failed to load skill #{path}: #{e.message}"
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Directories to scan for skills, in priority order.
|
|
88
|
+
def self.scan_dirs(cwd)
|
|
89
|
+
dirs = []
|
|
90
|
+
|
|
91
|
+
# Project-local
|
|
92
|
+
project = File.join(cwd, ".brute", "skills")
|
|
93
|
+
dirs << project if File.directory?(project)
|
|
94
|
+
|
|
95
|
+
# Global
|
|
96
|
+
global = File.join(Dir.home, ".config", "brute", "skills")
|
|
97
|
+
dirs << global if File.directory?(global)
|
|
98
|
+
|
|
99
|
+
dirs
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Split YAML frontmatter from markdown body.
|
|
103
|
+
# Returns [hash, string] or [nil, nil].
|
|
104
|
+
def self.parse_frontmatter(raw)
|
|
105
|
+
return [nil, nil] unless raw.start_with?("---")
|
|
106
|
+
|
|
107
|
+
parts = raw.split(/^---\s*$/, 3)
|
|
108
|
+
return [nil, nil] if parts.length < 3
|
|
109
|
+
|
|
110
|
+
frontmatter = YAML.safe_load(parts[1])
|
|
111
|
+
return [nil, nil] unless frontmatter.is_a?(Hash)
|
|
112
|
+
|
|
113
|
+
[frontmatter, parts[2]]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private_class_method :scan_dirs, :parse_frontmatter
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/brute/system_prompt.rb
CHANGED
|
@@ -1,88 +1,166 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Brute
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# Deferred system prompt builder.
|
|
5
|
+
#
|
|
6
|
+
# The block passed to +build+ is stored — not executed — until +prepare+
|
|
7
|
+
# is called with a runtime context hash (provider_name, model_name, cwd, etc).
|
|
8
|
+
#
|
|
9
|
+
# sp = Brute::SystemPrompt.build do |prompt, ctx|
|
|
10
|
+
# prompt << Brute::Prompts::Identity.call(ctx)
|
|
11
|
+
# prompt << Brute::Prompts::ToneAndStyle.call(ctx)
|
|
12
|
+
# prompt << Brute::Prompts::Environment.call(ctx)
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# result = sp.prepare(provider_name: "anthropic", model_name: "claude-sonnet-4-20250514", cwd: Dir.pwd)
|
|
16
|
+
# result.to_s # single joined string
|
|
17
|
+
# result.sections # array of strings (one per p.system call)
|
|
6
18
|
#
|
|
7
|
-
# Modeled after forgecode's SystemPrompt which composes a static agent
|
|
8
|
-
# personality block with dynamic environment/tool information.
|
|
9
19
|
class SystemPrompt
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@custom_rules = custom_rules
|
|
20
|
+
# Build a deferred system prompt. The block is stored and called later by +prepare+.
|
|
21
|
+
def self.build(&block)
|
|
22
|
+
new(block)
|
|
14
23
|
end
|
|
15
24
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
# Return the default system prompt. Selects the right provider stack at
|
|
26
|
+
# prepare-time, then appends conditional sections based on runtime state.
|
|
27
|
+
def self.default
|
|
28
|
+
build do |prompt, ctx|
|
|
29
|
+
# Provider-specific base stack.
|
|
30
|
+
# For gateway providers (opencode_zen, opencode_go), infer the
|
|
31
|
+
# upstream model family from the model name so we use the most
|
|
32
|
+
# appropriate prompt stack (e.g., anthropic stack for claude-*).
|
|
33
|
+
provider = ctx[:provider_name].to_s
|
|
34
|
+
stack_key = if provider.start_with?("opencode")
|
|
35
|
+
infer_stack_from_model(ctx[:model_name].to_s)
|
|
36
|
+
else
|
|
37
|
+
provider
|
|
38
|
+
end
|
|
39
|
+
STACKS.fetch(stack_key, STACKS["default"]).each do |mod|
|
|
40
|
+
prompt << mod.call(ctx)
|
|
41
|
+
end
|
|
25
42
|
|
|
26
|
-
|
|
43
|
+
# Conditional: agent-specific reminders
|
|
44
|
+
if ctx[:agent] == "plan"
|
|
45
|
+
prompt << Prompts::PlanReminder.call(ctx)
|
|
46
|
+
end
|
|
27
47
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
48
|
+
if ctx[:agent_switched] == "build"
|
|
49
|
+
prompt << Prompts::BuildSwitch.call(ctx)
|
|
50
|
+
end
|
|
31
51
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
if ctx[:max_steps_reached]
|
|
53
|
+
prompt << Prompts::MaxSteps.call(ctx)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
36
56
|
end
|
|
37
57
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
58
|
+
# Pre-configured prompt stacks per provider.
|
|
59
|
+
# Each maps a provider name to an ordered list of prompt modules.
|
|
60
|
+
STACKS = {
|
|
61
|
+
# Claude — full-featured with task management and detailed tool policy
|
|
62
|
+
"anthropic" => [
|
|
63
|
+
Prompts::Identity,
|
|
64
|
+
Prompts::ToneAndStyle,
|
|
65
|
+
Prompts::Objectivity,
|
|
66
|
+
Prompts::TaskManagement,
|
|
67
|
+
Prompts::DoingTasks,
|
|
68
|
+
Prompts::ToolUsage,
|
|
69
|
+
Prompts::Conventions,
|
|
70
|
+
Prompts::GitSafety,
|
|
71
|
+
Prompts::CodeReferences,
|
|
72
|
+
Prompts::Environment,
|
|
73
|
+
Prompts::Skills,
|
|
74
|
+
Prompts::Instructions,
|
|
75
|
+
],
|
|
42
76
|
|
|
43
|
-
|
|
44
|
-
|
|
77
|
+
# GPT-4 / o1 / o3 — pragmatic engineer persona, editing focus, autonomy
|
|
78
|
+
"openai" => [
|
|
79
|
+
Prompts::Identity,
|
|
80
|
+
Prompts::EditingApproach,
|
|
81
|
+
Prompts::Autonomy,
|
|
82
|
+
Prompts::EditingConstraints,
|
|
83
|
+
Prompts::FrontendTasks,
|
|
84
|
+
Prompts::ToneAndStyle,
|
|
85
|
+
Prompts::Conventions,
|
|
86
|
+
Prompts::GitSafety,
|
|
87
|
+
Prompts::CodeReferences,
|
|
88
|
+
Prompts::Environment,
|
|
89
|
+
Prompts::Skills,
|
|
90
|
+
Prompts::Instructions,
|
|
91
|
+
],
|
|
45
92
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
93
|
+
# Gemini — formal/structured, explicit workflows, security focus
|
|
94
|
+
"google" => [
|
|
95
|
+
Prompts::Identity,
|
|
96
|
+
Prompts::Conventions,
|
|
97
|
+
Prompts::DoingTasks,
|
|
98
|
+
Prompts::ToneAndStyle,
|
|
99
|
+
Prompts::SecurityAndSafety,
|
|
100
|
+
Prompts::ToolUsage,
|
|
101
|
+
Prompts::GitSafety,
|
|
102
|
+
Prompts::CodeReferences,
|
|
103
|
+
Prompts::Environment,
|
|
104
|
+
Prompts::Skills,
|
|
105
|
+
Prompts::Instructions,
|
|
106
|
+
],
|
|
49
107
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
108
|
+
# Fallback — conservative, concise, fewer than 4 lines
|
|
109
|
+
"default" => [
|
|
110
|
+
Prompts::Identity,
|
|
111
|
+
Prompts::ToneAndStyle,
|
|
112
|
+
Prompts::Proactiveness,
|
|
113
|
+
Prompts::Conventions,
|
|
114
|
+
Prompts::CodeStyle,
|
|
115
|
+
Prompts::DoingTasks,
|
|
116
|
+
Prompts::ToolUsage,
|
|
117
|
+
Prompts::GitSafety,
|
|
118
|
+
Prompts::CodeReferences,
|
|
119
|
+
Prompts::Environment,
|
|
120
|
+
Prompts::Skills,
|
|
121
|
+
Prompts::Instructions,
|
|
122
|
+
],
|
|
123
|
+
}.freeze
|
|
53
124
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
125
|
+
# Infer the best prompt stack from a model name.
|
|
126
|
+
# Used for gateway providers that route to multiple upstream model families.
|
|
127
|
+
def self.infer_stack_from_model(model_name)
|
|
128
|
+
case model_name
|
|
129
|
+
when /\bclaude\b/i, /\bbig.?pickle\b/i
|
|
130
|
+
"anthropic"
|
|
131
|
+
when /\bgpt\b/i, /\bo[134]\b/i, /\bcodex\b/i
|
|
132
|
+
"openai"
|
|
133
|
+
when /\bgemini\b/i, /\bgemma\b/i
|
|
134
|
+
"google"
|
|
135
|
+
else
|
|
136
|
+
"default"
|
|
137
|
+
end
|
|
64
138
|
end
|
|
65
139
|
|
|
66
|
-
def
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<<~SECTION
|
|
70
|
-
# Environment
|
|
140
|
+
def initialize(block)
|
|
141
|
+
@block = block
|
|
142
|
+
end
|
|
71
143
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
SECTION
|
|
144
|
+
# Execute the stored block with the given context and return a Result.
|
|
145
|
+
def prepare(ctx)
|
|
146
|
+
sections = []
|
|
147
|
+
@block.call(sections, ctx)
|
|
148
|
+
Result.new(sections.compact.reject { |s| s.respond_to?(:empty?) && s.empty? })
|
|
78
149
|
end
|
|
79
150
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
151
|
+
# Immutable result of a prepared system prompt.
|
|
152
|
+
Result = Struct.new(:sections) do
|
|
153
|
+
def to_s
|
|
154
|
+
sections.join("\n\n")
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def each(&block)
|
|
158
|
+
sections.each(&block)
|
|
159
|
+
end
|
|
83
160
|
|
|
84
|
-
|
|
85
|
-
|
|
161
|
+
def empty?
|
|
162
|
+
sections.empty?
|
|
163
|
+
end
|
|
86
164
|
end
|
|
87
165
|
end
|
|
88
166
|
end
|
data/lib/brute/tools/delegate.rb
CHANGED
|
@@ -28,7 +28,31 @@ module Brute
|
|
|
28
28
|
rounds += 1
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
{result: res
|
|
31
|
+
{result: extract_content(res, sub)}
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Safely extract text content from the sub-agent response.
|
|
37
|
+
#
|
|
38
|
+
# When the LLM returns only tool calls (no text content block),
|
|
39
|
+
# res.content raises NoMethodError because the response adapter's
|
|
40
|
+
# choices array is empty (it only maps over text blocks), or
|
|
41
|
+
# returns nil when the response has no text. Fall back to the
|
|
42
|
+
# last assistant text in the conversation history.
|
|
43
|
+
def extract_content(res, context)
|
|
44
|
+
text = begin
|
|
45
|
+
res.content
|
|
46
|
+
rescue NoMethodError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
return text if text.is_a?(::String) && !text.empty?
|
|
50
|
+
|
|
51
|
+
last_assistant = context.messages.to_a
|
|
52
|
+
.select(&:assistant?)
|
|
53
|
+
.reverse
|
|
54
|
+
.find { |m| m.content.is_a?(::String) && !m.content.empty? }
|
|
55
|
+
last_assistant&.content || "(sub-agent completed but produced no text response)"
|
|
32
56
|
end
|
|
33
57
|
end
|
|
34
58
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Tools
|
|
5
|
+
class Question < LLM::Tool
|
|
6
|
+
name "question"
|
|
7
|
+
description "Ask the user questions during execution. Use this to gather preferences, " \
|
|
8
|
+
"clarify ambiguous instructions, get decisions on implementation choices, or " \
|
|
9
|
+
"offer choices about direction. Users can always select \"Other\" to provide " \
|
|
10
|
+
"custom text input. Answers are returned as arrays of labels; set multiple: true " \
|
|
11
|
+
"to allow selecting more than one. If you recommend a specific option, make it " \
|
|
12
|
+
"the first option and add \"(Recommended)\" at the end of the label."
|
|
13
|
+
|
|
14
|
+
params do |s|
|
|
15
|
+
s.object(
|
|
16
|
+
questions: s.array(
|
|
17
|
+
s.object(
|
|
18
|
+
question: s.string.required,
|
|
19
|
+
header: s.string.required,
|
|
20
|
+
options: s.array(
|
|
21
|
+
s.object(
|
|
22
|
+
label: s.string.required,
|
|
23
|
+
description: s.string.required,
|
|
24
|
+
)
|
|
25
|
+
).required,
|
|
26
|
+
multiple: s.boolean,
|
|
27
|
+
)
|
|
28
|
+
).required
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def call(questions:)
|
|
33
|
+
handler = Thread.current[:on_question]
|
|
34
|
+
unless handler
|
|
35
|
+
return { error: true, message: "Cannot ask questions in non-interactive mode" }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
queue = Queue.new
|
|
39
|
+
handler.call(questions, queue)
|
|
40
|
+
answers = queue.pop
|
|
41
|
+
|
|
42
|
+
format_answers(questions, answers)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def format_answers(questions, answers)
|
|
48
|
+
pairs = questions.each_with_index.map do |q, i|
|
|
49
|
+
q = q.transform_keys(&:to_s) if q.is_a?(Hash)
|
|
50
|
+
header = q["header"]
|
|
51
|
+
answer = answers[i] || []
|
|
52
|
+
"\"#{header}\" = #{answer.join(', ')}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
{ response: "User answered: #{pairs.join('; ')}" }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/brute/version.rb
CHANGED
data/lib/brute.rb
CHANGED
|
@@ -27,6 +27,30 @@ require_relative 'brute/file_mutation_queue'
|
|
|
27
27
|
require_relative 'brute/doom_loop'
|
|
28
28
|
require_relative 'brute/hooks'
|
|
29
29
|
require_relative 'brute/compactor'
|
|
30
|
+
require_relative 'brute/prompts/base'
|
|
31
|
+
require_relative 'brute/prompts/identity'
|
|
32
|
+
require_relative 'brute/prompts/tone_and_style'
|
|
33
|
+
require_relative 'brute/prompts/objectivity'
|
|
34
|
+
require_relative 'brute/prompts/task_management'
|
|
35
|
+
require_relative 'brute/prompts/doing_tasks'
|
|
36
|
+
require_relative 'brute/prompts/tool_usage'
|
|
37
|
+
require_relative 'brute/prompts/conventions'
|
|
38
|
+
require_relative 'brute/prompts/git_safety'
|
|
39
|
+
require_relative 'brute/prompts/code_references'
|
|
40
|
+
require_relative 'brute/prompts/environment'
|
|
41
|
+
require_relative 'brute/prompts/instructions'
|
|
42
|
+
require_relative 'brute/prompts/editing_approach'
|
|
43
|
+
require_relative 'brute/prompts/autonomy'
|
|
44
|
+
require_relative 'brute/prompts/editing_constraints'
|
|
45
|
+
require_relative 'brute/prompts/frontend_tasks'
|
|
46
|
+
require_relative 'brute/prompts/proactiveness'
|
|
47
|
+
require_relative 'brute/prompts/code_style'
|
|
48
|
+
require_relative 'brute/prompts/security_and_safety'
|
|
49
|
+
require_relative 'brute/prompts/skills'
|
|
50
|
+
require_relative 'brute/prompts/plan_reminder'
|
|
51
|
+
require_relative 'brute/prompts/max_steps'
|
|
52
|
+
require_relative 'brute/prompts/build_switch'
|
|
53
|
+
require_relative 'brute/skill'
|
|
30
54
|
require_relative 'brute/system_prompt'
|
|
31
55
|
require_relative 'brute/message_store'
|
|
32
56
|
require_relative 'brute/session'
|
|
@@ -64,6 +88,14 @@ require_relative 'brute/tools/net_fetch'
|
|
|
64
88
|
require_relative 'brute/tools/todo_write'
|
|
65
89
|
require_relative 'brute/tools/todo_read'
|
|
66
90
|
require_relative 'brute/tools/delegate'
|
|
91
|
+
require_relative 'brute/tools/question'
|
|
92
|
+
|
|
93
|
+
# Providers
|
|
94
|
+
require_relative 'brute/providers/shell_response'
|
|
95
|
+
require_relative 'brute/providers/shell'
|
|
96
|
+
require_relative 'brute/providers/models_dev'
|
|
97
|
+
require_relative 'brute/providers/opencode_zen'
|
|
98
|
+
require_relative 'brute/providers/opencode_go'
|
|
67
99
|
|
|
68
100
|
# Orchestrator (depends on tools, middleware, and infrastructure)
|
|
69
101
|
require_relative 'brute/orchestrator'
|
|
@@ -81,7 +113,8 @@ module Brute
|
|
|
81
113
|
Tools::NetFetch,
|
|
82
114
|
Tools::TodoWrite,
|
|
83
115
|
Tools::TodoRead,
|
|
84
|
-
Tools::Delegate
|
|
116
|
+
Tools::Delegate,
|
|
117
|
+
Tools::Question
|
|
85
118
|
].freeze
|
|
86
119
|
|
|
87
120
|
# Default provider, resolved from environment.
|
|
@@ -94,13 +127,15 @@ module Brute
|
|
|
94
127
|
end
|
|
95
128
|
|
|
96
129
|
# Create a new orchestrator with sensible defaults.
|
|
97
|
-
def self.agent(cwd: Dir.pwd, tools: TOOLS, session: nil, reasoning: {}, **callbacks)
|
|
130
|
+
def self.agent(cwd: Dir.pwd, model: nil, tools: TOOLS, session: nil, reasoning: {}, agent_name: nil, **callbacks)
|
|
98
131
|
Orchestrator.new(
|
|
99
132
|
provider: provider,
|
|
133
|
+
model: model,
|
|
100
134
|
tools: tools,
|
|
101
135
|
cwd: cwd,
|
|
102
136
|
session: session,
|
|
103
137
|
reasoning: reasoning,
|
|
138
|
+
agent_name: agent_name,
|
|
104
139
|
**callbacks
|
|
105
140
|
)
|
|
106
141
|
end
|
|
@@ -111,9 +146,50 @@ module Brute
|
|
|
111
146
|
'google' => ->(key) { LLM.google(key: key) },
|
|
112
147
|
'deepseek' => ->(key) { LLM.deepseek(key: key) },
|
|
113
148
|
'ollama' => ->(key) { LLM.ollama(key: key) },
|
|
114
|
-
'xai' => ->(key) { LLM.xai(key: key) }
|
|
149
|
+
'xai' => ->(key) { LLM.xai(key: key) },
|
|
150
|
+
'opencode_zen' => ->(key) { LLM::OpencodeZen.new(key: key) },
|
|
151
|
+
'opencode_go' => ->(key) { LLM::OpencodeGo.new(key: key) },
|
|
152
|
+
'shell' => ->(_key) { Providers::Shell.new },
|
|
115
153
|
}.freeze
|
|
116
154
|
|
|
155
|
+
# List provider names that have API keys configured in the environment.
|
|
156
|
+
# The shell provider is always available (no key needed).
|
|
157
|
+
def self.configured_providers
|
|
158
|
+
PROVIDERS.keys.select { |name| api_key_for(name) }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Build a provider instance for the given name using available API keys.
|
|
162
|
+
# Returns nil if no key is found.
|
|
163
|
+
def self.provider_for(name)
|
|
164
|
+
key = api_key_for(name)
|
|
165
|
+
return nil unless key
|
|
166
|
+
|
|
167
|
+
factory = PROVIDERS[name]
|
|
168
|
+
return nil unless factory
|
|
169
|
+
|
|
170
|
+
factory.call(key)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Look up the API key for a given provider name.
|
|
174
|
+
def self.api_key_for(name)
|
|
175
|
+
# Shell provider needs no key.
|
|
176
|
+
return "none" if name == "shell"
|
|
177
|
+
|
|
178
|
+
# OpenCode providers: check OPENCODE_API_KEY, fall back to "public" for anonymous access.
|
|
179
|
+
if name == "opencode_zen" || name == "opencode_go"
|
|
180
|
+
return ENV["OPENCODE_API_KEY"] || "public"
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Explicit generic key always works
|
|
184
|
+
return ENV["LLM_API_KEY"] if ENV["LLM_API_KEY"]
|
|
185
|
+
|
|
186
|
+
case name
|
|
187
|
+
when "anthropic" then ENV["ANTHROPIC_API_KEY"]
|
|
188
|
+
when "openai" then ENV["OPENAI_API_KEY"]
|
|
189
|
+
when "google" then ENV["GOOGLE_API_KEY"]
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
117
193
|
# Resolve the LLM provider from environment variables.
|
|
118
194
|
#
|
|
119
195
|
# Checks in order:
|
|
@@ -121,6 +197,7 @@ module Brute
|
|
|
121
197
|
# 2. ANTHROPIC_API_KEY (implicit: provider = anthropic)
|
|
122
198
|
# 3. OPENAI_API_KEY (implicit: provider = openai)
|
|
123
199
|
# 4. GOOGLE_API_KEY (implicit: provider = google)
|
|
200
|
+
# 5. OPENCODE_API_KEY (implicit: provider = opencode_zen)
|
|
124
201
|
#
|
|
125
202
|
# Returns nil if no key is found. Error is deferred to Orchestrator#run.
|
|
126
203
|
def self.resolve_provider
|
|
@@ -136,6 +213,9 @@ module Brute
|
|
|
136
213
|
elsif ENV['GOOGLE_API_KEY']
|
|
137
214
|
key = ENV['GOOGLE_API_KEY']
|
|
138
215
|
name = 'google'
|
|
216
|
+
elsif ENV['OPENCODE_API_KEY']
|
|
217
|
+
key = ENV['OPENCODE_API_KEY']
|
|
218
|
+
name = 'opencode_zen'
|
|
139
219
|
else
|
|
140
220
|
return nil
|
|
141
221
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: brute
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Brute Contributors
|
|
@@ -114,7 +114,54 @@ files:
|
|
|
114
114
|
- lib/brute/patches/anthropic_tool_role.rb
|
|
115
115
|
- lib/brute/patches/buffer_nil_guard.rb
|
|
116
116
|
- lib/brute/pipeline.rb
|
|
117
|
+
- lib/brute/prompts/autonomy.rb
|
|
118
|
+
- lib/brute/prompts/base.rb
|
|
119
|
+
- lib/brute/prompts/build_switch.rb
|
|
120
|
+
- lib/brute/prompts/code_references.rb
|
|
121
|
+
- lib/brute/prompts/code_style.rb
|
|
122
|
+
- lib/brute/prompts/conventions.rb
|
|
123
|
+
- lib/brute/prompts/doing_tasks.rb
|
|
124
|
+
- lib/brute/prompts/editing_approach.rb
|
|
125
|
+
- lib/brute/prompts/editing_constraints.rb
|
|
126
|
+
- lib/brute/prompts/environment.rb
|
|
127
|
+
- lib/brute/prompts/frontend_tasks.rb
|
|
128
|
+
- lib/brute/prompts/git_safety.rb
|
|
129
|
+
- lib/brute/prompts/identity.rb
|
|
130
|
+
- lib/brute/prompts/instructions.rb
|
|
131
|
+
- lib/brute/prompts/max_steps.rb
|
|
132
|
+
- lib/brute/prompts/objectivity.rb
|
|
133
|
+
- lib/brute/prompts/plan_reminder.rb
|
|
134
|
+
- lib/brute/prompts/proactiveness.rb
|
|
135
|
+
- lib/brute/prompts/security_and_safety.rb
|
|
136
|
+
- lib/brute/prompts/skills.rb
|
|
137
|
+
- lib/brute/prompts/task_management.rb
|
|
138
|
+
- lib/brute/prompts/text/agents/compaction.txt
|
|
139
|
+
- lib/brute/prompts/text/agents/explore.txt
|
|
140
|
+
- lib/brute/prompts/text/agents/summary.txt
|
|
141
|
+
- lib/brute/prompts/text/agents/title.txt
|
|
142
|
+
- lib/brute/prompts/text/doing_tasks/anthropic.txt
|
|
143
|
+
- lib/brute/prompts/text/doing_tasks/default.txt
|
|
144
|
+
- lib/brute/prompts/text/doing_tasks/google.txt
|
|
145
|
+
- lib/brute/prompts/text/identity/anthropic.txt
|
|
146
|
+
- lib/brute/prompts/text/identity/default.txt
|
|
147
|
+
- lib/brute/prompts/text/identity/google.txt
|
|
148
|
+
- lib/brute/prompts/text/identity/openai.txt
|
|
149
|
+
- lib/brute/prompts/text/tone_and_style/anthropic.txt
|
|
150
|
+
- lib/brute/prompts/text/tone_and_style/default.txt
|
|
151
|
+
- lib/brute/prompts/text/tone_and_style/google.txt
|
|
152
|
+
- lib/brute/prompts/text/tone_and_style/openai.txt
|
|
153
|
+
- lib/brute/prompts/text/tool_usage/anthropic.txt
|
|
154
|
+
- lib/brute/prompts/text/tool_usage/default.txt
|
|
155
|
+
- lib/brute/prompts/text/tool_usage/google.txt
|
|
156
|
+
- lib/brute/prompts/tone_and_style.rb
|
|
157
|
+
- lib/brute/prompts/tool_usage.rb
|
|
158
|
+
- lib/brute/providers/models_dev.rb
|
|
159
|
+
- lib/brute/providers/opencode_go.rb
|
|
160
|
+
- lib/brute/providers/opencode_zen.rb
|
|
161
|
+
- lib/brute/providers/shell.rb
|
|
162
|
+
- lib/brute/providers/shell_response.rb
|
|
117
163
|
- lib/brute/session.rb
|
|
164
|
+
- lib/brute/skill.rb
|
|
118
165
|
- lib/brute/snapshot_store.rb
|
|
119
166
|
- lib/brute/system_prompt.rb
|
|
120
167
|
- lib/brute/todo_store.rb
|
|
@@ -126,6 +173,7 @@ files:
|
|
|
126
173
|
- lib/brute/tools/fs_undo.rb
|
|
127
174
|
- lib/brute/tools/fs_write.rb
|
|
128
175
|
- lib/brute/tools/net_fetch.rb
|
|
176
|
+
- lib/brute/tools/question.rb
|
|
129
177
|
- lib/brute/tools/shell.rb
|
|
130
178
|
- lib/brute/tools/todo_read.rb
|
|
131
179
|
- lib/brute/tools/todo_write.rb
|