view release on metacpan or search on metacpan
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
karr list --sort priority --reverse # sort and reverse
karr list --claimed-by agent-1 # filter by claim owner
karr list --compact # one-line output (agent-friendly)
karr list --json # JSON output
```
### Show task
```bash
karr show ID
karr show # most recently updated task
karr show --last 5 # the 5 most recent
karr show --me # the task you most recently acted on (re-orient)
karr show --agent NAME # the task most recently claimed by NAME
```
### Move task
```bash
karr move ID STATUS # move to specific status
karr move ID --next # advance one status
.claude/skills/kanban-issues-karr-cli/SKILL.md view on Meta::CPAN
## Stored task format
```markdown
---
id: 1
title: Set up CI pipeline
status: backlog
priority: high
class: standard
created: 2026-03-12T10:00:00Z
updated: 2026-03-12T10:00:00Z
tags:
- devops
---
Optional body with more detail.
```
Tasks are stored under `refs/karr/tasks/*/data`. During command execution `karr`
materializes the same Markdown shape into a temporary task directory, so this
format still matters when reading or generating tasks programmatically.
- karr-foundation: `claude: true` synthesizes the canonical claude
invocation so you needn't retype it; `claude_bin`, `claude_max_turns`
and `claude_permission_mode` override the parts. The agent instruction
is exposed as the `$PROMPT` substitution variable (settable via `prompt`
in .karr or `default_prompt` in config), usable in any command template.
- Activity log entries are now keyed by a role-qualified identity
(`refs/karr/log/<role>/<email>`, role `user` or `agent`) so a human and
an AI sharing one Git config are told apart. The role propagates to
nested karr calls via the KARR_ROLE env var (foundation sets `agent`);
pre-existing bare-email logs are still read for the `user` role.
- karr show: with no ID shows the single most recently updated task;
`--last N` widens that, `--me` shows the task(s) the current identity
most recently acted on (via the activity log), and `--agent NAME` shows
the task(s) most recently claimed by that agent name.
- karr board: hide the `@claimed_by` badge and claimed-count for tasks in
a terminal status (done/archived) â a claim is an active lease, and the
history remains in the activity log.
- sync: surface the real libgit2 error on a failed pull/push instead of a
meaningless "(exit code $?)" (native libgit2 operations have no shell
exit code). New Git `last_error` accessor records the last remote-op
exception.
docs/superpowers/plans/2026-03-19-v0004-implementation.md view on Meta::CPAN
use App::karr::Task;
my $content = <<'MD';
---
id: 42
title: Test from_string
status: todo
priority: high
class: standard
created: 2026-03-19T10:00:00Z
updated: 2026-03-19T10:00:00Z
---
This is the body.
MD
# Test from_string
my $task = App::karr::Task->from_string($content);
is $task->id, 42, 'from_string: id';
is $task->title, 'Test from_string', 'from_string: title';
is $task->status, 'todo', 'from_string: status';
docs/superpowers/plans/2026-03-22-docker-runtime-images.md view on Meta::CPAN
- Modify: `README.md`
- Modify: `lib/App/karr.pm`
- Modify: `share/claude-skill.md`
- [ ] **Step 1: Update build hooks so local builds create both `latest` and `user` tags from the correct targets**
- [ ] **Step 2: Update release hooks so published images include the new `user` tag without introducing a UID-tag matrix**
- [ ] **Step 3: Document the recommended Docker alias for `latest` and explain when to use `:user`**
- [ ] **Step 4: Mention the dynamic ownership behavior in the main POD and keep Docker guidance aligned with the README**
- [ ] **Step 5: Refresh the bundled skill text if Docker usage guidance is referenced there**
### Task 4: Verify, install the updated skill, and commit
**Files:**
- Modify: repository working tree as needed
- [ ] **Step 1: Run `prove -l t`**
- [ ] **Step 2: Run `podchecker lib/App/karr.pm lib/App/karr/Cmd/*.pm lib/App/karr/*.pm lib/App/karr/Role/*.pm`**
- [ ] **Step 3: Run `perl -c` across the modified Perl files**
- [ ] **Step 4: Build both Docker targets locally and smoke-test ownership behavior where practical**
- [ ] **Step 5: Reinstall the Codex skill from `share/claude-skill.md` and create a commit with Codex trailers**
docs/superpowers/specs/2026-03-19-v0004-release-design.md view on Meta::CPAN
# Agent B picks next available (task 3 is now claimed):
karr pick --claim agent-b --move in-progress --json
# â picks task 7 (next highest, task 3 locked by agent-a)
# Agent C picks:
karr pick --claim agent-c --move in-progress --json
# â picks task 12
# Agent A finishes, hands off:
karr handoff 3 --claim agent-a --note "done" -t
# Sync pushes â agents B and C see updated board on next sync
# Agent B checks what's blocked:
karr list --status review --json
```
**Integration hint:** Use `karr pick --claim $NAME --tags backend` to let agents specialize by domain. Tag tasks with `backend`, `frontend`, `docs` etc.
### Scenario 3: Cross-Repo Agent Coordination
Human manages GitHub Issues. Bridge agent transfers selected issues to karr. Worker agents pick and execute.
docs/superpowers/specs/2026-03-22-helper-refs-design.md view on Meta::CPAN
### `karr set-refs <ref> <content...>`
- Accepts either a bare ref suffix such as `superpowers/spec/1234.md` or a full
ref name such as `refs/superpowers/spec/1234.md`.
- Normalizes bare values to `refs/...`.
- Joins all remaining positional arguments into the stored payload separated by
single spaces.
- Writes status information to `stderr`.
- Writes nothing to `stdout` on success.
- Pushes exactly the updated ref to the configured remote.
### `karr get-refs <ref>`
- Accepts the same normalized ref syntax as `set-refs`.
- Fetches exactly the requested ref from the remote before reading.
- Writes status information to `stderr`.
- Writes only the ref payload to `stdout`.
## Validation
docs/superpowers/specs/2026-03-22-ref-first-board-design.md view on Meta::CPAN
### Write commands
Commands such as `create`, `edit`, `move`, `archive`, `delete`, `pick`,
`handoff`, and `config set` should follow this sequence:
1. fetch current refs
2. load canonical state from refs
3. apply the requested mutation
4. write only the affected refs
5. push updated refs
There should be no persistent repo-local board cache.
## Temporary materialization
The pragmatic implementation path is to keep task/config parsing formats but
move any file materialization into a temp area created per command execution.
That gives us:
docs/superpowers/specs/2026-05-15-role-boardaccess-split-design.md view on Meta::CPAN
- Create `lib/App/karr/SyncGuard.pm`
- Modify `lib/App/karr/Role/BoardAccess.pm` â may be removed or kept as empty alias for backward compat
- Modify all 20+ command modules to compose `Role::BoardDiscovery` + `Role::SyncLifecycle`
- Modify `App::karr` (main app) to compose roles
- Update `.gitignore` to ensure `tasks/` is ignored
- Update `AGENTS.md` and `CLAUDE.md` to reflect new architecture
## Risks
- `sync_after` called twice if command calls it explicitly AND guard DESTROY runs â mitigated by `_done` flag
- Commands that currently read `.md` files directly need to be updated to use `$store->load_tasks()`
lib/App/karr/Cmd/List.pm view on Meta::CPAN
option claimed_by => (
is => 'ro',
format => 's',
doc => 'Filter by claim owner',
);
option sort => (
is => 'ro',
format => 's',
default => sub { 'id' },
doc => 'Sort by: id, status, priority, created, updated, due',
);
option reverse => (
is => 'ro',
short => 'r',
doc => 'Reverse sort order',
);
sub execute {
my ($self, $args_ref, $chain_ref) = @_;
lib/App/karr/Cmd/List.pm view on Meta::CPAN
=item * C<--assignee>, C<--tag>, C<--claimed-by>
Limit the result set to a specific assignee, tag, or claim owner.
=item * C<-s>, C<--search>
Performs a case-insensitive substring search across title, body, and tags.
=item * C<--sort>, C<--reverse>
Sort by C<id>, C<status>, C<priority>, C<created>, C<updated>, or C<due>, and
optionally reverse the result order.
=back
=head1 SEE ALSO
L<karr>, L<App::karr>, L<App::karr::Cmd::Show>, L<App::karr::Cmd::Board>,
L<App::karr::Cmd::Create>, L<App::karr::Cmd::Pick>
=head1 SUPPORT
lib/App/karr/Cmd/Show.pm view on Meta::CPAN
printf "Status: %s\n", $task->status;
printf "Priority: %s\n", $task->priority;
printf "Class: %s\n", $task->class;
printf "Assignee: %s\n", $task->assignee if $task->has_assignee;
printf "Tags: %s\n", join(', ', @{$task->tags}) if @{$task->tags};
printf "Due: %s\n", $task->due if $task->has_due;
printf "Estimate: %s\n", $task->estimate if $task->has_estimate;
printf "Claimed: %s\n", $task->claimed_by if $task->has_claimed_by;
printf "Blocked: %s\n", $task->blocked if $task->has_blocked;
printf "Created: %s\n", $task->created;
printf "Updated: %s\n", $task->updated;
if ($task->body) {
print "\n" . $task->body . "\n";
}
}
# Tasks sorted most-recently-updated first.
sub _by_updated {
my ($self, @tasks) = @_;
return sort { ($b->updated // '') cmp ($a->updated // '') } @tasks;
}
# Task ids the current identity most recently acted on, newest first, deduped.
sub _my_recent_ids {
my ($self, $limit) = @_;
my @ids;
my %seen;
for my $entry (reverse $self->activity_log->entries) {
my $tid = $entry->{task_id};
next unless defined $tid;
lib/App/karr/Cmd/Show.pm view on Meta::CPAN
my $limit = $self->last > 0 ? $self->last : 1;
if ($self->me) {
my @tasks = grep { defined } map { $self->find_task($_) } $self->_my_recent_ids($limit);
return @tasks;
}
if (defined $self->agent) {
my @claimed = grep { $_->has_claimed_by && $_->claimed_by eq $self->agent } $self->load_tasks;
my @sorted = $self->_by_updated(@claimed);
return splice(@sorted, 0, $limit);
}
my @sorted = $self->_by_updated($self->load_tasks);
return splice(@sorted, 0, $limit);
}
sub execute {
my ($self, $args_ref, $chain_ref) = @_;
my @tasks = $self->_select_tasks($args_ref->[0]);
unless (@tasks) {
print "No tasks found.\n" unless $self->json;
lib/App/karr/Cmd/Show.pm view on Meta::CPAN
App::karr::Cmd::Show - Show full details of a task
=head1 VERSION
version 0.303
=head1 SYNOPSIS
karr show 12 # a specific task
karr show # the most recently updated task
karr show --last 5 # the 5 most recently updated tasks
karr show --me # the last task my identity acted on
karr show --agent fox-owl # the last task claimed by that agent
karr show 12 --json
=head1 DESCRIPTION
Shows the full details of a task, including optional metadata such as tags, due
date, estimate, claim state, and the Markdown body. This is the most complete
human-readable view of an individual card.
With no C<ID>, shows the most recently updated task. C<--last N> widens that to
the C<N> most recently updated. C<--me> instead resolves the task(s) the
current identity most recently acted on (via the activity log). C<--agent NAME>
shows the task(s) most recently claimed by that agent name. C<ID> always wins
over the selector options.
=head1 SEE ALSO
L<karr>, L<App::karr>, L<App::karr::Cmd::List>, L<App::karr::Cmd::Edit>,
L<App::karr::Cmd::Move>, L<App::karr::Cmd::Archive>, L<App::karr::Cmd::Log>
=head1 SUPPORT
lib/App/karr/Cmd/Skill.pm view on Meta::CPAN
printf "%-12s not installed (run 'karr skill install' first)\n", $agent unless $self->json;
next;
}
my $installed = $file->slurp_utf8;
if ($installed eq $content) {
push @results, { agent => $agent, status => 'current' };
printf "%-12s already current\n", $agent unless $self->json;
} else {
$file->spew_utf8($content);
push @results, { agent => $agent, status => 'updated' };
printf "%-12s updated\n", $agent unless $self->json;
}
}
if ($self->json) {
$self->print_json(\@results);
}
}
sub _target_agents {
my ($self) = @_;
lib/App/karr/Foundation.pm view on Meta::CPAN
# (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 ) {
lib/App/karr/Foundation.pm view on Meta::CPAN
# 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,
lib/App/karr/Task.pm view on Meta::CPAN
has priority => ( is => 'rw', default => sub { 'medium' } );
has assignee => ( is => 'rw', predicate => 1, clearer => 1 );
has tags => ( is => 'rw', default => sub { [] } );
has due => ( is => 'rw', predicate => 1, clearer => 1 );
has estimate => ( is => 'rw', predicate => 1, clearer => 1 );
has class => ( is => 'rw', default => sub { 'standard' } );
has parent => ( is => 'rw', predicate => 1, clearer => 1 );
has depends_on => ( is => 'rw', default => sub { [] } );
has body => ( is => 'rw', default => sub { '' } );
has created => ( is => 'ro', default => sub { gmtime->datetime . 'Z' } );
has updated => ( is => 'rw', default => sub { gmtime->datetime . 'Z' } );
has claimed_by => ( is => 'rw', predicate => 1, clearer => 1 );
has claimed_at => ( is => 'rw', predicate => 1, clearer => 1 );
has blocked => ( is => 'rw', predicate => 1, clearer => 1 );
has started => ( is => 'rw', predicate => 1, clearer => 1 );
has completed => ( is => 'rw', predicate => 1, clearer => 1 );
has file_path => ( is => 'rw', predicate => 1 );
# Optional fields are addressed through their predicate everywhere (pick,
# board, list, show, handoff all treat has_X as "is this set"). Clearing one
# by assigning undef would leave the predicate true, so callers must use the
lib/App/karr/Task.pm view on Meta::CPAN
}
sub to_frontmatter {
my ($self) = @_;
my %fm = (
id => $self->id,
title => $self->title,
status => $self->status,
priority => $self->priority,
created => $self->created,
updated => $self->updated,
class => $self->class,
);
$fm{assignee} = $self->assignee if $self->has_assignee;
$fm{tags} = $self->tags if @{$self->tags};
$fm{due} = $self->due if $self->has_due;
$fm{estimate} = $self->estimate if $self->has_estimate;
$fm{parent} = $self->parent if $self->has_parent;
$fm{depends_on} = $self->depends_on if @{$self->depends_on};
$fm{claimed_by} = $self->claimed_by if $self->has_claimed_by;
$fm{claimed_at} = $self->claimed_at if $self->has_claimed_at;
lib/App/karr/Task.pm view on Meta::CPAN
sub from_file {
my ($class, $file) = @_;
$file = path($file);
my ($fm, $body) = $class->_parse_content($file->slurp_utf8);
return $class->new(%$fm, body => $body, file_path => $file);
}
sub save {
my ($self, $dir) = @_;
$self->updated(gmtime->datetime . 'Z');
my $file = $dir ? path($dir)->child($self->filename) : path($self->file_path);
$file->spew_utf8($self->to_markdown);
$self->file_path($file);
return $file;
}
1;
__END__
share/claude-skill.md view on Meta::CPAN
## Stored task format
```markdown
---
id: 1
title: Set up CI pipeline
status: backlog
priority: high
class: standard
created: 2026-03-12T10:00:00Z
updated: 2026-03-12T10:00:00Z
tags:
- devops
---
Optional body with more detail.
```
Tasks are stored under `refs/karr/tasks/*/data`. During command execution `karr`
materializes the same Markdown shape into a temporary task directory, so this
format still matters when reading or generating tasks programmatically.
t/06-config-cmd.t view on Meta::CPAN
my $file = path($dir)->child('config.yml');
DumpFile($file->stringify, App::karr::Config->default_config);
my $config = App::karr::Config->new(file => $file);
is $config->claim_timeout, '1h', 'default timeout';
$config->data->{claim_timeout} = '30m';
$config->save;
my $config2 = App::karr::Config->new(file => $file);
is $config2->claim_timeout, '30m', 'timeout updated';
};
# Regression: `karr config show` crashed with
# Can't locate object method "board_dir" via package "App::karr::Cmd::Config"
# because the command read its config from $self->board_dir->child('config.yml')
# instead of the ref-first store. These subtests drive the real command via a
# mock store so `show`/`get`/`set` no longer reference the dead board_dir method.
subtest 'config show runs through the command (no board_dir)' => sub {
my $cmd = App::karr::Cmd::Config->new( store => MockStore->new );
my $out;
t/07-context.t view on Meta::CPAN
$file->spew_utf8("# My Agents\n\n" . $context);
# Update with new context
my $new_context = "<!-- BEGIN kanban-md context -->\n## Board: Updated\n<!-- END kanban-md context -->\n";
my $content = $file->slurp_utf8;
$content =~ s/<!-- BEGIN kanban-md context -->.*<!-- END kanban-md context -->\n?/$new_context/s;
$file->spew_utf8($content);
my $result = $file->slurp_utf8;
like $result, qr/# My Agents/, 'preserves existing content';
like $result, qr/Board: Updated/, 'updated context';
unlike $result, qr/Board: Test/, 'old context replaced';
};
# Regression: karr context crashed with
# Can't locate object method "strftime" via package "Sun May 24 ..."
# because Cmd::Context never did `use Time::Piece`, so gmtime returned a
# plain string in the overdue / recently-completed / _count_overdue paths.
# These subtests drive the real command through every gmtime->strftime branch.
subtest 'context command runs without strftime crash' => sub {
my $today = gmtime->strftime('%Y-%m-%d');
t/14-task-parse.t view on Meta::CPAN
use App::karr::Task;
my $content = <<'MD';
---
id: 42
title: Test from_string
status: todo
priority: high
class: standard
created: 2026-03-19T10:00:00Z
updated: 2026-03-19T10:00:00Z
---
This is the body.
MD
# Test from_string
my $task = App::karr::Task->from_string($content);
is $task->id, 42, 'from_string: id';
is $task->title, 'Test from_string', 'from_string: title';
is $task->status, 'todo', 'from_string: status';
t/14-task-parse.t view on Meta::CPAN
# Test content without body
my $no_body = <<'MD';
---
id: 99
title: No body task
status: backlog
priority: low
class: standard
created: 2026-03-19T10:00:00Z
updated: 2026-03-19T10:00:00Z
---
MD
my $nb = App::karr::Task->from_string($no_body);
is $nb->id, 99, 'no-body task: id';
is $nb->body, '', 'no-body task: empty body';
done_testing;
t/24-ref-first-board-access.t view on Meta::CPAN
# Write a task directly to refs
$git->write_ref( 'refs/karr/config', Dump({ version => 1 }) );
$git->write_ref( 'refs/karr/meta/next-id', "1\n" );
my $task_yaml = Dump({
id => 1,
title => 'Test task',
status => 'backlog',
priority => 'high',
class => 'standard',
created => '2026-05-15T10:00:00Z',
updated => '2026-05-15T10:00:00Z',
});
$git->write_ref( 'refs/karr/tasks/1/data', "---\n$task_yaml\n---\n\nTest body" );
my $board = TestBoard->new( dir => $repo );
my @tasks = $board->load_tasks;
is( scalar @tasks, 1, 'load_tasks returns one task' );
is( $tasks[0]->id, 1, 'task id is correct' );
is( $tasks[0]->title, 'Test task', 'task title is correct' );
t/30-foundation.t view on Meta::CPAN
my $dir = tempdir( CLEANUP => 1 );
my $f = new_foundation();
is $f->_state_get( $dir, 'hash' ), undef, 'undef before any state written';
$f->_state_set( $dir, hash => 'abc123', last_exit => 0 );
is $f->_state_get( $dir, 'hash' ), 'abc123', 'hash persisted';
is $f->_state_get( $dir, 'last_exit' ), 0, 'last_exit persisted';
$f->_state_set( $dir, hash => 'def456' );
is $f->_state_get( $dir, 'hash' ), 'def456', 'hash updated';
is $f->_state_get( $dir, 'last_exit' ), 0, 'last_exit preserved on partial update';
};
# ---------------------------------------------------------------------------
# .karr file parsing
# ---------------------------------------------------------------------------
subtest '_load_karr: missing file â empty hash' => sub {
my $dir = tempdir( CLEANUP => 1 );
my $f = new_foundation();
t/31-foundation-drain.t view on Meta::CPAN
is $f->_match_error( "something kaboom", $cp ), 'kaboom', 'custom pattern matches';
};
# ---------------------------------------------------------------------------
# Unit: stuck-task detection
# ---------------------------------------------------------------------------
subtest '_stuck_tasks' => sub {
my $f = App::karr::Foundation->new;
my $before = {
1 => { status => 'in-progress', claimed_by => 'a', updated => 'T1' },
2 => { status => 'todo', claimed_by => undef, updated => 'T1' },
3 => { status => 'in-progress', claimed_by => 'a', updated => 'T1' },
};
my $after = {
1 => { status => 'in-progress', claimed_by => 'a', updated => 'T1' }, # unchanged -> stuck
2 => { status => 'todo', claimed_by => undef, updated => 'T1' }, # not claimed -> ignore
3 => { status => 'done', claimed_by => 'a', updated => 'T2' }, # advanced -> not stuck
};
is_deeply [ $f->_stuck_tasks( $before, $after ) ], [1], 'only the unchanged claimed task is stuck';
# blocked task is never stuck
my $b2 = { 1 => { status => 'in-progress', claimed_by => 'a', updated => 'T1' } };
my $a2 = { 1 => { status => 'in-progress', claimed_by => 'a', updated => 'T1', blocked => 1 } };
is_deeply [ $f->_stuck_tasks( $b2, $a2 ) ], [], 'blocked task drops out';
};
# ---------------------------------------------------------------------------
# Unit: attempts counter
# ---------------------------------------------------------------------------
subtest 'attempts counter' => sub {
my $repo = tempdir( CLEANUP => 1 );
my $f = App::karr::Foundation->new;
t/32-show.t view on Meta::CPAN
system( 'git', '-C', $repo, 'config', 'user.name', 'Test User' );
return $repo;
}
my $repo = _init_repo();
my $git = App::karr::Git->new( dir => $repo );
$git->write_ref( 'refs/karr/config', Dump( { version => 1, board => { name => 'T' } } ) );
$git->write_ref( 'refs/karr/meta/next-id', "9\n" );
my $store = App::karr::BoardStore->new( git => $git );
# Three tasks, deterministic 'updated' so recency ordering is stable.
my %updated = (
1 => '2026-01-01T00:00:00Z',
2 => '2026-03-01T00:00:00Z',
3 => '2026-02-01T00:00:00Z',
);
for my $id ( 1, 2, 3 ) {
my $t = App::karr::Task->new(
id => $id,
title => "Task $id",
status => 'backlog',
priority => 'medium',
class => 'standard',
);
$t->updated( $updated{$id} );
$t->claimed_by('fox-owl') if $id == 3;
$store->save_task($t);
}
my $ids = sub { map { $_->id } @_ };
subtest 'explicit id wins over selectors' => sub {
my $cmd = App::karr::Cmd::Show->new( store => $store, me => 1, last => 5 );
my @t = $cmd->_select_tasks(1);
is_deeply [ $ids->(@t) ], [1], 'returns exactly the requested task';
};
subtest 'no id, default last=1 -> most recently updated' => sub {
my $cmd = App::karr::Cmd::Show->new( store => $store );
my @t = $cmd->_select_tasks(undef);
is_deeply [ $ids->(@t) ], [2], 'task 2 is newest by updated';
};
subtest '--last 2 -> two newest, descending' => sub {
my $cmd = App::karr::Cmd::Show->new( store => $store, last => 2 );
my @t = $cmd->_select_tasks(undef);
is_deeply [ $ids->(@t) ], [ 2, 3 ], 'two newest in updated-desc order';
};
subtest '--agent filters by claimed_by' => sub {
my $cmd = App::karr::Cmd::Show->new( store => $store, agent => 'fox-owl', last => 5 );
my @t = $cmd->_select_tasks(undef);
is_deeply [ $ids->(@t) ], [3], 'only the task claimed by fox-owl';
};
subtest '--me resolves via the activity log identity' => sub {
# Two entries for this identity; task 1 is the most recent action.
t/36-task-clear.t view on Meta::CPAN
subtest 'an explicit null in a loaded file is normalized to unset' => sub {
# Older karr writes and external kanban-md edits may carry explicit nulls.
my $legacy = App::karr::Task->from_string(<<'MD');
---
id: 3
title: legacy null
status: todo
priority: medium
class: standard
created: 2026-03-19T10:00:00Z
updated: 2026-03-19T10:00:00Z
claimed_by: ~
claimed_at: ~
blocked: ~
---
MD
ok !$legacy->has_claimed_by, 'null claimed_by loads as unset';
ok !$legacy->has_claimed_at, 'null claimed_at loads as unset';
ok !$legacy->has_blocked, 'null blocked loads as unset';
};