brute 0.1.7 → 0.1.9
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 +16 -1
- data/lib/brute/message_store.rb +269 -0
- data/lib/brute/middleware/compaction_check.rb +5 -2
- data/lib/brute/middleware/message_tracking.rb +209 -0
- data/lib/brute/middleware/otel/span.rb +75 -0
- data/lib/brute/middleware/otel/token_usage.rb +30 -0
- data/lib/brute/middleware/otel/tool_calls.rb +39 -0
- data/lib/brute/middleware/otel/tool_results.rb +37 -0
- data/lib/brute/middleware/otel.rb +29 -0
- data/lib/brute/middleware/tool_use_guard.rb +66 -23
- data/lib/brute/orchestrator.rb +46 -13
- 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/session.rb +109 -34
- data/lib/brute/skill.rb +118 -0
- data/lib/brute/system_prompt.rb +119 -64
- data/lib/brute/tools/question.rb +59 -0
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +62 -2
- metadata +52 -2
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Middleware
|
|
5
|
+
module OTel
|
|
6
|
+
# Records tool calls the LLM requested as span events.
|
|
7
|
+
#
|
|
8
|
+
# Runs POST-call: after the LLM responds, inspects ctx.functions
|
|
9
|
+
# for any tool calls the model wants to make, and adds a span event
|
|
10
|
+
# for each one with the tool name, call ID, and arguments.
|
|
11
|
+
#
|
|
12
|
+
class ToolCalls < Base
|
|
13
|
+
def call(env)
|
|
14
|
+
response = @app.call(env)
|
|
15
|
+
|
|
16
|
+
span = env[:span]
|
|
17
|
+
if span
|
|
18
|
+
functions = env[:context].functions
|
|
19
|
+
if functions && !functions.empty?
|
|
20
|
+
span.set_attribute("brute.tool_calls.count", functions.size)
|
|
21
|
+
|
|
22
|
+
functions.each do |fn|
|
|
23
|
+
attrs = {
|
|
24
|
+
"tool.name" => fn.name.to_s,
|
|
25
|
+
"tool.id" => fn.id.to_s,
|
|
26
|
+
}
|
|
27
|
+
args = fn.arguments
|
|
28
|
+
attrs["tool.arguments"] = args.to_json if args
|
|
29
|
+
span.add_event("tool_call", attributes: attrs)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
response
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Middleware
|
|
5
|
+
module OTel
|
|
6
|
+
# Records tool results being sent back to the LLM as span events.
|
|
7
|
+
#
|
|
8
|
+
# Runs PRE-call: when env[:tool_results] is present, the orchestrator
|
|
9
|
+
# is sending tool execution results back to the LLM. Each result gets
|
|
10
|
+
# a span event with the tool name and success/error status.
|
|
11
|
+
#
|
|
12
|
+
class ToolResults < Base
|
|
13
|
+
def call(env)
|
|
14
|
+
span = env[:span]
|
|
15
|
+
|
|
16
|
+
if span && (results = env[:tool_results])
|
|
17
|
+
span.set_attribute("brute.tool_results.count", results.size)
|
|
18
|
+
|
|
19
|
+
results.each do |name, value|
|
|
20
|
+
error = value.is_a?(Hash) && value[:error]
|
|
21
|
+
attrs = { "tool.name" => name.to_s }
|
|
22
|
+
if error
|
|
23
|
+
attrs["tool.status"] = "error"
|
|
24
|
+
attrs["tool.error"] = value[:error].to_s
|
|
25
|
+
else
|
|
26
|
+
attrs["tool.status"] = "ok"
|
|
27
|
+
end
|
|
28
|
+
span.add_event("tool_result", attributes: attrs)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@app.call(env)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Middleware
|
|
5
|
+
# OpenTelemetry instrumentation for the LLM pipeline.
|
|
6
|
+
#
|
|
7
|
+
# Each middleware is independent and communicates through env[:span].
|
|
8
|
+
# OTel::Span must be outermost — it creates the span. The rest
|
|
9
|
+
# decorate it with events and attributes from their position in the
|
|
10
|
+
# pipeline.
|
|
11
|
+
#
|
|
12
|
+
# All middlewares are no-ops when opentelemetry-sdk is not loaded.
|
|
13
|
+
#
|
|
14
|
+
# Usage in pipeline:
|
|
15
|
+
#
|
|
16
|
+
# use Brute::Middleware::OTel::Span
|
|
17
|
+
# use Brute::Middleware::OTel::ToolResults
|
|
18
|
+
# use Brute::Middleware::OTel::ToolCalls
|
|
19
|
+
# use Brute::Middleware::OTel::TokenUsage
|
|
20
|
+
#
|
|
21
|
+
module OTel
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
require_relative "otel/span"
|
|
27
|
+
require_relative "otel/tool_results"
|
|
28
|
+
require_relative "otel/tool_calls"
|
|
29
|
+
require_relative "otel/token_usage"
|
|
@@ -11,8 +11,16 @@ module Brute
|
|
|
11
11
|
# blocks is lost. This causes "unexpected tool_use_id" on the next call
|
|
12
12
|
# because tool_result references a tool_use that's missing from the buffer.
|
|
13
13
|
#
|
|
14
|
-
# This middleware runs post-call and
|
|
15
|
-
#
|
|
14
|
+
# This middleware runs post-call and ensures every pending tool_use ID
|
|
15
|
+
# is covered by an assistant message in the buffer. It handles three
|
|
16
|
+
# cases:
|
|
17
|
+
#
|
|
18
|
+
# 1. ctx.functions is non-empty and the assistant message exists → no-op
|
|
19
|
+
# 2. ctx.functions is non-empty but the assistant message is missing
|
|
20
|
+
# (or has different IDs) → inject synthetic message
|
|
21
|
+
# 3. ctx.functions is empty (nil-choice bug) but the stream recorded
|
|
22
|
+
# tool calls → inject synthetic message using stream metadata
|
|
23
|
+
#
|
|
16
24
|
class ToolUseGuard
|
|
17
25
|
def initialize(app)
|
|
18
26
|
@app = app
|
|
@@ -22,32 +30,67 @@ module Brute
|
|
|
22
30
|
response = @app.call(env)
|
|
23
31
|
|
|
24
32
|
ctx = env[:context]
|
|
25
|
-
functions = ctx.functions
|
|
26
33
|
|
|
27
|
-
#
|
|
28
|
-
#
|
|
34
|
+
# Collect pending tool data from ctx.functions (primary) or the
|
|
35
|
+
# stream's recorded metadata (fallback for nil-choice bug).
|
|
36
|
+
tool_data = collect_tool_data(ctx, env)
|
|
37
|
+
return response if tool_data.empty?
|
|
38
|
+
|
|
39
|
+
# Find all tool_use IDs already covered by assistant messages.
|
|
40
|
+
covered_ids = covered_tool_ids(ctx)
|
|
41
|
+
|
|
42
|
+
# Inject a synthetic assistant message for any uncovered tool calls.
|
|
43
|
+
uncovered = tool_data.reject { |td| covered_ids.include?(td[:id]) }
|
|
44
|
+
inject_synthetic!(ctx, uncovered) unless uncovered.empty?
|
|
45
|
+
|
|
46
|
+
response
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def collect_tool_data(ctx, env)
|
|
52
|
+
functions = ctx.functions
|
|
29
53
|
if functions && !functions.empty?
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{ "type" => "tool_use", "id" => fn.id, "name" => fn.name, "input" => fn.arguments || {} }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
synthetic = LLM::Message.new(:assistant, "", {
|
|
43
|
-
tool_calls: tool_calls,
|
|
44
|
-
original_tool_calls: original_tool_calls,
|
|
45
|
-
})
|
|
46
|
-
ctx.messages.concat([synthetic])
|
|
54
|
+
functions.map { |fn| { id: fn.id, name: fn.name, arguments: fn.arguments } }
|
|
55
|
+
elsif env[:streaming]
|
|
56
|
+
stream = resolve_stream(ctx)
|
|
57
|
+
if stream
|
|
58
|
+
data = stream.pending_tool_calls.dup
|
|
59
|
+
stream.clear_pending_tool_calls!
|
|
60
|
+
data
|
|
61
|
+
else
|
|
62
|
+
[]
|
|
47
63
|
end
|
|
64
|
+
else
|
|
65
|
+
[]
|
|
48
66
|
end
|
|
67
|
+
end
|
|
49
68
|
|
|
50
|
-
|
|
69
|
+
def resolve_stream(ctx)
|
|
70
|
+
stream = ctx.instance_variable_get(:@params)&.dig(:stream)
|
|
71
|
+
stream if stream.respond_to?(:pending_tool_calls)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def covered_tool_ids(ctx)
|
|
75
|
+
ctx.messages.to_a
|
|
76
|
+
.select { |m| m.role.to_s == "assistant" && m.tool_call? }
|
|
77
|
+
.flat_map { |m| (m.extra.original_tool_calls || []).map { |tc| tc["id"] } }
|
|
78
|
+
.to_set
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def inject_synthetic!(ctx, uncovered)
|
|
82
|
+
tool_calls = uncovered.map do |td|
|
|
83
|
+
LLM::Object.from(id: td[:id], name: td[:name], arguments: td[:arguments])
|
|
84
|
+
end
|
|
85
|
+
original_tool_calls = uncovered.map do |td|
|
|
86
|
+
{ "type" => "tool_use", "id" => td[:id], "name" => td[:name], "input" => td[:arguments] || {} }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
synthetic = LLM::Message.new(:assistant, "", {
|
|
90
|
+
tool_calls: tool_calls,
|
|
91
|
+
original_tool_calls: original_tool_calls,
|
|
92
|
+
})
|
|
93
|
+
ctx.messages.concat([synthetic])
|
|
51
94
|
end
|
|
52
95
|
end
|
|
53
96
|
end
|
data/lib/brute/orchestrator.rb
CHANGED
|
@@ -20,31 +20,42 @@ module Brute
|
|
|
20
20
|
class Orchestrator
|
|
21
21
|
MAX_REQUESTS_PER_TURN = 100
|
|
22
22
|
|
|
23
|
-
attr_reader :context, :session, :pipeline, :env, :barrier
|
|
23
|
+
attr_reader :context, :session, :pipeline, :env, :barrier, :message_store
|
|
24
24
|
|
|
25
25
|
def initialize(
|
|
26
26
|
provider:,
|
|
27
|
+
model: nil,
|
|
27
28
|
tools: Brute::TOOLS,
|
|
28
29
|
cwd: Dir.pwd,
|
|
29
30
|
session: nil,
|
|
30
31
|
compactor_opts: {},
|
|
31
32
|
reasoning: {},
|
|
33
|
+
agent_name: nil,
|
|
32
34
|
on_content: nil,
|
|
33
35
|
on_reasoning: nil,
|
|
34
36
|
on_tool_call: nil,
|
|
35
37
|
on_tool_result: nil,
|
|
38
|
+
on_question: nil,
|
|
36
39
|
logger: nil
|
|
37
40
|
)
|
|
38
41
|
@provider = provider
|
|
42
|
+
@model = model
|
|
43
|
+
@agent_name = agent_name
|
|
39
44
|
@tool_classes = tools
|
|
40
45
|
@cwd = cwd
|
|
41
46
|
@session = session || Session.new
|
|
42
47
|
@logger = logger || Logger.new($stderr, level: Logger::INFO)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@system_prompt =
|
|
48
|
+
@message_store = @session.message_store
|
|
49
|
+
|
|
50
|
+
# Build system prompt via deferred builder
|
|
51
|
+
@system_prompt_builder = SystemPrompt.default
|
|
52
|
+
@system_prompt = @system_prompt_builder.prepare(
|
|
53
|
+
provider_name: @provider&.name,
|
|
54
|
+
model_name: @model || @provider&.default_model,
|
|
55
|
+
cwd: @cwd,
|
|
56
|
+
custom_rules: load_custom_rules,
|
|
57
|
+
agent: @agent_name,
|
|
58
|
+
).to_s
|
|
48
59
|
|
|
49
60
|
# Initialize the LLM context (with streaming when callbacks provided)
|
|
50
61
|
@stream = if on_content || on_reasoning
|
|
@@ -53,10 +64,13 @@ module Brute
|
|
|
53
64
|
on_reasoning: on_reasoning,
|
|
54
65
|
on_tool_call: on_tool_call,
|
|
55
66
|
on_tool_result: on_tool_result,
|
|
67
|
+
on_question: on_question,
|
|
56
68
|
)
|
|
57
69
|
end
|
|
58
|
-
|
|
59
|
-
|
|
70
|
+
ctx_opts = { tools: @tool_classes }
|
|
71
|
+
ctx_opts[:model] = @model if @model
|
|
72
|
+
ctx_opts[:stream] = @stream if @stream
|
|
73
|
+
@context = LLM::Context.new(@provider, **ctx_opts)
|
|
60
74
|
|
|
61
75
|
# Build the middleware pipeline
|
|
62
76
|
compactor = Compactor.new(provider, **compactor_opts)
|
|
@@ -65,6 +79,7 @@ module Brute
|
|
|
65
79
|
session: @session,
|
|
66
80
|
logger: @logger,
|
|
67
81
|
reasoning: reasoning,
|
|
82
|
+
message_store: @message_store,
|
|
68
83
|
)
|
|
69
84
|
|
|
70
85
|
# The shared env hash — passed to every pipeline.call()
|
|
@@ -82,6 +97,7 @@ module Brute
|
|
|
82
97
|
on_reasoning: on_reasoning,
|
|
83
98
|
on_tool_call: on_tool_call,
|
|
84
99
|
on_tool_result: on_tool_result,
|
|
100
|
+
on_question: on_question,
|
|
85
101
|
},
|
|
86
102
|
}
|
|
87
103
|
end
|
|
@@ -115,7 +131,7 @@ module Brute
|
|
|
115
131
|
|
|
116
132
|
# --- Agent loop ---
|
|
117
133
|
loop do
|
|
118
|
-
break if @context.functions.empty?
|
|
134
|
+
break if @context.functions.empty? && (!@stream || @stream.queue.empty?)
|
|
119
135
|
|
|
120
136
|
# Collect tool results.
|
|
121
137
|
# Streaming: tools already spawned threads during the LLM response — just join them.
|
|
@@ -135,7 +151,7 @@ module Brute
|
|
|
135
151
|
@request_count += 1
|
|
136
152
|
|
|
137
153
|
# Check limits
|
|
138
|
-
break if @context.functions.empty?
|
|
154
|
+
break if @context.functions.empty? && (!@stream || @stream.queue.empty?)
|
|
139
155
|
break if @request_count >= MAX_REQUESTS_PER_TURN
|
|
140
156
|
break if @env[:metadata][:tool_error_limit_reached]
|
|
141
157
|
end
|
|
@@ -149,28 +165,42 @@ module Brute
|
|
|
149
165
|
# Pipeline construction
|
|
150
166
|
# ------------------------------------------------------------------
|
|
151
167
|
|
|
152
|
-
def build_pipeline(compactor:, session:, logger:, reasoning:)
|
|
168
|
+
def build_pipeline(compactor:, session:, logger:, reasoning:, message_store:)
|
|
153
169
|
sys_prompt = @system_prompt
|
|
154
170
|
tools = @tool_classes
|
|
171
|
+
stream = @stream
|
|
155
172
|
|
|
156
173
|
Pipeline.new do
|
|
157
|
-
#
|
|
174
|
+
# OTel span lifecycle (outermost — creates env[:span])
|
|
175
|
+
use Middleware::OTel::Span
|
|
176
|
+
|
|
177
|
+
# Timing and logging
|
|
158
178
|
use Middleware::Tracing, logger: logger
|
|
159
179
|
|
|
180
|
+
# OTel: record tool results being sent back (pre-call)
|
|
181
|
+
use Middleware::OTel::ToolResults
|
|
182
|
+
|
|
160
183
|
# Retry transient errors (wraps everything below)
|
|
161
184
|
use Middleware::Retry
|
|
162
185
|
|
|
163
186
|
# Save after each successful LLM call
|
|
164
187
|
use Middleware::SessionPersistence, session: session
|
|
165
188
|
|
|
189
|
+
# Record structured messages in OpenCode {info, parts} format
|
|
190
|
+
use Middleware::MessageTracking, store: message_store
|
|
191
|
+
|
|
166
192
|
# Track cumulative token usage
|
|
167
193
|
use Middleware::TokenTracking
|
|
168
194
|
|
|
195
|
+
# OTel: record token usage from response (post-call)
|
|
196
|
+
use Middleware::OTel::TokenUsage
|
|
197
|
+
|
|
169
198
|
# Check context size and compact if needed
|
|
170
199
|
use Middleware::CompactionCheck,
|
|
171
200
|
compactor: compactor,
|
|
172
201
|
system_prompt: sys_prompt,
|
|
173
|
-
tools: tools
|
|
202
|
+
tools: tools,
|
|
203
|
+
stream: stream
|
|
174
204
|
|
|
175
205
|
# Track per-tool errors
|
|
176
206
|
use Middleware::ToolErrorTracking
|
|
@@ -184,6 +214,9 @@ module Brute
|
|
|
184
214
|
# Guard against tool-only responses dropping the assistant message
|
|
185
215
|
use Middleware::ToolUseGuard
|
|
186
216
|
|
|
217
|
+
# OTel: record tool calls the LLM requested (post-call, after ToolUseGuard)
|
|
218
|
+
use Middleware::OTel::ToolCalls
|
|
219
|
+
|
|
187
220
|
# Innermost: the actual LLM call
|
|
188
221
|
run Middleware::LLMCall.new
|
|
189
222
|
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
module Autonomy
|
|
6
|
+
TEXT = <<~TXT
|
|
7
|
+
# Autonomy and persistence
|
|
8
|
+
|
|
9
|
+
Unless the user explicitly asks for a plan, asks a question about the code, is brainstorming potential solutions, or some other intent that makes it clear that code should not be written, assume the user wants you to make code changes or run tools to solve the user's problem. If you encounter challenges or blockers, you should attempt to resolve them yourself.
|
|
10
|
+
|
|
11
|
+
Persist until the task is fully handled end-to-end within the current turn whenever feasible: do not stop at analysis or partial fixes; carry changes through implementation, verification, and a clear explanation of outcomes unless the user explicitly pauses or redirects you.
|
|
12
|
+
|
|
13
|
+
If you notice unexpected changes in the worktree or staging area that you did not make, continue with your task. NEVER revert, undo, or modify changes you did not make unless the user explicitly asks you to. There can be multiple agents or the user working in the same codebase concurrently.
|
|
14
|
+
TXT
|
|
15
|
+
|
|
16
|
+
def self.call(_ctx)
|
|
17
|
+
TEXT
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
TEXT_DIR = File.expand_path("text", __dir__)
|
|
6
|
+
|
|
7
|
+
# Resolve a provider-specific text file.
|
|
8
|
+
# Looks for +section/provider_name.txt+, falls back to +section/default.txt+.
|
|
9
|
+
def self.read(section, provider_name)
|
|
10
|
+
provider = provider_name.to_s
|
|
11
|
+
path = File.join(TEXT_DIR, section, "#{provider}.txt")
|
|
12
|
+
path = File.join(TEXT_DIR, section, "default.txt") unless File.exist?(path)
|
|
13
|
+
return nil unless File.exist?(path)
|
|
14
|
+
File.read(path)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Read a named agent prompt (e.g. "explore", "compaction").
|
|
18
|
+
def self.agent_prompt(name)
|
|
19
|
+
path = File.join(TEXT_DIR, "agents", "#{name}.txt")
|
|
20
|
+
File.exist?(path) ? File.read(path) : nil
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
module BuildSwitch
|
|
6
|
+
TEXT = <<~TXT
|
|
7
|
+
<system-reminder>
|
|
8
|
+
Your operational mode has changed from plan to build.
|
|
9
|
+
You are no longer in read-only mode.
|
|
10
|
+
You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed.
|
|
11
|
+
</system-reminder>
|
|
12
|
+
TXT
|
|
13
|
+
|
|
14
|
+
def self.call(_ctx)
|
|
15
|
+
TEXT
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
module CodeReferences
|
|
6
|
+
TEXT = <<~'TXT'
|
|
7
|
+
# Code References
|
|
8
|
+
When referencing specific functions or pieces of code include the pattern `file_path:line_number` to allow the user to easily navigate to the source code location.
|
|
9
|
+
|
|
10
|
+
<example>
|
|
11
|
+
user: Where are errors from the client handled?
|
|
12
|
+
assistant: Clients are marked as failed in the `connectToServer` function in src/services/process.ts:712.
|
|
13
|
+
</example>
|
|
14
|
+
TXT
|
|
15
|
+
|
|
16
|
+
def self.call(_ctx)
|
|
17
|
+
TEXT
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
module Conventions
|
|
6
|
+
TEXT = <<~TXT
|
|
7
|
+
# Following conventions
|
|
8
|
+
When making changes to files, first understand the file's code conventions. Mimic code style, use existing libraries and utilities, and follow existing patterns.
|
|
9
|
+
- NEVER assume that a given library is available, even if it is well known. Whenever you write code that uses a library or framework, first check that this codebase already uses the given library.
|
|
10
|
+
- When you create a new component, first look at existing components to see how they're written; then consider framework choice, naming conventions, typing, and other conventions.
|
|
11
|
+
- When you edit a piece of code, first look at the code's surrounding context (especially its imports) to understand the code's choice of frameworks and libraries. Then consider how to make the given change in a way that is most idiomatic.
|
|
12
|
+
- Always follow security best practices. Never introduce code that exposes or logs secrets and keys. Never commit secrets or keys to the repository.
|
|
13
|
+
TXT
|
|
14
|
+
|
|
15
|
+
def self.call(_ctx)
|
|
16
|
+
TEXT
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
module EditingApproach
|
|
6
|
+
TEXT = <<~TXT
|
|
7
|
+
# Editing Approach
|
|
8
|
+
|
|
9
|
+
- The best changes are often the smallest correct changes.
|
|
10
|
+
- When you are weighing two correct approaches, prefer the more minimal one (less new names, helpers, tests, etc).
|
|
11
|
+
- Keep things in one function unless composable or reusable.
|
|
12
|
+
- Do not add backward-compatibility code unless there is a concrete need, such as persisted data, shipped behavior, external consumers, or an explicit user requirement; if unclear, ask one short question instead of guessing.
|
|
13
|
+
TXT
|
|
14
|
+
|
|
15
|
+
def self.call(_ctx)
|
|
16
|
+
TEXT
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
module EditingConstraints
|
|
6
|
+
TEXT = <<~TXT
|
|
7
|
+
# Editing constraints
|
|
8
|
+
|
|
9
|
+
- Default to ASCII when editing or creating files. Only introduce non-ASCII or other Unicode characters when there is a clear justification and the file already uses them.
|
|
10
|
+
- Add succinct code comments that explain what is going on if code is not self-explanatory. Usage of these comments should be rare.
|
|
11
|
+
- Always use the patch tool for manual code edits. Do not use shell commands when creating or editing files.
|
|
12
|
+
- NEVER revert existing changes you did not make unless explicitly requested, since these changes were made by the user.
|
|
13
|
+
- You may be in a dirty git worktree.
|
|
14
|
+
* If asked to make a commit or code edits and there are unrelated changes to your work or changes that you didn't make in those files, don't revert those changes.
|
|
15
|
+
* If the changes are in files you've touched recently, read carefully and understand how you can work with the changes rather than reverting them.
|
|
16
|
+
* If the changes are in unrelated files, just ignore them and don't revert them.
|
|
17
|
+
TXT
|
|
18
|
+
|
|
19
|
+
def self.call(_ctx)
|
|
20
|
+
TEXT
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
module Environment
|
|
6
|
+
def self.call(ctx)
|
|
7
|
+
cwd = ctx[:cwd] || Dir.pwd
|
|
8
|
+
model = ctx[:model_name].to_s
|
|
9
|
+
git = File.exist?(File.join(cwd, ".git"))
|
|
10
|
+
|
|
11
|
+
parts = []
|
|
12
|
+
parts << "You are powered by the model named #{model}." unless model.empty?
|
|
13
|
+
parts << ""
|
|
14
|
+
parts << "Here is some useful information about the environment you are running in:"
|
|
15
|
+
parts << "<env>"
|
|
16
|
+
parts << " Working directory: #{cwd}"
|
|
17
|
+
parts << " Is directory a git repo: #{git ? "yes" : "no"}"
|
|
18
|
+
parts << " Platform: #{RUBY_PLATFORM}"
|
|
19
|
+
parts << " Today's date: #{Time.now.strftime("%a %b %d %Y")}"
|
|
20
|
+
parts << "</env>"
|
|
21
|
+
parts.join("\n")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
module FrontendTasks
|
|
6
|
+
TEXT = <<~TXT
|
|
7
|
+
# Frontend tasks
|
|
8
|
+
|
|
9
|
+
When doing frontend design tasks, avoid collapsing into bland, generic layouts.
|
|
10
|
+
- Ensure the page loads properly on both desktop and mobile.
|
|
11
|
+
- Overall: Avoid boilerplate layouts and interchangeable UI patterns. Vary themes, type families, and visual languages across outputs.
|
|
12
|
+
|
|
13
|
+
Exception: If working within an existing website or design system, preserve the established patterns, structure, and visual language.
|
|
14
|
+
TXT
|
|
15
|
+
|
|
16
|
+
def self.call(_ctx)
|
|
17
|
+
TEXT
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
module GitSafety
|
|
6
|
+
TEXT = <<~TXT
|
|
7
|
+
# Git safety
|
|
8
|
+
- NEVER commit changes unless the user explicitly asks you to.
|
|
9
|
+
- NEVER use destructive commands like `git reset --hard` or `git checkout --` unless specifically requested.
|
|
10
|
+
- Do not amend commits unless explicitly requested.
|
|
11
|
+
- Prefer non-interactive git commands. Avoid `git rebase -i` or `git add -i`.
|
|
12
|
+
TXT
|
|
13
|
+
|
|
14
|
+
def self.call(_ctx)
|
|
15
|
+
TEXT
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Brute
|
|
4
|
+
module Prompts
|
|
5
|
+
module Instructions
|
|
6
|
+
def self.call(ctx)
|
|
7
|
+
rules = ctx[:custom_rules]
|
|
8
|
+
return nil if rules.nil? || rules.strip.empty?
|
|
9
|
+
|
|
10
|
+
<<~TXT
|
|
11
|
+
# Project-Specific Rules
|
|
12
|
+
|
|
13
|
+
#{rules}
|
|
14
|
+
TXT
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|