App-karr
view release on metacpan or search on metacpan
.claude/skills/karr-foundation-cli/SKILL.md view on Meta::CPAN
---
name: karr-foundation-cli
description: Use when running karr-foundation â periodic agent execution across several karr boards, drain loops, ticket mode, named agents, the coordination agent and its assignment, the hub chain and question mailbox, auto-block logic.
---
# karr-foundation â Periodic Agent Executor for karr Boards
Single-shot daemon that monitors multiple karr boards and runs an agent command
when work is available. Designed for cron/systemd-timer invocation.
## Quick start
```bash
# Config at ~/.config/karr-foundation/config.yml
dirs:
- /path/to/repo1
- /path/to/repo2
scan:
- /path/to/parent-dir # finds dirs with .karr file
bin/karr-foundation view on Meta::CPAN
karr-foundation answer 7 darkpan
# Write the fleet's plan into the hub, and execute it out of there
karr-foundation plan < chain.yml
karr-foundation chain
=head1 DESCRIPTION
F<karr-foundation> reads a config file listing karr board directories, detects
board changes or open tasks, and invokes a configured agent command for each
active repo. It is designed to be called repeatedly -- by cron, a systemd timer,
or a tight while-loop.
See L<App::karr::Foundation> for full documentation.
=head1 EXIT CODES
Cron only ever asked whether the tick failed, but the mailbox commands are
typed by a person and scripted by agents, so F<karr-foundation> keeps the same
exit-code contract as F<karr> (see F<docs/adr/0002-exit-code-contract.md>):
ex/README.md view on Meta::CPAN
---
## 4. `karr-foundation` â the coordinator
A board is half the battle. `karr-foundation` is the other half and the reason
why `karr` isn't just a file-based kanban.
It's a single-shot, idempotent companion binary. It watches **many**
repositories, decides per board whether there's work there, and lets the
configured agent command run until the board stops moving. Cron, a systemd
timer or a `while` loop point at it; every tick is complete in itself.
And it has two operating modes, each useful on its own:
| If you want | you call | you get |
|---|---|---|
| a picture of every board | `karr-foundation --status` | status counters, in-progress/blocked cards, lock, cooldown, agent state, open questions â read-only, an agent is never started |
| agents to work the boards | `karr-foundation` | one agent per repository, per the `.karr` file in it. If there is no `.karr` anywhere, it prints the overview instead |
**Agent execution is opt-in.** That isn't reticence but the security design: a
default that suddenly starts four agents on an operator's laptop would be a
lib/App/karr/Foundation.pm view on Meta::CPAN
# Read-only overview of every board (no agent runs)
karr-foundation --status
# Write the fleet's plan into the hub, and execute it out of there
karr-foundation plan < chain.yml
karr-foundation chain
=head1 DESCRIPTION
F<karr-foundation> is a single-shot, idempotent CLI meant to be invoked
periodically (cron, systemd-timer, while-loop). It scans configured karr
boards, detects changes or open work, and B<drains> each board by invoking the
configured agent command repeatedly until no actionable task remains.
B<Using this class as a library.> F<bin/karr-foundation> is what most callers
run, and it is also where karr's character/octet boundary gets set up (see
L<App::karr::Encoding>) before any command code runs: a C<:encoding(UTF-8)>
layer goes on C<STDOUT>/C<STDERR>, and C<@ARGV> is decoded before
C<new_with_options> reads it into option values. This class does not repeat
either step -- both are the program's decision, not one a class it merely
loads should make for it (see L<App::karr::Encoding/enable_std_utf8> and
lib/App/karr/Foundation.pm view on Meta::CPAN
exit App::karr::Foundation->new_with_options->run(@ARGV);
The single entry point, invoked by F<bin/karr-foundation>. What is left of
C<@ARGV> after option parsing is a hub command and is answered first: C<ask> and
C<answer> (see "The question mailbox" above) work on the hub alone, discover no
board and start no agent, and C<chain> (see "Running the chain" above) executes
the fleet's plan through L<App::karr::Foundation::Executor>. An argument that is
none of the three is a user error rather than a silent drain. With no arguments -- how cron invokes it --
it is one pass over every configured repo, then returns -- there is no internal loop; running
periodically is left to cron/systemd-timer/an external C<while> loop, per
L</DESCRIPTION>. Returns C<1> (a process exit code, not an exception) when
C<_discover_repos> finds nothing at all -- an empty C<dirs>/C<scan> in the
config, or a config file that does not exist -- and C<0> otherwise, including
when individual repos error out: a repo whose C<_process_repo> dies is
C<warn>ed and skipped, never propagated, so one broken board cannot stop the
rest of the run.
With C<--status> it prints L<App::karr::Foundation::Overview>'s read-only
overview and returns without touching any board. Without it, C<run> first
checks whether B<any> repo has an agent configured at all (per repo,
lib/App/karr/Foundation/Runner.pm view on Meta::CPAN
open( STDOUT, '>&', $writer ) or die "dup stdout: $!";
open( STDERR, '>&STDOUT' ) or die "dup stderr: $!";
# The agent becomes its own process group leader so the runner can signal
# the whole tree (the agent, its forked grandchildren, anything it
# backgrounded) without reaching the runner itself (#148). Before this the
# timeout SIGTERM hit only the shell â `sleep 300 & wait`, a pipeline, any
# command the agent backgrounded, all survived the kill because they were
# children of /bin/sh, not of the runner. setpgrp(0,0) puts the child in a
# group whose pgid is its own pid; the parent signals that group with
# kill 'TERM', -$pid. SIGALRM is also reset to default in the child â the
# timeout timer is the runner's, not the agent's.
setpgid( 0, 0 ) if defined &setpgid;
POSIX::setsid() if !defined &setpgid; # fall back if POSIX::setpgid isn't there
$SIG{ALRM} = 'DEFAULT';
exec( '/bin/sh', '-c', $command ) or die "exec: $!";
}
# parent. From here to the waitpid below there is a running agent, so nothing
# in between may die: no croaking call, and no unguarded call into the
# foundation (its _append_log throws when the log file is gone). Keep it that
# way â the tee loop below reports its errors by ending, not by dying.
lib/App/karr/Foundation/Runner.pm view on Meta::CPAN
my $timed_out = 0;
my $sel = IO::Select->new($reader);
# Deadline arming: the deadline must fire regardless of IO activity, because
# an agent that closes its stdout/stderr while still running ends the read
# loop on EOF with $timed_out still 0, and the runner falls into a bare
# blocking waitpid that holds .karr.lock forever (#161). SIGALRM with a
# handler that sets $timed_out keeps the deadline independent of the read
# loop: the alarm fires at the deadline, the handler arms the flag, the
# next loop iteration sees it and ends the loop. arm_alarm() also re-arms on
# each can_read wakeup so a long-running command never gets a stale timer
# from a prior iteration â every iteration arms for "remaining from now",
# which is what the user expects max_runtime to mean.
my $alarm_target;
if ( $max_runtime > 0 ) {
$alarm_target = $started + $max_runtime;
$SIG{ALRM} = sub {
$timed_out = 1;
# Closing the read end of the pipe unblocks can_read with no data so
# the loop wakes immediately rather than waiting for the alarm delivery
# to reach it through sysread's EINTR. Cheap and signal-safe.
( run in 4.253 seconds using v1.01-cache-2.11-cpan-ad66724bd6a )