App-karr
view release on metacpan or search on metacpan
never looked at the card again, so it claimed tasks that had been taken
in the meantime: 12 parallel picks on a fresh 12-task board told nine
agents they owned task 1, while the card named only the last of them. The
lock was not the hole â its holder identity is the clone's `user.email`,
which every agent on one machine shares, so all 12 acquired it quite
legitimately. Each candidate is now re-read from its ref under its lock,
re-tested with the same predicate, and written back under a
compare-and-swap on the OID it was read from; an agent that loses that
swap picks nothing and moves on. Verified with 12 forked contenders
behind a barrier: 12 picks, 12 different tasks, and every agent named on
the card it was told it got.
- Fixed one orphaned lock ref bricking every command on the board.
`list_task_refs` matched `refs/karr/tasks/N/lock` as well as `.../data`,
so a lock left behind by an agent that died mid-pick made its task id
exist after the card was deleted; `load_tasks` mapped that id to undef
and `list`, `board`, `materialize` and `pick` all died on it, with no way
out short of `git update-ref -d`. Only the data ref makes a task exist
now, and the board list never contains undef.
- `karr pick` no longer publishes its lock to the remote or strands its log
entry. The lock was released, and the pick logged, after the push, so the
remote kept the lock ref forever and the activity-log entry never left
the clone. Both now happen before the push.
- Locks expire. An agent that died between acquiring and releasing left a
lock nothing could ever clear, and its task stayed unpickable forever. A
lock older than the new `lock_timeout` board setting (default `5m`) may
be taken over, itself by compare-and-swap against the revision whose age
was judged, so a holder that refreshes in between is never silently
evicted. This is deliberately not `claim_timeout` (default `1h`): a claim
covers a work session, a lock covers one pick.
- New command `karr unlock`: with no arguments it lists the pick locks
currently held, with their holder, age, and whether they have expired;
given task ids or `--all` it breaks them. The manual escape hatch for a
stuck board, and the only one on a board that sets `lock_timeout` to
`0s`. Breaking a lock cannot corrupt a concurrent pick â the claim is
bound by the compare-and-swap on the card, not by the lock.
- Board ref commits carry the time they were written. The git signature was
built once and cached for the life of the process, so every ref a
long-running driver (`karr-foundation`) wrote was stamped with the time
of its first write.
- Fixed ordinary ref contention aborting commands with a raw libgit2 error
("failed to lock file '.../lock.lock' for writing") followed by a stack
trace of module paths and line numbers. Losing the race for a ref's lock
file is now retried with a randomised backoff, and a ref write that
genuinely fails reports a single karr-level line.
- Raised the minimum Git::Native to 0.004 and Git::Libgit2 to 0.005. Those
releases add compare-and-swap reference updates (`expected_old`) and
per-ref outcomes from fetch/push, which karr needs to make ID allocation
and lock acquisition atomic and to notice a server-rejected push. They
also fix git+ssh remotes under libgit2 < 1.7 by verifying the hostkey
against `~/.ssh/known_hosts`.
- Fixed a frontmatter value ending in `---` corrupting the task and
bricking the board. The closing delimiter was not anchored to the start
of a line, so a value that merely ended in `---` â `karr edit 1 --block
"waiting ---"` was enough, and YAML dumps such a value unquoted â cut the
frontmatter mid-line. Every command that loads the board then died with
"Missing required arguments: id, title", `delete` included, so the board
could not be repaired with karr at all. The parser now scans for `---` at
a line start, matching kanban-md.
- Fixed UTF-8 being encoded twice everywhere. karr passed YAML::XS::Dump
output (octets) around as characters, mixed that with character-level
file I/O, and never decoded `@ARGV`. Non-ASCII text was therefore stored
mojibaked in the refs, handed to agents mojibaked through `--json`,
written three encodes deep by `materialize`, and destroyed by
`backup`/`restore`; a correctly encoded kanban-md task file could not be
imported at all ("invalid trailing UTF-8 octet"). `karr show` looked
right only because two errors cancelled out. karr now keeps character
strings internally and encodes only at its edges â argv, stdout/stderr,
Git ref blobs, YAML and JSON â so non-ASCII titles, bodies, tags, and
board names round-trip and kanban-md interop works outside ASCII.
- Boards written by earlier versions keep working and are read correctly:
the double encoding is undone on load for any board without the new
`refs/karr/meta/encoding` marker. New command `karr repair` makes that
permanent â it reports by default, rewrites the affected refs with
`--yes`, and stamps the marker so nothing guesses at the board's bytes
again. It is idempotent, never rewrites a ref whose payload is ASCII, and
preserves task timestamps. `karr init` and `karr import --yes` stamp the
marker themselves.
- Fixed a runaway that could take the whole machine down: after a command
died (a plain usage error was enough), the sync guard was only reaped in
Perl's global destruction and pushed from there. libgit2 is reached
through FFI::Platypus, whose type parser and library-search tables are
already being freed in that phase, so the push re-entered them and
recursed without bound â observed at 53 GB RSS on a 62 GB box, killable
only from outside. It needed a board with a remote to trigger, which is
why boards without one never showed it. App::karr::Git now refuses every
native operation while `${^GLOBAL_PHASE}` is `DESTRUCT`, so the whole
class of teardown re-entry degrades into an ordinary error.
- Behaviour change on the die path: a sync guard reaped in global
destruction reports instead of pushing. When refs were written but never
pushed it now prints "Local refs are intact. Run 'karr sync' to push
them."; when the command died before writing anything it stays silent.
The automatic insurance push promised for this window never actually
worked (it either recursed as above or was lost as an "(in cleanup)"
warning) â making it deterministic is what exposed that. Restoring a real
push is tracked separately.
- And now restored: a command that dies after writing refs pushes them
before the process exits again, instead of only advising `karr sync`.
Armed sync guards register in a process-wide registry that `karr` drains
from an `END` block â the last point at which pushing is still safe, and
one that also covers the `exit` calls inside command bodies. A command
that died before writing anything still pushes nothing and says nothing,
and a push that fails there warns without touching the exit code. The
global-destruction report above stays as the last resort for embedders
that never drain the registry.
- A writing command whose push fails no longer retries six times. Its
`sync_after` disarms the guard after spending its own three attempts, so
the new `END` flush does not repeat the identical failing push on a
command that is already reporting the failure.
- `karr skill show` no longer warns "Wide character in print". The bundled
skill file is read decoded, so it is now encoded back to UTF-8 bytes at
the one print site. The output bytes were always correct, but the warning
was noise on stderr â and it ended up inside the written file whenever
someone refreshed an installed SKILL.md with `karr skill show >file 2>&1`.
- The installed executables `karr` and `karr-foundation` now carry a
`$VERSION`. Both shipped versionless through 0.400, 0.401 and 0.402: the
woven POD had a VERSION section (generated from the dist version), but the
code itself declared none. Dist::Zilla only inserts a `$VERSION` into a
file that has a `package` statement, which a script does not â so the
line has to exist once, after which every release keeps it in step.
- Fixed silent loss of another agent's work on every shared board. `push`
sent `refs/karr/*` with a forced refspec but `pull` fetched with a
non-forced one, and since karr writes every board ref as a parentless
commit, no update is ever a fast-forward. libgit2 declined the update
without reporting an error, so `karr sync` said "Done." and exited 0 while
the local ref stayed stale â and the next write force-pushed that stale
ref over the other agent's version. A pull now really applies what the
remote has, so the board settles on last-writer-wins with no stale read in
between. The first sync of a task always worked, which is why this hid.
- A task deleted in one clone stays deleted instead of being resurrected by
the next clone that writes. Push already pruned, so `karr delete` was not
durable in any multi-clone setup.
- `pull` no longer fetches straight into the board. The remote state lands
in a per-remote tracking mirror under `refs/karr-remote/`, and the board is
reconciled against it. That mirror is what tells a ref the remote deleted
apart from one that only exists locally because its push failed â the
first is pruned, the second is kept, which is what karr promises with
"Local refs are intact. Run 'karr sync' to retry.". It also means a board
that `karr init` created and never pushed, and a clone that has never
fetched, both survive their first sync untouched.
- A ref that changed on both sides since the last sync is no longer resolved
in silence. The remote version takes the slot, the local one is kept under
`refs/karr-conflict/` and a warning names both, so the overwrite is
reported and recoverable rather than invisible. Neither `refs/karr-remote/`
nor `refs/karr-conflict/` is ever pushed or shown on the board.
- Helper refs (`karr set-refs` / `get-refs`) are fetched forced for the same
reason, so `karr get-refs` no longer serves a stale local copy after the
ref changed on the remote.
- The git-CLI transport fallback and libgit2 no longer disagree on a
diverged board. The fallback used to fail with "non-fast-forward" on every
ref, which made every writing command fail permanently â including `karr
sync`, so the clone could only be recovered with raw git. Both transports
now take the same refspec, and the reconciliation that follows the fetch
is the same code either way, so they reach the same board state.
- The git-CLI transport no longer reports success when `git` dies from a
signal. It checked `$? >> 8`, which is 0 for a signal death as well as for
a clean exit, so a `git` stopped by the OOM killer, a Ctrl-C on the process
group or a SIGPIPE was announced as a completed transport while the remote
received nothing. The signal is now reported as a failure, with its number.
- The git-CLI transport no longer deadlocks past 64 KiB of stderr. It read
stdout to EOF before touching stderr, so once the child filled the stderr
pipe buffer neither side moved again â reachable with roughly 700 rejected
refs, and possible after the command had already printed its result. Both
streams are drained together now, and a run is bounded by a wall-clock
timeout (120s by default, `KARR_TRANSPORT_TIMEOUT` overrides, `0` disables)
after which the child is killed and the transport reported as failed.
- `set-refs`, `get-refs` and `agent-name` are reachable again with `--dir`.
The dashed spellings were rewritten to their internal command names only
at `$ARGV[0]`, so `karr --dir PATH get-refs REF` â the shape an
orchestrator driving karr from outside the target repository uses â died
with "Unknown command: get-refs". They are now registered as command names
( run in 1.359 second using v1.01-cache-2.11-cpan-364913b4093 )