App-karr
view release on metacpan or search on metacpan
lib/App/karr/Foundation.pm view on Meta::CPAN
return if $self->dry_run;
$repo->child('.karr.log')->append_utf8( $line );
}
sub _say_verbose {
my ( $self, $msg ) = @_;
print "$msg\n" if $self->verbose;
}
# ---------------------------------------------------------------------------
# .karr file
# ---------------------------------------------------------------------------
sub _load_karr {
my ( $self, $repo ) = @_;
my $karr_file = $repo->child('.karr');
return {} unless $karr_file->exists;
my $data = try {
YAML::XS::LoadFile("$karr_file");
} catch {
warn "karr-foundation: cannot parse $karr_file: $_\n";
{};
};
return ref $data eq 'HASH' ? $data : {};
}
# ---------------------------------------------------------------------------
# Agent command resolution
# ---------------------------------------------------------------------------
# The resolved agent command string, or undef when no agent is configured.
# Priority: CLI --command > config default_command > .karr command >
# 'claude: true' shorthand (per-repo, then global).
sub _agent_command {
my ( $self, $repo, $karr ) = @_;
my $cfg = $self->_config_data;
for my $candidate ( $self->command, $cfg->{default_command}, $karr->{command} ) {
return $candidate if defined $candidate && length $candidate;
}
my $claude = exists $karr->{claude} ? $karr->{claude} : $cfg->{claude};
return $self->_claude_command($karr) if $claude;
return undef;
}
# Synthesize the canonical claude invocation behind 'claude: true'. The $PROMPT
# variable is substituted from $ENV{PROMPT} at run time (see _run_command), so
# users never retype the long flag set. claude_bin / claude_max_turns /
# 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
=head1 NAME
App::karr::Foundation - Single-shot foundation daemon â periodic agent execution across karr boards
=head1 VERSION
version 0.401
=head1 SYNOPSIS
# Typical cron entry â run every 5 minutes
*/5 * * * * /path/to/karr-foundation
# Force a run regardless of board state
karr-foundation --force
# Preview what would run
karr-foundation --dry-run --verbose
# Read-only overview of every board (no agent runs)
karr-foundation --status
=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<Config file:> C<~/.config/karr-foundation/config.yml> (or C<--config>).
dirs:
- /path/to/repo1
- /path/to/repo2
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
output is streamed to the terminal in real time as foundation reads it; it is
always appended to F<.karr.log> regardless of TTY. To shape what is shown, the
command may emit stream-json and filter it, e.g.:
command: >-
claude -p "$PROMPT"
--output-format stream-json --verbose --include-partial-messages
--permission-mode bypassPermissions --max-turns 10
2>&1 | jq -r 'select(.type == "stream_event") | .event.delta.text // empty'
Set C<max_runtime: 0> in F<.karr> to disable the per-run timeout entirely
(agent runs until completion with no SIGKILL).
B<Drain semantics.> Each iteration runs C<command> once, then classifies the
result from what foundation can observe â exit code, board ref movement, and
the run's captured output:
=over 4
=item * B<progress> â the board changed; keep draining.
=item * B<stall> â a task the agent claimed / left C<in-progress> did not move.
That task's attempt counter is bumped; at C<max_attempts> it is auto-blocked
(C<blocked: auto-block: no progress after N attempts (foundation)>) so it drops
out of the actionable set and the drain can finish. The agent may always set a
better reason itself with C<karr edit --block>; the auto-block is a fallback.
=item * B<common-error> â a non-zero/timeout exit or a C<error_patterns> match
(rate limit, auth, network, 5xx, â¦). No task is penalized; the repo enters an
exponential cooldown (C<cooldown_base> Ã 2^level minutes, capped at
C<cooldown_max>, reset on the next clean run) and is skipped until it expires.
=item * B<idle> â the agent did nothing and grabbed nothing; stop.
=back
All state files are gitignored: C<.karr.state> (board hash, per-task attempts,
cooldown, last error), C<.karr.lock>, C<.karr.log>.
=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.
( run in 1.264 second using v1.01-cache-2.11-cpan-0b5f733616e )