App-karr
view release on metacpan or search on metacpan
lib/App/karr/Foundation.pm view on Meta::CPAN
};
$self->_release_lock( $repo );
# Exponential cooldown bookkeeping: grow on common-error, reset otherwise
if ( ( $result->{outcome} // '' ) eq 'common-error' ) {
$self->_set_cooldown( $repo, $karr );
} else {
$self->_clear_cooldown( $repo );
}
# Update state
$self->_state_set( $repo,
hash => $self->_ref_hash( $repo ) // '',
last_run => localtime->datetime,
last_exit => $result->{exit} // 0,
);
}
# ---------------------------------------------------------------------------
# Sync
# ---------------------------------------------------------------------------
sub _sync_pull {
my ( $self, $repo ) = @_;
$self->_say_verbose("sync --pull $repo");
return if $self->dry_run;
my $git = App::karr::Git->new( dir => "$repo" );
return unless $git->is_repo;
$git->pull;
}
# ---------------------------------------------------------------------------
# Ref hash (detect board changes)
# ---------------------------------------------------------------------------
sub _ref_hash {
my ( $self, $repo ) = @_;
my $git = App::karr::Git->new( dir => "$repo" );
return undef unless $git->is_repo;
my $oids = $git->ref_oids('refs/karr/') or return undef;
# Deterministic fingerprint of refs/karr/* (ref name + target OID).
my $out = join '', map { "$_ $oids->{$_}\n" } sort keys %$oids;
return md5_hex( $out );
}
# ---------------------------------------------------------------------------
# Task state / actionability
# ---------------------------------------------------------------------------
# A task is actionable when an agent could still pick it: not terminal
# (done/archived) and not blocked. Mirrors `karr pick` eligibility.
sub _is_actionable {
my ( $self, $st ) = @_;
return 0 unless $st;
return 0 if $st->{blocked};
my $status = $st->{status} // '';
return 0 if $status eq 'done' || $status eq 'archived';
return 1;
}
# Snapshot every task as id => { status, claimed_by, updated, blocked }.
sub _task_states {
my ( $self, $repo ) = @_;
my $git = App::karr::Git->new( dir => "$repo" );
return () unless $git->is_repo;
my $store = App::karr::BoardStore->new( git => $git );
my %states;
for my $t ( $store->load_tasks ) {
next unless $t;
$states{ $t->id } = {
status => $t->status,
claimed_by => ( $t->has_claimed_by ? $t->claimed_by : undef ),
updated => $t->updated,
blocked => ( $t->has_blocked ? 1 : 0 ),
};
}
return %states;
}
sub _has_actionable_tasks {
my ( $self, $repo ) = @_;
my %states = $self->_task_states( $repo );
for my $id ( keys %states ) {
return 1 if $self->_is_actionable( $states{$id} );
}
return 0;
}
# Tasks the agent engaged (claimed / in-progress) but did not move across a
# run â still actionable and byte-identical before/after. These are the only
# tasks that count toward an auto-block.
sub _stuck_tasks {
my ( $self, $before, $after ) = @_;
my @stuck;
for my $id ( sort { $a <=> $b } keys %$after ) {
my $a = $after->{$id};
next unless $self->_is_actionable( $a );
next unless defined $a->{claimed_by} || ( $a->{status} // '' ) eq 'in-progress';
my $b = $before->{$id} or next; # newly created this run â give it grace
next if ( $b->{status} // '' ) ne ( $a->{status} // '' );
next if ( $b->{updated} // '' ) ne ( $a->{updated} // '' );
push @stuck, $id;
}
return @stuck;
}
# ---------------------------------------------------------------------------
# Drain loop
# ---------------------------------------------------------------------------
# Run the agent repeatedly until the board has no actionable tasks left,
# auto-blocking tasks the agent keeps failing on. Returns
# { outcome => progress|idle|common-error|error, exit => N }.
sub _drain_repo {
my ( $self, $repo, $karr, $cmd ) = @_;
my $max_runtime = $karr->{max_runtime} // 1800;
my $max_attempts = $karr->{max_attempts} // 2;
my $max_iter = $karr->{max_iterations} // 50;
my $drain = exists $karr->{drain} ? $karr->{drain} : 1;
my $patterns = $self->_error_patterns( $karr );
# Use the resolved command, not $karr->{command}
$cmd //= $karr->{command};
my $loop_start = time;
my $last_exit = 0;
my $outcome = 'idle';
my $first = 1;
my $iter = 0;
while ( 1 ) {
my %before = $self->_task_states( $repo );
my @actionable = grep { $self->_is_actionable( $before{$_} ) } keys %before;
# Once we have run at least once, stop when the board is drained, the
# wall-clock budget is spent, or we hit the hard iteration cap.
last if !$first && !@actionable;
last if !$first && ( time - $loop_start ) >= $max_runtime;
last if $iter >= $max_iter;
my $hash_before = $self->_ref_hash( $repo ) // '';
my ( $exit, $output ) = $self->_run_command( $repo, $karr, $cmd );
$last_exit = $exit;
$first = 0;
$iter++;
# Common error we can observe (bad exit, timeout, or a known log pattern):
# don't penalize any task â leave the board untouched and back off.
my $err = ( $exit != 0 ) ? "exit=$exit" : undef;
$err //= $self->_match_error( $output, $patterns );
if ( defined $err ) {
$self->_append_log( $repo, "COMMON-ERROR $err" );
$self->_state_set( $repo, last_error => $err );
$outcome = 'common-error';
last;
}
my $hash_after = $self->_ref_hash( $repo ) // '';
my $progressed = ( $hash_before ne $hash_after ) ? 1 : 0;
$outcome = 'progress' if $progressed;
( run in 0.378 second using v1.01-cache-2.11-cpan-bbe5e583499 )