brute 0.4.1 → 1.0.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/agent.rb +14 -0
  3. data/lib/brute/diff.rb +18 -28
  4. data/lib/brute/loop/agent_stream.rb +118 -0
  5. data/lib/brute/loop/agent_turn.rb +520 -0
  6. data/lib/brute/{compactor.rb → loop/compactor.rb} +2 -0
  7. data/lib/brute/{doom_loop.rb → loop/doom_loop.rb} +2 -0
  8. data/lib/brute/loop/step.rb +332 -0
  9. data/lib/brute/loop/tool_call_step.rb +90 -0
  10. data/lib/brute/middleware/compaction_check.rb +60 -146
  11. data/lib/brute/middleware/doom_loop_detection.rb +95 -92
  12. data/lib/brute/middleware/llm_call.rb +78 -80
  13. data/lib/brute/middleware/message_tracking.rb +115 -162
  14. data/lib/brute/middleware/otel/span.rb +25 -106
  15. data/lib/brute/middleware/otel/token_usage.rb +29 -84
  16. data/lib/brute/middleware/otel/tool_calls.rb +23 -107
  17. data/lib/brute/middleware/otel/tool_results.rb +22 -86
  18. data/lib/brute/middleware/reasoning_normalizer.rb +78 -103
  19. data/lib/brute/middleware/retry.rb +95 -76
  20. data/lib/brute/middleware/session_persistence.rb +38 -37
  21. data/lib/brute/middleware/token_tracking.rb +64 -63
  22. data/lib/brute/middleware/tool_error_tracking.rb +108 -82
  23. data/lib/brute/middleware/tool_use_guard.rb +57 -90
  24. data/lib/brute/middleware/tracing.rb +53 -63
  25. data/lib/brute/middleware.rb +18 -0
  26. data/lib/brute/orchestrator/turn.rb +105 -0
  27. data/lib/brute/pipeline.rb +77 -133
  28. data/lib/brute/prompts/build_switch.rb +21 -25
  29. data/lib/brute/prompts/environment.rb +31 -35
  30. data/lib/brute/prompts/identity.rb +22 -29
  31. data/lib/brute/prompts/instructions.rb +15 -18
  32. data/lib/brute/prompts/max_steps.rb +18 -25
  33. data/lib/brute/prompts/plan_reminder.rb +18 -26
  34. data/lib/brute/prompts/skills.rb +8 -30
  35. data/lib/brute/prompts.rb +28 -0
  36. data/lib/brute/providers/ollama.rb +135 -0
  37. data/lib/brute/providers/shell.rb +2 -2
  38. data/lib/brute/providers/shell_response.rb +2 -2
  39. data/lib/brute/providers.rb +62 -0
  40. data/lib/brute/queue/base_queue.rb +222 -0
  41. data/lib/brute/{file_mutation_queue.rb → queue/file_mutation_queue.rb} +28 -26
  42. data/lib/brute/queue/parallel_queue.rb +66 -0
  43. data/lib/brute/queue/sequential_queue.rb +63 -0
  44. data/lib/brute/store/message_store.rb +362 -0
  45. data/lib/brute/store/session.rb +106 -0
  46. data/lib/brute/{snapshot_store.rb → store/snapshot_store.rb} +2 -0
  47. data/lib/brute/{todo_store.rb → store/todo_store.rb} +2 -0
  48. data/lib/brute/system_prompt.rb +81 -194
  49. data/lib/brute/tools/delegate.rb +46 -116
  50. data/lib/brute/tools/fs_patch.rb +36 -37
  51. data/lib/brute/tools/fs_remove.rb +2 -2
  52. data/lib/brute/tools/fs_undo.rb +2 -2
  53. data/lib/brute/tools/fs_write.rb +29 -41
  54. data/lib/brute/tools/todo_read.rb +1 -1
  55. data/lib/brute/tools/todo_write.rb +1 -1
  56. data/lib/brute/tools.rb +31 -0
  57. data/lib/brute/version.rb +1 -1
  58. data/lib/brute.rb +40 -204
  59. metadata +31 -20
  60. data/lib/brute/agent_stream.rb +0 -181
  61. data/lib/brute/hooks.rb +0 -84
  62. data/lib/brute/message_store.rb +0 -463
  63. data/lib/brute/orchestrator.rb +0 -550
  64. data/lib/brute/session.rb +0 -161
@@ -0,0 +1,105 @@
1
+ module Brute
2
+ class Orchestrator
3
+ class Turn
4
+ def initialize(env:, pending:)
5
+ @env = env
6
+ @pending = pending
7
+ end
8
+
9
+ def perform
10
+ @env.dig(:callbacks, :on_tool_call_start).then do |on_start|
11
+ on_start&.call(
12
+ @pending.map do |tool, _|
13
+ {
14
+ name: tool.name,
15
+ arguments: tool.arguments
16
+ }
17
+ end
18
+ )
19
+ end
20
+
21
+ execute_tool_calls.tap do |results|
22
+ errors.each do |_, err|
23
+ on_result = @env.dig(:callbacks, :on_tool_result)
24
+ on_result&.call(err.name, result_value(err))
25
+ results << err
26
+ end
27
+ end
28
+ end
29
+
30
+ def errors = @pending.select { |_, err| err }
31
+ def executable = @pending.reject { |_, err| err }.map(&:first)
32
+
33
+ def execute_tool_calls
34
+ if executable.empty?
35
+ []
36
+ else
37
+ # Questions block execution — they must complete before other tools
38
+ # run, since the LLM may need the answer to inform subsequent work.
39
+ # Execute any question tools first (sequentially), then dispatch
40
+ # the remaining tools concurrently.
41
+ questions, others = executable.partition { _1.name == "question" }
42
+
43
+ Array.new.tap do |results|
44
+ if questions.any?
45
+ results.concat(execute_sequential(questions))
46
+ end
47
+
48
+ if others.size <= 1
49
+ results.concat(execute_sequential(others))
50
+ else
51
+ results.concat(execute_parallel(others))
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ # Run a single tool call synchronously.
58
+ def execute_sequential(functions)
59
+ on_result = @env.dig(:callbacks, :on_tool_result)
60
+ on_question = @env.dig(:callbacks, :on_question)
61
+
62
+ functions.map do |fn|
63
+ Thread.current[:on_question] = on_question
64
+ result = fn.call
65
+ on_result&.call(fn.name, result_value(result))
66
+ result
67
+ end
68
+ end
69
+
70
+ # Run all pending tool calls concurrently via Async::Barrier.
71
+ #
72
+ # Each tool runs in its own fiber. File-mutating tools are safe because
73
+ # they go through FileMutationQueue, whose Mutex is fiber-scheduler-aware
74
+ # in Ruby 3.4 — a fiber blocked on a per-file mutex yields to other
75
+ # fibers instead of blocking the thread.
76
+ #
77
+ # The barrier is stored in @barrier so abort! can cancel in-flight tools.
78
+ #
79
+ def execute_parallel(functions)
80
+ on_result = @env.dig(:callbacks, :on_tool_result)
81
+ on_question = @env.dig(:callbacks, :on_question)
82
+
83
+ Array.new(functions.size).tap do |results|
84
+ Async do
85
+ @barrier = Async::Barrier.new
86
+
87
+ functions.each_with_index do |fn, i|
88
+ @barrier.async do
89
+ Thread.current[:on_question] = on_question
90
+ results[i] = fn.call
91
+ r = results[i]
92
+ on_result&.call(r.name, result_value(r))
93
+ end
94
+ end
95
+
96
+ @barrier.wait
97
+ ensure
98
+ @barrier&.stop
99
+ @barrier = nil
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
4
+ require "brute"
5
+
3
6
  module Brute
4
7
  # Rack-style middleware pipeline for LLM calls.
5
8
  #
@@ -16,13 +19,19 @@ module Brute
16
19
  # ## The env hash
17
20
  #
18
21
  # {
19
- # context: LLM::Context, # conversation state
20
- # provider: LLM::Provider, # the LLM provider
21
- # input: <prompt/results>, # what to pass to context.talk()
22
- # tools: [Tool, ...], # tool classes
23
- # params: {}, # extra LLM call params (reasoning config, etc.)
24
- # metadata: {}, # shared scratchpad for middleware state
25
- # callbacks: {}, # :on_content, :on_tool_call_start, :on_tool_result
22
+ # provider: LLM::Provider, # the LLM provider
23
+ # model: String|nil, # model override
24
+ # input: <prompt/results>, # what to pass to LLM
25
+ # tools: [Tool, ...], # tool classes
26
+ # messages: [LLM::Message], # conversation history (Brute-owned)
27
+ # stream: AgentStream|nil, # streaming bridge
28
+ # params: {}, # extra LLM call params
29
+ # metadata: {}, # shared scratchpad for middleware state
30
+ # callbacks: {}, # :on_content, :on_tool_call_start, :on_tool_result
31
+ # tool_results: Array|nil, # tool results from previous iteration
32
+ # streaming: Boolean, # whether streaming is active
33
+ # should_exit: Hash|nil, # exit signal from middleware
34
+ # pending_functions: [LLM::Function], # tool calls from last LLM response
26
35
  # }
27
36
  #
28
37
  # ## The response
@@ -80,137 +89,72 @@ module Brute
80
89
  end
81
90
  end
82
91
 
83
- if __FILE__ == $0
84
- require_relative "../../spec/spec_helper"
85
-
86
- RSpec.describe "Middleware Pipeline Integration" do
87
- let(:provider) { MockProvider.new }
88
- let(:log_output) { StringIO.new }
89
- let(:logger) { Logger.new(log_output) }
90
- let(:session) { double("session", save: nil) }
91
- let(:compactor) { double("compactor", should_compact?: false) }
92
-
93
- describe "full orchestrator pipeline" do
94
- it "passes env through all middleware and returns the response" do
95
- response = MockResponse.new(content: "integrated response")
96
- allow(provider).to receive(:complete).and_return(response)
97
-
98
- ctx = LLM::Context.new(provider, tools: [])
99
- prompt = ctx.prompt { |p| p.system("sys"); p.user("hello") }
100
-
101
- pipeline = Brute::Pipeline.new
102
- pipeline.use(Brute::Middleware::Tracing, logger: logger)
103
- pipeline.use(Brute::Middleware::Retry, max_attempts: 3, base_delay: 2)
104
- pipeline.use(Brute::Middleware::SessionPersistence, session: session)
105
- pipeline.use(Brute::Middleware::TokenTracking)
106
- pipeline.use(Brute::Middleware::CompactionCheck, compactor: compactor, system_prompt: "sys", tools: [])
107
- pipeline.use(Brute::Middleware::ToolErrorTracking)
108
- pipeline.use(Brute::Middleware::DoomLoopDetection, threshold: 3)
109
- pipeline.use(Brute::Middleware::ToolUseGuard)
110
- pipeline.run(Brute::Middleware::LLMCall.new)
111
-
112
- env = {
113
- context: ctx,
114
- provider: provider,
115
- input: prompt,
116
- tools: [],
117
- params: {},
118
- metadata: {},
119
- callbacks: {},
120
- tool_results: nil,
121
- streaming: false,
122
- }
123
-
124
- result = pipeline.call(env)
125
-
126
- expect(result).not_to be_nil
127
- expect(env[:metadata][:timing]).to include(:llm_call_count, :total_llm_elapsed)
128
- expect(env[:metadata][:tokens]).to include(:total_input, :total_output, :call_count)
129
- expect(session).to have_received(:save)
130
- expect(log_output.string).to include("LLM call #1")
131
- end
132
- end
92
+ test do
93
+ require_relative "../../spec/support/mock_provider"
94
+ require_relative "../../spec/support/mock_response"
133
95
 
134
- describe "Retry + Tracing combo" do
135
- it "Tracing sees the full elapsed time including retries" do
136
- call_count = 0
137
- response = MockResponse.new(content: "recovered")
96
+ def make_env(provider:, input:)
97
+ { provider: provider, model: nil, input: input, tools: [], messages: [],
98
+ stream: nil, params: {}, metadata: {}, callbacks: {}, tool_results: nil,
99
+ streaming: false, should_exit: nil, pending_functions: [] }
100
+ end
138
101
 
139
- allow(provider).to receive(:complete) do |*_args|
140
- call_count += 1
141
- raise LLM::RateLimitError, "rate limited" if call_count <= 1
142
- response
143
- end
102
+ it "full pipeline passes env through all middleware" do
103
+ provider = MockProvider.new
104
+ session = Struct.new(:saved) { def save_messages(m); self.saved = m; end }.new
105
+ compactor = Object.new
106
+ compactor.define_singleton_method(:should_compact?) { |_msgs, **_| false }
107
+ log_output = StringIO.new
108
+
109
+ pipeline = Brute::Pipeline.new
110
+ pipeline.use(Brute::Middleware::Tracing, logger: Logger.new(log_output))
111
+ pipeline.use(Brute::Middleware::Retry, max_attempts: 3, base_delay: 2)
112
+ pipeline.use(Brute::Middleware::SessionPersistence, session: session)
113
+ pipeline.use(Brute::Middleware::TokenTracking)
114
+ pipeline.use(Brute::Middleware::CompactionCheck, compactor: compactor, system_prompt: "sys")
115
+ pipeline.use(Brute::Middleware::ToolErrorTracking)
116
+ pipeline.use(Brute::Middleware::DoomLoopDetection, threshold: 3)
117
+ pipeline.use(Brute::Middleware::ToolUseGuard)
118
+ pipeline.run(Brute::Middleware::LLMCall.new)
119
+
120
+ env = make_env(provider: provider, input: "hello")
121
+ result = pipeline.call(env)
122
+ result.should.not.be.nil
123
+ end
144
124
 
145
- ctx = LLM::Context.new(provider, tools: [])
146
- prompt = ctx.prompt { |p| p.system("sys"); p.user("hi") }
147
-
148
- pipeline = Brute::Pipeline.new
149
- pipeline.use(Brute::Middleware::Tracing, logger: logger)
150
- pipeline.use(Brute::Middleware::Retry, max_attempts: 3, base_delay: 0)
151
- pipeline.run(Brute::Middleware::LLMCall.new)
152
-
153
- env = {
154
- context: ctx,
155
- provider: provider,
156
- input: prompt,
157
- tools: [],
158
- params: {},
159
- metadata: {},
160
- callbacks: {},
161
- tool_results: nil,
162
- streaming: false,
163
- }
164
-
165
- result = pipeline.call(env)
166
-
167
- expect(result).not_to be_nil
168
- expect(env[:metadata][:timing][:llm_call_count]).to eq(1)
169
- end
170
- end
125
+ it "pipeline populates timing metadata" do
126
+ provider = MockProvider.new
127
+ session = Struct.new(:saved) { def save_messages(m); self.saved = m; end }.new
171
128
 
172
- describe "TokenTracking + SessionPersistence combo" do
173
- it "session receives save after tokens are tracked" do
174
- response = MockResponse.new(content: "tracked and saved")
175
- allow(provider).to receive(:complete).and_return(response)
176
-
177
- ctx = LLM::Context.new(provider, tools: [])
178
- prompt = ctx.prompt { |p| p.system("sys"); p.user("hi") }
179
-
180
- save_args = []
181
- allow(session).to receive(:save) { |ctx_arg| save_args << ctx_arg }
182
-
183
- pipeline = Brute::Pipeline.new
184
- pipeline.use(Brute::Middleware::SessionPersistence, session: session)
185
- pipeline.use(Brute::Middleware::TokenTracking)
186
- pipeline.run(Brute::Middleware::LLMCall.new)
187
-
188
- env = {
189
- context: ctx,
190
- provider: provider,
191
- input: prompt,
192
- tools: [],
193
- params: {},
194
- metadata: {},
195
- callbacks: {},
196
- tool_results: nil,
197
- streaming: false,
198
- }
199
-
200
- pipeline.call(env)
201
-
202
- expect(env[:metadata][:tokens]).to include(:total_input)
203
- expect(save_args.size).to eq(1)
204
- end
205
- end
129
+ pipeline = Brute::Pipeline.new
130
+ pipeline.use(Brute::Middleware::Tracing, logger: Logger.new(StringIO.new))
131
+ pipeline.use(Brute::Middleware::SessionPersistence, session: session)
132
+ pipeline.use(Brute::Middleware::TokenTracking)
133
+ pipeline.run(Brute::Middleware::LLMCall.new)
206
134
 
207
- describe "Pipeline builder" do
208
- it "raises when no terminal app is set" do
209
- pipeline = Brute::Pipeline.new
210
- pipeline.use(Brute::Middleware::TokenTracking)
135
+ env = make_env(provider: provider, input: "hello")
136
+ pipeline.call(env)
137
+ env[:metadata][:timing][:llm_call_count].should == 1
138
+ end
211
139
 
212
- expect { pipeline.call({}) }.to raise_error(RuntimeError, /no terminal app/)
213
- end
214
- end
140
+ it "pipeline populates token metadata" do
141
+ provider = MockProvider.new
142
+ session = Struct.new(:saved) { def save_messages(m); self.saved = m; end }.new
143
+
144
+ pipeline = Brute::Pipeline.new
145
+ pipeline.use(Brute::Middleware::Tracing, logger: Logger.new(StringIO.new))
146
+ pipeline.use(Brute::Middleware::SessionPersistence, session: session)
147
+ pipeline.use(Brute::Middleware::TokenTracking)
148
+ pipeline.run(Brute::Middleware::LLMCall.new)
149
+
150
+ env = make_env(provider: provider, input: "hello")
151
+ pipeline.call(env)
152
+ env[:metadata][:tokens][:total_input].should.be > 0
153
+ end
154
+
155
+ it "raises when no terminal app is set" do
156
+ pipeline = Brute::Pipeline.new
157
+ pipeline.use(Brute::Middleware::TokenTracking)
158
+ lambda { pipeline.call({}) }.should.raise(RuntimeError)
215
159
  end
216
160
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
4
+ require "brute"
5
+
3
6
  module Brute
4
7
  module Prompts
5
8
  module BuildSwitch
@@ -18,35 +21,28 @@ module Brute
18
21
  end
19
22
  end
20
23
 
21
- if __FILE__ == $0
22
- require_relative "../../../spec/spec_helper"
23
-
24
- RSpec.describe Brute::Prompts::BuildSwitch do
25
- subject(:text) { described_class.call({}) }
26
-
27
- it "returns a string" do
28
- expect(text).to be_a(String)
29
- end
24
+ test do
25
+ it "returns a string" do
26
+ Brute::Prompts::BuildSwitch.call({}).should.be.kind_of(String)
27
+ end
30
28
 
31
- it "wraps content in system-reminder tags" do
32
- expect(text).to include("<system-reminder>")
33
- expect(text).to include("</system-reminder>")
34
- end
29
+ it "wraps content in system-reminder tags" do
30
+ Brute::Prompts::BuildSwitch.call({}).should =~ /system-reminder/
31
+ end
35
32
 
36
- it "announces the mode change from plan to build" do
37
- expect(text).to include("plan to build")
38
- end
33
+ it "announces mode change from plan to build" do
34
+ Brute::Prompts::BuildSwitch.call({}).should =~ /plan to build/
35
+ end
39
36
 
40
- it "states the agent is no longer in read-only mode" do
41
- expect(text).to include("no longer in read-only mode")
42
- end
37
+ it "states no longer in read-only mode" do
38
+ Brute::Prompts::BuildSwitch.call({}).should =~ /no longer in read-only mode/
39
+ end
43
40
 
44
- it "permits tool use" do
45
- expect(text).to include("permitted to make file changes")
46
- end
41
+ it "permits tool use" do
42
+ Brute::Prompts::BuildSwitch.call({}).should =~ /permitted to make file changes/
43
+ end
47
44
 
48
- it "ignores context (static content)" do
49
- expect(described_class.call({ agent_switched: "build" })).to eq(described_class.call({}))
50
- end
45
+ it "ignores context (static content)" do
46
+ Brute::Prompts::BuildSwitch.call({ agent_switched: "build" }).should == Brute::Prompts::BuildSwitch.call({})
51
47
  end
52
48
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
4
+ require "brute"
5
+
3
6
  module Brute
4
7
  module Prompts
5
8
  module Environment
@@ -24,49 +27,42 @@ module Brute
24
27
  end
25
28
  end
26
29
 
27
- if __FILE__ == $0
28
- require_relative "../../../spec/spec_helper"
30
+ test do
31
+ require "tmpdir"
32
+ require "fileutils"
29
33
 
30
- RSpec.describe Brute::Prompts::Environment do
31
- it "includes cwd from context" do
32
- text = described_class.call(cwd: "/some/path", model_name: "test")
33
- expect(text).to include("/some/path")
34
- end
34
+ it "includes cwd from context" do
35
+ Brute::Prompts::Environment.call(cwd: "/some/path", model_name: "test").should =~ /\/some\/path/
36
+ end
35
37
 
36
- it "includes model name from context" do
37
- text = described_class.call(cwd: "/tmp", model_name: "claude-sonnet-4-20250514")
38
- expect(text).to include("claude-sonnet-4-20250514")
39
- end
38
+ it "includes model name from context" do
39
+ Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "claude-sonnet").should =~ /claude-sonnet/
40
+ end
40
41
 
41
- it "wraps environment info in <env> tags" do
42
- text = described_class.call(cwd: "/tmp", model_name: "test")
43
- expect(text).to include("<env>")
44
- expect(text).to include("</env>")
45
- end
42
+ it "wraps environment info in env tags" do
43
+ Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "test").should =~ /<env>/
44
+ end
46
45
 
47
- it "detects git repo when .git exists" do
48
- Dir.mktmpdir do |dir|
49
- Dir.mkdir(File.join(dir, ".git"))
50
- text = described_class.call(cwd: dir, model_name: "test")
51
- expect(text).to include("Is directory a git repo: yes")
52
- end
46
+ it "detects git repo when .git exists" do
47
+ Dir.mktmpdir do |dir|
48
+ Dir.mkdir(File.join(dir, ".git"))
49
+ text = Brute::Prompts::Environment.call(cwd: dir, model_name: "test")
50
+ text.should =~ /Is directory a git repo: yes/
53
51
  end
52
+ end
54
53
 
55
- it "detects non-git directory" do
56
- Dir.mktmpdir do |dir|
57
- text = described_class.call(cwd: dir, model_name: "test")
58
- expect(text).to include("Is directory a git repo: no")
59
- end
54
+ it "detects non-git directory" do
55
+ Dir.mktmpdir do |dir|
56
+ text = Brute::Prompts::Environment.call(cwd: dir, model_name: "test")
57
+ text.should =~ /Is directory a git repo: no/
60
58
  end
59
+ end
61
60
 
62
- it "includes the platform" do
63
- text = described_class.call(cwd: "/tmp", model_name: "test")
64
- expect(text).to include(RUBY_PLATFORM)
65
- end
61
+ it "includes the platform" do
62
+ Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "test").should =~ /#{RUBY_PLATFORM}/
63
+ end
66
64
 
67
- it "defaults cwd to Dir.pwd when not provided" do
68
- text = described_class.call(model_name: "test")
69
- expect(text).to include(Dir.pwd)
70
- end
65
+ it "defaults cwd to Dir.pwd when not provided" do
66
+ Brute::Prompts::Environment.call(model_name: "test").should =~ /#{Regexp.escape(Dir.pwd)}/
71
67
  end
72
68
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
4
+ require "brute"
5
+
3
6
  module Brute
4
7
  module Prompts
5
8
  module Identity
@@ -10,38 +13,28 @@ module Brute
10
13
  end
11
14
  end
12
15
 
13
- if __FILE__ == $0
14
- require_relative "../../../spec/spec_helper"
15
-
16
- RSpec.describe Brute::Prompts::Identity do
17
- it "returns provider-specific text for anthropic" do
18
- text = described_class.call(provider_name: "anthropic")
19
- expect(text).to be_a(String)
20
- expect(text).not_to be_empty
21
- end
16
+ test do
17
+ it "returns a string for anthropic" do
18
+ Brute::Prompts::Identity.call(provider_name: "anthropic").should.be.kind_of(String)
19
+ end
22
20
 
23
- it "returns provider-specific text for openai" do
24
- text = described_class.call(provider_name: "openai")
25
- expect(text).to be_a(String)
26
- expect(text).not_to be_empty
27
- end
21
+ it "returns non-empty text for anthropic" do
22
+ Brute::Prompts::Identity.call(provider_name: "anthropic").should.not.be.empty
23
+ end
28
24
 
29
- it "returns provider-specific text for google" do
30
- text = described_class.call(provider_name: "google")
31
- expect(text).to be_a(String)
32
- expect(text).not_to be_empty
33
- end
25
+ it "returns non-empty text for openai" do
26
+ Brute::Prompts::Identity.call(provider_name: "openai").should.not.be.empty
27
+ end
34
28
 
35
- it "falls back to default.txt for unknown providers" do
36
- default_text = described_class.call(provider_name: "default")
37
- unknown_text = described_class.call(provider_name: "nonexistent_provider")
38
- expect(unknown_text).to eq(default_text)
39
- end
29
+ it "falls back to default for unknown providers" do
30
+ default_text = Brute::Prompts::Identity.call(provider_name: "default")
31
+ unknown_text = Brute::Prompts::Identity.call(provider_name: "nonexistent_provider")
32
+ unknown_text.should == default_text
33
+ end
40
34
 
41
- it "returns different text for different providers" do
42
- anthropic = described_class.call(provider_name: "anthropic")
43
- openai = described_class.call(provider_name: "openai")
44
- expect(anthropic).not_to eq(openai)
45
- end
35
+ it "returns different text for different providers" do
36
+ anthropic = Brute::Prompts::Identity.call(provider_name: "anthropic")
37
+ openai = Brute::Prompts::Identity.call(provider_name: "openai")
38
+ (anthropic != openai).should.be.true
46
39
  end
47
40
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
4
+ require "brute"
5
+
3
6
  module Brute
4
7
  module Prompts
5
8
  module Instructions
@@ -17,26 +20,20 @@ module Brute
17
20
  end
18
21
  end
19
22
 
20
- if __FILE__ == $0
21
- require_relative "../../../spec/spec_helper"
22
-
23
- RSpec.describe Brute::Prompts::Instructions do
24
- it "returns nil when custom_rules is nil" do
25
- expect(described_class.call(custom_rules: nil)).to be_nil
26
- end
23
+ test do
24
+ it "returns nil when custom_rules is nil" do
25
+ Brute::Prompts::Instructions.call(custom_rules: nil).should.be.nil
26
+ end
27
27
 
28
- it "returns nil when custom_rules is empty" do
29
- expect(described_class.call(custom_rules: "")).to be_nil
30
- end
28
+ it "returns nil when custom_rules is empty" do
29
+ Brute::Prompts::Instructions.call(custom_rules: "").should.be.nil
30
+ end
31
31
 
32
- it "returns nil when custom_rules is whitespace-only" do
33
- expect(described_class.call(custom_rules: " \n ")).to be_nil
34
- end
32
+ it "returns nil when custom_rules is whitespace-only" do
33
+ Brute::Prompts::Instructions.call(custom_rules: " \n ").should.be.nil
34
+ end
35
35
 
36
- it "wraps custom_rules in a Project-Specific Rules header" do
37
- text = described_class.call(custom_rules: "Always use tabs.")
38
- expect(text).to include("Project-Specific Rules")
39
- expect(text).to include("Always use tabs.")
40
- end
36
+ it "wraps custom_rules in a Project-Specific Rules header" do
37
+ Brute::Prompts::Instructions.call(custom_rules: "Always use tabs.").should =~ /Project-Specific Rules/
41
38
  end
42
39
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "bundler/setup"
4
+ require "brute"
5
+
3
6
  module Brute
4
7
  module Prompts
5
8
  module MaxSteps
@@ -29,34 +32,24 @@ module Brute
29
32
  end
30
33
  end
31
34
 
32
- if __FILE__ == $0
33
- require_relative "../../../spec/spec_helper"
34
-
35
- RSpec.describe Brute::Prompts::MaxSteps do
36
- subject(:text) { described_class.call({}) }
37
-
38
- it "returns a string" do
39
- expect(text).to be_a(String)
40
- end
41
-
42
- it "announces maximum steps reached" do
43
- expect(text).to include("MAXIMUM STEPS REACHED")
44
- end
35
+ test do
36
+ it "returns a string" do
37
+ Brute::Prompts::MaxSteps.call({}).should.be.kind_of(String)
38
+ end
45
39
 
46
- it "states tools are disabled" do
47
- expect(text).to include("Tools are disabled")
48
- end
40
+ it "announces maximum steps reached" do
41
+ Brute::Prompts::MaxSteps.call({}).should =~ /MAXIMUM STEPS REACHED/
42
+ end
49
43
 
50
- it "requires a text-only response" do
51
- expect(text).to include("text ONLY")
52
- end
44
+ it "states tools are disabled" do
45
+ Brute::Prompts::MaxSteps.call({}).should =~ /Tools are disabled/
46
+ end
53
47
 
54
- it "requires summary of work done" do
55
- expect(text).to include("Summary of what has been accomplished")
56
- end
48
+ it "requires a text-only response" do
49
+ Brute::Prompts::MaxSteps.call({}).should =~ /text ONLY/
50
+ end
57
51
 
58
- it "ignores context (static content)" do
59
- expect(described_class.call({ max_steps_reached: true })).to eq(described_class.call({}))
60
- end
52
+ it "ignores context (static content)" do
53
+ Brute::Prompts::MaxSteps.call({ max_steps_reached: true }).should == Brute::Prompts::MaxSteps.call({})
61
54
  end
62
55
  end