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.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/agent_stream.rb +16 -1
  3. data/lib/brute/message_store.rb +269 -0
  4. data/lib/brute/middleware/compaction_check.rb +5 -2
  5. data/lib/brute/middleware/message_tracking.rb +209 -0
  6. data/lib/brute/middleware/otel/span.rb +75 -0
  7. data/lib/brute/middleware/otel/token_usage.rb +30 -0
  8. data/lib/brute/middleware/otel/tool_calls.rb +39 -0
  9. data/lib/brute/middleware/otel/tool_results.rb +37 -0
  10. data/lib/brute/middleware/otel.rb +29 -0
  11. data/lib/brute/middleware/tool_use_guard.rb +66 -23
  12. data/lib/brute/orchestrator.rb +46 -13
  13. data/lib/brute/prompts/autonomy.rb +21 -0
  14. data/lib/brute/prompts/base.rb +23 -0
  15. data/lib/brute/prompts/build_switch.rb +19 -0
  16. data/lib/brute/prompts/code_references.rb +21 -0
  17. data/lib/brute/prompts/code_style.rb +16 -0
  18. data/lib/brute/prompts/conventions.rb +20 -0
  19. data/lib/brute/prompts/doing_tasks.rb +11 -0
  20. data/lib/brute/prompts/editing_approach.rb +20 -0
  21. data/lib/brute/prompts/editing_constraints.rb +24 -0
  22. data/lib/brute/prompts/environment.rb +25 -0
  23. data/lib/brute/prompts/frontend_tasks.rb +21 -0
  24. data/lib/brute/prompts/git_safety.rb +19 -0
  25. data/lib/brute/prompts/identity.rb +11 -0
  26. data/lib/brute/prompts/instructions.rb +18 -0
  27. data/lib/brute/prompts/max_steps.rb +30 -0
  28. data/lib/brute/prompts/objectivity.rb +16 -0
  29. data/lib/brute/prompts/plan_reminder.rb +40 -0
  30. data/lib/brute/prompts/proactiveness.rb +19 -0
  31. data/lib/brute/prompts/security_and_safety.rb +17 -0
  32. data/lib/brute/prompts/skills.rb +22 -0
  33. data/lib/brute/prompts/task_management.rb +59 -0
  34. data/lib/brute/prompts/text/agents/compaction.txt +15 -0
  35. data/lib/brute/prompts/text/agents/explore.txt +17 -0
  36. data/lib/brute/prompts/text/agents/summary.txt +11 -0
  37. data/lib/brute/prompts/text/agents/title.txt +40 -0
  38. data/lib/brute/prompts/text/doing_tasks/anthropic.txt +11 -0
  39. data/lib/brute/prompts/text/doing_tasks/default.txt +6 -0
  40. data/lib/brute/prompts/text/doing_tasks/google.txt +9 -0
  41. data/lib/brute/prompts/text/identity/anthropic.txt +5 -0
  42. data/lib/brute/prompts/text/identity/default.txt +3 -0
  43. data/lib/brute/prompts/text/identity/google.txt +1 -0
  44. data/lib/brute/prompts/text/identity/openai.txt +3 -0
  45. data/lib/brute/prompts/text/tone_and_style/anthropic.txt +5 -0
  46. data/lib/brute/prompts/text/tone_and_style/default.txt +9 -0
  47. data/lib/brute/prompts/text/tone_and_style/google.txt +6 -0
  48. data/lib/brute/prompts/text/tone_and_style/openai.txt +17 -0
  49. data/lib/brute/prompts/text/tool_usage/anthropic.txt +16 -0
  50. data/lib/brute/prompts/text/tool_usage/default.txt +4 -0
  51. data/lib/brute/prompts/text/tool_usage/google.txt +4 -0
  52. data/lib/brute/prompts/tone_and_style.rb +11 -0
  53. data/lib/brute/prompts/tool_usage.rb +11 -0
  54. data/lib/brute/session.rb +109 -34
  55. data/lib/brute/skill.rb +118 -0
  56. data/lib/brute/system_prompt.rb +119 -64
  57. data/lib/brute/tools/question.rb +59 -0
  58. data/lib/brute/version.rb +1 -1
  59. data/lib/brute.rb +62 -2
  60. 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 injects a synthetic assistant message
15
- # when tool calls exist but no assistant message was recorded.
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
- # If there are pending tool calls, ensure the buffer has an assistant
28
- # message with tool_use blocks.
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
- messages = ctx.messages.to_a
31
- last_assistant = messages.reverse.find { |m| m.role.to_s == "assistant" }
32
-
33
- unless last_assistant&.tool_call?
34
- # Build a synthetic assistant message with the tool_use data
35
- tool_calls = functions.map do |fn|
36
- LLM::Object.from(id: fn.id, name: fn.name, arguments: fn.arguments)
37
- end
38
- original_tool_calls = functions.map do |fn|
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
- response
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
@@ -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
- # Build system prompt
45
- custom_rules = load_custom_rules
46
- prompt_builder = SystemPrompt.new(cwd: @cwd, tools: @tool_classes, custom_rules: custom_rules)
47
- @system_prompt = prompt_builder.build
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
- @context = LLM::Context.new(@provider, tools: @tool_classes,
59
- **(@stream ? {stream: @stream} : {}))
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
- # Outermost: timing and logging (sees total elapsed including retries)
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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brute
4
+ module Prompts
5
+ module CodeStyle
6
+ TEXT = <<~TXT
7
+ # Code style
8
+ - IMPORTANT: DO NOT ADD ***ANY*** COMMENTS unless asked
9
+ TXT
10
+
11
+ def self.call(_ctx)
12
+ TEXT
13
+ end
14
+ end
15
+ end
16
+ 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brute
4
+ module Prompts
5
+ module DoingTasks
6
+ def self.call(ctx)
7
+ Prompts.read("doing_tasks", ctx[:provider_name])
8
+ end
9
+ end
10
+ end
11
+ 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,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brute
4
+ module Prompts
5
+ module Identity
6
+ def self.call(ctx)
7
+ Prompts.read("identity", ctx[:provider_name])
8
+ end
9
+ end
10
+ end
11
+ 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