Claude-Agent-Code-Refactor
view release on metacpan or search on metacpan
lib/Claude/Agent/Code/Refactor.pm view on Meta::CPAN
package Claude::Agent::Code::Refactor;
use 5.020;
use strict;
use warnings;
use Exporter 'import';
our @EXPORT_OK = qw(refactor refactor_issues refactor_until_clean);
use Claude::Agent qw(query);
use Claude::Agent::Options;
use Claude::Agent::Code::Review qw(review_files);
use Claude::Agent::Code::Refactor::Options;
use Claude::Agent::Code::Refactor::Result;
use IO::Async::Loop;
use Future::AsyncAwait;
use Time::HiRes qw(time);
our $VERSION = '0.02';
=head1 NAME
Claude::Agent::Code::Refactor - Automated code refactoring with review-fix loops
=head1 VERSION
Version 0.02
=head1 SYNOPSIS
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
);
my $result = $future->get;
High-level refactor function that auto-detects the target type.
=cut
async sub refactor {
my (%args) = @_;
my $target = $args{target} // die "refactor() requires 'target' argument";
my $options = $args{options} // Claude::Agent::Code::Refactor::Options->new();
my $loop = $args{loop} // IO::Async::Loop->new;
my @paths;
if (-d $target || -f $target) {
@paths = ($target);
}
else {
die "Unknown target type: $target (must be file or directory)";
}
return await refactor_until_clean(
paths => \@paths,
options => $options,
loop => $loop,
);
}
=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;
}
my @issues = @{$report->issues // []};
my $issues_found = scalar @issues;
# Record initial issues on first iteration
if ($iteration == 1) {
$result->{initial_issues} = $issues_found;
}
# Step 2: Check if we're done
if ($issues_found == 0) {
$result->{success} = 1;
$result->{final_issues} = 0;
$result->{final_report} = $report;
$result->add_iteration(
issues_found => 0,
issues_fixed => 0,
files_modified => [],
);
last;
}
# Step 3: Fix the issues
my $fix_result;
if ($options->dry_run) {
# Dry run - just report what would be fixed
$fix_result = {
issues_fixed => 0,
files_modified => [],
};
}
else {
$fix_result = await _fix_issues(\@issues, $options, $loop);
}
# Step 4: Record this iteration
$result->add_iteration(
issues_found => $issues_found,
issues_fixed => $fix_result->{issues_fixed} // 0,
files_modified => $fix_result->{files_modified} // [],
);
# Simplified: stop if no fixes were applied (stall detection)
if ($iteration > 1 && $fix_result->{issues_fixed} == 0) {
# No progress or not improving - stop to avoid infinite loop
# Note: This check may not catch all stall conditions
$result->{final_issues} = $issues_found;
$result->{final_report} = $report;
if ($fix_result->{error}) {
lib/Claude/Agent/Code/Refactor.pm view on Meta::CPAN
return $result;
}
my $fix_result = await _fix_issues($issues, $options, $loop);
$result->add_iteration(
issues_found => scalar(@$issues),
issues_fixed => $fix_result->{issues_fixed} // 0,
files_modified => $fix_result->{files_modified} // [],
);
my $remaining = scalar(@$issues) - ($fix_result->{issues_fixed} // 0);
$result->{final_issues} = $remaining > 0 ? $remaining : 0;
$result->{success} = ($result->{final_issues} == 0);
$result->{duration_ms} = int((time() - $start_time) * 1000);
if ($fix_result->{error}) {
$result->{error} = $fix_result->{error};
}
return $result;
}
# Internal: Fix a set of issues using Claude
async sub _fix_issues {
my ($issues, $options, $loop) = @_;
return { issues_fixed => 0, files_modified => [] } unless @$issues;
# Sort by severity (critical first)
my %severity_rank = (critical => 5, high => 4, medium => 3, low => 2, info => 1);
my @sorted = sort {
($severity_rank{$b->severity} // 0) <=> ($severity_rank{$a->severity} // 0)
} @$issues;
# Build the fix prompt
my $prompt = _build_fix_prompt(\@sorted);
# Build Claude options for fixing
my %claude_args = (
allowed_tools => ['Read', 'Edit', 'Glob', 'Grep'],
permission_mode => $options->permission_mode,
system_prompt => _get_fix_system_prompt(),
max_turns => $options->max_turns_per_fix,
);
$claude_args{model} = $options->model if defined $options->model;
my $claude_options = Claude::Agent::Options->new(%claude_args);
# Run the fix query
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;
}
}
}
}
}
elsif ($msg->isa('Claude::Agent::Message::Result')) {
last;
}
}
};
# Cleanup SDK server sockets before returning
$iter->cleanup();
return $timeout_result if $timeout_result;
my $error = $@; # Capture $@ immediately to prevent clobbering
if ($error) {
warn "Fix error: $error"; # Log for debugging
my $safe_error = "An error occurred during fix operation";
return { issues_fixed => 0, files_modified => [], error => $safe_error };
}
return {
issues_fixed => $edit_count, # Count of Edit operations attempted
files_modified => [keys %files_modified],
};
}
# Internal: Build prompt for fixing issues
# SECURITY NOTE: Issue data (file, description, suggestion, code_before, code_after)
# is concatenated directly into the prompt. This function assumes issue data comes
# from trusted sources (e.g., Claude::Agent::Code::Review). If issue data originates
# from untrusted external sources, sanitization should be performed by the caller.
sub _build_fix_prompt {
my ($issues) = @_;
my $prompt = "Fix the following issues found in code review:\n\n";
for my $issue (@$issues) {
$prompt .= sprintf("## Issue in %s (line %d)\n", $issue->file, $issue->line);
$prompt .= "Severity: " . $issue->severity . "\n";
$prompt .= "Category: " . $issue->category . "\n";
$prompt .= "Description: " . $issue->description . "\n";
if ($issue->has_explanation) {
$prompt .= "Explanation: " . $issue->explanation . "\n";
}
if ($issue->has_suggestion) {
$prompt .= "Suggestion: " . $issue->suggestion . "\n";
( run in 0.627 second using v1.01-cache-2.11-cpan-71847e10f99 )