App-karr

 view release on metacpan or  search on metacpan

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

board (status counts, in-progress/blocked tasks, lock and cooldown state, which
agent a board uses and whether it currently works); 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 -- the run's own report where it made
one, otherwise the 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 B<this run's agent engaged> 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.

B<Engaged> means foundation can prove the agent worked on that card during
B<this> drain: the agent runs with C<KARR_ROLE=agent>, so every C<karr> write
it makes is recorded in the board's own activity log under the C<agent>
identity, and only the tasks named there -- held by nobody, or by a claim name
the agent itself wrote under -- can be penalized. A card somebody else holds is
never touched, and neither is one the agent merely left claimed in an earlier
run: a stale claim is what C<claim_timeout> and C<karr unlock> are for. Where
that evidence is missing altogether -- an agent that does not write through
C<karr>, an unreadable log -- foundation auto-blocks B<nothing> rather than
guess: the drain then simply ends on its iteration cap, which is far cheaper
than blocking a human's in-progress card out from under them (#158).

=item * B<common-error> -- a non-zero/timeout exit, or an error pattern in the
output of a run that moved B<nothing> (rate limit, auth, network, 5xx, ...). No
task is penalized; the repo enters an exponential cooldown (C<cooldown_base> x
2^level minutes, capped at C<cooldown_max>, reset on the next clean run) and is
skipped until it expires.

What the run did is asked before what it printed: a run that exited 0 and moved
the board is progress whatever text scrolled past, and is never reclassified by
its own transcript. The scan is evidence only where there is no other -- a run
that produced no board movement at all, which is what a rate-limited or
unauthenticated agent looks like. A pattern seen in a run that B<did> move the
board is noted in F<.karr.log> and otherwise ignored.

The default patterns are correspondingly narrow: a symptom word counts next to
a failure word on the same line ("network error", "invalid credentials",
"quota exceeded"), not on its own, and an HTTP status counts only where
something adjacent marks it as one ("API error: 429", "429 Too Many Requests"),
not in a diffstat or a line number. Before this, an agent that printed its own
board tripped the scan on a backlog title, and a diffstat of 403 changed lines
tripped it on C<403> (#160).

=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



( run in 0.763 second using v1.01-cache-2.11-cpan-007c89162af )