App-karr
view release on metacpan or search on metacpan
lib/App/karr/Cmd/Context.pm view on Meta::CPAN
# ABSTRACT: Generate board context summary for embedding
package App::karr::Cmd::Context;
our $VERSION = '0.302';
use Moo;
use MooX::Cmd;
use MooX::Options (
usage_string => 'USAGE: karr context [--write-to FILE] [--sections LIST] [--days N] [--json]',
);
use Time::Piece;
use App::karr::Role::BoardAccess;
use App::karr::Role::Output;
use App::karr::Task;
use App::karr::Config;
with 'App::karr::Role::BoardAccess', 'App::karr::Role::Output';
option write_to => (
is => 'ro',
format => 's',
doc => 'Write context to file (create or update)',
);
option sections => (
is => 'ro',
format => 's',
doc => 'Comma-separated section filter (in-progress,blocked,overdue,recently-completed)',
);
option days => (
is => 'ro',
format => 'i',
default => sub { 7 },
doc => 'Lookback days for recently-completed (default: 7)',
);
sub execute {
my ($self, $args_ref, $chain_ref) = @_;
my $ec = $self->store->effective_config;
my @tasks = $self->load_tasks;
my @statuses = $self->store->all_status_names;
# Determine terminal and first statuses
my $first_status = $statuses[0];
# Exclude archived from all operations
my @active_tasks = grep { !$self->store->is_terminal_status($_->status) } @tasks;
# Build summary
my $board_name = $ec->{board}{name} // 'Kanban Board';
my $total = scalar @active_tasks;
my $active = grep { $_->status ne $first_status && !$self->store->is_terminal_status($_->status) } @active_tasks;
my $blocked = grep { $_->has_blocked } @active_tasks;
my $overdue = $self->_count_overdue(\@active_tasks);
# Build sections
my %wanted_sections;
if ($self->sections) {
%wanted_sections = map { $_ => 1 } split /,/, $self->sections;
}
my @section_data;
my @all_sections = qw(in-progress blocked overdue recently-completed);
for my $sec (@all_sections) {
next if $self->sections && !$wanted_sections{$sec};
my @items;
if ($sec eq 'in-progress') {
@items = map { $self->_task_item($_) }
sort { $self->_pri_order($a) <=> $self->_pri_order($b) }
grep { $_->status ne $first_status && !$self->store->is_terminal_status($_->status) && !$_->has_blocked }
@active_tasks;
} elsif ($sec eq 'blocked') {
@items = map { $self->_task_item($_, 'blocked: ' . ($_->blocked // '')) }
grep { $_->has_blocked }
@active_tasks;
} elsif ($sec eq 'overdue') {
my $now = gmtime->strftime('%Y-%m-%d');
@items = map { $self->_task_item($_, 'due ' . $_->due) }
grep { $_->has_due && $_->due lt $now && !$self->store->is_terminal_status($_->status) }
@active_tasks;
} elsif ($sec eq 'recently-completed') {
my $cutoff = (gmtime() - ($self->days * 86400))->strftime('%Y-%m-%d');
@items = map { $self->_task_item($_, 'completed ' . ($_->completed // '')) }
sort { ($b->completed // '') cmp ($a->completed // '') }
grep { $self->store->is_terminal_status($_->status) && $_->status ne 'archived' && $_->has_completed && $_->completed ge $cutoff }
@active_tasks;
}
push @section_data, { name => $sec, items => \@items } if @items;
}
if ($self->json) {
my $out = {
board_name => $board_name,
summary => {
total_tasks => $total,
active => $active,
blocked => $blocked,
overdue => $overdue,
},
sections => \@section_data,
};
$self->print_json($out);
return;
}
# Render markdown
my $md = $self->_render_markdown($board_name, $total, $active, $blocked, $overdue, \@section_data);
if ($self->write_to) {
$self->_write_to_file($md);
} else {
print $md;
}
}
sub _render_markdown {
my ($self, $board_name, $total, $active, $blocked, $overdue, $sections) = @_;
my $md = "<!-- BEGIN kanban-md context -->\n";
$md .= "## Board: $board_name\n\n";
$md .= "**$total tasks** | $active active | $blocked blocked | $overdue overdue\n\n";
my %section_title = (
'in-progress' => 'In Progress',
'blocked' => 'Blocked',
'overdue' => 'Overdue',
'recently-completed' => 'Recently Completed',
);
for my $sec (@$sections) {
$md .= "### " . ($section_title{$sec->{name}} // $sec->{name}) . "\n\n";
for my $item (@{$sec->{items}}) {
$md .= sprintf "- **#%d** %s (%s", $item->{id}, $item->{title}, $item->{priority};
$md .= ", \@$item->{assignee}" if $item->{assignee};
$md .= ")";
$md .= " â $item->{note}" if $item->{note};
$md .= "\n";
}
$md .= "\n";
}
$md .= "<!-- END kanban-md context -->\n";
return $md;
}
sub _write_to_file {
my ($self, $md) = @_;
require Path::Tiny;
my $file = Path::Tiny::path($self->write_to);
if ($file->exists) {
my $content = $file->slurp_utf8;
if ($content =~ /<!-- BEGIN kanban-md context -->.*<!-- END kanban-md context -->/s) {
$content =~ s/<!-- BEGIN kanban-md context -->.*<!-- END kanban-md context -->\n?/$md/s;
$file->spew_utf8($content);
} else {
my $sep = $content =~ /\n$/ ? "\n" : "\n\n";
$file->spew_utf8($content . $sep . $md);
}
} else {
$file->spew_utf8($md);
}
printf "Context written to %s\n", $self->write_to;
}
sub _task_item {
my ($self, $task, $note) = @_;
return {
id => $task->id,
title => $task->title,
status => $task->status,
priority => $task->priority,
($task->has_assignee ? (assignee => $task->assignee) : ()),
($note ? (note => $note) : ()),
};
}
sub _pri_order {
my ($self, $task) = @_;
my %order = App::karr::Config->priority_order;
return $order{$task->priority} // 2;
}
sub _count_overdue {
my ($self, $tasks) = @_;
my $now = gmtime->strftime('%Y-%m-%d');
return scalar grep { $_->has_due && $_->due lt $now && !$self->store->is_terminal_status($_->status) } @$tasks;
}
sub _load_tasks {
my ($self) = @_;
return $self->load_tasks;
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
App::karr::Cmd::Context - Generate board context summary for embedding
=head1 VERSION
version 0.302
=head1 SYNOPSIS
karr context
karr context --sections blocked,overdue
karr context --write-to AGENTS.md --days 14
karr context --json
=head1 DESCRIPTION
Builds a concise board summary suitable for embedding into agent context files
such as F<AGENTS.md>. The command can print Markdown directly, emit structured
JSON, or update an existing file between sentinel comments.
=head1 SECTIONS
The generated context can include C<in-progress>, C<blocked>, C<overdue>, and
C<recently-completed>. Use C<--sections> with a comma-separated list to limit
the output to a subset.
=head1 FILE UPDATE MODE
When C<--write-to> is used, the command replaces the content between
C<BEGIN kanban-md context> and C<END kanban-md context> if those sentinels are
already present; otherwise it appends the generated block to the file.
=head1 SEE ALSO
L<karr>, L<App::karr>, L<App::karr::Cmd::Board>, L<App::karr::Cmd::List>,
L<App::karr::Cmd::Config>, L<App::karr::Cmd::Skill>
=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 0.647 second using v1.01-cache-2.11-cpan-2398b32b56e )