lex-autofix 0.1.5 → 0.1.8

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: f46f6aa06b02c6485e2961a8e6c0b7af4f29a23ed37b9892cdc2b9c6cf866753
4
- data.tar.gz: 146efbb61abce2ec7e173fd868ebe638ecfad4a00c25882bfc5431bbf91a35eb
3
+ metadata.gz: 5c27afcb0c60cb91e4200e4a3bb7a32b94396524aafadaa3cc9ef8d927e826ca
4
+ data.tar.gz: 2ada02009c3c3743a0b1582977440acfea287147e7150816756d8e9875556dad
5
5
  SHA512:
6
- metadata.gz: baab241f9021c2436e4eaf90bf3e66001c5fa6dcf056e914a2519a00a314772dd8e8f6b190749bd80c7eae17e8092ebc9d17866f9b499c25fb82fe8faf4d87e7
7
- data.tar.gz: 6fc0e1c7f2b3d864e6ca3f5e0f571bf70e2388353829683ab56c43c8c3cda23c648a4211f2f0af3b6ca933a4e09e00cf9cfa87405e2995b14e30eb9bc97cabcc
6
+ metadata.gz: 6796f6a043c8b8af11239b738adcffd1ce6a64da2d14a495c3f789146f4c7849f5b7f96b822992eae59d9752ea2c86d49eb392ad83372b1f77b7fe0504e4c68e
7
+ data.tar.gz: '08f196e5d2c79792731ba2e103b19f69adec94b9c8b315198a742162496cc7765da36f3b0adbe5e0bb2e8795d2bc1e873bc4b7e9dfd7414b27a83fd4d33d1f42'
data/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.8] - 2026-03-27
6
+
7
+ ### Fixed
8
+ - Add `=> e` capture and `log.warn` logging to all four bare `rescue StandardError` clauses in `Pipeline` (`resolve_token`, `resolve_org`, `resolve_max_retries`, `resolve_checkout_dir`)
9
+ - Replace `log_info`/`log_warn` wrapper methods with direct `log.info`/`log.warn` calls at call sites and remove the now-unused wrappers
10
+ - Replace direct `Legion::Cache.get` calls with `cache_get` helper (private method defined on `Pipeline` using the full qualified key, avoiding namespace double-prefixing)
11
+
12
+ ## [0.1.7] - 2026-03-27
13
+
14
+ ### Changed
15
+ - Update `Transport.additional_e_to_q` to emit three targeted bindings (`legion.logging.exception.warn.#`, `legion.logging.exception.error.#`, `legion.logging.exception.fatal.#`) instead of the broad `legion.#` catch-all
16
+ - Update `BatchBuffer#build_key` to use `error_fingerprint` as the group key when present, falling back to `lex:exception_class`
17
+ - Update `Pipeline#handle_log_event` to check fingerprint cache (`autofix:wip:` and `autofix:fixed:` keys via `Legion::Cache`) and skip buffering for in-progress or already-fixed errors
18
+ - Update `Fix#extract_file_paths` to strip `gem_path` prefix from `caller_file` and backtrace paths using new `strip_gem_prefix` helper
19
+ - All keys consumed from events are now flat (`caller_file`, `caller_line`, `gem_path`, `error_fingerprint`, `exception_class`) — no nested `:caller` or `:exception` sub-hashes
20
+
21
+ ## [0.1.6] - 2026-03-25
22
+
23
+ ### Added
24
+ - Add repo governance files: CODEOWNERS, dependabot.yml, and reusable CI workflows
25
+
26
+ ## [0.1.5] - 2026-03-24
27
+
5
28
  ### Fixed
6
29
  - Fix actor discovery: rename `module Actors` to `module Actor` (singular) to match framework convention
7
30
  - Add explicit `runner_class` override to LogConsumer pointing to `Runners::Pipeline` where `handle_log_event` lives
data/README.md CHANGED
@@ -1,6 +1,55 @@
1
1
  # lex-autofix
2
2
 
3
- Autonomous error fix agent for LegionIO. Subscribes to the `legion.logging` RabbitMQ exchange, batches and triages errors via LLM, manages GitHub issues, and opens PRs with automated code fixes.
3
+ Autonomous error fix agent for LegionIO. Subscribes to structured exception events from the `legion.logging` exchange, batches and deduplicates errors by fingerprint, triages them via LLM, manages GitHub issues, and opens PRs with automated code fixes.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ legion.logging exchange
9
+ legion.logging.exception.warn.# ─┐
10
+ legion.logging.exception.error.# ─┼─> autofix.ingest queue --> [LogConsumer] --> [BatchBuffer]
11
+ legion.logging.exception.fatal.# ─┘ │
12
+
13
+ [Pipeline Runner]
14
+ triage -> diagnose -> fix -> ship
15
+ ```
16
+
17
+ ### Event Payload
18
+
19
+ Each consumed message is a structured exception payload with the following fields:
20
+
21
+ | Field | Description |
22
+ |---|---|
23
+ | `exception_class` | Ruby exception class name (e.g. `NoMethodError`) |
24
+ | `message` | Exception message string |
25
+ | `backtrace` | Array of backtrace lines |
26
+ | `caller_file` | Source file where the exception originated |
27
+ | `caller_line` | Line number within `caller_file` |
28
+ | `caller_function` | Method name at the call site |
29
+ | `gem_name` | Name of the gem that raised the exception |
30
+ | `gem_version` | Version of that gem |
31
+ | `component_type` | Component type (runner, actor, helper, etc.) |
32
+ | `error_fingerprint` | Stable hash for deduplication across occurrences |
33
+ | `handled` | Whether the exception was caught and handled |
34
+ | `lex` | Extension name (used as suggested repo target) |
35
+ | `user` | Identity of the user or caller at the time of the error |
36
+ | `task_id` | Task ID if the error occurred within a task |
37
+ | `conversation_id` | Conversation ID if the error occurred during a chat session |
38
+
39
+ ### BatchBuffer
40
+
41
+ Events are grouped by `error_fingerprint` (falling back to `lex:exception_class` when no fingerprint is present). A flush is triggered when any group reaches `batch.count_threshold` events or the `batch.window_seconds` time window elapses.
42
+
43
+ ### Pipeline
44
+
45
+ `handle_log_event` checks the fingerprint cache before buffering:
46
+
47
+ - `autofix:wip:<fingerprint>` — a fix is in progress; skip
48
+ - `autofix:fixed:<fingerprint>` — already resolved; skip
49
+
50
+ On flush, the pipeline runs: **triage** (LLM clusters actionable vs transient errors) → **diagnose** (search or create a GitHub issue) → **fix** (`Fix` runner extracts file paths from `caller_file` and `backtrace`, applies LLM-generated edits, validates with rspec + rubocop) → **ship** (commit, push branch, open PR).
51
+
52
+ Only active when `Legion::LLM.started?` returns true.
4
53
 
5
54
  ## Installation
6
55
 
@@ -13,13 +62,13 @@ gem 'lex-autofix'
13
62
  ## Configuration
14
63
 
15
64
  | Setting | Default | Purpose |
16
- |---------|---------|---------|
17
- | `autofix.batch.window_seconds` | 300 | Time window before batch flush |
18
- | `autofix.batch.count_threshold` | 3 | Error count per group to trigger flush |
19
- | `autofix.llm.max_retries` | 3 | Max LLM fix attempts before issue-only |
20
- | `autofix.github.token` | nil | GitHub PAT (supports `vault://`) |
21
- | `autofix.github.org` | LegionIO | GitHub org for operations |
22
- | `autofix.checkout_dir` | ~/.legionio/autofix/ | Temp directory for checkouts |
65
+ |---|---|---|
66
+ | `autofix.batch.window_seconds` | `300` | Time window before batch flush |
67
+ | `autofix.batch.count_threshold` | `3` | Error count per group to trigger flush |
68
+ | `autofix.llm.max_retries` | `3` | Max LLM fix attempts before issue-only |
69
+ | `autofix.github.token` | `nil` | GitHub PAT (supports `vault://`) |
70
+ | `autofix.github.org` | `LegionIO` | GitHub org for issue and PR operations |
71
+ | `autofix.checkout_dir` | `~/.legionio/autofix/` | Temp directory for repo checkouts |
23
72
 
24
73
  ## License
25
74
 
@@ -56,6 +56,8 @@ module Legion
56
56
  private
57
57
 
58
58
  def build_key(event)
59
+ return event[:error_fingerprint] if event[:error_fingerprint]
60
+
59
61
  lex = event[:lex] || 'core'
60
62
  exception_class = event[:exception_class] || 'unknown'
61
63
  "#{lex}:#{exception_class}"
@@ -53,6 +53,7 @@ module Legion
53
53
  tc.cleanup(checkout_path)
54
54
  { success: false, reason: "fix failed after max retries (#{max_retries})" }
55
55
  rescue StandardError => e
56
+ log.log_exception(e, context: 'autofix: attempt_fix failed')
56
57
  { success: false, reason: e.message }
57
58
  end
58
59
 
@@ -75,19 +76,28 @@ module Legion
75
76
  private
76
77
 
77
78
  def extract_file_paths(error_details)
79
+ gem_base = error_details[:gem_path].to_s
78
80
  paths = []
79
- paths << error_details[:caller_file] if error_details[:caller_file]
81
+ paths << strip_gem_prefix(error_details[:caller_file].to_s, gem_base)
80
82
 
81
83
  Array(error_details[:backtrace]).each do |line|
82
84
  file = line.to_s.split(':').first
83
- paths << file if file && !file.empty?
85
+ next if file.nil? || file.empty?
86
+
87
+ paths << strip_gem_prefix(file, gem_base)
84
88
  end
85
89
 
86
- paths = paths.uniq
90
+ paths = paths.uniq.reject(&:empty?)
87
91
  spec_paths = paths.map { |p| p.sub('lib/', 'spec/').sub(/\.rb$/, '_spec.rb') }
88
92
  (paths + spec_paths).uniq
89
93
  end
90
94
 
95
+ def strip_gem_prefix(path, gem_base)
96
+ return path if gem_base.empty?
97
+
98
+ path.delete_prefix("#{gem_base}/")
99
+ end
100
+
91
101
  def build_messages(attempt:, error_details:, files:, test_output:)
92
102
  prompt = if attempt.zero?
93
103
  Helpers::Prompts.fix(error_details: error_details, files: files)
@@ -29,6 +29,12 @@ module Legion
29
29
  end
30
30
 
31
31
  def handle_log_event(**event)
32
+ fingerprint = event[:error_fingerprint]
33
+ if fingerprint
34
+ return { success: true, action: :skip_wip } unless cache_get("autofix:wip:#{fingerprint}").nil?
35
+ return { success: true, action: :skip_fixed } unless cache_get("autofix:fixed:#{fingerprint}").nil?
36
+ end
37
+
32
38
  Pipeline.buffer.add(event)
33
39
  run_pipeline if Pipeline.buffer.flush_ready?
34
40
  { success: true }
@@ -40,7 +46,7 @@ module Legion
40
46
  return triage_result unless triage_result[:success]
41
47
 
42
48
  triage_result[:non_actionable].each do |cluster|
43
- log_info("autofix: skipping non-actionable cluster: #{cluster[:summary]}")
49
+ log.info("autofix: skipping non-actionable cluster: #{cluster[:summary]}")
44
50
  end
45
51
 
46
52
  triage_result[:actionable].each do |cluster|
@@ -79,7 +85,7 @@ module Legion
79
85
  )
80
86
 
81
87
  unless fix_result[:success]
82
- log_warn("autofix: fix failed for cluster #{cluster[:summary]}: #{fix_result[:reason]}")
88
+ log.warn("autofix: fix failed for cluster #{cluster[:summary]}: #{fix_result[:reason]}")
83
89
  return fix_result
84
90
  end
85
91
 
@@ -99,25 +105,29 @@ module Legion
99
105
 
100
106
  def resolve_token
101
107
  Legion::Settings.dig(:autofix, :github, :token)
102
- rescue StandardError
108
+ rescue StandardError => e
109
+ log.warn("autofix: could not resolve token: #{e.message}")
103
110
  nil
104
111
  end
105
112
 
106
113
  def resolve_org
107
114
  Legion::Settings.dig(:autofix, :github, :org) || 'LegionIO'
108
- rescue StandardError
115
+ rescue StandardError => e
116
+ log.warn("autofix: could not resolve org: #{e.message}")
109
117
  'LegionIO'
110
118
  end
111
119
 
112
120
  def resolve_max_retries
113
121
  Legion::Settings.dig(:autofix, :llm, :max_retries) || 3
114
- rescue StandardError
122
+ rescue StandardError => e
123
+ log.warn("autofix: could not resolve max_retries: #{e.message}")
115
124
  3
116
125
  end
117
126
 
118
127
  def resolve_checkout_dir
119
128
  Legion::Settings.dig(:autofix, :checkout_dir)
120
- rescue StandardError
129
+ rescue StandardError => e
130
+ log.warn("autofix: could not resolve checkout_dir: #{e.message}")
121
131
  nil
122
132
  end
123
133
 
@@ -125,12 +135,18 @@ module Legion
125
135
  text.to_s.downcase.gsub(/[^a-z0-9]+/, '-').slice(0, 40).chomp('-')
126
136
  end
127
137
 
128
- def log_info(msg)
129
- log.info(msg)
138
+ def cache_get(key)
139
+ return unless defined?(Legion::Cache)
140
+
141
+ cache = Legion::Cache
142
+ cache.get(key)
130
143
  end
131
144
 
132
- def log_warn(msg)
133
- log.warn(msg)
145
+ def cache_set(key, value, ttl: 60)
146
+ return unless defined?(Legion::Cache)
147
+
148
+ cache = Legion::Cache
149
+ cache.set(key, value, ttl)
134
150
  end
135
151
  end
136
152
  end
@@ -7,7 +7,9 @@ module Legion
7
7
  extend Legion::Extensions::Transport if defined?(Legion::Extensions::Transport)
8
8
 
9
9
  def self.additional_e_to_q
10
- [{ from: 'legion.logging', to: 'autofix.ingest', routing_key: 'legion.#' }]
10
+ %w[warn error fatal].map do |level|
11
+ { from: 'legion.logging', to: 'autofix.ingest', routing_key: "legion.logging.exception.#{level}.#" }
12
+ end
11
13
  end
12
14
  end
13
15
  end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Autofix
6
- VERSION = '0.1.5'
6
+ VERSION = '0.1.8'
7
7
  end
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-autofix
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO