NAME

App::karr::Role::TaskMutation - The one guarded path for changing an existing task

VERSION

version 0.500

DESCRIPTION

Commands that change a task that already exists -- move, edit, delete, archive, handoff -- share three things through this role: the compare-and-swap loop that persists the change, the single implementation of "this task's status becomes that", and the batch loop the id-list commands run that pair over.

Claim ownership is checked by the caller, inside the callback it hands to update_task_guarded, rather than by update_task_guarded itself, because edit --release deliberately acts on somebody else's claim. Putting the check in the callback is what keeps it under the same guard as the write: a check made before the loop is a check made against a revision that may no longer be there (tickets #44, #46, #56).

SEE ALSO

karr, App::karr, App::karr::Role::ClaimTimeout, App::karr::Cmd::Move, App::karr::Cmd::Edit, App::karr::Cmd::Delete, App::karr::Cmd::Archive, App::karr::Cmd::Handoff

run_batch

Runs one callback per id and keeps going when an id fails, so that a bad id in the middle of the list cannot skip the ids after it. Returns the collected per-id results and the number of failures.

my ( $results, $failed ) = $self->run_batch( \@ids, sub {
    my ($id) = @_;
    ...
    return { id => $id, title => $title };
} );

Whatever the callback returns is appended to the results; a callback that dies contributes { id => $id, error => $message } instead and the message is also warned to STDERR unless --json is in force. Usage errors are re-thrown rather than collected: they condemn the whole invocation, not one id.

report_batch_failure

$self->report_batch_failure( $failed, scalar @ids );

Ends a batch that had failures with exit code 1 and a one-line summary, after the ids that did succeed have been committed. A no-op when nothing failed.

update_task_guarded

Reads the task, runs the callback against it, and writes it back only if the task ref is still exactly where it was when it was read. If another agent got in first the callback's work is discarded and the callback is re-run against the fresh task, so the decision it makes and the bytes that land are always the same revision. Returns the written task.

my $task = $self->update_task_guarded( $id, sub {
    my ($task) = @_;
    $self->check_claim( $task, $self->claim );
    $task->title('New title');
} );

The callback runs once per attempt, so it must be a function of the task it is handed -- read $task->status, never a status captured beforehand -- and anything it does besides changing that task has to be safe to do twice. A side effect outside the task object is allowed where a repeat replaces it instead of adding to it: "apply_status_change" calls "check_dependencies" in App::karr::Role::DependencyCheck, which records into a slot keyed by task id and clears that slot on entry, so what a losing attempt wrote is overwritten by the attempt that wins rather than added to. Appending to a list, incrementing a counter or printing would each have come out once per attempt -- printing is why the dependency warnings are emitted by "dependency_report" in App::karr::Role::DependencyCheck once the write has landed, and never from inside the callback.

delete_task_guarded

Deletes a task, but only if the task ref is still exactly where it was when the claim rule was applied to it. If another agent got in first the check is re-run against the fresh task -- so a claim that lands in the window blocks the delete instead of being deleted with the card -- and a task another agent deleted meanwhile is reported as not found. Returns the deleted task.

$self->delete_task_guarded( $id, undef );

apply_status_change

The only place a task's status is assigned. Rejects a status the board does not configure, applies require_claim and the lifecycle stamps, records any unsatisfied dependencies ("check_dependencies" in App::karr::Role::DependencyCheck -- recorded here, emitted by the caller once the write has landed), and returns the status the task had before the change.

my $old_status = $self->apply_status_change( $task, 'in-progress', $claimant );

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; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.