App-karr
view release on metacpan or search on metacpan
ex/README.md view on Meta::CPAN
# App::karr â a runnable sandbox
This directory is a self-contained sandbox for [App::karr](..): it builds two
sample repositories with real boards, wires them into one
[karr-foundation](../bin/karr-foundation) fleet, and lets you run every scenario
the documentation talks about â ticket mode, drain mode, review cards staying
actionable, stalls and auto-blocking, cooldown, and the chain with its question
mailbox â against a real setup, on one machine, without a server or a remote.
Everything generated here is **machine-local and never committed**: the board
state lives in git refs (`refs/karr/*`), and refs are not carried by `git
clone`, so the boards are built at run time by `setup.sh`. What *is* committed
is this README, `setup.sh`, `bin/`, `scripts/` and the article below.
## Prerequisites
- Perl (5.20+) with the dependencies of this checkout installed. From the
repository root, `cpanm --installdeps .` or whatever your
[Dist::Zilla](https://dzil.org) setup prefers; running `prove -l t/` once
from the root is a good sanity check.
- `git` on `PATH`.
- Nothing else: no server, no network, no credentials.
## Build the sandbox
```bash
./ex/setup.sh # create (idempotent: existing pieces are left alone)
./ex/setup.sh --reset # wipe the generated state and rebuild from scratch
```
The script creates `ex/webapp/` and `ex/docs-site/` (each a git repo with a
karr board seeded with sample cards and a `.karr` file), `ex/fleet-hub/` (the
fleet's hub repository holding the chain), `ex/config.yml` (the
karr-foundation config pointing at all of them), and writes a demo chain into
the hub.
By default the sandbox calls the `karr`/`karr-foundation` binaries from this
checkout (`perl -Ilib bin/karr â¦`). Set `KARR_BIN` to point at an installed
binary if you would rather exercise that: `KARR_BIN="$(command -v karr)"
./ex/setup.sh`.
## Look at the boards
```bash
perl -Ilib bin/karr --dir ex/webapp board
perl -Ilib bin/karr --dir ex/docs-site board
```
`webapp` carries six cards â a login bug being worked, a rate-limit card that
depends on it, release notes in review, a blocked TLS upgrade, integration
tests, and a cross-board release card waiting on `docs-site#1`. `docs-site`
carries two.
## Run the fleet
A karr-foundation run is one pass over every repository in the config:
```bash
perl -Ilib bin/karr-foundation --config ex/config.yml --status # read-only overview
perl -Ilib bin/karr-foundation --config ex/config.yml --dry_run # what would run
perl -Ilib bin/karr-foundation --config ex/config.yml # the run
```
What you see on a fresh sandbox:
```
TICKET task#5
START agent=demo command=.../fake-agent.sh
fake-agent: working on #5
Moved task 5: backlog -> review
Handed off task 5 -> review (claim released)
START command=.../drain-agent.sh
drain-agent: working on #1
Moved task 1: backlog -> done
drain-agent: nothing to pick
```
- **webapp runs in `mode: ticket`.** karr-foundation picks the next assignable
card itself and tells the agent about it (`$KARR_TASK`, plus a line in the
prompt). `fake-agent.sh` moves it to review and hands it off under a fresh
claim, releasing the claim at the end.
- **docs-site runs in `mode: drain`.** The agent picks its own work
ex/README.md view on Meta::CPAN
sync --pull /srv/webapp
[2026-08-18T05:35:34] 1565842: TICKET task#1
[2026-08-18T05:35:34] 1565842: START command=my-agent --task "$KARR_TASK" --prompt "$PROMPT"
exec in /srv/webapp: my-agent --task "$KARR_TASK" --prompt "$PROMPT"
[2026-08-18T05:35:34] 1565842: DRY-RUN (skipped)
[2026-08-18T05:35:34] 1565842: STALL task#1 â no report from the agent
```
A dry run starts nothing and writes nothing â no agent, no `.karr.state`, no
`.karr.log`, and not even the pull that the first line announces. It's also mute
without `--verbose`: those lines are the verbose stream, not a report.
Then the real thing:
```console
$ karr-foundation --verbose
sync --pull /srv/webapp
[2026-08-18T05:35:34] 1565844: TICKET task#1
[2026-08-18T05:35:34] 1565844: START command=my-agent --task "$KARR_TASK" --prompt "$PROMPT"
exec in /srv/webapp: my-agent --task "$KARR_TASK" --prompt "$PROMPT"
working on #1 as fund-duty
[2026-08-18T05:35:35] 1565844: END elapsed=1s exit=0
```
The agent's output is streamed to the terminal in real time when there is one
(or with `--verbose`), and always appended to `.karr.log`. The card has moved,
so there's nothing more to say. `.karr.state` now carries the board fingerprint
against which the next tick compares:
```json
{"hash":"e256a65404833f0e801b323e1e2301fd","last_exit":0,"last_run":"2026-08-18T05:35:35"}
```
The command is a **shell template**, not a string that `karr` rewrites. Exported
into the child's environment: `PROMPT` (the instruction), `KARR_REPO` (where it
is), `KARR_ROLE` (`agent` or `hook`) and `KARR_TASK` (the identifier in ticket
mode, otherwise empty).
### How a run is judged
After every agent run, foundation classifies the result from what it can observe
â the run's own report where the agent gave one, otherwise from the exit code,
the board's ref movement and the captured output:
| Result | What it means | What follows |
|---|---|---|
| **progress** | the board has moved (in ticket mode: *this* card has moved) | keep draining |
| **stall** | a card the agent was working on hasn't moved | increment that card's attempt counter; at `max_attempts` automatically block |
| **common-error** | non-zero exit, timeout, or an error pattern in a run that moved nothing | no card is punished; the repository goes into exponential cooldown |
| **idle** | the agent did nothing and took nothing | stop |
The order of evidence is intentional: **what the run did is asked before what it
printed.** A run that ended with 0 and moved the board is progress, no matter
what text scrolled by, and is never reclassified by its own transcript. The text
scan is evidence only where there is no other â for a run that produced no board
movement at all, which looks exactly like a rate-limited or unauthenticated
agent. A pattern in a run that moved the board **after all** is noted in
`.karr.log` and otherwise ignored.
Correspondingly tight are the default patterns: a symptom word only counts next
to an error word in the same line ("network error", "invalid credentials",
"quota exceeded"), not on its own, and an HTTP status only counts where
something next to it identifies it as such ("API error: 429", "429 Too Many
Requests") â not in a diffstat and not in a line number. Before that, an agent
that printed its own board triggered the scan on a backlog title, and a diffstat
with 403 changed lines on the `403`. `error_patterns:` in the `.karr` adds your
own case-insensitive substrings.
**The run's own report.** An agent invoked with `--output-format json` ends its
output with one line: a JSON object that says whether the run failed, how it
ended, how many turns it needed, how long it ran and what it cost. Where there
is such a report, foundation classifies from it, and the text scan doesn't run
at all.
Foundation isn't **configured** for it and doesn't inspect the command for it
either â it reads the end of the output, because that's where the format puts
its result, and then nothing has to be kept in agreement. Only the **last**
non-empty line counts: prose before it is irrelevant, prose that itself contains
such an object can't be confused with it, and anything after it makes the run
unstructured again, upon which the scan takes over.
The **kind** of the reported error then decides:
| Reported kind | Consequence |
|---|---|
| `api_error_status` (a provider status) | exactly the case the scan was written for: board goes into cooldown |
| `error_max_turns` (turn budget exhausted) | **no** cooldown. The agent worked, the provider answered, the task was bigger than the budget â it's logged, the board isn't parked, the run is judged afterwards by what it moved |
| everything else (`error_during_execution`) | keeps its name and cools the board down |
A non-zero exit that a report of *success* doesn't explain is still a common
error: the report belongs to the agent, the exit code maybe to its wrapper.
In ticket mode the report finally separates the two stalls that previously
looked the same â "the agent reports it can't get further" and "the agent did
nothing" â and `.karr.log` names which one it was (`STALL task#N â the agent ran
out of turns`). Without a report it says exactly that instead of guessing.
### Stall, auto-blocking, and what "engaged" means
A stall that repeats ends the loop instead of spinning on it. Here the same
agent ran twice and left its card where it was â only the end of each tick:
```console
$ karr-foundation --force --verbose
...
I cannot make progress on #2
[2026-08-18T05:35:43] 1565933: END elapsed=0s exit=0
[2026-08-18T05:35:43] 1565933: STALL task#2 â no report from the agent
$ karr-foundation --force --verbose
...
I cannot make progress on #2
[2026-08-18T05:35:43] 1565937: END elapsed=0s exit=0
[2026-08-18T05:35:43] 1565937: STALL task#2 â no report from the agent
[2026-08-18T05:35:43] 1565937: AUTOBLOCK task#2: auto-block: no progress after 2 attempts (foundation)
```
The auto-block is a **fallback, not a verdict**: the agent may set a better
reason itself at any time (`karr edit --block`), and a card someone else holds
is never blocked on foundation's word.
( run in 1.366 second using v1.01-cache-2.11-cpan-007c89162af )