App-karr

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

      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
      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

Changes  view on Meta::CPAN


    - karr-foundation no longer throws away a successful agent run because of
      something it printed (ticket #160). The common-error scan ran on every
      run before anything asked whether the run had worked, over the whole
      transcript, against bare substrings — network, quota, credentials, 401,
      403, 429, 503. An agent working a karr board prints the board, so a
      backlog line reading "retry the network fetch on 503" matched, and so
      did a diffstat of 403 changed lines. The drain aborted, the cards the
      agent had just moved were credited to nobody, and the cooldown climbed
      1m, 2m, 4m … 64m without ever resetting, because the next run printed
      the same words: a healthy board throttled to one discarded run per hour.
      What a run did is now asked before what it printed. A run that exited 0
      and moved the board is progress whatever scrolled past, and is never
      reclassified by its own output; the scan is evidence only where there is
      none other, a run that moved nothing — which is what a rate-limited or
      unauthenticated agent looks like. A pattern seen in a run that did move
      the board is noted in `.karr.log` and otherwise ignored. The default
      patterns are narrow to match: a symptom word counts next to a failure
      word on the same line ("network error", "invalid credentials", "quota
      exceeded"), never on its own, and an HTTP status only where something
      adjacent marks it as one ("API error: 429", "429 Too Many Requests") —
      not in a diffstat, a byte count, a line number or a commit hash. Genuine
      failures reported by an agent that still exits 0 keep triggering the
      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.

Changes  view on Meta::CPAN

      on top of the two the distribution configures by name. That one has no
      C<target>, so it built the last stage in the F<Dockerfile> — which is
      C<runtime-user>, not C<runtime-root> — and no C<tags>, so it inherited
      the plugin default C<latest %V %v>: exactly the tags C<runtime-root>
      publishes. Which image C<raudssus/karr:latest> ended up carrying
      therefore depended on the order the plugins happened to run in. Only
      the two named builds run now.
    - The bundled agent skill documents C<karr materialize>, C<karr import>
      and C<karr repair>, which it had never mentioned, spells
      C<karr agent-name> the way the command table does, and no longer
      describes C<karr handoff> as moving to a literal C<review> — since
      ticket #102 the target is the board's review column, or its last
      non-terminal column on a board that has none. This applies to
      F<share/claude-skill.md>, the copy C<karr skill install> writes into
      other projects, so an agent set up by karr gets the corrected text
      (ticket #117 tracks that this copy and the one in this repository are
      kept in step by hand).
    - App::karr::Git now resolves every path it hands git from the work tree
      root, on both routes into is_tracked_under (tickets #113 and #114). The
      string comes out of _relative_to_root, which measures from the root, and
      libgit2 resolves it that way by itself — but the `git ls-files` fallback
      ran as `git -C ->dir`, and a pathspec is resolved against the process
      cwd. Build the class on a subdirectory, as its own SYNOPSIS shows with
      `dir => '.'`, and it asked about `subdir/tasks` while the caller asked
      about `tasks`; a pathspec that matches nothing exits 0 with no output,
      which reads back as "not tracked", so a project that owns `tasks/` would
      be told it does not — the symptom ticket #89 removed, through a different
      door. The CLI is now pinned to the root, which the transport verbs cannot
      tell apart. The root itself is `.`, a pathspec git understands but not a
      path the index can hold — entries are stored as `tasks/a.md`, never
      `./tasks/a.md` — so the native route answered 0 for a repository full of
      tracked files; at the root the question is now whether the index holds
      anything at all, which is what `ls-files -- .` answers there too. No
      karr command changes behaviour: every is_tracked_under call goes through
      the store's Git, which App::karr::Role::BoardDiscovery builds at the
      repository root, and none of them passes the root as the path. Both were
      latent, and each was a wrong answer rather than a failure — the kind that
      would have surfaced as the answer depending on whether libgit2 was
      available to ask.
    - App::karr::Git::is_tracked_under now reads the index natively, through
      Git::Native::Index, and only falls back to `git ls-files` when libgit2
      declines to answer (ticket #107). That question decides whether `karr
      init` and `karr materialize` may claim `tasks/` and `config.yml` in
      .gitignore, and it used to be asked through the git CLI unconditionally
      — not as a fallback, but because the Git::Native of the day exposed no
      index at all. With no `git` on PATH the run simply failed, the answer
      came back "not tracked", and both commands wrote the entries over paths
      the project already tracks, undoing ticket #89 in that configuration.
      The native route needs no `git` binary, so that configuration now
      answers correctly; the CLI remains for an index libgit2 cannot read,
      with the reason in last_error. Requires Git::Native 0.005 and
      Git::Libgit2 0.006.
    - Fixed the em dash literals that reached users double-encoded (ticket
      #108). No file under lib/ or bin/ says `use utf8`, deliberately: non-ASCII
      belongs in data, and App::karr::Encoding owns every character/octet
      crossing. Eleven string literals in executable code carried a pasted em
      dash anyway, so Perl read its three bytes as three Latin-1 characters and
      the `:encoding(UTF-8)` layer encoded each of them again — the user saw a
      stray a-circumflex and two control characters where a dash belonged. The
      worst was `karr context`, which renders one on every noted item in the
      blocked, overdue and recently-completed sections, both on stdout and into
      the file `--write-to` names; `karr-foundation` accounted for the other
      ten, including the TIMEOUT notice App::karr::Foundation::Runner appends to
      `.karr.log`, which corrupted a file on disk and not merely a terminal. All
      eleven now spell the character `"\x{2014}"`, which also restores byte
      compatibility with kanban-md's own context block. t/124-source-ascii-only.t
      polices the class from here on, using PPI so that the em dashes in POD and
      comments — harmless, and plentiful — raise nothing.
    - Closed the last of the role import leaks: App::karr::Role::ClaimTimeout
      and App::karr::Role::TaskMutation no longer compose Time::Piece's
      `localtime` and `gmtime` into the commands that consume them — `move`,
      `edit`, `delete`, `archive`, `handoff`, `pick` and `unlock` (ticket #105,
      finishing #38). These were the worse half of that family, because the two
      shadow builtins: a later `sub localtime` or an attribute of that name on a
      command class would have fought an inherited Time::Piece export, and the
      failure would have read as a core function misbehaving. Time::Piece is not
      a drop-in for the usual cure — replacing the builtins is its whole point —
      so the call sites were decided one at a time instead of swept.
      ClaimTimeout keeps the module and spells its one live call
      `Time::Piece::gmtime()`, because `_claim_expired` needs the overloaded
      object and the builtin would hand that subtraction a string; TaskMutation
      never asked for the time at all and drops the module, since the lifecycle
      stamps are written by App::karr::Task. Nothing called either as a method,
      so no behaviour changes, and t/121-role-import-leakage.t now runs with an
      empty allow-list.
    - Finished the sweep that stopped karr's own source locations reaching the
      user (ticket #77). `croak` appends " at Some/Module.pm line 42." even to a
      message that already ends in a newline, so anyone who ran `karr list`
      outside a repository was told "Not a git repository. karr requires Git."
      and then handed the file and line of the builder that said so; every
      remote failure ended with a line number in whichever `Cmd/*` had called
      the sync; and `karr-foundation` reported a broken config the same way.
      Those, plus the pipe/fork/log-open failures in the foundation runner, now
      go through `App::karr::Error::user_error` and print the message alone. The
      four commands that let a Path::Tiny error out raw — `karr restore
      --input` on an unreadable file, `karr backup --output` and `karr context
      --write` into a directory karr may not write, `karr init --claude-skill`
      into an unwritable `.claude` — now name the path the user typed and the
      reason the OS gave, and nothing else. Two errors deliberately keep their
      call site, because there it is the useful part: saving an unpersisted
      ref-backed task, which is a programming error, and `croak` in
      App::karr::Foundation's YAML report, whose parser message names its own
      document, line and column and is passed through whole rather than reduced
      to one line.
    - A failed sync now shows git's error once instead of twice. The message
      that ended the command embedded another copy of the multi-line error that
      had already been printed the moment it happened — so one failed pull put
      the same "does not appear to be a git repository" block on the screen
      twice, and `--quiet`, which suppresses the retry banners and never the
      errors (that is deliberate, ticket #27), made no difference to the
      duplicate. `sync_before` now ends on the verdict alone, the way
      `sync_after` always has: "Pull failed after 3 attempts. Nothing was
      changed. / Run 'karr sync' to retry." A cause that *changes* between
      attempts is still reported each time.
    - App::karr::Role::BoardDiscovery and App::karr::Role::SyncLifecycle no
      longer compose their imports into the ~20 command classes that consume
      them (ticket #38). A Moo::Role copies every sub in its package into its
      consumers, imported ones included, so `use Path::Tiny;` and
      `use Carp qw( croak );` in a role made `$cmd->path(...)` and
      `$cmd->croak(...)` callable on every command. Nothing called them, so
      nothing was broken — but the first command class to want an attribute

Changes  view on Meta::CPAN

      `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`
      out of it for good — `karr create` typed in the wrong directory was
      enough, and `karr destroy --yes` was the only way back. A board now
      counts as existing only when `refs/karr/config` is present; the commands
      that write to the board refuse with "No karr board found" when it is
      absent, and `karr init` completes a half-board (keeping its ID counter,
      so existing tasks are not overwritten) instead of refusing. `backup`,
      `destroy`, `materialize` and `repair` still work on whatever is under
      `refs/karr/`, so a half-board an older karr left behind can still be
      inspected and removed. `karr import --yes` is still allowed to bootstrap
      a board from a bare kanban-md `tasks/` view, and now writes the default
      config ref when the view has no `config.yml`, so the board it leaves
      behind is one the writing commands accept. The read-only commands
      (`list`, `board`, `show`, `context`, `log`, `config get`) are unchanged:
      they still report an empty board with the default config rather than
      refusing.
    - Fixed a ref deletion that did not happen reporting success. `delete_ref`
      discarded the libgit2 error and always incremented the write counter
      SyncGuard reads to decide whether local refs still need pushing, so a
      failed or no-op delete both claimed success and left the push insurance
      believing there was unpushed work. It now returns false when nothing was
      removed, counts only deletes that landed, and retries lock contention
      like every other ref write; clearing a whole namespace re-reads it
      afterwards, so `karr destroy` can no longer report success over refs that
      are still there.
    - Fixed every task write path accepting a status, priority, class or due
      date that does not exist. `karr move 1 totally-invalid`, `karr create x
      --priority bogus`, `karr edit 1 --status bogus` and `karr pick --move
      bogus` all exited 0 and wrote the value to the board, which then sat in
      no column — invisible on `karr board`, still counted in the total, and
      `karr move --next` died on it. Those values are now checked against the
      board config before anything is written and rejected with exit 2, the
      usage-error code from ADR 0002. Status names are checked in the one
      shared status-change path, so `move` and `edit --status` cannot drift
      apart. A due date must be a real calendar date in `YYYY-MM-DD`, so
      `2026-02-30` is refused as well. A batch `edit` or `move` writes nothing
      at all rather than updating half the ids, and a rejected `create` no
      longer consumes a task id. Validation is on the write path only: a board
      that already carries a bad value stays readable so `karr move` can put it
      back.
    - Fixed `blocked` being incompatible with kanban-md. karr stored the
      blocking reason as free text in `blocked`; kanban-md has a boolean
      `blocked` plus a `block_reason` string, and its parser refuses a string
      there outright — a karr-blocked task was skipped as malformed and
      vanished from its board. karr now writes the kanban-md shape, and
      `--json` reports `blocked` as a JSON boolean instead of sometimes a
      string and sometimes `true`. `karr edit --block "reason"` and `karr
      handoff --block` are unchanged and still set both fields. Existing boards
      need no migration: a legacy free-text `blocked` is recognised on read and
      converted on the next write of that task.
    - Fixed unknown frontmatter fields being deleted on the first write. Any
      key karr did not model — a newer kanban-md field, a note added by hand in
      an editor — was dropped when the task was next saved. Unknown keys are
      now carried through untouched. They are not order-preserved: karr's YAML
      output is key-sorted, so a passthrough field lands in alphabetical
      position.
    - Fixed the lifecycle timestamps. `completed` was never cleared when a task
      was reopened, so every reopened task still looked finished; `started` was
      only set for the literal status `in-progress` and `completed` only for
      the literal `done`, so a move straight to `done` or `archived` recorded
      neither; and `started` was written as a bare date while every other
      timestamp carried a time. All four are fixed, and `karr archive` and
      `karr handoff` now maintain the stamps as well. Unlike kanban-md, karr
      does not re-stamp `completed` when a finished task is archived — the date
      it was actually finished is kept.
    - Fixed `karr create --body 0` silently dropping the body, and `karr show`
      not printing a body of `0`.
    - Fixed a body losing one trailing newline on every save. The two storage
      paths disagreed about it, so a body ending in blank lines shrank each
      time the task was written. Both paths now agree: a stored body never ends
      in a newline.
    - Fixed `claim_timeout` silently meaning one hour for any compound
      duration. Only `1h` and `30m` shapes were understood, so a config
      imported from kanban-md with `claim_timeout: 1h30m` meant 90 minutes
      there and 60 here. The full Go duration grammar is now parsed (`1h30m`,
      `90s`, `2h45m30s`, `0.5h`); `7d` is still rejected, as it is by Go.
    - Fixed task filenames being cut mid-word at 50 characters while kanban-md
      trims to the last word boundary, which gave the same task two different
      filenames in a shared `tasks/` directory.
    - Fixed `karr import` accepting a `config.yml` that does not validate — a
      `defaults.status` naming a status that does not exist was written to the
      board and then applied to every new task. The board config is now
      validated wherever karr writes it (`import`, `config set`, `disable`).
      `karr restore` is deliberately not covered: it replaces refs verbatim
      from a snapshot. `karr config show` also no longer dies with a raw Perl
      error on a board whose config is already broken.
    - Fixed parallel `karr create` silently losing tasks. Task IDs were handed
      out by reading a counter ref and writing it back, so two agents that read
      it at the same time were given the same ID and the second task ref
      overwrote the first — 40 successful creates produced 32 tasks, with no
      warning on either side. Allocation is now a compare-and-swap against the
      counter ref, retried on contention, so concurrent agents always get
      distinct IDs.
    - Fixed task locking granting the same lock to every agent at once. Lock
      acquisition checked the lock ref and then wrote it, so all 16 contenders
      in a race passed the check and all 16 were told they had acquired it,
      while the ref could only hold one. Acquisition is now an atomic
      create-if-absent: exactly one agent wins and the rest get the usual
      "locked by ..." answer. On its own that does not make `karr pick` safe —
      see the entry below, which is what actually fixes concurrent picking.
    - Fixed `karr pick` handing the same task to several agents. Pick ranked
      candidates from a snapshot of the board read before any lock existed and
      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

Changes  view on Meta::CPAN

    - An unknown subcommand (`karr definitely-not-a-command`) now fails loudly
      with "Unknown command: ..." on STDERR and a non-zero exit instead of
      silently printing the board summary with exit 0 — a typo like
      `karr agent-name` (for `agentname`) used to look like success. Bare
      `karr` (with or without options like `--done`) still renders the board.
    - `karr archive` with IDs that do not exist now exits non-zero, matching
      the die-based behaviour of every other id-taking command (show, move,
      edit, delete, handoff already did this). In a comma-separated batch
      (`karr archive 5,99`) the existing tasks are still archived and the
      missing ones reported — partial success is kept, the exit code reports
      the failure (kanban-md parity). Re-archiving an already-archived task
      remains a successful no-op.
    - `karr board` (and bare `karr`) no longer lists done tasks by default —
      on a living board the Done section grows forever and drowns the open
      work. The footer instead notes how many were hidden ("10 tasks (5 done
      hidden)"), and the new `--done` flag restores the full listing. Applies
      to the default, `--tags`, and `--json` renderings (JSON keeps the done
      column and its real count but empties its task list unless `--done` is
      given); `--compact` still shows every status. This deliberately deviates
      from kanban-md, whose board always renders done tasks.
    - Fix the `updated` timestamp never being bumped when a task is mutated
      through the ref-backed store: move, edit, pick, handoff, and archive all
      left `updated` at its previous value, so `karr show` (most recently
      updated), `karr show --last N`, and `karr list --sort updated` gave
      wrong answers. The bump now happens centrally in the board store
      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
      (`latest %V %v`) applies. `runtime-user` keeps a `-user`
      suffix on each tag.
    - Add `karr-foundation` binary and `App::karr::Foundation` module:
      single-shot daemon for periodic agent execution across multiple karr
      boards. Reads `~/.config/karr-foundation/config.yml` (dirs: / scan:),
      checks each repo for board changes or open tasks, and invokes the
      per-repo `.karr` command. Supports `--force`, `--dry-run`, `--verbose`.
      Per-repo state in `.karr.state` / `.karr.lock` / `.karr.log` (gitignored).

0.202     2026-05-17 05:17:07Z

    - Fix `karr list` crashing with "Can't locate object method 'load_tasks'":
      Cmd::List was missing `with 'App::karr::Role::BoardAccess'` (the role was
      `use`d but never consumed). Surfaced while writing worktree tests.
    - Add t/29-worktree.t covering init/create/list inside `git worktree`
      directories and verifying refs/karr/* are correctly shared between the
      main work-tree and additional worktrees.

0.200     2026-05-16 17:45:23Z

    - Centralize config knowledge: priority_order(), class_order(),
      terminal_statuses(), is_terminal_status(), status_requires_claim()
      moved to Config and BoardStore (no more duplication across commands).
    - Add all_status_names(), status_requires_claim(), is_terminal_status()
      to BoardStore for encapsulated status config access.
    - Convert all require Time::Piece to use Time::Piece (Pick, Move, Edit).
    - Extract append_log into App::karr::ActivityLog module.
    - Architecture refactor: split Role::BoardAccess into Role::BoardDiscovery +
      Role::SyncLifecycle. Commands now work directly on refs via BoardStore
      instead of via a materialized temp directory.
    - Add SyncGuard (push insurance on die/croak), effective_config() on BoardStore,
      and $self->config via Role::BoardDiscovery.
    - Add tasks/ to .gitignore (never commit materialized view).
    - Fix CPAN smoker failures: skip git tests on old git (< 1.8.5, no -C flag)
    - Fix skip() without SKIP block in t/11-git-impl.t (Test::More crash)
    - Skip user.email test gracefully when not configured

0.101     2026-03-23 03:02:05Z

    - Strengthen docs and GitHub landing pages



( run in 1.286 second using v1.01-cache-2.11-cpan-788537b7465 )