App-karr
view release on metacpan or search on metacpan
lib/App/karr/Cmd/Pick.pm view on Meta::CPAN
);
use App::karr::Role::BoardAccess;
use App::karr::Role::Output;
use App::karr::Task;
use App::karr::Config;
use Time::Piece;
with 'App::karr::Role::BoardAccess', 'App::karr::Role::Output', 'App::karr::Role::ClaimTimeout';
option claim => (
is => 'ro',
format => 's',
required => 1,
doc => 'Agent name to claim the task for',
);
option status => (
is => 'ro',
format => 's',
doc => 'Source status(es) to pick from (comma-separated)',
);
option move => (
is => 'ro',
format => 's',
doc => 'Move picked task to this status',
);
option tags => (
is => 'ro',
format => 's',
doc => 'Only pick tasks matching at least one tag',
);
sub execute {
my ($self, $args_ref, $chain_ref) = @_;
$self->sync_before;
my $ec = $self->store->effective_config;
my @tasks = $self->load_tasks;
# Filter by status
if ($self->status) {
my %allowed = map { $_ => 1 } split /,/, $self->status;
@tasks = grep { $allowed{$_->status} } @tasks;
} else {
# Exclude terminal statuses
# Exclude terminal statuses
@tasks = grep { !App::karr::Config->is_terminal_status($_->status) } @tasks;
}
# Exclude claimed tasks (unless claim expired)
my $timeout = $self->_parse_timeout($ec->{claim_timeout} // '1h');
@tasks = grep {
!$_->has_claimed_by || $self->_claim_expired($_, $timeout)
} @tasks;
# Exclude blocked
@tasks = grep { !$_->has_blocked } @tasks;
# Filter by tags
if ($self->tags) {
my %wanted = map { $_ => 1 } split /,/, $self->tags;
@tasks = grep {
my $t = $_;
grep { $wanted{$_} } @{$t->tags};
} @tasks;
}
# Sort by class priority, then by priority
my %class_order = App::karr::Config->class_order;
my %pri_order = App::karr::Config->priority_order;
@tasks = sort {
($class_order{$a->class} // 2) <=> ($class_order{$b->class} // 2)
|| ($pri_order{$a->priority} // 2) <=> ($pri_order{$b->priority} // 2)
|| $a->id <=> $b->id
} @tasks;
unless (@tasks) {
print "No available tasks to pick.\n";
return;
}
# Try to lock + claim
my $use_lock = $self->git->is_repo;
my $lock;
if ($use_lock) {
require App::karr::Lock;
$lock = App::karr::Lock->new(git => $self->git);
}
my $email = $use_lock ? ($self->git->git_user_email || $self->claim) : $self->claim;
my $picked;
for my $task (@tasks) {
if ($use_lock) {
my ($ok, $msg) = $lock->acquire($task->id, $email);
next unless $ok;
}
$task->claimed_by($self->claim);
$task->claimed_at(gmtime->datetime . 'Z');
if ($self->move) {
$task->status($self->move);
if ($self->move eq 'in-progress' && !$task->has_started) {
$task->started(gmtime->strftime('%Y-%m-%d'));
}
}
$self->save_task($task);
$picked = $task;
last;
}
unless ($picked) {
print "No available tasks to pick (all locked).\n";
return;
}
# Serialize + push BEFORE releasing lock
$self->sync_after;
# Log the pick action
if ($use_lock) {
$self->append_log($self->git,
agent => $self->claim,
action => 'pick',
task_id => $picked->id,
detail => $picked->status,
);
}
# Release lock AFTER sync
if ($use_lock) {
$lock->release($picked->id, $email);
}
if ($self->json) {
my $data = $picked->to_frontmatter;
$data->{body} = $picked->body if $picked->body;
$self->print_json($data);
return;
}
printf "Picked task %d: %s (claimed by %s)\n", $picked->id, $picked->title, $self->claim;
printf "Status: %s | Priority: %s | Class: %s\n", $picked->status, $picked->priority, $picked->class;
if ($picked->body) {
print "\n" . $picked->body . "\n";
}
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
App::karr::Cmd::Pick - Atomically find and claim the next available task
=head1 VERSION
version 0.302
=head1 SYNOPSIS
karr pick --claim agent-fox
karr pick --claim agent-fox --status todo --move in-progress
karr pick --claim agent-fox --tags backend,urgent --json
=head1 DESCRIPTION
Selects the next available task for an agent, taking class of service,
priority, blocked state, and claim expiry into account. When the board lives in
a Git repository, the command also uses lock refs so concurrent agents do not
pick the same task.
=head1 SELECTION RULES
=over 4
=item * Eligible statuses
If C<--status> is omitted, tasks in C<done> and C<archived> are excluded.
=item * Claim timeout
Already claimed tasks are ignored unless their claim timestamp has expired
according to C<claim_timeout>.
=item * Ordering
Candidates are sorted by class of service, then by priority, then by task id.
=item * C<--move>
Optionally updates the picked task to a new status such as C<in-progress>.
=back
=head1 SEE ALSO
L<karr>, L<App::karr>, L<App::karr::Cmd::List>, L<App::karr::Cmd::Move>,
L<App::karr::Cmd::Handoff>, L<App::karr::Cmd::AgentName>
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/karr/issues>.
=head2 IRC
Join C<#langertha> on C<irc.perl.org> or message Getty directly.
=head1 CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
=head1 AUTHOR
Torsten Raudssus <getty@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> L<https://raudssus.de/>.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut
( run in 2.184 seconds using v1.01-cache-2.11-cpan-2398b32b56e )