App-karr
view release on metacpan or search on metacpan
ending in `shipped` releases there), and a card leaving a terminal
status for a non-terminal one has `claimed_by`/`claimed_at` cleared
unless the command names a claimant, so the reopened card is free
rather than leased to whoever finished it; `done` to `archived` keeps
the name. `karr pick` still refuses to hand a terminal card out, and
`--json`/`karr show` still print `claimed_by` as provenance.
- Expired claims are handled honestly at both ends (k177, k232).
Stepping over a claim older than `claim_timeout` is allowed, as it
always was, but the five mutating commands said nothing while
`move`/`handoff --claim` re-stamped `claimed_by` -- leaving
karr-foundation attributing a stall to a name nobody held; the
override is now reported once the write lands (`Warning: ... overriding
the expired claim held by X` on STDERR, an `expired_claim` object under
`--json`, silenced by `--quiet`) (k177). And `claim_timeout: 0s` means
claims never expire, as it always said: `_claim_expired` read the zero
as a duration, so `(now - claimed_at) > 0` handed every claim older
than a second to the next agent -- the setting a board uses to make
claims binding was the one that gave them away. A negative value is
answered like zero, and the config POD now says `0s to disable`
(k232).
- The `$(karr agentname)` idiom no longer teaches an agent to lose its
claim (k176). Every `karr agentname` mints a new name and stores none,
so the single-`pick` example became wrong the moment it was copied to
the handoff at the end of the work. The POD, the shipped skill and the
release scenario now show only `NAME=$(karr agentname)`, warn that the
name is not remembered, and point at `karr show ID` to read a lost
claim back off the board. The generator stays random and stateless on
purpose -- any stable handle is shared by every concurrent agent on
the board.
- `--compact` is declared only by the nine commands that render one
(k251, k254). It used to sit beside `--json` in the shared role, so
all twenty-two advertised it and thirteen accepted and dropped it;
those thirteen now answer `Unknown option: compact` and exit 2. `karr
show`, `context`, `log` and `config show` are new to it, and `karr
pick --compact` ends after the assignment line instead of printing the
whole card as before -- an accepted option that changed nothing
(k225's class). `--json` is unchanged and still wins where both are
given; the shipped skill knows which nine take it.
- `karr context --write-to FILE` is a side effect, and the output flags
decide stdout (k260). `--json --write-to` returned JSON and wrote no
file; `--compact --write-to` wrote the file and dropped its four
numbers. The block between the kanban-md sentinels is Markdown by the
interop contract whatever the caller asked on stdout, so all three
spellings write the same block and differ only in what they print;
with an output flag the `Context written to` line moves to STDERR so a
redirect decodes whole.
- karr's output streams read in the order it printed them, and dialogue
goes to STDERR (k241, k248, k249, k253). The `:encoding(UTF-8)` layer
buffers STDERR, so both handles flushed at exit STDOUT-first and every
warning landed under every result on `2>&1` or a pipe; `enable_std_utf8`
now autoflushes both, so a combined stream reads in print order and
`karr-foundation`'s teed run log fills as the run happens (k249). The
`karr delete` confirmation is unbuffered before the read (k241) and
goes to STDERR unconditionally (k248) -- a question is dialogue, not a
result, so `printf "n\n" | karr delete 1 --json` no longer puts a bare
prompt in front of an object that then will not decode; the outcome
stays on STDOUT. And `karr log` dropped its own dead no-repository
branch, which printed plain text ahead of the `--json` check but could
never run behind `git_root` and `require_local_board` anyway (k253).
- `karr delete` names what points at the card before it goes (k236,
k242, k250). Dependents are searched backwards -- a `depends_on` entry
or `parent` set to the id -- and each is reported on STDERR before the
confirmation so it can change the answer, offering `karr archive ID`
to keep the card; the cross-board far card is named too, out of the
card's own `escalated-from:`/`needs:` tag, under a
`cross_board_warnings` key (k236, k242). The archive advice now says a
settled link reads as finished-or-not, since `done` and `archived` are
both terminal and neither claims success (k250, `docs/adr/0004`). All
three ride STDERR, carry into `--json`, and are silenced by `--quiet`;
the warnings come under `--yes` too, the mode agents delete in.
- karr-foundation grew the coordination layer of the fleet epic (k194):
it can run several boards at once and drive a stored, fleet-wide chain
of work. Concurrency is bounded by the tightest of three levels -- the
machine ceiling, an agent's estimated session limit, and a chain
header's own `limits:` -- and defaults to 1, the serial runner it has
always been; one agent per repository stays, each a forked child
holding that board's `.karr.lock` (k186). The chain lives in
`refs/karr-foundation/*` as a DAG of steps (ticket, shell, question or
plan) with edges, prechecks, timeouts and `on_*` policies, stored with
compare-and-swap and segmented run logs (k189); `karr-foundation
chain` picks a ready step, checks its precheck by measuring the board,
runs it through the target repo's existing mode, and writes its state
back (k202). A step whose precheck no longer holds is marked stale and
the planner recorded as wanted; a shell step with no command is refused
when the chain is written. The whole binary joins the 0/1/2 exit-code
contract (k201), and `karr pick` and ticket mode select through one
`Role::PickRules` so a coordinator cannot argue with its own board
(k198).
- The fleet plans, asks and routes (k191, k200, k210, k213). A question
mailbox under `refs/karr-foundation/questions/*` (`ask`/`answer`,
`--policy block|use_default|escalate_to_ai`, a deadline) lets a `kind:
question` step wait on an answer while the rest of the chain runs
(k191); a ready question step resolves it -- waiting on `open`,
finishing on `answered`, doing what the asker wrote on `overdue`
(k200). `karr-foundation plan` writes a chain from a YAML/JSON document
on stdin or `--input`, validating ids, edges, kinds, prechecks and the
cycle check before a ref is written, and carries the coordination
agent's prompt (k213). That coordination agent is the fleet's judgement
layer (k210): marked `role: coordinator`, it runs almost never -- once
at the end of a tick, carrying every deviation the tick met -- and
writes an assignment (repository to an ordered agent list with an
explicit `WAIT`) that karr-foundation then reads with no AI in the hot
path. A fleet that marks no coordinator behaves exactly as before.
- karr-foundation learned per-repo modes and named agents (k185, k187,
k188). `mode: ticket` picks one card by `karr pick`'s ranking minus
the claim, names it in the prompt and as `$KARR_TASK`, runs the agent
once and reports what happened to that card; a stall costs one attempt
and auto-blocks at `max_attempts` (k185). The local config defines
named agents -- `command`, `kind`, `probe_every`, a prose `description`
the routing agent reads -- picked per board with `agent:`, and
definitions stay local and never sync (k188). A `kind: claude-code`
run is classified from the JSON result object it ends with, not its
transcript, so a spent turn budget is a reason to run again while a
provider status backs the board off, and availability is tracked as ok
/ failing-since-X with a next attempt (k187, k160's last-line rule).
- Chain steps and boards can wait on work in another repository (k192,
k209). Cross-board links are `BOARD#ID`, never a path (a path is one
machine's property): `karr create --needs other#7` /
`--escalated-from home#5`, `karr needs` reporting both ends with the
far card's status, and `karr needs --resolve` dropping a link whose
far card reached the FAR board's own terminal status and lifting the
blocked flag (k192). A chain precheck can be about a far card through
the `ticket_links` fact -- `settled` when every `needs:` link is
terminal, `open`/`missing` otherwise, and ABSENT for a board this
machine does not hold, which makes the precheck not hold so the
machine that does have the board runs the step (k209). The executor
measures and never resolves: nothing is fetched, and lifting `blocked`
stays `karr needs --resolve`.
- `on_drained`, the domain hook (k193): when a board has no actionable
task left, a configured command runs in it and karr does not know what
it does -- the seam that keeps karr generic (in the fleet this came
from, it drives a release gate). Its exit code is interpreted by
nobody, it never marks the agent failing, and it runs under the
board's own lock with `KARR_ROLE=hook`. A drain ending in a
common-error does not count as drained; the round in which the hook
files new work is counted and capped at `on_drained_max_rounds`, and
the same board state is never asked twice.
- `karr sync` now carries `refs/karr-foundation/*` as well as the board
(k190), since the chain, run logs and design document are coordination
state that has to reach every machine; deletions travel as tombstones
under `refs/karr-local/foundation-deleted/` with an explicit delete
refspec, never a pruning push (k178's rule), and the fleet half runs
second and never alone, behind the board's identity (k95) and
wholesale-wipe (k82) checks. Foundation also prints plain ASCII when
loaded as a module rather than run through `bin/karr-foundation`
(k214), its EXIT CODES POD no longer lumps every bad option value under
exit 2 (k217), five tests stop reading the runner's real
`~/.config/karr-foundation/config.yml` by isolating `$ENV{HOME}`
(k222), the `enable_std_utf8()` contract a direct library loader must
run is written into `App::karr::Foundation`'s POD (k221), and
`enable_std_utf8`'s own POD now names all four readers that decode
STDIN themselves (restore, set-refs, `foundation plan`, the delete
confirmation) instead of only `karr restore` (k246).
- A remote that accepts the connection and then goes silent no longer
hangs karr (k170, k174). `KARR_TRANSPORT_TIMEOUT` bounded only the
git-CLI fallback, which runs after the native call returns, so the one
path that could block was the one it could not reach; it now sets
libgit2's own network timeouts too, covering git://, http:// and
https:// (k170). ssh:// stayed unbounded there because libssh2 does
its own reads past libgit2's timeout -- fixed upstream in libgit2
1.9.3, enforced by pinning `Alien::Libgit2 0.002` (pkg-config floor
1.9.3) so a distribution libgit2 below the fix falls through to the
absolute path they wrote (k226).
- New `karr dashboard` command (k220): a configuration-free, multi-column
overview of every karr board found by recursively searching a directory
tree for Git repositories, one entry per repository with a block per
open task coloured by status. Status names and which one is terminal
come from each board's own config (k67), so two boards side by side may
use entirely different status lists. `--depth` bounds the search,
`--hide-no-board`/`--show-no-board` control the board-less list, and
`--json`/`--compact` follow the rest of the CLI. Read-only and offline;
no rendered line exceeds the terminal width.
0.500 2026-08-13 23:57:25Z
- Three karr-foundation fixes (#165, #166, #168). `max_runtime: 0`
no longer silently turns `drain: true` into a single run: the
drain's wall-clock guard now reads `&& $max_runtime > 0`, so 0
means "no per-run timeout and no drain budget" matching the
documented intent, and a positive value bounds the drain as
before (#165). `_discover_repos` deduplicates by canonical path,
so a repo reachable through both `dirs:` and `scan:` is processed
exactly once per tick: realpath (with absolute as fallback) keyed
by path, first-seen order preserved so an explicit `dirs:` entry
wins over a `scan:` hit (#166). A pull that refuses no longer
aborts the whole foundation run: `_process_repo`'s pull is now
wrapped in the same try/catch that already protects the other
per-repo steps (`_drain_repo` below it, `_process_repo` itself
from #162), so a refusal from the wholesale-wipe guard, the
board-identity guard, or the unapplied-refs guard warns and lets
the run continue â and the board whose pull refused is not then
processed as if it were up to date (#168).
- Four fixes in karr's character/octet boundary and refs-backed
storage guarantees (tickets #155, #156, #157, #167). `karr restore`
is now atomic across its write phase: `replace_board_refs` snapshots
every `refs/karr/*` OID and every ref the snapshot is about to
introduce before the first `_write_ref_oid` call, and any die out
of the write loop unwinds every ref that landed â restoring the
original OID for refs that existed, deleting refs the snapshot
managed to create â so the board reads back exactly as it did
before the failed restore. `Cmd::Restore`'s POD promise ('a snapshot
karr cannot apply ... is refused with the board exactly as it was')
is now true for the directory/file name conflict that previously
half-applied, and for the CAS-exhaustion path that previously
half-applied without any manual editing at all (#155). The
activity log no longer loses entries under concurrency: log_entry
wraps its read-and-write in `write_ref_cas` + `retry_contended`,
matching `save_task_cas` and `allocate_next_id_ref`, so the
existing CAS plumbing handles contention transparently and a
board running N parallel `karr create` writes N log entries
(#156). `git_user_name` and friends no longer leak libgit2's
octets into karr's character strings: `Git.pm:_config_string`
and `_run_git`'s captured stderr decode through `from_octets`,
so a non-ASCII `user.name` is no longer written double-encoded
into the log ref and `karr repair` does not need to undo it on
read (#157). `%ENV` is now an octet crossing `App::karr::Encoding`
owns: two new helpers, `to_octets_for_env` and
`from_octets_from_env`, match the POD style of the existing
helpers and delegate to the canonical codec, and the three
`Foundation/Runner.pm` writes go through `to_octets_for_env` â
so the 'Wide character in setenv' warning on a non-ASCII prompt
is gone, and the house rule that Encoding owns every crossing
is complete (#167).
- Three board commands no longer treat a value the user did pass as
if it had not been given (tickets #151, #152, #153). `Cmd/Log.pm`
refused `--last < 1` only via truth, so `karr log --last 0` dumped
the full log (the bound silently removed) and `karr log --last -3`
reported an empty log and exited 0 â indistinguishable from a board
with no activity. `Cmd/Archive.pm:55` read `$pos[0] under `or die`,
so the truthy comma in `karr archive ,` passed the guard, parse_ids
split to nothing, and run_batch iterated zero items with no output
and exit 0. `Cmd/{Edit,Create,Handoff}.pm` carried 17 sibling
options whose presence was tested with `if ($self->foo)` rather
than `defined && length`, so the literal value `0` was
indistinguishable from "not given" â the write still ran, `updated`
was bumped, an activity-log entry was appended, the command printed
success, and `--block 0` left the card unblocked (the sharp edge:
`karr pick` would have handed it out). The fix is the rule already
written down for `--body` in ticket #78 (`defined && length`)
applied to the siblings; `--last < 1` raises a usage error matching
`Show.pm:161-162` and `Context.pm:97-99` exactly (same exit 2,
same error format); `karr archive ,` raises the same usage error
as `move ,` / `edit ,` / `delete ,` already do. The audit trail no
longer records edits that did not happen.
- `karr-foundation` now keeps an agent it started alive in three
situations where it used to silently lose it: a pipeline/`&`/shell-
builtin command where the real agent was the shell's child, not the
shell (#148); an agent that closed its stdout before max_runtime
elapsed, where the runner fell through to a bare blocking waitpid
that held `.karr.lock` forever (#161); and a SIGTERM/INT/HUP to
foundation mid-drain, where the agent was reparented to init and
`.karr.lock` named a dead pid the next tick read as free (#163).
The runner wraps every agent in its own process group with
`setpgid(0,0)` in the child and `setpgid($pid,$pid)` in the parent
(the second call wins the fork race idempotently); the timeout,
SIGTERM and SIGKILL all signal the group with a negative pid, so
the shell, the agent and any grandchildren the agent forked all
receive the kill. `max_runtime` is now enforced independently of
IO activity by a SIGALRM handler that closes the read end of the
pipe, and the post-EOF wait is a deadline-aware WNOHANG poll that
falls through to the SIGTERM/SIGKILL/reap path when the wall clock
beats the child. Foundation installs a SIGTERM/INT/HUP handler for
the lifetime of `run()` that kills the agent's group, force-releases
the lock, and `POSIX::_exit(128 + signum)` â the conventional shell
exit shape, so systemd/cron see a signal-death exit and an operator
reading the log does not need a special case for "killed cleanly
mid-drain".
- `.karr.lock` is now a `flock(2)` on an open file descriptor the
foundation keeps for the lifetime of the lock, not an advisory pid
that two ticks could each write their own value into (#162). Two
ticks that overlap â the normal case, since a drain may run for
`max_runtime` (default 1800s) while cron fires every few minutes â
race on the file: the second tick gets `EWOULDBLOCK` from
`LOCK_EX|LOCK_NB` and returns immediately, without overwriting the
existing pid. `_release_lock` closes the open fd (closing drops the
flock) and only unlinks the file if the recorded pid still matches
`$$`, so a pid-recycled foundation cannot unlock its successor's
lock. `_lock_held` is the flock check, not a `kill(0,$pid)` against
backoff, which is what the scan is for. A board's own `error_patterns`
are unchanged: plain case-insensitive substrings.
- `.karr.state` no longer keeps a `last_error` from a run three cooldowns
ago sitting next to `last_exit: 0` with nothing to explain the pair
(ticket #160). `last_error` describes the last run and is dropped by the
next run that is not a common error. Where the pair is real â an agent
that reports a rate limit and still exits 0 â it is now said out loud:
`.karr.log` records "COMMON-ERROR rate limit â agent exited 0, run
discarded", and `karr-foundation --status` names the reason beside the
wait ("cooldown 240s (rate limit)").
- Fixed data loss when a pull could not write a ref (ticket #154). The
apply step of the reconciliation used an unretried ref write whose
failure nobody checked, so a ref whose `.lock` file was held â by
another karr mid-write, or left behind by one that was killed â was not
applied, while the `refs/karr-remote/` mirror was advanced as if it had
been. The next reconciliation then read the stale local ref as unpushed
work and the forced, pruning push wrote it over the remote's newer card,
in every clone, at exit 0. Those writes now retry on the same terms as
every other ref write in `App::karr::Git`, a ref that still cannot be
applied leaves the mirror at its pre-fetch value so the next sync
decides it again, and the pull fails with a non-zero exit naming the ref
instead of proceeding to the push. The same fix covers a remote deletion
that could not be applied (which used to be pushed back as a
resurrection), a conflict whose local version could not be parked (the
local version is now kept rather than replaced), a mirror rollback
behind a refusal that only half succeeded (now reported), and the
board-identity stamp the mirror could not record.
- karr-foundation no longer auto-blocks tasks its agent never touched
(ticket #158). `_stuck_tasks` claimed to return "tasks the agent engaged
(claimed / in-progress) but did not move" and tested only whether the
card carried *any* claim or sat in `in-progress` â who held it was never
compared against anything. Every drain iteration in which the agent moved
some other card therefore charged an attempt against every card somebody
else was holding, and since `max_attempts` (default 2) can be spent
inside a single drain, a human's in-progress card was blocked with
`auto-block: no progress after N attempts (foundation)` and pushed to the
remote within seconds â a destructive write to shared board state about
work foundation never attempted, dropping that card out of `karr pick`'s
actionable set behind its owner's back and giving a reason that is
factually wrong. Engagement is now proven rather than assumed: foundation
runs the agent with `KARR_ROLE=agent`, so the agent's `karr` writes are
recorded in the board's own activity log under the `agent` identity, and
only cards named there during that drain â held by nobody, or under a
claim name the agent itself wrote with â can be penalized. A card the
agent merely left claimed in an earlier run no longer counts either; a
stale claim is what `claim_timeout` and `karr unlock` are for. Where that
evidence is missing altogether â an agent command that never calls
`karr`, an unreadable log â foundation now auto-blocks nothing rather
than guess: a drain that ends on its iteration cap costs an iteration,
blocking the wrong card costs somebody their work. The ownership test is
repeated at the write itself, which is the only place foundation mutates
a board, so a future caller inherits the guarantee instead of having to
remember it.
- karr-foundation no longer splices environment values into the agent
command string before `/bin/sh` parses it (ticket #159). `PROMPT`,
`KARR_REPO` and `KARR_ROLE` are exported into the child's environment
and the shell expands them, as it already could. Previously a prompt's
backtick spans and `$(...)` â board content, written in Markdown â were
executed as shell commands in the board's own directory, and the agent
then received an instruction nobody wrote; and the substitution reached
inside single quotes, where sh guarantees a literal, so the documented
output-shaping technique broke silently (`awk '{print $2}'` arrived at
awk as `'{print }'`). Every variable a command template could reference
before still expands, the `${VAR}` form included. The START line in
`.karr.log` now records the command template â the exact string handed
to `/bin/sh` â instead of the substituted result, and so no longer
copies environment values, a wrapper's API key included, into a
plaintext log.
- karr-foundation can no longer start an agent and then walk away from it
(ticket #147). `App::karr::Foundation::Runner` opened `.karr.log` after
the fork, so a log it could not open was reported with the agent already
exec'd, and that `user_error` came before the parent's own `waitpid`.
Not fatal to the run, which is what made it expensive: `_run_command` is
called from the drain loop, which `_process_repo` catches per repo and
then releases the board's lock anyway, so every affected board was left
with a live, unwatched agent and a lock file saying nobody was running â
and the next tick would start a second one on top of it. The log is now
opened before the fork, which turns an unwritable log into a refusal with
nothing started: the same answer the foundation's own `_append_log` for
the START line already gives one call earlier, and the reason that window
needed a race to reach at all, since a log that is a directory or
unwritable fails there first. The one that needed no race is the
`TIMEOUT` line, appended between the read loop and the
SIGTERM/SIGKILL/`waitpid` that are the only things that stop a hung
agent: an agent that removed or replaced `.karr.log` during its own
half-hour run took that append down with it and outlived the timeout it
had earned. That append is now best-effort, and its failure is warned
once the child is safely reaped instead of thrown in front of the kill;
the END line still raises it for real if the log is unwritable by then.
Nothing between the fork and the `waitpid` can throw any more.
t/148-foundation-runner-child-leak.t pins both halves, and t/122's #143
assertion that the child gets reaped became the assertion that there is
no child to reap.
- The lookup for the bundled skill file has one implementation instead of
two (ticket #146). `App::karr::Cmd::Init::_find_skill_source` and
`App::karr::Cmd::Skill::_skill_content` were the same sub twice over â
byte-identical apart from the `$INC` key each read to find its own source
tree for the development fallback â which is the shape that made ticket
#142 fix the skill *write* in one command and left #145 to fix it again
in the other three commits later. Both are now `_skill_content` on
`App::karr::Role::SkillFile`, next to the `_write_skill` #145 collapsed
the same way, and the role still requires nothing of its consumer, which
is what lets it serve board-less `karr skill` and board-composing
`karr init` alike. The fallback anchors on the role's own loaded path
rather than on a command's: naming either command's file would answer for
one caller and send the other silently on to "Could not find
claude-skill.md", and since MooX::Cmd decides which command classes get
loaded, whether that happened would depend on how karr was invoked. No
change in behaviour: both lookups, both fallback triggers and both
commands are exercised end to end in t/26-skill-share-dir.t, including
through the CLI with File::ShareDir made to fail the way an uninstalled
dist makes it fail.
- `karr metrics` no longer averages impossible cycle times (ticket #140).
A completed card whose `completed` precedes its `started` measures a
kanban-md config.yml. `karr board`'s claim counting and the `completed`
stamp followed â see the entries above.
- New `karr list --archived`, which shows the archive and only the
archive, matching kanban-md's flag of the same name. It replaces the
other status filtering rather than narrowing it, so it wins over
`--status`. Note that `karr list` still excludes the whole terminal
group by default where kanban-md excludes only `archived`: it is the
agent's "what is open" view, and that difference is now documented
rather than merely undocumented.
- Fixed `karr move`, `karr edit` and `karr delete` abandoning the rest of a
batch after one bad id. A missing id died from inside the loop, so every
id after it was skipped and the outcome depended on where the bad id sat
in the list: `karr move 1,999,2 todo` moved 1 and never looked at 2,
while `karr move 999,1,2 todo` moved nothing. All four id-list commands â
`archive` included, which already behaved this way â now share one batch
loop: every id is attempted, each failure is reported on STDERR with a
`N of M ids failed` summary, and the command exits 1 while keeping the
work that did succeed, which is the contract ADR 0002 already documented.
A usage error is deliberately not a per-id failure: `karr move 1,2,3
no-such-status` is wrong for every id at once, so it still rejects the
whole invocation with exit 2 and writes nothing. With `--json` the
results array is now printed even when part of the batch failed (`move`,
`edit` and `delete` previously printed no JSON at all in that case), and
a failed id appears in it as `{"id":999,"error":"Task 999 not found"}` â
`archive`, which already reported failures there, used to give the
shorter `"not found"` for the same field.
- Fixed `karr archive` ignoring claims. It set the status to `archived` and
saved with no claim check at all, so it could archive a card another
agent was holding â the one door into a status change that `karr move`
and `karr edit --status` did not cover. Archiving now applies the same
claim rule as `move`, `edit` and `delete`, with the same message, and is
refused while a live claim is on the card; release it with
`karr edit ID --release` or let `claim_timeout` expire it. Re-archiving
an already-archived task changes nothing and stays a success whatever its
claim says. Because archiving is now an ordinary status change, a board
whose configured `statuses` do not include `archived` will have
`karr archive` refuse rather than write a status the board does not have.
- Fixed `karr handoff` overwriting concurrent changes. It read the task,
changed it and saved it back without checking the card had not moved in
between, so a claim landing in that window was silently replaced instead
of obeyed. The handoff now goes through the same compare-and-swap and the
same status-change path as `karr move`, so the claim rule is applied to
the revision that actually gets written.
- Fixed `karr pick` locks being published to the remote. Lock refs lived at
`refs/karr/tasks/N/lock`, inside the namespace karr pushes, so any sync
that fired while a lock was held put it on the remote; other clones then
pulled a lock whose holder they could not see, could not outlive and
could only clear with `karr unlock`. Board backups snapshotted it too.
Locks are process-local state, so they now live under `refs/karr-local/`,
which nothing pushes, fetches, prunes or snapshots â and which
`karr set-refs` refuses, so no refspec can reach them at all. Board
state, including the `refs/karr/log/*` activity log, is unaffected and
still syncs. Locks left at the old address by an older karr, or pulled
from a remote that still has them, are not acted on â a lock from another
clone says nothing about this process â but `karr unlock` lists them,
marked as strays, and clears them.
- Fixed the last race in `karr delete`'s claim guard. `App::karr::Git`
could only delete a ref by name, through a libgit2 call that takes no
expected-old OID, so no delete could be guarded: re-reading the task and
re-applying the claim rule closed the minutes-long window behind the
confirmation prompt but left the microseconds after it, in which a claim
landing on the card was deleted along with it. There is now a
compare-and-swap delete (`delete_ref_cas`) reporting the same retryable
outcomes the existing retry loop understands, and the guarded delete
paths â `karr delete` and giving back a `karr pick` lock â go through it.
The unguarded `delete_ref` is unchanged, because `karr destroy` and
breaking a lock deliberately remove whatever is there.
- Fixed the push insurance retrying a push the remote had already refused.
When a command dies after writing refs, karr pushes from an END block so
the writes are not stranded; that path still made three attempts a second
apart at a refusal the far side had already given its answer to, and then
told the user to run `karr sync` â a command that would be refused
identically. It now stops at the refusal and reports what the remote
refused, ref by ref, on both the native and the git-CLI transport. An
ordinary transport failure is still retried three times and still advises
a sync, because there retrying can work.
- Fixed `karr materialize` silently deleting and overwriting tracked
project files. It wrote its file view straight into the working tree â
replacing `config.yml` and removing every `tasks/*.md` â so a repository
that already kept its own `tasks/` directory or `config.yml` lost them,
from a command that only reads the board. Materialize now refuses to run
when it would overwrite or delete anything Git tracks, names each such
path, and writes nothing on that path; `--force` overrides it. The sweep
of stale cards is also limited to files named the way karr and kanban-md
name them (`NNN-slug.md`), so unrelated files in `tasks/` are left alone.
- Fixed `karr import --yes` wiping the board when `tasks/` exists but holds
no cards. The guard only checked that the directory was there, so an
empty view imported zero tasks, deleted every task ref and exited 0
reporting success. An empty view is now refused.
- Fixed `karr import` leaving the board half-written when one file is
malformed. Refs were written as files were parsed, so a bad card aborted
the run mid-way with some tasks updated, the prune never reached, and a
bare "Invalid task format" that named no file. The whole view is now
parsed before any ref is touched: the import either applies completely or
changes nothing, and every rejected file is listed with its reason.
`App::karr::Task::from_file` names the file in all its errors.
- Fixed `karr materialize` writing a `config.yml` that kanban-md refuses to
load, which defeated the purpose of the file view. Perl's `1`/`0` were
dumped as YAML integers where kanban-md's schema wants booleans
(`require_claim`, `bypass_column_wip`), and `next_id`, which it validates
as `>= 1`, was never written at all. The view now carries real YAML
booleans and `next_id`; the counter itself stays in
`refs/karr/meta/next-id` and import continues to ignore the file's copy.
- Fixed `karr edit --status` bypassing every rule `karr move` applies.
`karr move 1 in-progress` refused without `--claim`, while `karr edit 1
--status in-progress` set the field and exited 0 â so `require_claim`,
the guarantee karr's multi-agent coordination rests on, was one flag
away from optional, by accident as easily as on purpose. `edit --status`
also skipped the `started`/`completed` lifecycle stamps that `move`
writes. Both commands now change a status through one shared path and
behave identically, including the status-name check described below.
- Fixed `karr move`, `karr edit` and `karr delete` taking over another
agent's live claim without a word. None of the three asked who held the
claim, so `karr move 1 review --claim mallory` on a task alice was
working on simply reassigned it, and `karr delete 1 --yes` removed it.
All three now apply the same claim rule `karr handoff` and `karr pick`
already applied: an unclaimed task is free, the holder may proceed, an
expired claim blocks nobody, and anything else is refused with "Task N
is claimed by X". `karr edit --release` stays exempt, because it is the
only way to break a claim a crashed agent left behind. `karr delete`
refuses on any live claim whoever holds it, matching kanban-md, which
gives delete no `--claim` either. For move and edit the check and the
write are now one compare-and-swap against the task ref, so a claim
taken between the two no longer loses; delete re-checks immediately
before removing the ref, which is as close as libgit2's unguarded ref
delete allows.
- Fixed claim expiry misreading every timestamp that carries a UTC offset.
kanban-md stamps claims as RFC3339 with the agent's local offset and
nanoseconds (`2026-08-09T17:28:46.449764553+02:00`) and karr discarded
both, reading the stamp as if it were UTC. A claim stamped `+02:00`
never expired, so `karr pick` would not take over stale work; one
stamped `-05:00` expired five hours early, so pick stole claims whose
owners were still working. Every call also printed "Garbage at end of
string in strptime" to stderr, on every single `karr pick`. Offsets and
fractional seconds are now parsed and normalised to UTC, and the
warnings are gone with them.
- Fixed `karr delete` without `--yes` printing "Use of uninitialized value
$answer" twice when stdin reaches EOF without an answer â which is every
agent and CI invocation that forgets the flag â and then silently
skipping the task with exit 0. When there is no answer and stdin is not
a terminal it now refuses with "No answer on stdin and stdin is not a
terminal. Re-run with --yes." and exits 1, the way karr's other
destructive commands already refuse without `--yes`. Ctrl-D at a real
terminal still means "no", and piping `y` or `n` into the prompt still
works.
- Fixed `karr move , todo`, `karr edit ,` and `karr delete ,` â an id list
that contains no ids â doing nothing at all and exiting 0. The comma got
past the "an id is required" check because it is a non-empty string, and
then split to an empty list, so the per-id loop never ran and no error
was raised for the exit-code contract to classify. All three now report
a usage error and exit 2.
- Fixed a push the remote rejected being reported as a completed sync.
libgit2 returns success from `git_remote_push` even when the server
refused every ref â a pre-receive hook, a protected ref, a
non-fast-forward on a non-forced refspec â so the refusal exists only in
the per-ref result Git::Native 0.004 hands back, which karr discarded.
The board then diverged from the remote with no signal at all. A push
with rejected refs now fails, naming every refused ref with the reason
the server gave, on both transports: the git-CLI fallback pushes with
`--porcelain` and reports the same refs and reasons. Such a push is no
longer retried â the remote was reached and gave its answer â except
from the insurance push that fires when a command dies mid-body, which
still makes its three attempts and still ends with the generic "run
karr sync to retry" advice.
- Sync failures are no longer repeated once per retry attempt. Both retry
loops printed the error on every attempt, so one failed sync produced
three copies of it â and with the per-ref rejection message above, three
copies of a block several lines long. An error identical to the one just
shown is now dropped; an error that differs still gets its own line.
`--quiet` is unchanged: it silences the retry announcements, never the
errors.
- Fixed a remote that is empty for the wrong reason reconciling the whole
board away. Pulls are reconciled against a tracking mirror, so "the
remote had these refs at the last sync and does not have them now" is
acted on â which is what makes a delete propagate between clones, and
also exactly what a re-created origin, a remote URL edited to point
somewhere else, or a rolled-back hosting-side restore look like. In
those, a routine writing command deleted the entire board in one step,
silently. A reconciliation that would remove every remaining board ref
is now refused: the command stops, the mirror is left as it was so the
next command refuses again rather than quietly republishing, and the
message points at `karr sync --push` to republish this board or at the
new `karr sync --prune` to accept the deletion (which is how a
`karr destroy` on another clone now reaches this one). This guard
catches the total wipe only; the remote swapped for a different,
non-empty board â which leaves refs standing and so slips past it â is
caught by the board identity described above (ticket #95).
- Fixed a failed `karr restore` destroying the board instead of restoring
it. Restore deleted `refs/karr/*` first and wrote the snapshot back
afterwards, so a snapshot karr could not write took the board with it: a
single unusable ref name left the board empty locally, and then on the
remote too, because the push insurance faithfully mirrored the
half-executed destruction. Every ref name is now validated and every
commit object built before the first ref moves, so a snapshot karr cannot
apply is refused with the board untouched, and the refs it can apply are
overwritten in place instead of starting from an empty namespace. The ref
updates themselves are still a loop rather than one transaction â an I/O
failure part-way through can still leave a board holding a mix of old and
new refs â but the board is no longer emptied before the first write, so
no failure can leave it with nothing in it. A snapshot may also no longer
address refs outside `refs/karr/`, which previously let a hand-edited
backup overwrite a branch.
- Fixed any write command silently seeding a partial board in whichever
repository it was run in, and that partial board then locking `karr init`
whenever an existing task ref is saved â matching kanban-md, which
stamps `Updated` in every mutating command. Creating a task keeps
`updated` equal to `created`, restore/import paths preserve the
original timestamps verbatim, and materializing the on-disk view no
longer rewrites `updated` to the current time (it copies the ref values
unchanged).
- Fix `karr archive` dying with an opaque Path::Tiny error ("paths require
defined, positive-length parts") on ref-backed tasks â the normal case
since boards moved to `refs/karr/*`. Archive was the only mutating
command still calling `$task->save` (which needs an on-disk `file_path`)
instead of persisting through the board store like move/edit/pick/
handoff do. `Task::save` without a directory argument now croaks with a
clear message when the task has no `file_path`, instead of the
Path::Tiny error.
0.303 2026-06-28 02:07:23Z
- Docker: build Alien::FFI against the system libffi (apt libffi-dev) instead
of fetching a libffi tarball from a GitHub release page, which broke the
image build intermittently in CI (Alien::Build itself warns the
release-page download negotiator "will typically not work"). The runtime
image now ships libffi8 for the dynamically linked FFI::Platypus. The
vendored libgit2 (share) build is unchanged, so the runtime stays
self-contained.
0.302 2026-06-21 23:04:42Z
- `karr board` now renders a compact, Markdown-flavoured plaintext board
(board name as `#`, each status as `## Section`, one
`- id | title | meta...` line per task) instead of the coloured column
dashboard. The output stays clean when piped or redirected â colour is
added only when stdout is a terminal and `NO_COLOR` is unset. Default
(`medium`) priority is suppressed, and a new `--tags` flag prints each
task's tags on an extra indented line.
- Fix releasing a claim or unblocking a task leaving a null `claimed_by`,
`claimed_at`, or `blocked` field behind. Clearing now uses real Moo
clearers so the predicate drops and the field is omitted from the task
file, instead of being written as an explicit null that reloaded as
"still set" â which made `handoff` reject released tasks and `pick`
treat them as claimed. Explicit nulls in already-written or external
task files are normalized to "unset" on load.
0.301 2026-06-04 22:35:33Z
- karr-foundation: stream agent output to the terminal when interactive
(TTY detected) or --verbose is set. The parent process now reads the
child's output through a native pipe and fans it to the log, the
terminal, and an in-memory buffer â no external `tee` and no re-reading
the log by byte offset. The per-run timeout is `select`-based (robust
against Perl's deferred signals) and only fires when max_runtime > 0
(max_runtime: 0 disables it entirely). Output is always appended to
.karr.log regardless of TTY.
- karr-foundation is now a multi-board coordinator, not just an agent
runner. Agent execution is opt-in: with no agent configured on any
board, the default action is a read-only overview of every board
(status counts, in-progress/blocked, lock/cooldown state). `--status`
forces that overview regardless of configuration.
- karr-foundation: `claude: true` synthesizes the canonical claude
invocation so you needn't retype it; `claude_bin`, `claude_max_turns`
and `claude_permission_mode` override the parts. The agent instruction
is exposed as the `$PROMPT` substitution variable (settable via `prompt`
in .karr or `default_prompt` in config), usable in any command template.
- Activity log entries are now keyed by a role-qualified identity
(`refs/karr/log/<role>/<email>`, role `user` or `agent`) so a human and
an AI sharing one Git config are told apart. The role propagates to
nested karr calls via the KARR_ROLE env var (foundation sets `agent`);
pre-existing bare-email logs are still read for the `user` role.
- karr show: with no ID shows the single most recently updated task;
`--last N` widens that, `--me` shows the task(s) the current identity
most recently acted on (via the activity log), and `--agent NAME` shows
the task(s) most recently claimed by that agent name.
- karr board: hide the `@claimed_by` badge and claimed-count for tasks in
a terminal status (done/archived) â a claim is an active lease, and the
history remains in the activity log.
- sync: surface the real libgit2 error on a failed pull/push instead of a
meaningless "(exit code $?)" (native libgit2 operations have no shell
exit code). New Git `last_error` accessor records the last remote-op
exception.
0.300 2026-05-27 20:43:23Z
- Docker: bundle libgit2 (Alien::Libgit2 share build) so the runtime
image is self-contained. Builder installs cmake/pkg-config/zlib/
libssh2 dev headers and sets ALIEN_INSTALL_TYPE=share; runtime-base
installs libssl3/libssh2-1/zlib1g (the shared libs the vendored
libgit2.so links against). Needed since Git::Native moved to
Git::Libgit2 (libgit2 FFI).
- Add .github/workflows/ci.yml (perl 5.36/5.38/5.40) using the
[@Author::GETTY] dzil-test composite action; installs libgit2-dev so
Alien::Libgit2 links the system libgit2 (>= 1.5) in CI.
- Git.pm: read git config (user.name/email) and validate helper ref
names through Git::Native (Config + reference_name_is_valid) instead
of poking Git::Libgit2::FFI directly. New Git.pm `ref_oids` helper.
- karr-foundation: detect board changes via Git::Native instead of
shelling out to `git for-each-ref` â no git binary needed for that
path anymore. Sync (`--pull`) and open-task detection now run
in-process via App::karr::Git/BoardStore instead of forking the
`karr` CLI.
- karr-foundation: drain each board instead of a single run â invoke
the agent command repeatedly until no actionable task (non-terminal
and unblocked) remains. A task the agent claims but never moves is
auto-blocked after `max_attempts` stalls (default 2) so the drain
always terminates; the agent's own `--block` reason still wins.
Observable common errors (non-zero/timeout exit, or a log match
against rate-limit/auth/network/5xx patterns, extensible via
`error_patterns`) never penalize a task and instead trigger an
exponential per-repo cooldown (1, 2, 4, ⦠minutes, capped). New
`.karr` keys: `drain`, `max_attempts`, `max_iterations`,
`cooldown_base`, `cooldown_max`, `error_patterns`.
- cpanfile: require Git::Native 0.003 and Git::Libgit2 0.004.
- Fix `karr context` / `karr context --json` crashing with
"Can't locate object method 'strftime' via package 'Sun May ...'":
Cmd::Context now `use Time::Piece`, so `gmtime` returns a
Time::Piece object instead of a plain string. Added t/07-context.t
covering the plain, --json, and recently-completed cutoff paths.
- Fix `karr config show` (and get/set) crashing with
"Can't locate object method 'board_dir'": Cmd::Config now builds
its config via `$self->store->effective_config` and persists with
`$self->store->save_config`, instead of calling the non-existent
`board_dir` on itself. Added t/06-config-cmd.t.
- Drop hard-coded `tags = latest` / `tags = user` in the Docker
subsections so the new `[@Author::GETTY::Docker]` default
( run in 0.784 second using v1.01-cache-2.11-cpan-aadc1410aed )