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 1.833 second using v1.01-cache-2.11-cpan-ad66724bd6a )