App-karr

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

      `karr sync` has run, and `karr init` is the wrong answer there. That is
      the loop this came out of — an agent read an empty board, believed it,
      and reached for `init`.

    - The runtime images can reach an `ssh://` remote (ticket #134). They
      shipped without an ssh binary — `runtime-base` installed `git gosu passwd`
      and the shared libraries, and nothing else — so git's CLI fallback died
      with `error: cannot run ssh: No such file or directory` and a board on an
      ssh remote was unreachable from the published images. That fallback is not
      decoration: it is there for the ssh-config and `ProxyCommand` setups
      libgit2 cannot do, and it could never take a single one of them.
      `openssh-client` is now installed with the rest. The other half was the
      README's recommended alias, which mounted `.gitconfig`, `.claude`,
      `.codex` and `.cursor` but not `.ssh`, while setting `HOME=/home/karr` —
      so libgit2 looked for `known_hosts` in a directory that did not exist and
      reported every host as unknown, and the fix it printed
      (`ssh-keyscan … >> ~/.ssh/known_hosts`) was carried out on the host, where
      the container never saw it. The alias now mounts `~/.ssh` read-only, and
      an agent-forwarding variant is documented as a shell function, since
      `docker run` rejects the socket mount outright when no agent is running.
      Neither half helps alone: an ssh binary with no keys cannot authenticate,
      and mounted keys with no ssh binary cannot fall back. A third piece only
      turned up when the finished image was pointed at a real ssh remote: the
      root image drops to whoever owns F</work>, and that host uid has no
      C</etc/passwd> entry, so C<ssh> — which looks itself up with
      C<getpwuid()> — refused to start with `No user exists for uid 1000`. The
      entrypoint now writes an entry for the uid before dropping to it. The
      fixed-user image never had this problem, since C<useradd> wrote one at
      build time; the default image, the one the README recommends, had it for
      every ssh remote.

    - `karr init` no longer stamps the encoding marker on a board it is
      completing rather than creating (ticket #132). `init` accepts a
      half-board — task refs present, `refs/karr/config` missing — and finishes
      it (#62), but it used to write `refs/karr/meta/encoding` on the way out
      either way. On a board from 0.402 or earlier that marker asserts the
      opposite of what the adopted task refs carry: the read path stopped
      undoing their double-encoded UTF-8, every old card turned to mojibake
      (`… Transport prüfen —` became `… Transport prüfen â`), and `karr
      repair` then reported the board as already up to date and declined to
      migrate it, leaving hand-deleting a ref as the only way back. Not one
      byte in the refs changes in that failure, which is why it was invisible.
      The marker is now written only by an `init` that found nothing at all
      under `refs/karr/`, so a completed half-board keeps both the repair on
      read and `karr repair --yes`. `karr import --yes` had the same defect and
      the same fix: it rewrites the task refs from the file view but never the
      activity log under `refs/karr/log/` (nor the config, when the view
      carries no `config.yml`), so on a pre-0.403 board it stamped a claim it
      could not make and turned every old log entry into mojibake. It now
      stamps only a board that import itself created. `karr repair --yes` is
      unchanged and remains the one command that may stamp an existing board,
      because it is the one that rewrites every ref.
    - "No karr board found. Run 'karr init' to create one." no longer speaks
      for two different repositories (ticket #133). Write commands raised it
      whenever `refs/karr/config` was absent, including on a repository whose
      tasks, counter and log were all there — so a board missing exactly one
      ref reported itself as never having existed. An agent hit that on a
      repository holding 21 tickets, believed them gone, and ran `karr init`,
      which at the time also broke how they were read (#132 above). A
      repository with nothing under `refs/karr/` still gets the old sentence,
      the one `backup`, `destroy`, `materialize` and `repair` raise for the
      same state. A half-board now gets its own: it is named as one, the number
      of task refs at stake is stated, and `karr init` is described as
      completing the board while keeping what is already there. `karr init`
      likewise reports which of the two things it did.
    - `karr config get statuses` and `karr config get classes` are readable
      again (ticket #130). Both lists allow an entry to be either a bare name or
      a mapping — `{ name: in-progress, require_claim: true }`,
      `{ name: expedite, wip_limit: 1 }` — and the renderer joined the raw list,
      so every mapping printed as `HASH(0x558580cd1688)`. The default board hits
      it twice in `statuses` and, because all four of its classes are mappings,
      four times out of four in `classes`, where the answer carried no name at
      all. An entry now renders as its name followed by its per-entry settings
      in parentheses — `in-progress (require_claim: 1)`, `expedite
      (bypass_column_wip: 1, wip_limit: 1)` — and `karr config show` renders the
      two lists the same way instead of dropping the settings, so the overview
      and the single-key lookup cannot disagree about what the board's columns
      are. `--json` is unchanged: it already carried the entries as configured,
      and still does. This was not cosmetic — that output is what a reader
      consults to learn which columns a board has, and the unreadable entries
      led an agent to describe an "extended status set" with a `closed` status
      that no board in the tree configures.
    - The `runtime-user` image build no longer warns about `/home/karr`. The
      shared `runtime-base` stage used to create that directory, so
      `runtime-user`'s `useradd -m` was asking for a home that already existed
      and printed `useradd: warning: the home directory /home/karr already
      exists.` plus `Not copying any file from skel directory into it.` on every
      build. `runtime-base` now creates only `/work` and leaves the home to
      whoever owns it — `useradd -m` in `runtime-user`, and in `runtime-root` the
      entrypoint, which has to `mkdir` it anyway for the uid it drops to, so
      nothing there depended on the base stage doing it. Two visible
      consequences for the `-user` image: the home is now `0700` rather than
      `0755`, because `useradd` applies Debian's `HOME_MODE` instead of
      inheriting a `mkdir` default, and it carries the `/etc/skel` files
      (`.bashrc`, `.profile`, `.bash_logout`) that the warning had been skipping.
      Neither affects the image as documented — it runs fixed as `USER karr`,
      which owns that directory, and the README's `-v …:/home/karr/…` mounts are
      unaffected. Overriding the uid at `docker run --user` time is the one thing
      that gets stricter; that case is what the root image and its entrypoint are
      for.
    - The README's recipe for a custom fixed-user image passed `.` from a git
      checkout as the Docker build context, which cannot work: the `builder`
      stage installs the tree with `cpanm`, and a Dist::Zilla distribution has no
      `Makefile.PL` until it has been built. It now points at a built
      distribution — an unpacked CPAN tarball, or `dzil build --no-tgz` output —
      and mentions that `dzil build` already produces both published images
      itself through the `[@Author::GETTY::Docker]` sections.
    - `karr list --json` now carries the task body (ticket #129). It built its
      payload from `Task->to_frontmatter`, the YAML frontmatter view — and the
      body lives *below* the frontmatter in the file format, never inside it,
      so every card came out of `list --json` without its text while `show`,
      `pick` and `handoff`, which go through `Task->to_json_hash`, shipped the
      same card whole. Nothing documented the difference: `--json` was
      described as "machine-readable" without a caveat, and `to_json_hash`
      listed its users without noticing `list` was missing from them. kanban-md
      marshals the full task structs in `cmd/list.go`, with
      `json:"body,omitempty"` on `Body`, so this was also a parity gap. In
      practice it forced anyone reading ticket text by machine into one `show`
      per id — N+1 calls for what one call can answer. An absent body stays an
      absent key rather than an empty string, as kanban-md's `omitempty`
      spells it, and a body of `"0"` counts as text (the #78 rule: emptiness is

Changes  view on Meta::CPAN

      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
      named `path` would have fought an inherited Path::Tiny constructor for it,
      silently. Both roles now load what they need with an empty import list and
      qualify the call, which is what App::karr::Role::Output and
      App::karr::Role::TaskMutation already did. The `use Time::Piece;` that was
      out of scope here went with ticket #105 above. Still leaking, and not
      karr's to fix: MooX::Cmd::Role composes `croak` into every command class
      from upstream.
    - Fixed an ordinary kanban-md round trip silently switching a disabled
      board back on. kanban-md rewrites `config.yml` the moment it loads one —
      it migrates the schema version and re-serialises the file from its own
      structs — so afterwards every key that schema does not know is gone from
      the view, karr's `foundation` among them. `karr import --yes` then
      replaced `refs/karr/config` with what was left, and a board switched off
      with `karr disable` came back enabled, with karr-foundation resuming
      agent runs on it; nothing warned at any step. Import now reconciles the
      view against the board config instead of replacing it — the view speaks
      for the keys it carries and karr models, and every other key keeps what
      the board already said. `lock_timeout`, karr's other own key, was being
      reset to the default by the same route and is preserved likewise.
    - Fixed that same round trip recording kanban-md's migrated defaults as
      deliberate per-board overrides. Its rewritten `config.yml` carries
      `version: 10`, a fully expanded `statuses` list decorated with the
      `show_duration` flag karr does not model, and a whole `tui` block —
      diffed against karr's defaults, all of it looked changed, so the board
      froze a copy of another tool's defaults and stopped following karr's own.
      The view's keys are now pruned to what karr models and normalised to the
      shape karr writes them in, so a migrated config compares equal to the
      defaults and is not stored as an override. Two consequences worth
      knowing: a kanban-md `tui` or `wip_limits` block is no longer carried
      across the bridge, since it is not karr board config and `karr config`
      can neither show nor set it; and a board already polluted by an earlier
      import keeps its stale `version` until something rewrites the config.
    - Fixed `karr import` walking the task id counter backwards. `karr
      materialize` writes `next_id` into the file view because kanban-md
      requires it, but import threw that copy away and re-seeded purely from

Changes  view on Meta::CPAN

      and karr-foundation counted the card as one its agent had engaged, which
      is what feeds the auto-block. Rather than a fifth per-reader test, an
      optional field whose value has no length now loads as unset, once, in the
      task model — which is what `omitempty` on kanban-md's Go struct means and
      where the asymmetry came from. `0` and `"0"` are one character long and
      stay values. This changes what karr writes: a card imported carrying
      those empty keys loses them on its next write, which is exactly the shape
      kanban-md's own writer produces, verified by a round trip through both
      tools.
    - Fixed `completed` never being recorded on a board whose final column is
      not named `done`. Which move counts as finishing was decided against a
      hardcoded pair rather than against the board, so on a board ending in
      `shipped` `karr move 1 shipped` recorded the start of the work and never
      its finish — and everything built on that stamp, `karr context`'s
      recently-completed section and the cycle times `karr metrics` is meant to
      report, was empty for ever. `karr move`, `karr edit --status`,
      `karr archive` and `karr handoff` now hand the board's own configuration
      to the lifecycle rules.
    - Fixed `karr context`'s recently-completed section, which had never
      produced a single entry on any board: it was selected out of the
      non-terminal tasks, so the terminal statuses it looked for could not be
      among them. It now scans the whole board, bounded by the completion stamp
      within `--days` (default 7), the way kanban-md bounds it. Cards in
      `archived` stay out — those are filed away, not recent.
    - Fixed `karr init` adding `tasks/` and `config.yml` to .gitignore even
      when the project already tracks content at those paths. Git applies no
      ignore rule to a file it tracks, so the entries changed nothing at all
      while telling every later reader that karr owned paths the project owns —
      and they said so right where `karr materialize` refuses to write, for
      that very reason. init now leaves .gitignore alone in that case and
      reports what it did not do.
    - Changed `karr list --sort priority` to open with the most urgent task —
      critical down to low on a default board — instead of following the
      config's priority order ascending (ticket #91). Ascending order opened
      the list with the least urgent task, the exact opposite of what `karr
      pick` would hand out from the same board, and the two commands
      disagreeing about which end is urgent was a trap: the list now reads
      urgency the way pick and `karr context` already did. `--reverse` gives
      the least-urgent-first view, and every other sort key keeps its
      direction. This deviated from kanban-md when it landed; kanban-md has
      since made the same change (upstream `c783157`, "sort priority lists
      highest first"), so the two agree again. It gets there differently —
      kanban-md flips the reverse flag in `cmd/list.go` and leaves its
      comparator ascending, karr reads the config list backwards — but the
      resulting order is the same.
    - Fixed the wipe guard's blind spot: it caught a remote that had lost
      every board ref, but not one swapped for a DIFFERENT non-empty board —
      a stale clone, a re-initialised origin, a typo'd remote URL — which a
      pull then faithfully converged the local board onto, silently and
      totally, because the ref count was not zero. Boards now carry an
      identity in `refs/karr/meta/board-id` (#95). `karr init` and `karr
      import` stamp it for boards they create (and never re-key an existing
      one); a board from before this change is stamped by the first pull that
      finds no id on either side, with the ordinary push path carrying the
      stamp to the remote; a clone meeting a stamped remote for the first
      time adopts that id. A pull where both sides have an id and they differ
      is refused before any reconciliation, naming what happened and the two
      ways through: `karr sync --push` to republish this board over the wrong
      remote, or the new `karr sync --accept-foreign-board` to adopt the
      remote's board. `karr restore` keeps the board's standing identity when
      the snapshot predates the stamp, so restoring a backup onto its own
      board never looks like a foreign takeover.
    - Fixed `karr pick` being blind to a board imported from kanban-md. An
      optional frontmatter field set to the empty string counted as set,
      because a Moo predicate only knows whether the attribute was passed —
      and `claimed_by: ""` is exactly what a kanban-md card carries once it has
      been read and rewritten, or written by hand. `karr import` stored it
      verbatim, `karr pick` read it as a claim by somebody, and so on a freshly
      imported board pick answered "No available tasks to pick." while
      `karr list` showed the very same work sitting in backlog. An empty
      `claimed_by` is now no claim, the same test `karr move`/`edit`/`delete`
      already applied. Three more of the same: `karr list` printed a bare "@"
      for `assignee: ""`, `karr context` counted `due: ""` as overdue for ever
      (the empty string sorting before every real date), and `karr context
      --json` emitted an `assignee` key with nothing in it. The same assumption
      in `karr show`, `karr board` and the no-claim test the mutating commands
      share is fixed too — see the entry above, which moves the rule into the
      task model instead of into a fifth reader.
    - Fixed `karr pick --json` printing an English sentence when there is
      nothing to pick, on both of its empty paths, so the one karr command
      certain to be machine-parsed handed its consumer a JSON decode error
      instead of an empty result. It now prints `{"picked":null}`. A successful
      pick still prints the task object itself, and the exit status stays 0 on
      both paths — "nothing for you right now" is the normal answer to a poll,
      not a failure, so a drain loop must not stop on it.
    - Fixed "finished" being hardcoded to the `done` and `archived` columns. A
      board imported from kanban-md may end anywhere, and on one whose columns
      ran backlog / doing / shipped / archived, `karr pick` handed shipped
      cards straight back out as available work and `karr list` did not hide
      them. Terminal statuses are now read from the board's own `statuses`,
      by kanban-md's rule: the last configured status, or the one before it
      when the last is `archived`, with `archived` terminal either way. Nothing
      changes on a default board, and `statuses` remains read-only from the
      CLI, so a custom list still only arrives through `karr import` of a
      kanban-md config.yml. `karr board`'s claim counting and the `completed`
      stamp followed — see the entries above.
    - New `karr list --archived`, which shows the archive and only the
      archive, matching kanban-md's flag of the same name. It replaces the
      other status filtering rather than narrowing it, so it wins over
      `--status`. Note that `karr list` still excludes the whole terminal
      group by default where kanban-md excludes only `archived`: it is the
      agent's "what is open" view, and that difference is now documented
      rather than merely undocumented.
    - Fixed `karr move`, `karr edit` and `karr delete` abandoning the rest of a
      batch after one bad id. A missing id died from inside the loop, so every
      id after it was skipped and the outcome depended on where the bad id sat
      in the list: `karr move 1,999,2 todo` moved 1 and never looked at 2,
      while `karr move 999,1,2 todo` moved nothing. All four id-list commands —
      `archive` included, which already behaved this way — now share one batch
      loop: every id is attempted, each failure is reported on STDERR with a
      `N of M ids failed` summary, and the command exits 1 while keeping the
      work that did succeed, which is the contract ADR 0002 already documented.
      A usage error is deliberately not a per-id failure: `karr move 1,2,3
      no-such-status` is wrong for every id at once, so it still rejects the
      whole invocation with exit 2 and writes nothing. With `--json` the
      results array is now printed even when part of the batch failed (`move`,
      `edit` and `delete` previously printed no JSON at all in that case), and
      a failed id appears in it as `{"id":999,"error":"Task 999 not found"}` —
      `archive`, which already reported failures there, used to give the
      shorter `"not found"` for the same field.
    - Fixed `karr archive` ignoring claims. It set the status to `archived` and
      saved with no claim check at all, so it could archive a card another
      agent was holding — the one door into a status change that `karr move`
      and `karr edit --status` did not cover. Archiving now applies the same
      claim rule as `move`, `edit` and `delete`, with the same message, and is
      refused while a live claim is on the card; release it with
      `karr edit ID --release` or let `claim_timeout` expire it. Re-archiving
      an already-archived task changes nothing and stays a success whatever its
      claim says. Because archiving is now an ordinary status change, a board
      whose configured `statuses` do not include `archived` will have
      `karr archive` refuse rather than write a status the board does not have.
    - Fixed `karr handoff` overwriting concurrent changes. It read the task,
      changed it and saved it back without checking the card had not moved in
      between, so a claim landing in that window was silently replaced instead
      of obeyed. The handoff now goes through the same compare-and-swap and the
      same status-change path as `karr move`, so the claim rule is applied to
      the revision that actually gets written.
    - Fixed `karr pick` locks being published to the remote. Lock refs lived at
      `refs/karr/tasks/N/lock`, inside the namespace karr pushes, so any sync
      that fired while a lock was held put it on the remote; other clones then
      pulled a lock whose holder they could not see, could not outlive and
      could only clear with `karr unlock`. Board backups snapshotted it too.
      Locks are process-local state, so they now live under `refs/karr-local/`,
      which nothing pushes, fetches, prunes or snapshots — and which
      `karr set-refs` refuses, so no refspec can reach them at all. Board
      state, including the `refs/karr/log/*` activity log, is unaffected and
      still syncs. Locks left at the old address by an older karr, or pulled
      from a remote that still has them, are not acted on — a lock from another
      clone says nothing about this process — but `karr unlock` lists them,
      marked as strays, and clears them.
    - Fixed the last race in `karr delete`'s claim guard. `App::karr::Git`
      could only delete a ref by name, through a libgit2 call that takes no
      expected-old OID, so no delete could be guarded: re-reading the task and
      re-applying the claim rule closed the minutes-long window behind the
      confirmation prompt but left the microseconds after it, in which a claim
      landing on the card was deleted along with it. There is now a
      compare-and-swap delete (`delete_ref_cas`) reporting the same retryable
      outcomes the existing retry loop understands, and the guarded delete
      paths — `karr delete` and giving back a `karr pick` lock — go through it.
      The unguarded `delete_ref` is unchanged, because `karr destroy` and
      breaking a lock deliberately remove whatever is there.
    - Fixed the push insurance retrying a push the remote had already refused.
      When a command dies after writing refs, karr pushes from an END block so
      the writes are not stranded; that path still made three attempts a second
      apart at a refusal the far side had already given its answer to, and then
      told the user to run `karr sync` — a command that would be refused
      identically. It now stops at the refusal and reports what the remote
      refused, ref by ref, on both the native and the git-CLI transport. An
      ordinary transport failure is still retried three times and still advises
      a sync, because there retrying can work.
    - Fixed `karr materialize` silently deleting and overwriting tracked
      project files. It wrote its file view straight into the working tree —
      replacing `config.yml` and removing every `tasks/*.md` — so a repository
      that already kept its own `tasks/` directory or `config.yml` lost them,
      from a command that only reads the board. Materialize now refuses to run
      when it would overwrite or delete anything Git tracks, names each such
      path, and writes nothing on that path; `--force` overrides it. The sweep
      of stale cards is also limited to files named the way karr and kanban-md
      name them (`NNN-slug.md`), so unrelated files in `tasks/` are left alone.
    - Fixed `karr import --yes` wiping the board when `tasks/` exists but holds
      no cards. The guard only checked that the directory was there, so an
      empty view imported zero tasks, deleted every task ref and exited 0
      reporting success. An empty view is now refused.
    - Fixed `karr import` leaving the board half-written when one file is
      malformed. Refs were written as files were parsed, so a bad card aborted
      the run mid-way with some tasks updated, the prune never reached, and a
      bare "Invalid task format" that named no file. The whole view is now
      parsed before any ref is touched: the import either applies completely or
      changes nothing, and every rejected file is listed with its reason.
      `App::karr::Task::from_file` names the file in all its errors.
    - Fixed `karr materialize` writing a `config.yml` that kanban-md refuses to
      load, which defeated the purpose of the file view. Perl's `1`/`0` were
      dumped as YAML integers where kanban-md's schema wants booleans
      (`require_claim`, `bypass_column_wip`), and `next_id`, which it validates
      as `>= 1`, was never written at all. The view now carries real YAML
      booleans and `next_id`; the counter itself stays in
      `refs/karr/meta/next-id` and import continues to ignore the file's copy.
    - Fixed `karr edit --status` bypassing every rule `karr move` applies.
      `karr move 1 in-progress` refused without `--claim`, while `karr edit 1
      --status in-progress` set the field and exited 0 — so `require_claim`,
      the guarantee karr's multi-agent coordination rests on, was one flag
      away from optional, by accident as easily as on purpose. `edit --status`

Changes  view on Meta::CPAN

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

Changes  view on Meta::CPAN

      lock was not the hole — its holder identity is the clone's `user.email`,
      which every agent on one machine shares, so all 12 acquired it quite
      legitimately. Each candidate is now re-read from its ref under its lock,
      re-tested with the same predicate, and written back under a
      compare-and-swap on the OID it was read from; an agent that loses that
      swap picks nothing and moves on. Verified with 12 forked contenders
      behind a barrier: 12 picks, 12 different tasks, and every agent named on
      the card it was told it got.
    - Fixed one orphaned lock ref bricking every command on the board.
      `list_task_refs` matched `refs/karr/tasks/N/lock` as well as `.../data`,
      so a lock left behind by an agent that died mid-pick made its task id
      exist after the card was deleted; `load_tasks` mapped that id to undef
      and `list`, `board`, `materialize` and `pick` all died on it, with no way
      out short of `git update-ref -d`. Only the data ref makes a task exist
      now, and the board list never contains undef.
    - `karr pick` no longer publishes its lock to the remote or strands its log
      entry. The lock was released, and the pick logged, after the push, so the
      remote kept the lock ref forever and the activity-log entry never left
      the clone. Both now happen before the push.
    - Locks expire. An agent that died between acquiring and releasing left a
      lock nothing could ever clear, and its task stayed unpickable forever. A
      lock older than the new `lock_timeout` board setting (default `5m`) may
      be taken over, itself by compare-and-swap against the revision whose age
      was judged, so a holder that refreshes in between is never silently
      evicted. This is deliberately not `claim_timeout` (default `1h`): a claim
      covers a work session, a lock covers one pick.
    - New command `karr unlock`: with no arguments it lists the pick locks
      currently held, with their holder, age, and whether they have expired;
      given task ids or `--all` it breaks them. The manual escape hatch for a
      stuck board, and the only one on a board that sets `lock_timeout` to
      `0s`. Breaking a lock cannot corrupt a concurrent pick — the claim is
      bound by the compare-and-swap on the card, not by the lock.
    - Board ref commits carry the time they were written. The git signature was
      built once and cached for the life of the process, so every ref a
      long-running driver (`karr-foundation`) wrote was stamped with the time
      of its first write.
    - Fixed ordinary ref contention aborting commands with a raw libgit2 error
      ("failed to lock file '.../lock.lock' for writing") followed by a stack
      trace of module paths and line numbers. Losing the race for a ref's lock
      file is now retried with a randomised backoff, and a ref write that
      genuinely fails reports a single karr-level line.
    - Raised the minimum Git::Native to 0.004 and Git::Libgit2 to 0.005. Those
      releases add compare-and-swap reference updates (`expected_old`) and
      per-ref outcomes from fetch/push, which karr needs to make ID allocation
      and lock acquisition atomic and to notice a server-rejected push. They
      also fix git+ssh remotes under libgit2 < 1.7 by verifying the hostkey
      against `~/.ssh/known_hosts`.
    - Fixed a frontmatter value ending in `---` corrupting the task and
      bricking the board. The closing delimiter was not anchored to the start
      of a line, so a value that merely ended in `---` — `karr edit 1 --block
      "waiting ---"` was enough, and YAML dumps such a value unquoted — cut the
      frontmatter mid-line. Every command that loads the board then died with
      "Missing required arguments: id, title", `delete` included, so the board
      could not be repaired with karr at all. The parser now scans for `---` at
      a line start, matching kanban-md.
    - Fixed UTF-8 being encoded twice everywhere. karr passed YAML::XS::Dump
      output (octets) around as characters, mixed that with character-level
      file I/O, and never decoded `@ARGV`. Non-ASCII text was therefore stored
      mojibaked in the refs, handed to agents mojibaked through `--json`,
      written three encodes deep by `materialize`, and destroyed by
      `backup`/`restore`; a correctly encoded kanban-md task file could not be
      imported at all ("invalid trailing UTF-8 octet"). `karr show` looked
      right only because two errors cancelled out. karr now keeps character
      strings internally and encodes only at its edges — argv, stdout/stderr,
      Git ref blobs, YAML and JSON — so non-ASCII titles, bodies, tags, and
      board names round-trip and kanban-md interop works outside ASCII.
    - Boards written by earlier versions keep working and are read correctly:
      the double encoding is undone on load for any board without the new
      `refs/karr/meta/encoding` marker. New command `karr repair` makes that
      permanent — it reports by default, rewrites the affected refs with
      `--yes`, and stamps the marker so nothing guesses at the board's bytes
      again. It is idempotent, never rewrites a ref whose payload is ASCII, and
      preserves task timestamps. `karr init` and `karr import --yes` stamp the
      marker themselves.
    - Fixed a runaway that could take the whole machine down: after a command
      died (a plain usage error was enough), the sync guard was only reaped in
      Perl's global destruction and pushed from there. libgit2 is reached
      through FFI::Platypus, whose type parser and library-search tables are
      already being freed in that phase, so the push re-entered them and
      recursed without bound — observed at 53 GB RSS on a 62 GB box, killable
      only from outside. It needed a board with a remote to trigger, which is
      why boards without one never showed it. App::karr::Git now refuses every
      native operation while `${^GLOBAL_PHASE}` is `DESTRUCT`, so the whole
      class of teardown re-entry degrades into an ordinary error.
    - Behaviour change on the die path: a sync guard reaped in global
      destruction reports instead of pushing. When refs were written but never
      pushed it now prints "Local refs are intact. Run 'karr sync' to push
      them."; when the command died before writing anything it stays silent.
      The automatic insurance push promised for this window never actually
      worked (it either recursed as above or was lost as an "(in cleanup)"
      warning) — making it deterministic is what exposed that. Restoring a real
      push is tracked separately.
    - And now restored: a command that dies after writing refs pushes them
      before the process exits again, instead of only advising `karr sync`.
      Armed sync guards register in a process-wide registry that `karr` drains
      from an `END` block — the last point at which pushing is still safe, and
      one that also covers the `exit` calls inside command bodies. A command
      that died before writing anything still pushes nothing and says nothing,
      and a push that fails there warns without touching the exit code. The
      global-destruction report above stays as the last resort for embedders
      that never drain the registry.
    - A writing command whose push fails no longer retries six times. Its
      `sync_after` disarms the guard after spending its own three attempts, so
      the new `END` flush does not repeat the identical failing push on a
      command that is already reporting the failure.
    - `karr skill show` no longer warns "Wide character in print". The bundled
      skill file is read decoded, so it is now encoded back to UTF-8 bytes at
      the one print site. The output bytes were always correct, but the warning
      was noise on stderr — and it ended up inside the written file whenever
      someone refreshed an installed SKILL.md with `karr skill show >file 2>&1`.
    - The installed executables `karr` and `karr-foundation` now carry a
      `$VERSION`. Both shipped versionless through 0.400, 0.401 and 0.402: the
      woven POD had a VERSION section (generated from the dist version), but the
      code itself declared none. Dist::Zilla only inserts a `$VERSION` into a
      file that has a `package` statement, which a script does not — so the
      line has to exist once, after which every release keeps it in step.
    - Fixed silent loss of another agent's work on every shared board. `push`
      sent `refs/karr/*` with a forced refspec but `pull` fetched with a
      non-forced one, and since karr writes every board ref as a parentless
      commit, no update is ever a fast-forward. libgit2 declined the update
      without reporting an error, so `karr sync` said "Done." and exited 0 while

Changes  view on Meta::CPAN

      an already-imported task. Import now seeds next-id past the highest
      imported id when the stored next-id is missing or stale, while leaving a
      next-id that is already ahead of the view untouched.
    - New commands `karr materialize` and `karr import`: the file-view bridge
      from ADR 0001. `materialize` writes refs/karr/* out to the board root as
      a kanban-md compatible view (config.yml + tasks/*.md) for grepping or
      interop; it reads refs only and syncs nothing. `import` reads such a view
      back into refs, preserving task timestamps verbatim. Because it replaces
      task refs and drops refs with no matching file, it requires --yes and
      refuses to run when no tasks/ view is present, so it can never silently
      wipe the board. config.yml now joins tasks/ in .gitignore so the whole
      materialized view stays disposable.
    - CLI exit codes now follow a stable 0/1/2 contract (ADR 0002): 0 success,
      1 runtime failure (task not found, board missing, Git/sync error, a
      destructive command refused without --yes), 2 usage error (unknown
      command, unknown option, invalid option value, surplus or missing
      positional). Replaces the previous accidental 255/1/2 mix; scripting
      agents can now distinguish misuse (2) from operation failure (1). The
      contract is documented in the karr POD under EXIT CODES.
    - Fix the redundant push fired before every syncing command: the SyncGuard
      returned by sync_before was discarded immediately (nobody held it), so
      it pushed before the command body even ran — the doubled "Push attempt"
      seen on every karr move/handoff — and the documented die-insurance never
      actually engaged. The guard is now retained for the whole command body
      and neutralized after a successful sync_after, so a crash mid-command
      triggers exactly one insurance push and a clean run pushes exactly once.
    - Sync output is now retry-only: the first pull/push attempt is silent
      (no more "Pull attempt 1 of 3..." noise on every command), retries are
      announced from attempt 2 ("Pull retry 2 of 3..."), and errors always
      reach STDERR. A new --quiet flag on every syncing command additionally
      suppresses the retry announcements — never the errors. `karr sync` moves
      its progress lines from STDOUT to the same STDERR convention.
    - Fix SyncGuard's push-failure reporting: the message showed a meaningless
      shell "(exit code $?)" although all Git operations run natively through
      libgit2 (no shell involved); it now carries the real libgit2 error text.
      The guard also no longer die()s inside DESTROY — which Perl downgrades to
      a swallowed "(in cleanup)" warning while an exception is already
      unwinding — so the "local refs are intact, run 'karr sync' to retry"
      guidance now reliably reaches STDERR.
    - Internal refactor, no user-visible behavior change: the status/claim
      config helpers (`is_terminal_status`, `status_requires_claim`, hash
      merging) now live once in `App::karr::Config`, with `BoardStore` keeping
      thin delegating wrappers. While folding the duplicates, a latent
      divergence in dead code was fixed: `Config->status_requires_claim`
      claimed a bare-string status requires a claim, while the store copy every
      caller actually used says it does not — bare statuses never require a
      claim, only an explicit `require_claim: 1` does.
    - Internal refactor, no user-visible behavior change: the JSON output
      snippets copy-pasted across commands now live once —
      `Role::Output->print_json_results` (the shared move/edit/delete/archive
      results tail) and `Task->to_json_hash` (the frontmatter+body payload of
      show/pick/handoff). The previously untested `--json` paths of
      move/edit/show/pick/handoff are now pinned by tests.
    - Internal refactor, no user-visible behavior change: the ~900-line
      karr-foundation module is split along its natural seams into three
      focused collaborators — `Foundation::Runner` (agent command execution
      and common-error classification), `Foundation::State` (lock file, JSON
      state, cooldown backoff, attempt counters) and `Foundation::Overview`
      (the read-only dashboard) — with `Foundation` staying the orchestrator
      and delegating, so every existing call site keeps working unchanged.
    - `karr backup`, `karr restore` and `karr destroy` now run through the
      shared sync lifecycle instead of hand-rolled pull/push calls, so the
      most destructive commands get the same 3x pull/push retry and
      push-on-crash insurance as every other mutating command (backup is
      read-only and takes only the retrying pull — it never pushes).
    - `karr config` and `karr skill` now accept options placed before the
      action (`karr config --json show`, `karr skill --json install`) instead
      of misreading the leading flag as the action ("Unknown action: --json").
      Both commands read their action (and config's key/value) through the same
      option-aware positional parsing the id-taking commands already use, now
      extracted into a shared `App::karr::Role::CliArgs`. Surplus positionals
      are rejected too: `karr skill` takes exactly the action, and `karr config`
      enforces its per-action arity (`config get KEY extra` is rejected).
    - Internal dead-code cleanup, no user-visible behavior change: remove the
      unused `Config->next_id` method (ids are allocated from the next-id ref
      via `BoardStore->allocate_next_id`), the dead `board_dir` attribute and
      its lone passthrough, and the unused `BoardStore->temp_board_dir`; and
      simplify `karr pick`, whose locking path was gated on an `is_repo` check
      that is always true by the time it runs (a board lives in refs/karr/*,
      reachable only inside a Git repo), so the lock/log path is now
      unconditional.
    - Fix `karr agent-name` (the dashed spelling used throughout the docs)
      not running: it never reached the AgentName command and errored with
      "Unknown command" instead of printing a generated name. The dashed form
      is now aliased to the command like `set-refs`/`get-refs` already were.
      `karr --help` also now lists the `log` and `agent-name` commands, which
      were missing from its command summary.
    - Fix the karr-foundation board sweep silently skipping real boards whose
      refs had been packed (`git gc` / `git pack-refs`) or that live in a git
      worktree. Board detection tested for the loose file
      `.git/refs/karr/config`, which disappears once refs are packed and never
      exists under a worktree's gitdir indirection, so those boards were
      dropped from the scan. Detection now resolves `refs/karr/config` through
      libgit2; the scan-directory sweep additionally confirms the resolved
      repository root is the scanned child itself, so a plain directory nested
      inside a karr repo is not mistaken for a board.
    - Fix the documented global `--dir` option being silently ignored:
      commands always operated on the board of the current directory, so
      `karr --dir /other/repo create "X"` quietly wrote to — and synced —
      whatever board the caller happened to be standing in. `--dir` is now a
      real option on every board command (`karr list --dir PATH`, the root
      form `karr --dir PATH list`, and bare `karr --dir PATH` for the board
      summary all work), seeding repository discovery before any store or
      sync activity; init, backup, restore,
      and destroy, which previously hardcoded the current directory, honor
      it too. A `--dir` that does not lead to a Git repository fails loudly
      instead of falling back to the current directory.
    - Options may now be placed before, between, or after positional
      arguments (`karr archive --json 1`, `karr handoff --claim tester 1`,
      `karr edit --title New 3`), matching how the kanban-md CLI behaves.
      Previously the raw option token was read as the task id, giving opaque
      errors like "Task --claim not found" — and `karr show --last N` crashed
      outright. The real positionals are now extracted from argv using each
      command's own option metadata (value-taking options swallow their
      value, `--opt=value` and short aliases like `-a` included), which also
      closes a hole in the surplus-argument guard: `karr archive 1 --json 99`
      used to silently drop the 99 while still archiving task 1.
    - Fix `karr delete` crashing on every ref-backed task ("Can't call method
      \"remove\" on an undefined value") — it tried to unlink the task's
      on-disk file, which tasks loaded from `refs/karr/*` never have, and
      which would only have removed the materialized view anyway, never the
      canonical ref. Deletion now goes through the board store and removes
      the task ref itself, like every other mutating command. On a refs-first
      board, delete previously could not delete anything at all.
    - Surplus positional arguments are now rejected before a command does any
      work ("unexpected extra argument: '99'" plus the usage line, non-zero
      exit) instead of being silently dropped — `karr archive 4 99` used to
      archive task 4 and swallow the 99 without a trace, so batch-looking
      invocations quietly did less than asked. Matching kanban-md, the comma
      list (`karr archive 4,5,6`) stays the one batch syntax; show/archive/
      delete/edit/handoff/create accept one positional, move accepts two.
    - Fix `karr list --status archived` (and `--status done`) returning
      nothing: the default done/archived exclusion ran before the explicit
      `--status` filter, so terminal statuses could never be requested and
      archived tasks were unreachable except via `karr show ID`. The default
      exclusion now applies only when no `--status` filter is given
      (kanban-md parity), so plain `karr list` is unchanged while an explicit
      filter surfaces done/archived tasks — including combined with `--tag`,
      `--sort`, and `--json`.
    - 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

Changes  view on Meta::CPAN

      `.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
    - Add POD to all modules (bin/karr, BoardStore, commands, roles)

0.100     2026-03-23 01:50:27Z

    - Migrate board state to git refs only (refs/karr/*), drop karr/ directory
    - Add backup and restore commands for refs/karr snapshot export/import
    - Add destroy command (remove all board refs, local and remote)
    - Add set-refs / get-refs helper commands for arbitrary ref storage
    - Add skill command with File::ShareDir-based skill loading
    - Simplify board output and drop WIP limits from config
    - Split Docker runtime images (slim + full) with entrypoint script
    - Expand POD across Git.pm, Lock.pm, commands, and roles

0.003     2026-03-20 05:01:01Z

    - BREAKING: Git sync stores task data in commit-wrapped refs (pushable/fetchable)
    - Full board sync via refs/karr/* (fetch/materialize/serialize/push)
    - Add karr log command for activity trail (per-agent NDJSON refs)
    - Add --claimed-by filter to list command
    - Add Task->from_string for ref-based loading
    - Rewrite Git.pm with safe execution (_git_cmd, no shell injection)
    - Fix write_ref to create commit-wrapped refs (blob→tree→commit)
    - Fix is_repo to work from subdirectories (git rev-parse)
    - Fix push refspec to refs/karr/*:refs/karr/*
    - Pick command uses Lock for atomic task claiming
    - Remove .gitignore manipulation from init
    - Extract sync_before/sync_after into BoardAccess role
    - Extract _parse_timeout/_claim_expired into ClaimTimeout role
    - Refactor Lock.pm to accept pre-built Git object
    - Docker: add default git identity ENV vars
    - Add karr sync command (--push, --pull)
    - Add auto-sync to write commands (create, move, edit, delete, pick, handoff, archive)
    - Add App::karr::Git for Git operations via CLI
    - Add App::karr::Lock for task locking via refs/karr/tasks/<id>/lock
    - Add Docker support (raudssus/karr on GHCR)
    - Initial release
    - Implemented archive command (soft-delete to archived status)
    - Implemented handoff command (move to review with claim, note, block/release)
    - Implemented config command (show/get/set board configuration)
    - Implemented context command (generate markdown board summary for embedding)
    - Implemented agent-name command (random two-word name generator)
    - Implemented skill command (install/check/update/show agent skills)
    - Added batch operations (comma-separated IDs) to move, edit, delete, archive
    - Added --json output to all commands (show, move, edit, delete, archive, handoff, board, pick)
    - Added --compact output to board command
    - Renamed all Command packages to CamelCase
    - Moved shared helpers (find_task, load_tasks, parse_ids) into BoardAccess role
    - Added --claude-skill flag to init for installing Claude Code skill
    - Ships skill via File::ShareDir (share/claude-skill.md)
    - Extracted --json and --compact into App::karr::Role::Output



( run in 0.623 second using v1.01-cache-2.11-cpan-4ef0a570458 )