App-karr
view release on metacpan or search on metacpan
.claude/skills/karr-foundation-cli/SKILL.md view on Meta::CPAN
Place in repo root. All keys optional. Agent execution is opt-in: a board runs
an agent only if it has `command` **or** `claude: true`. With no agent on any
board, `karr-foundation` prints a read-only overview instead of running
anything (see "Overview").
```yaml
claude: true # synthesize the canonical claude command (opt-in)
claude_bin: claude # binary for claude: true (default: claude)
claude_max_turns: 30 # --max-turns for claude: true (default: 30)
claude_permission_mode: bypassPermissions # (default: bypassPermissions)
prompt: >- # agent instruction, exposed to the command as $PROMPT
Use the karr-coordinator skill: pick the next actionable task and move it.
# command: claude -p "$PROMPT" # explicit command; wins over claude: true
on_idle: skip # 'skip' (default) | 'always-run'
drain: true # loop until drained (default) | false for single run
max_runtime: 1800 # seconds: per-command SIGKILL (0 = no timeout)
max_attempts: 2 # stalls on one task before auto-block (default: 2)
max_iterations: 50 # hard cap on drain iterations / drain budget (default: 50)
cooldown_base: 1 # cooldown minutes at level 0 (default: 1)
cooldown_max: 64 # cooldown ceiling in minutes (default: 64)
error_patterns: # extra case-insensitive substrings â common-error
- my custom api error
```
`claude`, `claude_bin`, the `claude_*` knobs, `command` and `prompt` may also be
set globally in `config.yml` (`default_command` / `default_prompt`); the per-repo
`.karr` value wins.
## Overview
`karr-foundation --status` (and the default when no board has an agent) prints a
read-only dashboard of every board: status counts, in-progress/blocked tasks,
and lock/cooldown state. No agent is run â usable by a human to coordinate work.
## Options
.claude/skills/karr-foundation-cli/SKILL.md view on Meta::CPAN
.karr.log # run log
```
## Environment
During agent execution foundation sets:
- `KARR_REPO` â the repo path
- `KARR_ROLE=agent` â so nested `karr` calls log under the `agent` identity
(`refs/karr/log/agent/<email>`); a human defaults to `user`
- `PROMPT` â the resolved agent instruction (`prompt` / `default_prompt` /
built-in default), referenced as `$PROMPT` in the command template
## Cron example
```bash
# Every 5 minutes, all repos
*/5 * * * * karr-foundation
# With verbose logging to syslog
*/5 * * * * karr-foundation --verbose 2>&1 | logger -t karr-foundation
(max_runtime: 0 disables it entirely). Output is always appended to
.karr.log regardless of TTY.
- karr-foundation is now a multi-board coordinator, not just an agent
runner. Agent execution is opt-in: with no agent configured on any
board, the default action is a read-only overview of every board
(status counts, in-progress/blocked, lock/cooldown state). `--status`
forces that overview regardless of configuration.
- 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
lib/App/karr/Cmd/Delete.pm view on Meta::CPAN
Deletes one or more task files from the board. This is the destructive
alternative to L<App::karr::Cmd::Archive>, which only changes the status to
C<archived>.
=head1 OPTIONS
=over 4
=item * C<--yes>
Skips the interactive confirmation prompt for each task.
=back
=head1 SEE ALSO
L<karr>, L<App::karr>, L<App::karr::Cmd::Archive>,
L<App::karr::Cmd::Backup>, L<App::karr::Cmd::Destroy>
=head1 SUPPORT
lib/App/karr/Foundation.pm view on Meta::CPAN
# claude_permission_mode override the defaults (per-repo, then global).
sub _claude_command {
my ( $self, $karr ) = @_;
my $cfg = $self->_config_data;
my $bin = $karr->{claude_bin} // $cfg->{claude_bin} // 'claude';
my $turns = $karr->{claude_max_turns} // $cfg->{claude_max_turns} // 30;
my $perm = $karr->{claude_permission_mode} // $cfg->{claude_permission_mode} // 'bypassPermissions';
return qq{$bin -p "\$PROMPT" --permission-mode $perm --max-turns $turns};
}
# The agent instruction exposed as $PROMPT. .karr 'prompt' > config
# 'default_prompt' > the built-in default.
sub _prompt_for {
my ( $self, $karr ) = @_;
return $karr->{prompt}
// $self->_config_data->{default_prompt}
// $DEFAULT_PROMPT;
}
1;
__END__
=pod
=encoding UTF-8
lib/App/karr/Foundation.pm view on Meta::CPAN
scan:
- /path/to/parent-dir # finds all direct subdirs that have a .karr file
B<Per-repo .karr file:>
claude: true # synthesize the canonical claude command (opt-in)
claude_bin: claude # binary for claude: true (default: claude)
claude_max_turns: 30 # --max-turns for claude: true (default: 30)
claude_permission_mode: bypassPermissions # (default: bypassPermissions)
prompt: >- # agent instruction, exposed as $PROMPT
Use the karr-coordinator skill: pick the next actionable task and move it.
command: claude -p "$PROMPT" # explicit command; wins over claude: true
on_idle: skip # 'skip' (default) | 'always-run'
max_runtime: 1800 # seconds: per-command SIGKILL (0 = no limit)
drain: true # loop until drained (default) | false for single run
max_attempts: 2 # stalls on one task before auto-block (default: 2)
max_iterations: 50 # hard cap on drain iterations (default: 50)
cooldown_base: 1 # cooldown minutes at level 0 (default: 1)
cooldown_max: 64 # cooldown ceiling in minutes (default: 64)
error_patterns: # extra case-insensitive substrings â common-error
- my custom api error
C<claude>, C<claude_bin>, C<claude_max_turns>, C<claude_permission_mode>,
C<command> and C<prompt>/C<default_prompt> may also be set globally in the
config file; the per-repo F<.karr> value wins.
B<Coordinator and overview.> Agent execution is opt-in â a board runs an agent
only via C<command> or C<< claude: true >>. When B<no> board has an agent
configured, the default action is a read-only B<overview> of every board
(status counts, in-progress/blocked tasks, lock and cooldown state); a human
can use foundation purely to coordinate their own work. C<--status> forces the
overview regardless of configuration.
B<Live output.> When run interactively (TTY) or with C<--verbose>, the agent's
lib/App/karr/Foundation/Runner.pm view on Meta::CPAN
my ( $self, $repo, $karr, $cmd ) = @_;
my $command = $cmd // $karr->{command};
my $max_runtime = $karr->{max_runtime} // 1800;
my $stream_terms = $self->foundation->_stream_to_terminal;
# Environment for the child (and all karr calls it spawns). Set before the
# substitution so a command template â including the synthesized claude
# command â can reference $PROMPT, $KARR_REPO, etc.
local $ENV{KARR_REPO} = "$repo";
local $ENV{KARR_ROLE} = 'agent';
local $ENV{PROMPT} = $self->foundation->_prompt_for($karr);
# Env-var substitution in command string
$command =~ s/\$\{(\w+)\}/$ENV{$1} \/\/ ''/ge;
$command =~ s/\$(\w+)/$ENV{$1} \/\/ ''/ge;
$self->foundation->_append_log( $repo, "START command=$command" );
$self->foundation->_say_verbose("exec in $repo: $command");
if ( $self->foundation->dry_run ) {
$self->foundation->_append_log( $repo, "DRY-RUN (skipped)" );
lib/App/karr/Foundation/Runner.pm view on Meta::CPAN
=head1 DESCRIPTION
L<App::karr::Foundation::Runner> runs a single agent command for
L<App::karr::Foundation>. It forks the command under C</bin/sh -c>, reads its
combined stdout/stderr over a native pipe, and tees each chunk to the
persistent C<.karr.log>, the terminal (when streaming), and an in-memory buffer
used for error scanning, enforcing the per-run C<max_runtime> timeout. It also
classifies observable common errors (rate limit, auth, network, 5xx, ...). A
weak back-reference to the owning foundation supplies shared options and helpers
(C<dry_run>, C<_stream_to_terminal>, C<_prompt_for>, C<_append_log>,
C<_say_verbose>).
=head1 SUPPORT
=head2 Issues
Please report bugs and feature requests on GitHub at
L<https://github.com/Getty/karr/issues>.
=head2 IRC
t/33-foundation-run.t view on Meta::CPAN
my ( $code, $out ) = $f->_run_command( $repo, { max_runtime => 0 }, 'echo nolimit' );
is $code, 0, 'runs to completion with no deadline';
like $out, qr/nolimit/, 'output captured under undef-timeout path';
};
subtest 'timeout sends SIGKILL and reports exit -1' => sub {
my $repo = path( tempdir( CLEANUP => 1 ) );
my $start = time;
my ( $code, $out ) = $f->_run_command( $repo, { max_runtime => 1 }, 'sleep 30' );
is $code, -1, 'timed-out run reports exit -1';
ok time - $start < 10, 'killed promptly, not after the full sleep';
};
done_testing;
t/34-foundation-command.t view on Meta::CPAN
my $fc = App::karr::Foundation->new( command => 'cli', _config_data => { default_command => 'gd' } );
is $fc->_agent_command( $repo, { command => 'foo' } ), 'cli',
'CLI --command wins over everything';
my $fg = App::karr::Foundation->new( _config_data => { claude => 1 } );
like $fg->_agent_command( $repo, {} ), qr/^claude -p "\$PROMPT"/,
'global claude: true applies when no per-repo agent';
};
subtest '_prompt_for precedence' => sub {
my $f = App::karr::Foundation->new( _config_data => { default_prompt => 'CFG' } );
is $f->_prompt_for( { prompt => 'KARR' } ), 'KARR', '.karr prompt wins';
is $f->_prompt_for( {} ), 'CFG', 'config default_prompt next';
my $f2 = App::karr::Foundation->new( _config_data => {} );
like $f2->_prompt_for( {} ), qr/karr-coordinator skill/, 'built-in default last';
};
subtest '$PROMPT is substituted into the command at run time' => sub {
my $rdir = path( tempdir( CLEANUP => 1 ) );
my $f = App::karr::Foundation->new( _config_data => {} );
my ( $code, $out ) = $f->_run_command( $rdir, {}, 'printf "%s" "$PROMPT"' );
like $out, qr/karr-coordinator skill/, 'default prompt reaches $PROMPT';
my ( $c2, $o2 ) = $f->_run_command( $rdir, { prompt => 'CUSTOM-PROMPT' }, 'printf "%s" "$PROMPT"' );
is $o2, 'CUSTOM-PROMPT', 'per-repo prompt overrides';
};
subtest '_print_overview renders a board summary' => sub {
my $rdir = path( tempdir( CLEANUP => 1 ) );
system( 'git', 'init', '-q', "$rdir" );
system( 'git', '-C', "$rdir", 'config', 'user.email', 'o@test.com' );
system( 'git', '-C', "$rdir", 'config', 'user.name', 'O' );
my $git = App::karr::Git->new( dir => "$rdir" );
$git->write_ref( 'refs/karr/config', Dump( { version => 1 } ) );
$git->write_ref( 'refs/karr/meta/next-id', "3\n" );
( run in 0.656 second using v1.01-cache-2.11-cpan-0b5f733616e )