App-karr
view release on metacpan or search on metacpan
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
a recorded pid the foundation wrote itself â a stale lock whose
holder died is held=false and a fresh tick takes over without
manual cleanup. Path::Tiny's `slurp_utf8` does an internal blocking
flock that hangs forever when the same process already holds one,
so the metadata read is a raw `sysread` loop.
- An agent killed by a signal is now booked as `128 + signum`, not as
a clean exit 0 (#164). The runner used to compute `$exit_code =
$? >> 8` â the high 8 bits, which are 0 for any child that died
from a signal. The OOM-killer, an external SIGTERM, a SIGSEGV, and
any other signal-death shape were all booked as a clean run:
`last_error` stayed unset, the cooldown that exists to back off
after a machine-killing agent never engaged, and the next cron
tick re-launched at full rate. The fix reads both halves of `$?`:
signal death becomes `128 + signum` (the shell convention, so
SIGTERM=143, SIGKILL=137, SIGSEGV=139, SIGINT=130); a normal exit
falls through to `( $? >> 8 ) & 255`. The timeout path's exit code
already used this convention; the classifier now matches it for
every signal, including the ones we don't fire ourselves.
- `karr pick` ranks candidates and `karr context --sections in-progress`
lists in-progress tasks by the board's own `priorities` and `classes`
lists, not by a hardcoded table (ticket #149). `Cmd/Pick.pm` used to
sort through `App::karr::Config->priority_order` and `->class_order` â
class methods that only knew the four default priorities and four
default classes. On a board imported from kanban-md with a longer
( run in 1.931 second using v1.01-cache-2.11-cpan-14f38c9f855 )