App-karr

 view release on metacpan or  search on metacpan

lib/App/karr/Foundation.pm  view on Meta::CPAN

  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.
#
# With a $ticket the built-in default changes (the ordinary one opens by
# telling the agent to pick its own work) and the assignment sentence is
# appended to whatever prompt was resolved. Appending rather than replacing
# keeps a configured prompt doing its job — it is usually about which skill to
# use and how to report — while the last sentence, which is the one that wins
# with a language model, is the one naming the card. Without this the mode
# would be `drain: false` with extra steps: the agent would never learn which
# ticket it was given.
sub _prompt_for {
  my ( $self, $karr, $ticket ) = @_;
  my $configured = $karr->{prompt} // $self->_config_data->{default_prompt};
  return $configured // $DEFAULT_PROMPT unless defined $ticket;
  return ( $configured // $DEFAULT_TICKET_PROMPT ) . "\n\n"
       . sprintf( $TICKET_ASSIGNMENT, $ticket );
}

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.601

=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

    # 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
L<App::karr::Encoding/decode_argv>). A caller that loads
C<App::karr::Foundation> directly, instead of invoking that script, is
responsible for both:

    use App::karr::Encoding qw( decode_argv enable_std_utf8 );

    enable_std_utf8();
    decode_argv();
    App::karr::Foundation->new_with_options->run(@ARGV);

Skipping the handles does not fail outright: every fixed message this class
prints or warns is plain ASCII (ticket #214). What it does not cover is data
-- a non-ASCII repo path folded into a C<skip $repo -- $wait> line, or a YAML
error carried through C<clean_error> into a C<warn> -- which still risks
C<Wide character in print>/C<warn> the first time it reaches a handle nobody
configured. Skipping C<@ARGV> is quieter, not safer: option values built from
it hold raw UTF-8 octets instead of decoded characters, with no warning to
say so.

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

  concurrent: 4             # boards that may have an agent at once (default: 1)
  hub: /path/to/hub-repo    # the repository carrying refs/karr-foundation/*
  routing: >-               # prose for the coordination agent, never parsed
    minimax is cheap; never hand it a release.

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)
  mode: drain               # drain (default) | single | ticket
  drain: true               # older spelling of mode: true=drain, false=single
  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)

lib/App/karr/Foundation.pm  view on Meta::CPAN


=item * B<idle> -- the agent did nothing and grabbed nothing; stop.

=back

B<The run's own report.> An agent invoked with C<--output-format json> ends its
output with one line: a JSON object saying whether the run failed, how it
ended, how many turns it took, how long it ran and what it cost. Where a run
leaves one, foundation classifies from it and the text scan below does not run
at all.

Foundation is not configured for this and does not inspect the command string
for it -- it reads the tail of the output, because that is where the format puts
its result and nothing else has to be kept in step with anything. Only the
B<last> non-empty line counts: prose before the object is irrelevant, prose
containing one cannot be mistaken for it (an agent printing a board can print a
pasted result object the same way #160's board printed a C<503>), and anything
after it makes the run unstructured again, so the scan takes over. The
reasoning is written out at C<_run_result> in L<App::karr::Foundation::Runner>.

A reported error ranks with the exit code, not with the scan: it is the run's
statement about itself, not an inference drawn from its prose, so the
"what it did before what it printed" guard below does not apply to it. Its
B<kind> decides what happens next. A provider status (C<api_error_status>) is
the case the scan was written for and backs the board off as a rate limit
always did. A spent turn budget (C<error_max_turns>) is not: the agent worked,
the provider answered, and the task was simply larger than the budget it was
given -- so it is logged, the board is not parked, and the run is judged by what
it moved. Any other reported error keeps its own name (C<error_during_execution>)
and cools the board down. A non-zero exit a report of B<success> does not
account for is still a common error: the report is the agent's, the exit code
may be its wrapper's.

In ticket mode the report is what finally separates the two stalls that used to
look identical -- "the agent reports it could not proceed" and "the agent did
nothing" -- and F<.karr.log> names which one it was
(C<STALL task#N -- the agent ran out of turns>). With no report it says exactly
that rather than guessing.

All per-board state files are gitignored: C<.karr.state> (board hash, per-task
attempts, cooldown, last error, last report, and the hook's board fingerprint,
round count and last exit), C<.karr.lock>, C<.karr.log>.
Agent availability is not among them: it is not per board and does not live in
the repository at all (see "Agent availability" above). C<last_error>
describes the B<last> run and is removed again by the next run that is not a
common error, so it never outlives the cooldown it caused. C<last_result> is
the same for the report -- how the last run ended, its turns, duration and cost
-- and is dropped again by a run that reported nothing.

=head2 run

    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,
C<_agent_command>, excluding boards disabled via C<karr disable>); if none
do, it falls back to the same overview instead of doing nothing, since
agent execution is opt-in and a config with no agents configured is a
legitimate way to use foundation purely as a status board. Otherwise it calls
C<_process_repo> for each repo, which is what applies the disable flag, the
lock, the cooldown, the change/actionability check, and finally the drain loop
described under "Drain semantics" above.

One repo at a time unless the effective machine ceiling says otherwise, which
is what it says by default -- see "Concurrency" above for the three levels and
L<App::karr::Foundation::Limits> for how they combine. Above C<1>, each board
gets a forked child running the whole of C<_process_repo> for it, the parent
schedules within the global and per-agent caps, and the shutdown handler TERMs
the children rather than their agents so every board runs the cleanup it would
have run serially.

=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, licensed under:

  The Artistic License 2.0 (GPL Compatible)

=cut



( run in 1.791 second using v1.01-cache-2.11-cpan-ad66724bd6a )