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 +4 -4
- data/CHANGELOG.md +23 -0
- data/README.md +57 -8
- data/lib/legion/extensions/autofix/helpers/batch_buffer.rb +2 -0
- data/lib/legion/extensions/autofix/runners/fix.rb +13 -3
- data/lib/legion/extensions/autofix/runners/pipeline.rb +26 -10
- data/lib/legion/extensions/autofix/transport.rb +3 -1
- data/lib/legion/extensions/autofix/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c27afcb0c60cb91e4200e4a3bb7a32b94396524aafadaa3cc9ef8d927e826ca
|
|
4
|
+
data.tar.gz: 2ada02009c3c3743a0b1582977440acfea287147e7150816756d8e9875556dad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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`
|
|
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` |
|
|
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
|
|
|
@@ -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]
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
129
|
-
|
|
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
|
|
133
|
-
|
|
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
|
-
[
|
|
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
|