App-karr

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

      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

Changes  view on Meta::CPAN

      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

Changes  view on Meta::CPAN

      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

Changes  view on Meta::CPAN

      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`

Changes  view on Meta::CPAN

      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 )