Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
ci:
uses: LegionIO/.github/.github/workflows/ci.yml@main

lint:
uses: LegionIO/.github/.github/workflows/lint-patterns.yml@main
excluded-files:
uses: LegionIO/.github/.github/workflows/excluded-files.yml@main

security:
uses: LegionIO/.github/.github/workflows/security-scan.yml@main
Expand All @@ -27,7 +27,7 @@ jobs:
uses: LegionIO/.github/.github/workflows/stale.yml@main

release:
needs: [ci, lint]
needs: [ci, excluded-files]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
uses: LegionIO/.github/.github/workflows/release.yml@main
secrets:
Expand Down
55 changes: 6 additions & 49 deletions .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,53 +1,10 @@
AllCops:
TargetRubyVersion: 3.4
NewCops: enable
SuggestExtensions: false
inherit_gem:
rubocop-legion: config/lex.yml

Layout/LineLength:
Max: 160

Layout/SpaceAroundEqualsInParameterDefault:
EnforcedStyle: space

Layout/HashAlignment:
EnforcedHashRocketStyle: table
EnforcedColonStyle: table

Metrics/MethodLength:
Max: 50

Metrics/ClassLength:
Max: 1500

Metrics/ModuleLength:
Max: 1500

Metrics/BlockLength:
Max: 40
Exclude:
- 'spec/**/*'

Metrics/AbcSize:
Max: 60

Metrics/CyclomaticComplexity:
Max: 15

Metrics/PerceivedComplexity:
Max: 17

Style/Documentation:
Enabled: false

Style/SymbolArray:
Enabled: true

Style/FrozenStringLiteralComment:
Enabled: true
EnforcedStyle: always

Naming/FileName:
# Private helper methods return scalars/arrays — not public runner return values
Legion/Extension/RunnerReturnHash:
Enabled: false

Naming/PredicateMethod:
# Runner modules used as mixins — extend self not applicable
Legion/Extension/RunnerIncludeHelpers:
Enabled: false
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## [0.3.4] - 2026-03-30

### Changed
- update to rubocop-legion 0.1.7, resolve all offenses

## [0.3.3] - 2026-03-29

### Fixed
- `label_pull_request` now calls `add_labels_to_issue` (correct Octokit method name)

### Changed
- `run_lifecycle` accepts flat task payloads via `**payload` splat; `generation` and `review` are built from flat keys when not provided explicitly
- `LifecycleSubscriber` sets `check_subtask?` to `true` for task system conformance

## [0.3.2] - 2026-03-29

### Added
Expand Down
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ gemspec

gem 'rspec', '~> 3.13'
gem 'rubocop', '~> 1.75', require: false
gem 'rubocop-legion', '~> 0.1'
gem 'rubocop-rspec', require: false

if File.exist?(File.expand_path('../../legion-gaia', __dir__))
Expand Down
4 changes: 2 additions & 2 deletions lib/legion/extensions/swarm_github.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@
require 'legion/extensions/swarm_github/runners/review_notifier'
require 'legion/extensions/swarm_github/runners/pr_pipeline'
require 'legion/extensions/swarm_github/runners/extension_lifecycle'
require 'legion/extensions/swarm_github/actors/lifecycle_subscriber' if defined?(Legion::Extensions::Actors::Subscription)
require 'legion/extensions/swarm_github/actors/lifecycle_subscriber' if defined?(Legion::Extensions::Actors::Subscription) # rubocop:disable Legion/HelperMigration/RequireDefinedGuard

module Legion
module Extensions
module SwarmGithub
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core, false
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module Actor
class LifecycleSubscriber < Legion::Extensions::Actors::Subscription
def runner_class = self.class
def runner_function = 'action'
def check_subtask? = false
def check_subtask? = true
def generate_task? = false

def action(payload)
Expand Down Expand Up @@ -54,11 +54,7 @@ def github_lifecycle_enabled?
end

def log
return Legion::Logging if defined?(Legion::Logging)

@log ||= Object.new.tap do |nl|
%i[debug info warn error fatal].each { |m| nl.define_singleton_method(m) { |*| nil } }
end
Legion::Logging
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Legion
module Extensions
module SwarmGithub
module Actor
class StaleIssues < Legion::Extensions::Actors::Every
class StaleIssues < Legion::Extensions::Actors::Every # rubocop:disable Legion/Extension/EveryActorRequiresTime
def runner_class
Legion::Extensions::SwarmGithub::Runners::GithubSwarm
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ module Runners
module ExtensionLifecycle
extend self

def run_lifecycle(generation:, review:, review_k: nil, review_models: nil)
def run_lifecycle(generation: nil, review: nil, review_k: nil, review_models: nil, **payload)
generation ||= build_generation(payload)
review ||= build_review(payload)
config = github_config
return { success: false, error: :github_not_enabled } unless config[:enabled]
return { success: false, error: :target_repo_missing } unless config[:target_repo]
Expand Down Expand Up @@ -64,7 +66,7 @@ def commit_generated_files(owner:, repo:, branch:, generation:)
github_client.commit_files(owner: owner, repo: repo, branch: branch, files: files, message: message)
end

def open_pull_request(owner:, repo:, branch:, base:, generation:, review:) # rubocop:disable Metrics/ParameterLists
def open_pull_request(owner:, repo:, branch:, base:, generation:, review:)
return { success: false, error: :github_runner_unavailable } unless github_runner_available?

body = build_pr_body(generation: generation, review: review)
Expand All @@ -84,7 +86,7 @@ def label_pull_request(owner:, repo:, pull_number:, labels:)
return { success: true } unless pull_number && labels&.any?
return { success: false, error: :github_runner_unavailable } unless github_runner_available?

github_client.add_labels(owner: owner, repo: repo, issue_number: pull_number, labels: labels)
github_client.add_labels_to_issue(owner: owner, repo: repo, issue_number: pull_number, labels: labels)
rescue StandardError => e
log.warn("Label failed: #{e.message}")
{ success: false, error: e.message }
Expand Down Expand Up @@ -172,8 +174,9 @@ def run_adversarial_review(owner:, repo:, pull_number:, k:, models: []) # ruboco
Legion::Extensions::SwarmGithub::Runners::PullRequestReviewer.review_pull_request(**kwargs)
end

blocker_severities = %w[error critical].freeze
approvals = reviews.count do |r|
r[:status] == 'reviewed' && (r[:comments] || []).none? { |c| %w[error critical].include?(c[:severity]&.to_s) }
r[:status] == 'reviewed' && (r[:comments] || []).none? { |c| blocker_severities.include?(c[:severity]&.to_s) }
end
rejections = k - approvals

Expand All @@ -190,7 +193,7 @@ def run_adversarial_review(owner:, repo:, pull_number:, k:, models: []) # ruboco
{ success: true, skipped: true, reason: :review_error }
end

def handle_auto_merge(owner:, repo:, pull_number:, config:, review:, review_result: nil) # rubocop:disable Metrics/ParameterLists
def handle_auto_merge(owner:, repo:, pull_number:, config:, review:, review_result: nil)
return unless config[:auto_merge] && review[:verdict]&.to_sym == :approve
return if review_result && review_result[:consensus] == :request_changes

Expand Down Expand Up @@ -275,11 +278,31 @@ def github_connection_opts
end

def log
return Legion::Logging if defined?(Legion::Logging)
Legion::Logging
end

@log ||= Object.new.tap do |nl|
%i[debug info warn error fatal].each { |m| nl.define_singleton_method(m) { |*| nil } }
end
def build_generation(payload)
{
name: payload[:name],
generation_id: payload[:generation_id],
gap_id: payload[:gap_id],
gap_type: payload[:gap_type],
tier: payload[:tier],
code: payload[:code],
spec_code: payload[:spec_code],
file_path: payload[:file_path],
spec_path: payload[:spec_path]
}.compact
end

def build_review(payload)
{
verdict: payload[:verdict],
confidence: payload[:confidence],
stages: payload[:stages],
issues: payload[:issues],
passed: payload[:passed]
}.compact
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions lib/legion/extensions/swarm_github/runners/github_swarm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ module Extensions
module SwarmGithub
module Runners
module GithubSwarm
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
Legion::Extensions::Helpers.const_defined?(:Lex)
include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
Legion::Extensions::Helpers.const_defined?(:Lex, false)

def ingest_issue(repo:, issue_number:, title:, labels: [], **)
key = issue_tracker.track(repo: repo, issue_number: issue_number, title: title, labels: labels)
Expand Down
3 changes: 2 additions & 1 deletion lib/legion/extensions/swarm_github/runners/pr_pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def bridge_review_to_issue(repo:, issue_number:, review_result:)
return unless issue_number

review_comments = review_result.dig(:review, :comments) || []
approved = review_comments.none? { |c| %w[critical high].include?(c[:severity]&.to_s&.downcase) }
blocker_severities = %w[critical high].freeze
approved = review_comments.none? { |c| blocker_severities.include?(c[:severity]&.to_s&.downcase) }
@issue_tracker&.record_validation(
"#{repo}##{issue_number}",
validator: :code_review,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def fetch_pr_files(owner:, repo:, pull_number:)
owner: owner, repo: repo, pull_number: pull_number
)[:result] || []
rescue StandardError => e
log.warn(e.message) if respond_to?(:log, true)
log.warn(e.message) if respond_to?(:log, true) # rubocop:disable Legion/HelperMigration/LoggingGuard
[]
end

Expand All @@ -43,28 +43,28 @@ def generate_review(files, model: nil, provider: nil)
llm_kwargs = { message: prompt, caller: { extension: 'lex-swarm-github' } }
llm_kwargs[:model] = model if model
llm_kwargs[:provider] = provider if provider
response = Legion::LLM.chat(**llm_kwargs)
response = Legion::LLM.chat(**llm_kwargs) # rubocop:disable Legion/HelperMigration/DirectLlm
parse_review_response(response)
end
merge_chunk_reviews(reviews)
rescue StandardError => e
log.warn("Review generation failed: #{e.message}") if respond_to?(:log, true)
log.warn("Review generation failed: #{e.message}") if respond_to?(:log, true) # rubocop:disable Legion/HelperMigration/LoggingGuard
{ summary: 'Review generation failed', comments: [] }
end

def merge_chunk_reviews(reviews)
merged_comments = reviews.flat_map { |r| r[:comments] || [] }
summaries = reviews.map { |r| r[:summary] }.compact
summaries = reviews.filter_map { |r| r[:summary] }
{
summary: summaries.join("\n\n"),
comments: merged_comments
}
end

def parse_review_response(response)
Legion::JSON.parse(response)
json_parse(response)
rescue StandardError => e
log.warn(e.message) if respond_to?(:log, true)
log.warn(e.message) if respond_to?(:log, true) # rubocop:disable Legion/HelperMigration/LoggingGuard
{ summary: response.to_s, comments: [] }
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def notify_review(channel:, pull_ref:, review:, post_result:, **)

{ notified: result[:ok] == true, channel: channel, ts: result[:ts] }
rescue StandardError => e
log.warn(e.message) if respond_to?(:log, true)
log.warn(e.message) if respond_to?(:log, true) # rubocop:disable Legion/HelperMigration/LoggingGuard
{ notified: false, reason: "slack error: #{e.message}" }
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def post_review(owner:, repo:, pull_number:, review:, **)
review_id = result.dig(:result, 'id') || result.dig(:result, :id)
{ posted: true, review_id: review_id, comments_count: inline_comments.size }
rescue StandardError => e
log.warn(e.message) if respond_to?(:log, true)
log.warn(e.message) if respond_to?(:log, true) # rubocop:disable Legion/HelperMigration/LoggingGuard
{ posted: false, reason: "post failed: #{e.message}" }
end

Expand Down
2 changes: 1 addition & 1 deletion lib/legion/extensions/swarm_github/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
module Legion
module Extensions
module SwarmGithub
VERSION = '0.3.2'
VERSION = '0.3.4'
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class Subscription # rubocop:disable Lint/EmptyClass
end

describe '#check_subtask?' do
it { expect(actor.check_subtask?).to be false }
it { expect(actor.check_subtask?).to be true }
end

describe '#generate_task?' do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
allow(github_client).to receive(:create_pull_request).and_return(
{ result: { 'number' => 42, 'html_url' => 'https://github.com/org/repo/pull/42' } }
)
allow(github_client).to receive(:add_labels).and_return({ success: true })
allow(github_client).to receive(:add_labels_to_issue).and_return({ success: true })
allow(runner).to receive(:pr_reviewer_available?).and_return(false)
end

Expand Down Expand Up @@ -109,14 +109,46 @@
)
end

it 'calls add_labels' do
it 'calls add_labels_to_issue' do
runner.run_lifecycle(generation: generation, review: review)
expect(github_client).to have_received(:add_labels).with(
expect(github_client).to have_received(:add_labels_to_issue).with(
hash_including(labels: %w[auto-generated needs-review])
)
end
end

context 'when called with a flat payload (no generation/review keys)' do
let(:github_client) { double('Github::Client') }

before do
runner.instance_variable_set(:@github_client, nil)
allow(runner).to receive(:github_config).and_return(
{ enabled: true, target_repo: 'org/repo', target_branch: 'main',
auto_merge: false, pr_labels: [], branch_prefix: 'feature/auto-generated' }
)
stub_const('Legion::Extensions::Github::Client', Class.new)
allow(Legion::Extensions::Github::Client).to receive(:new).and_return(github_client)
allow(github_client).to receive(:create_branch).and_return({ success: true })
allow(github_client).to receive(:commit_files).and_return({ success: true })
allow(github_client).to receive(:create_pull_request).and_return(
{ result: { 'number' => 99, 'html_url' => 'https://github.com/org/repo/pull/99' } }
)
allow(runner).to receive(:pr_reviewer_available?).and_return(false)
end

it 'builds generation and review from flat keys and returns success' do
result = runner.run_lifecycle(
name: 'lex-bar', generation_id: 'gen-flat', gap_id: 'gap-002',
gap_type: 'missing_runner', tier: 'utility',
code: '# code', spec_code: '# spec', file_path: 'lib/foo.rb', spec_path: 'spec/foo_spec.rb',
verdict: 'approve', stages: {}
)
expect(result[:success]).to be true
expect(result[:pull_number]).to eq(99)
expect(result[:generation_id]).to eq('gen-flat')
end
end

context 'when branch creation fails' do
before do
runner.instance_variable_set(:@github_client, nil)
Expand Down
Loading