brute 0.1.5 → 0.1.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f2148d5db68a8400e3f3decc496561a7beca3225cc89171c013ba37f3ca48fa6
4
- data.tar.gz: bbea8457ac274e5953b55ea54ecc5f29a75cb05cceab3631ab2b29eb47c01346
3
+ metadata.gz: 4f0f32487b029541fdb462f5f4958a95e4727150911b51f6d8ab457b875187d8
4
+ data.tar.gz: f162d75e227b4270e4a56dba42fe3cdddd23f9492adeff5cd1f345cbbb811961
5
5
  SHA512:
6
- metadata.gz: b179135722aca285350004e8ce425a79f4b493d8417090bbd367fcff6fa0213a6cb207b2bd0ca3ede30ff11c7ee972bf7fce71406244f3dc5aa3a0190d939406
7
- data.tar.gz: 48eff670f59f22f5dc1389510835ae8f2ca50ca8ec8c0e615bbac10c2c559a50b708859cd47a01fa24c5c406d73000bb7e2f7b49f160832ac157c106ccaed6a5
6
+ metadata.gz: 9aa172f042960dc5c9ec3250cf27781614077be7db114edc65d4ce72e178dfcab9ed9a9918d6583ed190d36cc54e865d8118bea9ac9814064a12a3f2b7ac8627
7
+ data.tar.gz: c0cc5addf257b161cc06385417a24833bf9f99a73038040abc67c3caef75ab0a0b9e3d357364b0fa0d194ca3b259555b80f09bf805fc12d17be1e4386d8b061b
data/lib/brute/diff.rb ADDED
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'diff/lcs'
4
+ require 'diff/lcs/hunk'
5
+
6
+ module Brute
7
+ module Diff
8
+ # Generate a unified diff string from two texts.
9
+ def self.unified(old_text, new_text, context: 3)
10
+ old_lines = old_text.lines
11
+ new_lines = new_text.lines
12
+ diffs = ::Diff::LCS.diff(old_lines, new_lines)
13
+ return '' if diffs.empty?
14
+
15
+ output = +''
16
+ file_length_difference = 0
17
+ diffs.each do |piece|
18
+ hunk = ::Diff::LCS::Hunk.new(old_lines, new_lines, piece, context, file_length_difference)
19
+ file_length_difference = hunk.file_length_difference
20
+ output << hunk.diff(:unified)
21
+ output << "\n"
22
+ end
23
+ output
24
+ end
25
+ end
26
+ end
@@ -2,16 +2,19 @@
2
2
 
3
3
  module Brute
4
4
  module Middleware
5
- # Tracks per-tool error counts across LLM calls and signals when
6
- # the error ceiling is reached.
5
+ # Tracks per-tool error counts and total tool call count across LLM
6
+ # calls, and signals when the error ceiling is reached.
7
7
  #
8
8
  # This middleware doesn't execute tools itself — it inspects the tool
9
9
  # results that were sent as input to the LLM call (env[:tool_results])
10
- # and counts failures.
10
+ # and counts failures and totals.
11
11
  #
12
12
  # When any tool exceeds max_failures, it sets env[:metadata][:tool_error_limit_reached]
13
13
  # so the orchestrator can decide to stop.
14
14
  #
15
+ # Also stores env[:metadata][:tool_calls] with the cumulative number of
16
+ # tool invocations in the current session.
17
+ #
15
18
  class ToolErrorTracking < Base
16
19
  DEFAULT_MAX_FAILURES = 3
17
20
 
@@ -19,27 +22,30 @@ module Brute
19
22
  super(app)
20
23
  @max_failures = max_failures
21
24
  @errors = Hash.new(0) # tool_name → count
25
+ @total_tool_calls = 0
22
26
  end
23
27
 
24
28
  def call(env)
25
- # PRE: count errors from tool results that are about to be sent
29
+ # PRE: count errors and totals from tool results that are about to be sent
26
30
  if (results = env[:tool_results])
31
+ @total_tool_calls += results.size
32
+
27
33
  results.each do |name, result|
28
- if result.is_a?(Hash) && result[:error]
29
- @errors[name] += 1
30
- end
34
+ @errors[name] += 1 if result.is_a?(Hash) && result[:error]
31
35
  end
32
36
  end
33
37
 
38
+ env[:metadata][:tool_calls] = @total_tool_calls
34
39
  env[:metadata][:tool_errors] = @errors.dup
35
40
  env[:metadata][:tool_error_limit_reached] = @errors.any? { |_, c| c >= @max_failures }
36
41
 
37
42
  @app.call(env)
38
43
  end
39
44
 
40
- # Reset error counts (e.g., between user turns).
45
+ # Reset counts (e.g., between user turns).
41
46
  def reset!
42
47
  @errors.clear
48
+ @total_tool_calls = 0
43
49
  end
44
50
  end
45
51
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Brute
4
+ module Middleware
5
+ # Guards against tool-only LLM responses where the assistant message
6
+ # is dropped from the context buffer.
7
+ #
8
+ # When the LLM responds with only tool_use blocks (no text), llm.rb's
9
+ # response adapter produces empty choices. Context#talk appends nil,
10
+ # BufferNilGuard strips it, and the assistant message carrying tool_use
11
+ # blocks is lost. This causes "unexpected tool_use_id" on the next call
12
+ # because tool_result references a tool_use that's missing from the buffer.
13
+ #
14
+ # This middleware runs post-call and injects a synthetic assistant message
15
+ # when tool calls exist but no assistant message was recorded.
16
+ class ToolUseGuard
17
+ def initialize(app)
18
+ @app = app
19
+ end
20
+
21
+ def call(env)
22
+ response = @app.call(env)
23
+
24
+ ctx = env[:context]
25
+ functions = ctx.functions
26
+
27
+ # If there are pending tool calls, ensure the buffer has an assistant
28
+ # message with tool_use blocks.
29
+ 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])
47
+ end
48
+ end
49
+
50
+ response
51
+ end
52
+ end
53
+ end
54
+ end
@@ -2,31 +2,60 @@
2
2
 
3
3
  module Brute
4
4
  module Middleware
5
- # Logs timing and token usage for every LLM call.
5
+ # Logs timing and token usage for every LLM call, and tracks cumulative
6
+ # timing data in env[:metadata][:timing].
6
7
  #
7
- # Wraps the call with wall-clock timing. Logs:
8
- # PRE: request number, message count
9
- # POST: elapsed time, token usage, finish reason
8
+ # As the outermost middleware, it sees the full pipeline elapsed time per
9
+ # call. It also tracks total wall-clock time across all calls in a turn
10
+ # (including tool execution gaps between LLM calls).
11
+ #
12
+ # A new turn is detected when env[:tool_results] is nil (the orchestrator
13
+ # sets this on the first call of each run()).
14
+ #
15
+ # Stores in env[:metadata][:timing]:
16
+ # total_elapsed: wall-clock since the turn began (includes tool gaps)
17
+ # total_llm_elapsed: cumulative time spent inside LLM calls only
18
+ # llm_call_count: number of LLM calls so far
19
+ # last_call_elapsed: duration of the most recent LLM call
10
20
  #
11
21
  class Tracing < Base
12
22
  def initialize(app, logger:)
13
23
  super(app)
14
24
  @logger = logger
15
25
  @call_count = 0
26
+ @total_llm_elapsed = 0.0
27
+ @turn_start = nil
16
28
  end
17
29
 
18
30
  def call(env)
19
31
  @call_count += 1
32
+
33
+ # Detect new turn: tool_results is nil on the first pipeline call
34
+ if env[:tool_results].nil?
35
+ @turn_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
36
+ @total_llm_elapsed = 0.0
37
+ end
38
+
20
39
  messages = env[:context].messages.to_a
21
40
  @logger.debug("[brute] LLM call ##{@call_count} (#{messages.size} messages in context)")
22
41
 
23
42
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
24
43
  response = @app.call(env)
25
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
44
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
45
+ elapsed = now - start
46
+
47
+ @total_llm_elapsed += elapsed
26
48
 
27
- tokens = response.respond_to?(:usage) ? response.usage&.total_tokens : "?"
49
+ tokens = response.respond_to?(:usage) ? response.usage&.total_tokens : '?'
28
50
  @logger.info("[brute] LLM response ##{@call_count}: #{tokens} tokens, #{elapsed.round(2)}s")
29
51
 
52
+ env[:metadata][:timing] = {
53
+ total_elapsed: now - (@turn_start || start),
54
+ total_llm_elapsed: @total_llm_elapsed,
55
+ llm_call_count: @call_count,
56
+ last_call_elapsed: elapsed
57
+ }
58
+
30
59
  response
31
60
  end
32
61
  end
@@ -181,6 +181,9 @@ module Brute
181
181
  # Handle reasoning params and model-switch normalization (pre-call)
182
182
  use Middleware::ReasoningNormalizer, **reasoning unless reasoning.empty?
183
183
 
184
+ # Guard against tool-only responses dropping the assistant message
185
+ use Middleware::ToolUseGuard
186
+
184
187
  # Innermost: the actual LLM call
185
188
  run Middleware::LLMCall.new
186
189
  end
@@ -3,14 +3,14 @@
3
3
  module Brute
4
4
  module Tools
5
5
  class FSPatch < LLM::Tool
6
- name "patch"
7
- description "Replace a specific string in a file. The old_string must match exactly " \
8
- "(including whitespace and indentation). Always read a file before patching it."
6
+ name 'patch'
7
+ description 'Replace a specific string in a file. The old_string must match exactly ' \
8
+ '(including whitespace and indentation). Always read a file before patching it.'
9
9
 
10
- param :file_path, String, "Path to the file to patch", required: true
11
- param :old_string, String, "The exact text to find and replace", required: true
12
- param :new_string, String, "The replacement text", required: true
13
- param :replace_all, Boolean, "Replace all occurrences (default: false)"
10
+ param :file_path, String, 'Path to the file to patch', required: true
11
+ param :old_string, String, 'The exact text to find and replace', required: true
12
+ param :new_string, String, 'The replacement text', required: true
13
+ param :replace_all, Boolean, 'Replace all occurrences (default: false)'
14
14
 
15
15
  def call(file_path:, old_string:, new_string:, replace_all: false)
16
16
  path = File.expand_path(file_path)
@@ -23,13 +23,15 @@ module Brute
23
23
  Brute::SnapshotStore.save(path)
24
24
 
25
25
  updated = if replace_all
26
- original.gsub(old_string, new_string)
27
- else
28
- original.sub(old_string, new_string)
29
- end
26
+ original.gsub(old_string, new_string)
27
+ else
28
+ original.sub(old_string, new_string)
29
+ end
30
30
 
31
31
  File.write(path, updated)
32
- {success: true, file_path: path, replacements: replace_all ? original.scan(old_string).size : 1}
32
+ diff = Brute::Diff.unified(original, updated)
33
+ count = replace_all ? original.scan(old_string).size : 1
34
+ { success: true, file_path: path, replacements: count, diff: diff }
33
35
  end
34
36
  end
35
37
  end
@@ -1,24 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fileutils"
3
+ require 'fileutils'
4
4
 
5
5
  module Brute
6
6
  module Tools
7
7
  class FSWrite < LLM::Tool
8
- name "write"
8
+ name 'write'
9
9
  description "Write content to a file. Creates parent directories if they don't exist. " \
10
- "Use this for creating new files or completely replacing file contents."
10
+ 'Use this for creating new files or completely replacing file contents.'
11
11
 
12
- param :file_path, String, "Path to the file to write", required: true
13
- param :content, String, "The full content to write to the file", required: true
12
+ param :file_path, String, 'Path to the file to write', required: true
13
+ param :content, String, 'The full content to write to the file', required: true
14
14
 
15
15
  def call(file_path:, content:)
16
16
  path = File.expand_path(file_path)
17
17
  Brute::FileMutationQueue.serialize(path) do
18
+ old_content = File.exist?(path) ? File.read(path) : ''
18
19
  Brute::SnapshotStore.save(path)
19
20
  FileUtils.mkdir_p(File.dirname(path))
20
21
  File.write(path, content)
21
- {success: true, file_path: path, bytes: content.bytesize}
22
+ diff = Brute::Diff.unified(old_content, content)
23
+ { success: true, file_path: path, bytes: content.bytesize, diff: diff }
22
24
  end
23
25
  end
24
26
  end
data/lib/brute/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Brute
4
- VERSION = "0.1.5"
4
+ VERSION = "0.1.7"
5
5
  end
data/lib/brute.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "llm"
4
- require "timeout"
5
- require "logger"
3
+ require 'llm'
4
+ require 'timeout'
5
+ require 'logger'
6
6
 
7
7
  # Brute — a coding agent built on llm.rb
8
8
  #
@@ -11,7 +11,7 @@ require "logger"
11
11
  #
12
12
  # Tracing → Retry → Session → Tokens → Compaction → ToolErrors → DoomLoop → Reasoning → [LLM Call]
13
13
  #
14
- require_relative "brute/version"
14
+ require_relative 'brute/version'
15
15
 
16
16
  module Brute
17
17
  module Tools; end
@@ -20,51 +20,52 @@ module Brute
20
20
  end
21
21
 
22
22
  # Infrastructure
23
- require_relative "brute/snapshot_store"
24
- require_relative "brute/todo_store"
25
- require_relative "brute/file_mutation_queue"
26
- require_relative "brute/doom_loop"
27
- require_relative "brute/hooks"
28
- require_relative "brute/compactor"
29
- require_relative "brute/system_prompt"
30
- require_relative "brute/session"
31
- require_relative "brute/pipeline"
32
- require_relative "brute/agent_stream"
23
+ require_relative 'brute/diff'
24
+ require_relative 'brute/snapshot_store'
25
+ require_relative 'brute/todo_store'
26
+ require_relative 'brute/file_mutation_queue'
27
+ require_relative 'brute/doom_loop'
28
+ require_relative 'brute/hooks'
29
+ require_relative 'brute/compactor'
30
+ require_relative 'brute/system_prompt'
31
+ require_relative 'brute/session'
32
+ require_relative 'brute/pipeline'
33
+ require_relative 'brute/agent_stream'
33
34
 
34
35
  # Provider patches
35
- require_relative "brute/patches/anthropic_tool_role"
36
- require_relative "brute/patches/buffer_nil_guard"
36
+ require_relative 'brute/patches/anthropic_tool_role'
37
+ require_relative 'brute/patches/buffer_nil_guard'
37
38
 
38
39
  # Middleware (Rack-style)
39
- require_relative "brute/middleware/base"
40
- require_relative "brute/middleware/llm_call"
41
- require_relative "brute/middleware/retry"
42
- require_relative "brute/middleware/doom_loop_detection"
43
- require_relative "brute/middleware/token_tracking"
44
- require_relative "brute/middleware/compaction_check"
45
- require_relative "brute/middleware/session_persistence"
46
- require_relative "brute/middleware/tracing"
47
- require_relative "brute/middleware/tool_error_tracking"
48
- require_relative "brute/middleware/reasoning_normalizer"
40
+ require_relative 'brute/middleware/base'
41
+ require_relative 'brute/middleware/llm_call'
42
+ require_relative 'brute/middleware/retry'
43
+ require_relative 'brute/middleware/doom_loop_detection'
44
+ require_relative 'brute/middleware/token_tracking'
45
+ require_relative 'brute/middleware/compaction_check'
46
+ require_relative 'brute/middleware/session_persistence'
47
+ require_relative 'brute/middleware/tracing'
48
+ require_relative 'brute/middleware/tool_error_tracking'
49
+ require_relative 'brute/middleware/reasoning_normalizer'
50
+ require_relative "brute/middleware/tool_use_guard"
49
51
 
50
52
  # Tools
51
- require_relative "brute/tools/fs_read"
52
- require_relative "brute/tools/fs_write"
53
- require_relative "brute/tools/fs_patch"
54
- require_relative "brute/tools/fs_remove"
55
- require_relative "brute/tools/fs_search"
56
- require_relative "brute/tools/fs_undo"
57
- require_relative "brute/tools/shell"
58
- require_relative "brute/tools/net_fetch"
59
- require_relative "brute/tools/todo_write"
60
- require_relative "brute/tools/todo_read"
61
- require_relative "brute/tools/delegate"
53
+ require_relative 'brute/tools/fs_read'
54
+ require_relative 'brute/tools/fs_write'
55
+ require_relative 'brute/tools/fs_patch'
56
+ require_relative 'brute/tools/fs_remove'
57
+ require_relative 'brute/tools/fs_search'
58
+ require_relative 'brute/tools/fs_undo'
59
+ require_relative 'brute/tools/shell'
60
+ require_relative 'brute/tools/net_fetch'
61
+ require_relative 'brute/tools/todo_write'
62
+ require_relative 'brute/tools/todo_read'
63
+ require_relative 'brute/tools/delegate'
62
64
 
63
65
  # Orchestrator (depends on tools, middleware, and infrastructure)
64
- require_relative "brute/orchestrator"
66
+ require_relative 'brute/orchestrator'
65
67
 
66
68
  module Brute
67
-
68
69
  # The complete set of tools available to the agent.
69
70
  TOOLS = [
70
71
  Tools::FSRead,
@@ -77,7 +78,7 @@ module Brute
77
78
  Tools::NetFetch,
78
79
  Tools::TodoWrite,
79
80
  Tools::TodoRead,
80
- Tools::Delegate,
81
+ Tools::Delegate
81
82
  ].freeze
82
83
 
83
84
  # Default provider, resolved from environment.
@@ -102,12 +103,12 @@ module Brute
102
103
  end
103
104
 
104
105
  PROVIDERS = {
105
- "anthropic" => ->(key) { LLM.anthropic(key: key).tap { Patches::AnthropicToolRole.apply! } },
106
- "openai" => ->(key) { LLM.openai(key: key) },
107
- "google" => ->(key) { LLM.google(key: key) },
108
- "deepseek" => ->(key) { LLM.deepseek(key: key) },
109
- "ollama" => ->(key) { LLM.ollama(key: key) },
110
- "xai" => ->(key) { LLM.xai(key: key) },
106
+ 'anthropic' => ->(key) { LLM.anthropic(key: key).tap { Patches::AnthropicToolRole.apply! } },
107
+ 'openai' => ->(key) { LLM.openai(key: key) },
108
+ 'google' => ->(key) { LLM.google(key: key) },
109
+ 'deepseek' => ->(key) { LLM.deepseek(key: key) },
110
+ 'ollama' => ->(key) { LLM.ollama(key: key) },
111
+ 'xai' => ->(key) { LLM.xai(key: key) }
111
112
  }.freeze
112
113
 
113
114
  # Resolve the LLM provider from environment variables.
@@ -120,24 +121,24 @@ module Brute
120
121
  #
121
122
  # Returns nil if no key is found. Error is deferred to Orchestrator#run.
122
123
  def self.resolve_provider
123
- if ENV["LLM_API_KEY"]
124
- key = ENV["LLM_API_KEY"]
125
- name = ENV.fetch("LLM_PROVIDER", "anthropic").downcase
126
- elsif ENV["ANTHROPIC_API_KEY"]
127
- key = ENV["ANTHROPIC_API_KEY"]
128
- name = "anthropic"
129
- elsif ENV["OPENAI_API_KEY"]
130
- key = ENV["OPENAI_API_KEY"]
131
- name = "openai"
132
- elsif ENV["GOOGLE_API_KEY"]
133
- key = ENV["GOOGLE_API_KEY"]
134
- name = "google"
124
+ if ENV['LLM_API_KEY']
125
+ key = ENV['LLM_API_KEY']
126
+ name = ENV.fetch('LLM_PROVIDER', 'anthropic').downcase
127
+ elsif ENV['ANTHROPIC_API_KEY']
128
+ key = ENV['ANTHROPIC_API_KEY']
129
+ name = 'anthropic'
130
+ elsif ENV['OPENAI_API_KEY']
131
+ key = ENV['OPENAI_API_KEY']
132
+ name = 'openai'
133
+ elsif ENV['GOOGLE_API_KEY']
134
+ key = ENV['GOOGLE_API_KEY']
135
+ name = 'google'
135
136
  else
136
137
  return nil
137
138
  end
138
139
 
139
140
  factory = PROVIDERS[name]
140
- raise "Unknown LLM provider: #{name}. Available: #{PROVIDERS.keys.join(", ")}" unless factory
141
+ raise "Unknown LLM provider: #{name}. Available: #{PROVIDERS.keys.join(', ')}" unless factory
141
142
 
142
143
  factory.call(key)
143
144
  end
metadata CHANGED
@@ -1,56 +1,56 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brute
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brute Contributors
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 1980-01-01 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: llm.rb
13
+ name: async
14
14
  requirement: !ruby/object:Gem::Requirement
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: '4.11'
18
+ version: '2.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: '4.11'
25
+ version: '2.0'
26
26
  - !ruby/object:Gem::Dependency
27
- name: async
27
+ name: diff-lcs
28
28
  requirement: !ruby/object:Gem::Requirement
29
29
  requirements:
30
- - - "~>"
30
+ - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '2.0'
32
+ version: '1.5'
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "~>"
37
+ - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '2.0'
39
+ version: '1.5'
40
40
  - !ruby/object:Gem::Dependency
41
- name: minitest
41
+ name: llm.rb
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: '5.0'
47
- type: :development
46
+ version: '4.11'
47
+ type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: '5.0'
53
+ version: '4.11'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: rake
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '13.0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - "~>"
73
+ - !ruby/object:Gem::Version
74
+ version: '3.13'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: '3.13'
68
82
  description: Production-grade coding agent with tool execution, middleware pipeline,
69
83
  context compaction, session persistence, and multi-provider LLM support.
70
84
  executables: []
@@ -74,6 +88,7 @@ files:
74
88
  - lib/brute.rb
75
89
  - lib/brute/agent_stream.rb
76
90
  - lib/brute/compactor.rb
91
+ - lib/brute/diff.rb
77
92
  - lib/brute/doom_loop.rb
78
93
  - lib/brute/file_mutation_queue.rb
79
94
  - lib/brute/hooks.rb
@@ -86,6 +101,7 @@ files:
86
101
  - lib/brute/middleware/session_persistence.rb
87
102
  - lib/brute/middleware/token_tracking.rb
88
103
  - lib/brute/middleware/tool_error_tracking.rb
104
+ - lib/brute/middleware/tool_use_guard.rb
89
105
  - lib/brute/middleware/tracing.rb
90
106
  - lib/brute/orchestrator.rb
91
107
  - lib/brute/patches/anthropic_tool_role.rb