brute 0.4.0 → 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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/lib/brute/agent.rb +14 -0
  3. data/lib/brute/diff.rb +24 -0
  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 +70 -23
  11. data/lib/brute/middleware/doom_loop_detection.rb +110 -7
  12. data/lib/brute/middleware/llm_call.rb +88 -1
  13. data/lib/brute/middleware/message_tracking.rb +140 -10
  14. data/lib/brute/middleware/otel/span.rb +32 -2
  15. data/lib/brute/middleware/otel/token_usage.rb +38 -0
  16. data/lib/brute/middleware/otel/tool_calls.rb +30 -1
  17. data/lib/brute/middleware/otel/tool_results.rb +29 -1
  18. data/lib/brute/middleware/otel.rb +5 -0
  19. data/lib/brute/middleware/reasoning_normalizer.rb +94 -0
  20. data/lib/brute/middleware/retry.rb +113 -1
  21. data/lib/brute/middleware/session_persistence.rb +46 -3
  22. data/lib/brute/middleware/token_tracking.rb +78 -0
  23. data/lib/brute/middleware/tool_error_tracking.rb +128 -1
  24. data/lib/brute/middleware/tool_use_guard.rb +64 -28
  25. data/lib/brute/middleware/tracing.rb +63 -2
  26. data/lib/brute/middleware.rb +18 -0
  27. data/lib/brute/orchestrator/turn.rb +105 -0
  28. data/lib/brute/patches/buffer_nil_guard.rb +5 -0
  29. data/lib/brute/pipeline.rb +86 -7
  30. data/lib/brute/prompts/build_switch.rb +29 -0
  31. data/lib/brute/prompts/environment.rb +43 -0
  32. data/lib/brute/prompts/identity.rb +29 -0
  33. data/lib/brute/prompts/instructions.rb +21 -0
  34. data/lib/brute/prompts/max_steps.rb +25 -0
  35. data/lib/brute/prompts/plan_reminder.rb +25 -0
  36. data/lib/brute/prompts/skills.rb +13 -0
  37. data/lib/brute/prompts.rb +28 -0
  38. data/lib/brute/providers/ollama.rb +135 -0
  39. data/lib/brute/providers/opencode_go.rb +5 -0
  40. data/lib/brute/providers/opencode_zen.rb +7 -2
  41. data/lib/brute/providers/shell.rb +2 -2
  42. data/lib/brute/providers/shell_response.rb +7 -2
  43. data/lib/brute/providers.rb +62 -0
  44. data/lib/brute/queue/base_queue.rb +222 -0
  45. data/lib/brute/{file_mutation_queue.rb → queue/file_mutation_queue.rb} +28 -26
  46. data/lib/brute/queue/parallel_queue.rb +66 -0
  47. data/lib/brute/queue/sequential_queue.rb +63 -0
  48. data/lib/brute/{message_store.rb → store/message_store.rb} +155 -62
  49. data/lib/brute/store/session.rb +106 -0
  50. data/lib/brute/{snapshot_store.rb → store/snapshot_store.rb} +2 -0
  51. data/lib/brute/{todo_store.rb → store/todo_store.rb} +2 -0
  52. data/lib/brute/system_prompt.rb +101 -0
  53. data/lib/brute/tools/delegate.rb +59 -0
  54. data/lib/brute/tools/fs_patch.rb +54 -2
  55. data/lib/brute/tools/fs_read.rb +5 -0
  56. data/lib/brute/tools/fs_remove.rb +7 -2
  57. data/lib/brute/tools/fs_search.rb +5 -0
  58. data/lib/brute/tools/fs_undo.rb +7 -2
  59. data/lib/brute/tools/fs_write.rb +40 -2
  60. data/lib/brute/tools/net_fetch.rb +5 -0
  61. data/lib/brute/tools/question.rb +5 -0
  62. data/lib/brute/tools/shell.rb +5 -0
  63. data/lib/brute/tools/todo_read.rb +6 -1
  64. data/lib/brute/tools/todo_write.rb +6 -1
  65. data/lib/brute/tools.rb +31 -0
  66. data/lib/brute/version.rb +1 -1
  67. data/lib/brute.rb +40 -204
  68. metadata +31 -20
  69. data/lib/brute/agent_stream.rb +0 -63
  70. data/lib/brute/hooks.rb +0 -84
  71. data/lib/brute/orchestrator.rb +0 -391
  72. data/lib/brute/session.rb +0 -161
@@ -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 Middleware
5
8
  # Logs timing and token usage for every LLM call, and tracks cumulative
@@ -9,7 +12,7 @@ module Brute
9
12
  # call. It also tracks total wall-clock time across all calls in a turn
10
13
  # (including tool execution gaps between LLM calls).
11
14
  #
12
- # A new turn is detected when env[:tool_results] is nil (the orchestrator
15
+ # A new turn is detected when env[:tool_results] is nil (the agent loop
13
16
  # sets this on the first call of each run()).
14
17
  #
15
18
  # Stores in env[:metadata][:timing]:
@@ -36,7 +39,7 @@ module Brute
36
39
  @total_llm_elapsed = 0.0
37
40
  end
38
41
 
39
- messages = env[:context].messages.to_a
42
+ messages = env[:messages]
40
43
  @logger.debug("[brute] LLM call ##{@call_count} (#{messages.size} messages in context)")
41
44
 
42
45
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -61,3 +64,61 @@ module Brute
61
64
  end
62
65
  end
63
66
  end
67
+
68
+ test do
69
+ require_relative "../../../spec/support/mock_provider"
70
+ require_relative "../../../spec/support/mock_response"
71
+
72
+ def build_env(**overrides)
73
+ { provider: MockProvider.new, model: nil, input: "test prompt", tools: [],
74
+ messages: [], stream: nil, params: {}, metadata: {}, callbacks: {},
75
+ tool_results: nil, streaming: false, should_exit: nil, pending_functions: [] }.merge(overrides)
76
+ end
77
+
78
+ it "passes the response through unchanged" do
79
+ response = MockResponse.new(content: "traced response")
80
+ inner_app = ->(_env) { response }
81
+ middleware = Brute::Middleware::Tracing.new(inner_app, logger: Logger.new(StringIO.new))
82
+ result = middleware.call(build_env(tool_results: nil))
83
+ result.should == response
84
+ end
85
+
86
+ it "populates timing with llm_call_count" do
87
+ response = MockResponse.new(content: "traced response")
88
+ inner_app = ->(_env) { response }
89
+ middleware = Brute::Middleware::Tracing.new(inner_app, logger: Logger.new(StringIO.new))
90
+ env = build_env(tool_results: nil)
91
+ middleware.call(env)
92
+ env[:metadata][:timing][:llm_call_count].should == 1
93
+ end
94
+
95
+ it "populates timing with non-negative last_call_elapsed" do
96
+ response = MockResponse.new(content: "traced response")
97
+ inner_app = ->(_env) { response }
98
+ middleware = Brute::Middleware::Tracing.new(inner_app, logger: Logger.new(StringIO.new))
99
+ env = build_env(tool_results: nil)
100
+ middleware.call(env)
101
+ (env[:metadata][:timing][:last_call_elapsed] >= 0).should.be.true
102
+ end
103
+
104
+ it "accumulates call count across multiple calls" do
105
+ response = MockResponse.new(content: "traced response")
106
+ inner_app = ->(_env) { response }
107
+ middleware = Brute::Middleware::Tracing.new(inner_app, logger: Logger.new(StringIO.new))
108
+ env = build_env(tool_results: nil)
109
+ middleware.call(env)
110
+ env[:tool_results] = [["read", {}]]
111
+ middleware.call(env)
112
+ middleware.call(env)
113
+ env[:metadata][:timing][:llm_call_count].should == 3
114
+ end
115
+
116
+ it "logs LLM call and response messages" do
117
+ response = MockResponse.new(content: "traced response")
118
+ inner_app = ->(_env) { response }
119
+ log_output = StringIO.new
120
+ middleware = Brute::Middleware::Tracing.new(inner_app, logger: Logger.new(log_output))
121
+ middleware.call(build_env(tool_results: nil))
122
+ log_output.string.should =~ /LLM call #1/
123
+ end
124
+ end
@@ -0,0 +1,18 @@
1
+ require_relative 'middleware/base'
2
+ require_relative 'middleware/llm_call'
3
+ require_relative 'middleware/retry'
4
+ require_relative 'middleware/doom_loop_detection'
5
+ require_relative 'middleware/token_tracking'
6
+ require_relative 'middleware/compaction_check'
7
+ require_relative 'middleware/session_persistence'
8
+ require_relative 'middleware/message_tracking'
9
+ require_relative 'middleware/tracing'
10
+ require_relative 'middleware/tool_error_tracking'
11
+ require_relative 'middleware/reasoning_normalizer'
12
+ require_relative "middleware/tool_use_guard"
13
+ require_relative "middleware/otel"
14
+
15
+ module Brute
16
+ module Middleware
17
+ end
18
+ end
@@ -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,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ if __FILE__ == $0
4
+ require "bundler/setup"
5
+ require "brute"
6
+ end
7
+
3
8
  # Monkey-patch: Guard LLM::Buffer against nil entries.
4
9
  #
5
10
  # llm.rb's Context#talk can sometimes concatenate nil into the message
@@ -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
@@ -79,3 +88,73 @@ module Brute
79
88
  end
80
89
  end
81
90
  end
91
+
92
+ test do
93
+ require_relative "../../spec/support/mock_provider"
94
+ require_relative "../../spec/support/mock_response"
95
+
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
101
+
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
124
+
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
128
+
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)
134
+
135
+ env = make_env(provider: provider, input: "hello")
136
+ pipeline.call(env)
137
+ env[:metadata][:timing][:llm_call_count].should == 1
138
+ end
139
+
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)
159
+ end
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
@@ -17,3 +20,29 @@ module Brute
17
20
  end
18
21
  end
19
22
  end
23
+
24
+ test do
25
+ it "returns a string" do
26
+ Brute::Prompts::BuildSwitch.call({}).should.be.kind_of(String)
27
+ end
28
+
29
+ it "wraps content in system-reminder tags" do
30
+ Brute::Prompts::BuildSwitch.call({}).should =~ /system-reminder/
31
+ end
32
+
33
+ it "announces mode change from plan to build" do
34
+ Brute::Prompts::BuildSwitch.call({}).should =~ /plan to build/
35
+ end
36
+
37
+ it "states no longer in read-only mode" do
38
+ Brute::Prompts::BuildSwitch.call({}).should =~ /no longer in read-only mode/
39
+ end
40
+
41
+ it "permits tool use" do
42
+ Brute::Prompts::BuildSwitch.call({}).should =~ /permitted to make file changes/
43
+ end
44
+
45
+ it "ignores context (static content)" do
46
+ Brute::Prompts::BuildSwitch.call({ agent_switched: "build" }).should == Brute::Prompts::BuildSwitch.call({})
47
+ end
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
@@ -23,3 +26,43 @@ module Brute
23
26
  end
24
27
  end
25
28
  end
29
+
30
+ test do
31
+ require "tmpdir"
32
+ require "fileutils"
33
+
34
+ it "includes cwd from context" do
35
+ Brute::Prompts::Environment.call(cwd: "/some/path", model_name: "test").should =~ /\/some\/path/
36
+ end
37
+
38
+ it "includes model name from context" do
39
+ Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "claude-sonnet").should =~ /claude-sonnet/
40
+ end
41
+
42
+ it "wraps environment info in env tags" do
43
+ Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "test").should =~ /<env>/
44
+ end
45
+
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/
51
+ end
52
+ end
53
+
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/
58
+ end
59
+ end
60
+
61
+ it "includes the platform" do
62
+ Brute::Prompts::Environment.call(cwd: "/tmp", model_name: "test").should =~ /#{RUBY_PLATFORM}/
63
+ end
64
+
65
+ it "defaults cwd to Dir.pwd when not provided" do
66
+ Brute::Prompts::Environment.call(model_name: "test").should =~ /#{Regexp.escape(Dir.pwd)}/
67
+ end
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
@@ -9,3 +12,29 @@ module Brute
9
12
  end
10
13
  end
11
14
  end
15
+
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
20
+
21
+ it "returns non-empty text for anthropic" do
22
+ Brute::Prompts::Identity.call(provider_name: "anthropic").should.not.be.empty
23
+ end
24
+
25
+ it "returns non-empty text for openai" do
26
+ Brute::Prompts::Identity.call(provider_name: "openai").should.not.be.empty
27
+ end
28
+
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
34
+
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
39
+ end
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
@@ -16,3 +19,21 @@ module Brute
16
19
  end
17
20
  end
18
21
  end
22
+
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
+
28
+ it "returns nil when custom_rules is empty" do
29
+ Brute::Prompts::Instructions.call(custom_rules: "").should.be.nil
30
+ end
31
+
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
+
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/
38
+ end
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
@@ -28,3 +31,25 @@ module Brute
28
31
  end
29
32
  end
30
33
  end
34
+
35
+ test do
36
+ it "returns a string" do
37
+ Brute::Prompts::MaxSteps.call({}).should.be.kind_of(String)
38
+ end
39
+
40
+ it "announces maximum steps reached" do
41
+ Brute::Prompts::MaxSteps.call({}).should =~ /MAXIMUM STEPS REACHED/
42
+ end
43
+
44
+ it "states tools are disabled" do
45
+ Brute::Prompts::MaxSteps.call({}).should =~ /Tools are disabled/
46
+ end
47
+
48
+ it "requires a text-only response" do
49
+ Brute::Prompts::MaxSteps.call({}).should =~ /text ONLY/
50
+ end
51
+
52
+ it "ignores context (static content)" do
53
+ Brute::Prompts::MaxSteps.call({ max_steps_reached: true }).should == Brute::Prompts::MaxSteps.call({})
54
+ end
55
+ 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 PlanReminder
@@ -38,3 +41,25 @@ module Brute
38
41
  end
39
42
  end
40
43
  end
44
+
45
+ test do
46
+ it "returns a string" do
47
+ Brute::Prompts::PlanReminder.call({}).should.be.kind_of(String)
48
+ end
49
+
50
+ it "wraps content in system-reminder tags" do
51
+ Brute::Prompts::PlanReminder.call({}).should =~ /system-reminder/
52
+ end
53
+
54
+ it "declares READ-ONLY mode" do
55
+ Brute::Prompts::PlanReminder.call({}).should =~ /READ-ONLY/
56
+ end
57
+
58
+ it "forbids file edits" do
59
+ Brute::Prompts::PlanReminder.call({}).should =~ /STRICTLY FORBIDDEN/
60
+ end
61
+
62
+ it "ignores context (static content)" do
63
+ Brute::Prompts::PlanReminder.call({ agent: "plan" }).should == Brute::Prompts::PlanReminder.call({})
64
+ end
65
+ 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 Skills
@@ -20,3 +23,13 @@ module Brute
20
23
  end
21
24
  end
22
25
  end
26
+
27
+ test do
28
+ require "tmpdir"
29
+
30
+ it "returns nil when no skills are found" do
31
+ Dir.mktmpdir do |dir|
32
+ Brute::Prompts::Skills.call(cwd: dir).should.be.nil
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,28 @@
1
+ require_relative 'prompts/base'
2
+ require_relative 'prompts/identity'
3
+ require_relative 'prompts/tone_and_style'
4
+ require_relative 'prompts/objectivity'
5
+ require_relative 'prompts/task_management'
6
+ require_relative 'prompts/doing_tasks'
7
+ require_relative 'prompts/tool_usage'
8
+ require_relative 'prompts/conventions'
9
+ require_relative 'prompts/git_safety'
10
+ require_relative 'prompts/code_references'
11
+ require_relative 'prompts/environment'
12
+ require_relative 'prompts/instructions'
13
+ require_relative 'prompts/editing_approach'
14
+ require_relative 'prompts/autonomy'
15
+ require_relative 'prompts/editing_constraints'
16
+ require_relative 'prompts/frontend_tasks'
17
+ require_relative 'prompts/proactiveness'
18
+ require_relative 'prompts/code_style'
19
+ require_relative 'prompts/security_and_safety'
20
+ require_relative 'prompts/skills'
21
+ require_relative 'prompts/plan_reminder'
22
+ require_relative 'prompts/max_steps'
23
+ require_relative 'prompts/build_switch'
24
+
25
+ module Brute
26
+ module Prompts
27
+ end
28
+ end