NAME
App::karr::Role::PickRules - The one definition of which card karr pick may hand out, and in what order
VERSION
version 0.600
DESCRIPTION
Two things decide what karr pick hands an agent: whether a card is available at all, and which of the available ones comes first. Both were written twice -- once in App::karr::Cmd::Pick and once in App::karr::Foundation::Picker, which has to name the card the agent's own karr pick would have handed it or the coordinator is arguing with its own board. They agreed because they were copied; nothing kept them agreeing (ticket #198).
This role is the definition both call. It lives in a role rather than in App::karr::Task or App::karr::BoardStore because the eligibility test needs three things at once: the task, the board ($self->store for the board's terminal statuses and its priorities/classes lists), and the claim-expiry parser in App::karr::Role::ClaimTimeout -- which this role composes, so a consumer gets the whole rule by asking for one name. A function in Task would have had to be handed all three; a method on BoardStore would have put command-selection policy in the storage layer, which belongs to a different owner.
What is deliberately not here: claiming, locking and the compare-and-swap that binds a pick ("EXCLUSIVITY" in App::karr::Cmd::Pick), because foundation must not do any of them (App::karr::Foundation::Picker), and the --status and --tags option parsing, which stays with the command that has options. This role takes the filters already split, applies them, and stops.
Unmet dependencies are not filtered anywhere in here, matching karr pick: nothing about depends_on blocks anything in karr, the command hands the card over and warns (ticket #123).
pickable
$self->pickable( $task );
$self->pickable( $task, timeout => $secs, statuses => \@s, tags => \@t );
True when $task is available to be picked right now. In order: it exists; its status is in statuses if that filter was given, and is not one of the board's terminal statuses if it was not (the board's own final column and archived, never a hardcoded done); it is not held by a claim that is still live under timeout, where claimed_by set to the empty string is kanban-md for "unclaimed"; it is not blocked; and it carries at least one of tags if that filter was given.
timeout is the claim window in seconds and defaults to "claim_timeout_secs" in App::karr::Role::ClaimTimeout. Pass it explicitly when asking about many cards in one command run, so one answer covers the whole run. 0 is not the shortest window but no window at all: a board with claim_timeout: 0s never expires a claim, so every claimed card stays unpickable until the claim is released. statuses and tags are already-split lists, not the comma-separated option strings -- splitting belongs to the command that owns the option. An absent (or empty) filter is not the same as an empty list: no statuses means "anything but terminal", statuses => [] means nothing qualifies.
The claim half of the test is "claim_held" in App::karr::Role::ClaimTimeout, called rather than restated: karr list --unclaimed asks that same method about every card on the board, so what the list shows as free is what this method lets karr pick take (ticket #252). The blocked test deliberately stayed here and is not part of it -- a blocked card is unpickable, not claimed.
pick_rank
my @ranked = $self->pick_rank( @tasks );
karr pick's order: class of service first, then priority, then task id. Both lists come from the board's own priorities and classes config, so a board imported from kanban-md ranks by its own names (ticket #149). Lower class index is more urgent, higher priority index is more urgent -- kanban-md's convention, from its pick.go. The id tie-break makes the order total, so the first element is well defined however the sort was reached.
A name neither list carries still has to rank somewhere. An unlisted priority sorts below every listed one. An unlisted class -- a typo, or a card imported from a board with other classes -- takes standard's index, and index 0 where the board's classes does not name standard either, which puts it level with the first class the board does list.
That last 0 is a deliberate divergence from kanban-md. Its classOrder in internal/board/pick.go returns cfg.ClassIndex("standard") unchanged, which is -1 on a board without standard, so there an unlisted class outranks every configured one and is handed out first. karr keeps such a card ordinary instead: a class the board never heard of is far more likely a mistake than a claim to urgency, and a card with a typo in it jumping the queue is the worse of the two failures. The two can only differ on a board that replaced the default classes list -- which does name standard -- with one that does not, so nothing reaches the difference by accident (ticket #240).
One class breaks that order, and only against itself: where both cards are fixed-date, the due date decides before priority is asked (ticket #233). Earlier due first; a fixed-date card with no due date sorts behind every dated one; where neither card is dated, or both are due on the same day, priority decides as usual. Against any other class -- and between two cards of any other class -- due is not read at all: a fixed-date card meeting an expedite or a standard one is ranked by class index alone, however soon either of them is due. This is kanban-md's exception in sortPickCandidates/compareDue, and it is the class of service that exists because a date, not an urgency rating, decides.
pick_candidates
my @ranked = $self->pick_candidates( [ $self->load_tasks ], %filter );
"pickable" and "pick_rank" in one call: every eligible task, most urgent first. karr pick walks the whole list, because a candidate can still be lost to another agent's lock or compare-and-swap; App::karr::Foundation::Picker takes the first and stops.
The list is a ranking, not a decision -- nothing here reads a card under a lock and nothing writes. See "EXCLUSIVITY" in App::karr::Cmd::Pick for what has to happen on top before a pick is binding.
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/karr/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is Copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)