App-karr
view release on metacpan or search on metacpan
ex/README.md view on Meta::CPAN
and reading a board is execution. Everything about a board comes from **one**
board reading. `question_state` is the one fact that isn't measured at a board
at all â it comes from the mailbox, and only for a question step, because that's
the only kind a question has. Measuring it for every step would bring a fact
over which the precheck of no other kind could be, and would cost a mailbox
reading per step.
And the rule that makes the mechanism meaningful at all: **a fact that can't be
measured is absent** â a repository this machine doesn't have, a card that isn't
on the board, a question step that nothing in the mailbox names. An absent fact
does **not** let the precheck hold, whichever operator it uses. There's no
reading of `!=` under which "I couldn't find out" should let a step run. Every
uncertainty falls on the side that costs a planning round instead of the side
that executes the wrong thing.
A step whose precheck no longer holds isn't executed: it's marked `stale` and
the planner is noted as wanted. That's what keeps an outdated chain from doing
harm â it costs time, not correctness. For the same reason chains stay short: a
long chain goes stale faster than it is worked through.
**The pullâclaimâpush order** is the actual protection, not the compare-and-swap.
Two machines that never exchange refs would each read `pending` from their own
clone and each win their own local CAS. Hence:
- **First pull** â and abort the tick if the pull fails. Everywhere else in
`karr-foundation`, a failed fetch is a warning, because the fallback is this
machine's own view and that's the safe direction. Here, the fallback is
executing a step someone else is already executing â so the tick rather stops.
- **Publish the claim before the work begins.** The window that counts is the
length of the step, not the length of the write operation: a claim published
after a half-hour agent run would have left the step readable as `pending`
for that half hour. A claim that can't be published is rolled back locally to
`pending` â no other machine ever saw it â and the step stays for the next
tick.
- **Push result and run log**, best-effort: the work has already happened, the
state is written locally, the next tick publishes it. Refusing to record a
completed run would be the worse answer.
**Look first:**
```console
$ karr-foundation chain --dry-run
chain 20260818T053250Z-18641c: 2 step(s) ready (dry run, nothing pulled, claimed or executed)
step docs (shell) in /srv/docs-site: would run
step smoke (shell) in /srv/webapp: would run
```
**Then run it:**
```console
$ karr-foundation chain
step docs (shell) in /srv/docs-site: done â exit=0
step smoke (shell) in /srv/webapp: done â exit=0
step registry (question): left pending â question #1 is unanswered (policy: block)
chain 20260818T053250Z-18641c: 2 done, 1 pending
```
**What a failure does to the DAG: nothing** â and that's the design, not an
omission. A step only becomes ready when everything it `needs` is `done`. A step
that ends as `failed` or `stale` therefore stops its own branch
**constructively**: its dependents never become ready, no cascade has to be
computed, and every branch that doesn't run through it continues. The chain can
then no longer finish, and exactly this unreachability is the signal that
`on_stall: plan` names.
Three outcomes are deliberately **not** failures, because none of them is a
statement about the plan:
- A **common error** (a rate-limited or broken agent command) puts the step back
to `pending`. The board's cooldown and the agent's availability record have
already been written by the drain; the step just doesn't belong to this
machine right now.
- A **skipped board** â disabled, locked by another tick, in cooldown or on a
just-failed agent â is deferred just the same, with the note of which of the
cases it was.
- A step that **names a repository this machine doesn't have** stays untouched
and unclaimed. The chain is shared and the machines aren't; that's the normal
case in a fleet, not a broken plan.
**The run log** is one ref per run in the hub, and reading it is exactly what
`get-refs` is for:
```console
$ cd /srv/fleet-hub
$ git for-each-ref --format='%(refname)' refs/karr-foundation/log/
refs/karr-foundation/log/2026-08-18-053250a33ef5
refs/karr-foundation/log/2026-08-18-0532510c55f1
$ karr get-refs refs/karr-foundation/log/2026-08-18-053250a33ef5
{"chain":"20260818T053250Z-18641c","event":"start","host":"fleet-01","pid":1563039,"ts":"2026-08-18T05:32:50Z"}
{"event":"step","kind":"shell","repo":"/srv/docs-site","state":"running","step":"docs","ts":"2026-08-18T05:32:50Z"}
{"detail":"exit=0","event":"step","state":"done","step":"docs","ts":"2026-08-18T05:32:50Z"}
{"event":"step","kind":"shell","repo":"/srv/webapp","state":"running","step":"smoke","ts":"2026-08-18T05:32:50Z"}
{"detail":"exit=0","event":"step","state":"done","step":"smoke","ts":"2026-08-18T05:32:50Z"}
{"detail":"question #1 is unanswered (policy: block)","event":"step","kind":"question","state":"pending","step":"registry","ts":"2026-08-18T05:32:50Z"}
{"chain":"20260818T053250Z-18641c","done":2,"event":"end","pending":1,"ts":"2026-08-18T05:32:50Z"}
```
The waiting step has a `pending` entry and **no** `running` entry before it â
nothing was claimed and nothing was started.
Run logs are segmented and prune themselves. Segmented, because a ref holds one
blob and a blob is rewritten in full on every append: an uncapped log ref makes
every entry as expensive as a copy of the whole history. Quadratic, and measured
at about 4.6 GB of objects for 1 MB of log. So it's appended to the most recent
segment until it reaches 8 KB, and then the next entry opens
`...<run>+000001`. Retention is the other half of the bound: runs older than 14
days fall away, and regardless of age everything beyond the newest 500. That
runs on its own when a run log is opened â a retention policy that only runs
when someone types a command bounds nothing.
**Steps are executed one after another within a tick.** The chain's concurrency
is the one *across machines* â that's what the pull-claim-push order is for â
and the machine-local concurrency of several boards stays where it is.
And finally: **`chain` is a command of its own** and not something an ordinary
tick does along the way. `karr-foundation` without arguments means "drain the
boards in my configuration", for as long as the program exists. Picking up the
chain automatically would mean that every cron line of a fleet does something
different on the day someone writes a plan into it.
( run in 1.001 second using v1.01-cache-2.11-cpan-e7c6538aa59 )