view release on metacpan or search on metacpan
examples/refactor_until_clean.pl view on Meta::CPAN
use Claude::Agent::Code::Refactor qw(refactor_until_clean);
use Claude::Agent::Code::Refactor::Options;
use IO::Async::Loop;
# Create event loop
my $loop = IO::Async::Loop->new;
# Configure refactor options
my $options = Claude::Agent::Code::Refactor::Options->new(
max_iterations => 5, # Max review-fix cycles
min_severity => 'medium', # Only fix medium+ issues
categories => ['bugs', 'security'], # Focus on these
permission_mode => 'acceptEdits', # Auto-accept file edits
perlcritic => 1, # Include perlcritic analysis
perlcritic_severity => 4, # Perlcritic severity level
filter_false_positives => 1, # Filter out false positives
);
# Run the refactor loop on lib/
my $result = refactor_until_clean(
lib/Claude/Agent/Code/Refactor.pm view on Meta::CPAN
use Claude::Agent::Code::Refactor qw(refactor refactor_until_clean);
use IO::Async::Loop;
my $loop = IO::Async::Loop->new;
# Automatic review -> fix -> re-review loop
my $result = refactor_until_clean(
paths => ['lib/'],
options => Claude::Agent::Code::Refactor::Options->new(
max_iterations => 5,
min_severity => 'medium',
categories => ['bugs', 'security'],
permission_mode => 'acceptEdits',
),
loop => $loop,
)->get;
if ($result->is_clean) {
print "All issues resolved!\n";
} else {
print "Remaining issues: ", $result->final_issues, "\n";
}
=head1 DESCRIPTION
Claude::Agent::Code::Refactor provides automated code refactoring using the
Claude Agent SDK. It integrates with Claude::Agent::Code::Review to create
a review-fix-re-review loop that automatically fixes issues until the code
is clean (or max iterations reached).
=head1 EXPORTED FUNCTIONS
=head2 refactor
my $future = refactor(
target => $target, # file, dir, or 'staged'
options => $options, # Claude::Agent::Code::Refactor::Options
loop => $loop, # IO::Async::Loop
);
lib/Claude/Agent/Code/Refactor.pm view on Meta::CPAN
=head2 refactor_until_clean
my $future = refactor_until_clean(
paths => \@paths, # files and/or directories
options => $options,
loop => $loop,
);
my $result = $future->get;
Main refactoring loop: review -> fix -> re-review until clean or max iterations.
=cut
async sub refactor_until_clean {
my (%args) = @_;
my $paths = $args{paths} // die "refactor_until_clean() requires 'paths' argument";
my $options = $args{options} // Claude::Agent::Code::Refactor::Options->new();
my $loop = $args{loop} // IO::Async::Loop->new;
my $start_time = time();
my $result = Claude::Agent::Code::Refactor::Result->new();
my $review_options = $options->to_review_options;
for my $iteration (1 .. $options->max_iterations) {
# Step 1: Review the code
my $report = await review_files(
paths => $paths,
options => $review_options,
loop => $loop,
);
if (!defined $report) {
$result->{error} = 'Review failed to return a report';
last;
lib/Claude/Agent/Code/Refactor.pm view on Meta::CPAN
my $iter = query(
prompt => $prompt,
options => $claude_options,
loop => $loop,
);
# Track what gets fixed
my %files_modified;
my $edit_count = 0; # Track actual Edit operations attempted
my $max_iterations = 1000;
my $iteration_count = 0;
my $start_time = time();
my $timeout_seconds = 300; # 5 minute timeout
my $timeout_result;
eval {
while (my $msg = await $iter->next_async) {
# Check timeout at start of loop iteration
if ((time() - $start_time) > $timeout_seconds) {
$iter->cancel if $iter->can('cancel');
$timeout_result = { issues_fixed => $edit_count, files_modified => [keys %files_modified], error => 'Fix operation timed out after 5 minutes' };
last;
}
$iteration_count++;
last if $iteration_count >= $max_iterations;
if ($msg->isa('Claude::Agent::Message::Assistant')) {
for my $block (@{$msg->content_blocks}) {
if ($block->isa('Claude::Agent::Content::ToolUse')) {
if ($block->name eq 'Edit') {
$edit_count++; # Count each Edit operation
my $input = $block->input;
if ($input && ref($input) eq 'HASH') {
my $file = $input->{file_path};
$files_modified{$file} = 1 if defined $file && length $file;
lib/Claude/Agent/Code/Refactor/Options.pm view on Meta::CPAN
use warnings;
use Types::Common -types;
# Valid values for validation
my @VALID_SEVERITIES = qw(critical high medium low info);
my @VALID_CATEGORIES = qw(bugs security style performance maintainability);
use Marlin
# Loop control
'max_iterations' => sub { 5 },
'max_turns_per_fix' => sub { 20 },
'stop_on_critical' => sub { 1 },
# Issue filtering (passed to Code::Review)
'min_severity' => sub { 'low' },
'categories' => sub { ['bugs', 'security', 'style', 'performance', 'maintainability'] },
# Fix behavior
'fix_one_at_a_time' => sub { 0 },
'dry_run' => sub { 0 },
lib/Claude/Agent/Code/Refactor/Options.pm view on Meta::CPAN
die "Categories cannot be empty";
}
for my $cat (@{$self->categories}) {
unless (grep { $_ eq $cat } @VALID_CATEGORIES) {
my $safe_cat = $cat =~ s/[^a-zA-Z0-9_-]//gr;
die "Invalid category '$safe_cat'. Must be one of: " . join(', ', @VALID_CATEGORIES);
}
}
}
# Validate max_iterations
if (defined $self->max_iterations) {
if ($self->max_iterations < 1) {
die "max_iterations must be >= 1";
}
if ($self->max_iterations > 100) {
die "max_iterations must be <= 100 to prevent resource exhaustion";
}
}
# Validate max_turns_per_fix
if (defined $self->max_turns_per_fix) {
if ($self->max_turns_per_fix < 1) {
die "max_turns_per_fix must be >= 1";
}
if ($self->max_turns_per_fix > 100) {
die "max_turns_per_fix must be <= 100 to prevent resource exhaustion";
lib/Claude/Agent/Code/Refactor/Options.pm view on Meta::CPAN
=head1 NAME
Claude::Agent::Code::Refactor::Options - Configuration options for code refactoring
=head1 SYNOPSIS
use Claude::Agent::Code::Refactor::Options;
my $options = Claude::Agent::Code::Refactor::Options->new(
max_iterations => 5,
min_severity => 'medium',
categories => ['bugs', 'security'],
permission_mode => 'acceptEdits',
);
=head1 DESCRIPTION
Configuration object for Claude::Agent::Code::Refactor.
=head2 ATTRIBUTES
=over 4
=item * max_iterations - Maximum review-fix-review cycles (default: 5)
=item * max_turns_per_fix - Maximum Claude turns per fix attempt (default: 20)
=item * stop_on_critical - Halt if critical issue can't be fixed (default: 1)
=item * min_severity - Minimum severity to fix (default: 'low')
=item * categories - ArrayRef of categories to fix (default: all)
=item * fix_one_at_a_time - Fix issues one at a time vs all at once (default: 0)
lib/Claude/Agent/Code/Refactor/Result.pm view on Meta::CPAN
package Claude::Agent::Code::Refactor::Result;
use 5.020;
use strict;
use warnings;
use Types::Common -types;
use Marlin
'success' => sub { 0 },
'iterations' => sub { 0 },
'initial_issues' => sub { 0 },
'final_issues' => sub { 0 },
'fixes_applied' => sub { 0 },
'files_modified' => sub { [] },
'history' => sub { [] },
'final_report' => sub { undef },
'duration_ms' => sub { 0 },
'error' => sub { undef };
=head1 NAME
Claude::Agent::Code::Refactor::Result - Result of a refactoring operation
=head1 SYNOPSIS
my $result = Claude::Agent::Code::Refactor::Result->new(
success => 1,
iterations => 3,
initial_issues => 15,
final_issues => 0,
fixes_applied => 15,
files_modified => ['lib/Foo.pm', 'lib/Bar.pm'],
history => [...],
duration_ms => 45000,
);
if ($result->is_clean) {
print "All issues resolved!\n";
lib/Claude/Agent/Code/Refactor/Result.pm view on Meta::CPAN
Represents the result of a refactoring operation, including success status,
iteration history, and statistics.
=head2 ATTRIBUTES
=over 4
=item * success - True if all issues were resolved
=item * iterations - Number of review-fix cycles completed
=item * initial_issues - Number of issues found in first review
=item * final_issues - Number of issues remaining after refactoring
=item * fixes_applied - Total number of fixes applied
=item * files_modified - ArrayRef of files that were modified
=item * history - ArrayRef of per-iteration details
lib/Claude/Agent/Code/Refactor/Result.pm view on Meta::CPAN
push @{$self->history}, {
iteration => $iteration,
issues_found => $args{issues_found} // 0,
issues_fixed => $args{issues_fixed} // 0,
files_modified => $args{files_modified} // [],
};
# Update totals
$self->{fixes_applied} += $args{issues_fixed} // 0;
$self->{iterations} = $iteration;
# Track unique files modified
my %seen = map { $_ => 1 } @{$self->files_modified};
for my $file (@{$args{files_modified} // []}) {
push @{$self->files_modified}, $file unless $seen{$file}++;
}
return;
}
lib/Claude/Agent/Code/Refactor/Result.pm view on Meta::CPAN
push @lines, "Error: " . $self->error;
}
elsif ($self->is_clean) {
push @lines, "Status: SUCCESS - All issues resolved";
}
else {
push @lines, "Status: INCOMPLETE - Some issues remain";
}
push @lines, "";
push @lines, "Iterations: " . $self->iterations;
push @lines, "Initial issues: " . $self->initial_issues;
push @lines, "Final issues: " . $self->final_issues;
push @lines, "Fixes applied: " . $self->fixes_applied;
push @lines, "Fix rate: " . $self->fix_rate . "%";
push @lines, "Duration: " . sprintf("%.1f", $self->duration_ms / 1000) . "s";
push @lines, "";
if (@{$self->files_modified}) {
push @lines, "Files modified:";
for my $file (@{$self->files_modified}) {
lib/Claude/Agent/Code/Refactor/Result.pm view on Meta::CPAN
Returns a hashref representation of the result.
=cut
sub to_hash {
my ($self) = @_;
return {
success => $self->success ? 1 : 0,
iterations => $self->iterations,
initial_issues => $self->initial_issues,
final_issues => $self->final_issues,
fixes_applied => $self->fixes_applied,
fix_rate => $self->fix_rate,
files_modified => $self->files_modified,
history => $self->history,
duration_ms => $self->duration_ms,
($self->has_error ? (error => $self->error) : ()),
};
}
t/01-basic.t view on Meta::CPAN
use_ok('Claude::Agent::Code::Refactor::Options');
use_ok('Claude::Agent::Code::Refactor::Result');
# Test exports
can_ok('Claude::Agent::Code::Refactor', qw(refactor refactor_issues refactor_until_clean));
# Test Options
subtest 'Options defaults' => sub {
my $opts = Claude::Agent::Code::Refactor::Options->new();
is($opts->max_iterations, 5, 'default max_iterations');
is($opts->max_turns_per_fix, 20, 'default max_turns_per_fix');
is($opts->stop_on_critical, 1, 'default stop_on_critical');
is($opts->min_severity, 'low', 'default min_severity');
is_deeply($opts->categories,
['bugs', 'security', 'style', 'performance', 'maintainability'],
'default categories');
is($opts->fix_one_at_a_time, 0, 'default fix_one_at_a_time');
is($opts->dry_run, 0, 'default dry_run');
is($opts->create_backup, 0, 'default create_backup');
is($opts->perlcritic, 0, 'default perlcritic');
is($opts->perlcritic_severity, 4, 'default perlcritic_severity');
is($opts->filter_false_positives, 1, 'default filter_false_positives');
is($opts->permission_mode, 'acceptEdits', 'default permission_mode');
};
subtest 'Options custom values' => sub {
my $opts = Claude::Agent::Code::Refactor::Options->new(
max_iterations => 10,
max_turns_per_fix => 30,
min_severity => 'medium',
categories => ['bugs', 'security'],
dry_run => 1,
perlcritic => 1,
perlcritic_severity => 2,
model => 'claude-sonnet-4-5',
);
is($opts->max_iterations, 10, 'custom max_iterations');
is($opts->max_turns_per_fix, 30, 'custom max_turns_per_fix');
is($opts->min_severity, 'medium', 'custom min_severity');
is_deeply($opts->categories, ['bugs', 'security'], 'custom categories');
is($opts->dry_run, 1, 'custom dry_run');
is($opts->perlcritic, 1, 'custom perlcritic');
is($opts->perlcritic_severity, 2, 'custom perlcritic_severity');
is($opts->model, 'claude-sonnet-4-5', 'custom model');
};
subtest 'Options validation' => sub {
t/01-basic.t view on Meta::CPAN
throws_ok {
Claude::Agent::Code::Refactor::Options->new(categories => ['invalid']);
} qr/Invalid category/, 'rejects invalid category';
throws_ok {
Claude::Agent::Code::Refactor::Options->new(categories => []);
} qr/cannot be empty/, 'rejects empty categories';
throws_ok {
Claude::Agent::Code::Refactor::Options->new(max_iterations => 0);
} qr/must be >= 1/, 'rejects zero max_iterations';
throws_ok {
Claude::Agent::Code::Refactor::Options->new(max_turns_per_fix => 0);
} qr/must be >= 1/, 'rejects zero max_turns_per_fix';
throws_ok {
Claude::Agent::Code::Refactor::Options->new(perlcritic_severity => 6);
} qr/Invalid perlcritic_severity/, 'rejects invalid perlcritic_severity';
};
# Test Result
subtest 'Result defaults' => sub {
my $result = Claude::Agent::Code::Refactor::Result->new();
is($result->success, 0, 'default success');
is($result->iterations, 0, 'default iterations');
is($result->initial_issues, 0, 'default initial_issues');
is($result->final_issues, 0, 'default final_issues');
is($result->fixes_applied, 0, 'default fixes_applied');
is_deeply($result->files_modified, [], 'default files_modified');
is_deeply($result->history, [], 'default history');
is($result->duration_ms, 0, 'default duration_ms');
ok(!$result->has_error, 'no error by default');
};
subtest 'Result is_clean' => sub {
t/01-basic.t view on Meta::CPAN
subtest 'Result add_iteration' => sub {
my $result = Claude::Agent::Code::Refactor::Result->new();
$result->add_iteration(
issues_found => 10,
issues_fixed => 5,
files_modified => ['lib/Foo.pm'],
);
is($result->iterations, 1, 'iteration count');
is($result->fixes_applied, 5, 'fixes_applied updated');
is_deeply($result->files_modified, ['lib/Foo.pm'], 'files_modified');
is(scalar @{$result->history}, 1, 'history has one entry');
is($result->history->[0]{iteration}, 1, 'history iteration number');
is($result->history->[0]{issues_found}, 10, 'history issues_found');
is($result->history->[0]{issues_fixed}, 5, 'history issues_fixed');
$result->add_iteration(
issues_found => 5,
issues_fixed => 3,
files_modified => ['lib/Foo.pm', 'lib/Bar.pm'],
);
is($result->iterations, 2, 'iteration count after second');
is($result->fixes_applied, 8, 'fixes_applied cumulative');
is(scalar @{$result->files_modified}, 2, 'files_modified deduplicated');
is(scalar @{$result->history}, 2, 'history has two entries');
};
subtest 'Result has_error' => sub {
my $no_error = Claude::Agent::Code::Refactor::Result->new();
ok(!$no_error->has_error, 'no error');
my $with_error = Claude::Agent::Code::Refactor::Result->new(
error => 'Something went wrong',
);
ok($with_error->has_error, 'has error');
};
subtest 'Result as_text' => sub {
my $result = Claude::Agent::Code::Refactor::Result->new(
success => 1,
iterations => 2,
initial_issues => 10,
final_issues => 0,
fixes_applied => 10,
files_modified => ['lib/Foo.pm'],
duration_ms => 5000,
);
my $text = $result->as_text;
like($text, qr/REFACTOR RESULT/, 'has header');
like($text, qr/SUCCESS/, 'shows success');
like($text, qr/Iterations: 2/, 'shows iterations');
like($text, qr/Initial issues: 10/, 'shows initial issues');
like($text, qr/Fix rate: 100%/, 'shows fix rate');
like($text, qr/lib\/Foo\.pm/, 'shows modified files');
};
subtest 'Result to_hash' => sub {
my $result = Claude::Agent::Code::Refactor::Result->new(
success => 1,
iterations => 3,
initial_issues => 15,
final_issues => 0,
fixes_applied => 15,
);
my $hash = $result->to_hash;
is($hash->{success}, 1, 'hash success');
is($hash->{iterations}, 3, 'hash iterations');
is($hash->{initial_issues}, 15, 'hash initial_issues');
is($hash->{final_issues}, 0, 'hash final_issues');
is($hash->{fix_rate}, 100, 'hash fix_rate');
ok(!exists $hash->{error}, 'hash omits error when not set');
};
done_testing();
t/03-options.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use Test::Exception;
use_ok('Claude::Agent::Code::Refactor::Options');
subtest 'default values' => sub {
my $opts = Claude::Agent::Code::Refactor::Options->new();
is($opts->max_iterations, 5, 'default max_iterations is 5');
is($opts->max_turns_per_fix, 20, 'default max_turns_per_fix is 20');
is($opts->min_severity, 'low', 'default min_severity is low');
is($opts->permission_mode, 'acceptEdits', 'default permission_mode is acceptEdits');
is($opts->dry_run, 0, 'default dry_run is 0');
is($opts->perlcritic, 0, 'default perlcritic is 0');
is($opts->perlcritic_severity, 4, 'default perlcritic_severity is 4');
is($opts->filter_false_positives, 1, 'default filter_false_positives is 1');
is($opts->model, undef, 'default model is undef');
is_deeply(
$opts->categories,
['bugs', 'security', 'style', 'performance', 'maintainability'],
'default categories is all categories'
);
};
subtest 'custom values' => sub {
my $opts = Claude::Agent::Code::Refactor::Options->new(
max_iterations => 10,
max_turns_per_fix => 30,
min_severity => 'high',
permission_mode => 'bypassPermissions',
dry_run => 1,
perlcritic => 1,
perlcritic_severity => 2,
filter_false_positives => 0,
model => 'claude-sonnet-4',
categories => ['bugs', 'security'],
);
is($opts->max_iterations, 10, 'custom max_iterations');
is($opts->max_turns_per_fix, 30, 'custom max_turns_per_fix');
is($opts->min_severity, 'high', 'custom min_severity');
is($opts->permission_mode, 'bypassPermissions', 'custom permission_mode');
is($opts->dry_run, 1, 'custom dry_run');
is($opts->perlcritic, 1, 'custom perlcritic');
is($opts->perlcritic_severity, 2, 'custom perlcritic_severity');
is($opts->filter_false_positives, 0, 'custom filter_false_positives');
is($opts->model, 'claude-sonnet-4', 'custom model');
is_deeply($opts->categories, ['bugs', 'security'], 'custom categories');
};
t/03-options.t view on Meta::CPAN
throws_ok {
Claude::Agent::Code::Refactor::Options->new(categories => []);
} qr/cannot be empty/, 'rejects empty categories';
throws_ok {
Claude::Agent::Code::Refactor::Options->new(categories => 'bugs');
} qr/must be an array/, 'rejects non-array categories';
};
subtest 'max_iterations validation' => sub {
lives_ok {
Claude::Agent::Code::Refactor::Options->new(max_iterations => 1);
} 'accepts max_iterations = 1';
lives_ok {
Claude::Agent::Code::Refactor::Options->new(max_iterations => 100);
} 'accepts max_iterations = 100';
throws_ok {
Claude::Agent::Code::Refactor::Options->new(max_iterations => 0);
} qr/must be >= 1/, 'rejects max_iterations < 1';
throws_ok {
Claude::Agent::Code::Refactor::Options->new(max_iterations => 101);
} qr/must be <= 100/, 'rejects max_iterations > 100';
};
subtest 'max_turns_per_fix validation' => sub {
lives_ok {
Claude::Agent::Code::Refactor::Options->new(max_turns_per_fix => 1);
} 'accepts max_turns_per_fix = 1';
lives_ok {
Claude::Agent::Code::Refactor::Options->new(max_turns_per_fix => 100);
} 'accepts max_turns_per_fix = 100';
t/04-result.t view on Meta::CPAN
use strict;
use warnings;
use Test::More;
use_ok('Claude::Agent::Code::Refactor::Result');
subtest 'default values' => sub {
my $result = Claude::Agent::Code::Refactor::Result->new();
is($result->success, 0, 'default success is 0');
is($result->iterations, 0, 'default iterations is 0');
is($result->initial_issues, 0, 'default initial_issues is 0');
is($result->final_issues, 0, 'default final_issues is 0');
is($result->fixes_applied, 0, 'default fixes_applied is 0');
is_deeply($result->files_modified, [], 'default files_modified is empty');
is_deeply($result->history, [], 'default history is empty');
is($result->duration_ms, 0, 'default duration_ms is 0');
is($result->error, undef, 'default error is undef');
};
subtest 'is_clean' => sub {
t/04-result.t view on Meta::CPAN
subtest 'add_iteration' => sub {
my $result = Claude::Agent::Code::Refactor::Result->new();
$result->add_iteration(
issues_found => 10,
issues_fixed => 5,
files_modified => ['lib/Foo.pm', 'lib/Bar.pm'],
);
is($result->iterations, 1, 'iterations incremented');
is($result->fixes_applied, 5, 'fixes_applied updated');
is_deeply($result->files_modified, ['lib/Foo.pm', 'lib/Bar.pm'], 'files tracked');
is(scalar(@{$result->history}), 1, 'history has one entry');
is($result->history->[0]{iteration}, 1, 'iteration number recorded');
is($result->history->[0]{issues_found}, 10, 'issues_found recorded');
is($result->history->[0]{issues_fixed}, 5, 'issues_fixed recorded');
$result->add_iteration(
issues_found => 5,
issues_fixed => 3,
files_modified => ['lib/Bar.pm', 'lib/Baz.pm'],
);
is($result->iterations, 2, 'iterations incremented again');
is($result->fixes_applied, 8, 'fixes_applied accumulated');
is_deeply(
[sort @{$result->files_modified}],
['lib/Bar.pm', 'lib/Baz.pm', 'lib/Foo.pm'],
'files deduplicated'
);
is(scalar(@{$result->history}), 2, 'history has two entries');
};
subtest 'as_text' => sub {
t/04-result.t view on Meta::CPAN
);
$result->add_iteration(
issues_found => 10,
issues_fixed => 8,
files_modified => ['lib/Foo.pm'],
);
my $hash = $result->to_hash;
is($hash->{success}, 1, 'success in hash');
is($hash->{iterations}, 1, 'iterations in hash');
is($hash->{initial_issues}, 10, 'initial_issues in hash');
is($hash->{final_issues}, 2, 'final_issues in hash');
is($hash->{fixes_applied}, 8, 'fixes_applied in hash');
is($hash->{fix_rate}, 80, 'fix_rate in hash');
is_deeply($hash->{files_modified}, ['lib/Foo.pm'], 'files_modified in hash');
is($hash->{duration_ms}, 3000, 'duration_ms in hash');
ok(!exists $hash->{error}, 'no error key when no error');
};
subtest 'to_hash with error' => sub {
t/05-edge-cases.t view on Meta::CPAN
} qr/Unknown target type/, 'rejects non-existent path';
};
subtest 'Result iteration with missing values' => sub {
my $result = Claude::Agent::Code::Refactor::Result->new();
lives_ok {
$result->add_iteration();
} 'add_iteration works with no arguments';
is($result->iterations, 1, 'iteration counted');
is($result->fixes_applied, 0, 'fixes_applied defaults to 0');
is_deeply($result->files_modified, [], 'files_modified defaults to empty');
is($result->history->[0]{issues_found}, 0, 'issues_found defaults to 0');
is($result->history->[0]{issues_fixed}, 0, 'issues_fixed defaults to 0');
};
subtest 'Result multiple iterations tracking' => sub {
my $result = Claude::Agent::Code::Refactor::Result->new();
for my $i (1..5) {
$result->add_iteration(
issues_found => 10 - $i,
issues_fixed => 2,
files_modified => ["file$i.pm"],
);
}
is($result->iterations, 5, '5 iterations recorded');
is($result->fixes_applied, 10, 'total fixes accumulated');
is(scalar(@{$result->files_modified}), 5, 'all files tracked');
is(scalar(@{$result->history}), 5, 'history has 5 entries');
for my $i (0..4) {
is($result->history->[$i]{iteration}, $i + 1, "iteration " . ($i+1) . " numbered correctly");
}
};
subtest 'Options all severities' => sub {