App-karr

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

      the flag is `--wait`.

    - Parent/subtask links (kanban-md's `--parent`) are a documented
      non-goal, not a gap: the `parent` field round-trips through
      `karr import` unchanged, but no karr command sets, filters,
      renders or sorts it. Relatedness between cards is `depends_on`
      (local ordering) and `needs` (cross-board). CONTEXT.md and
      README's stored-task-shape section now say so.

0.600     2026-08-31 01:16:28Z

    - Karr writes its own ticket ids as `kNNN` in prose from now on,
      never as a bare `#NNN`. A `#`-number reads as an issue on whichever
      bug tracker the reader's repository carries, and karr tracks its
      work on its own board rather than in one, so `k243` in a changelog
      line or a commit message pointed at the wrong system for every
      reader but the author. The convention lives in the shared skills so
      commits and Changes entries use one spelling; the entries below
      already do.

    - BREAKING: the bundled agent skill installs as
      `kanban-issues-karr-cli`, not as `karr`. `karr skill install`
      and `karr init --claude-skill` now write
      `.claude/skills/kanban-issues-karr-cli/SKILL.md`. The dist shipped
      the skill under one name while this repository briefed its own agents
      under the other; the longer name is the more findable. A project
      still holding the old `.claude/skills/karr/` keeps it -- nothing
      removes it for you, so delete it after updating or the agent reads a
      skill nothing maintains any more.

    - The distribution is licensed under the Artistic License 2.0 instead
      of the same terms as Perl 5 itself; the dual grant carried a copyleft
      option this project does not want. The LICENSE file ships in the
      repository.

    - Board commands report errors an agent can act on (k263, k264).
      "Task 999 not found", shared by `move`, `edit`, `delete`, `archive`,
      `handoff` and `show`, now ends `on this board:` with `karr list
      --compact` on its own last line; `karr wibble` ends on a copyable
      `karr --help`; and an error about a missing or malformed option ends
      with the invocation that would have worked, built from the words the
      caller typed -- `karr move 79 in-progress` answers with `karr move 79
      in-progress --claim NAME` under it. That line comes last, because
      agents read karr through `tail -n`, and only where at least one word
      of it is the caller's own. `move`, `needs`, `import`, `materialize`,
      `repair`, `edit` and `archive` are covered, as are the option-parse
      errors karr does not word itself. Exit codes are unchanged and
      `--json` keeps its single-line `error` field.

    - `karr list` gains `--limit`/`-n`, `--class`, `--blocked`/
      `--not-blocked` and `--unclaimed`, and `--sort title` (k237, k252).
      The cut is the last stage, after filter and sort, so `--sort priority
      -n 5` is the five most urgent cards and it applies to `--json` and
      `--compact` alike; `0` is no limit and a negative count is a usage
      error. An unknown `--class` names the board's own classes and exits 2
      rather than printing an empty list, and `--blocked` with
      `--not-blocked` is refused. `--unclaimed` lists the cards no
      live claim holds -- `claimed_by` unset, empty, or a claim past
      `claim_timeout` -- which k237 had taken for a duplicate of
      `--claimed-by` but asks a different question; it shares one claim
      test with `karr pick` (`Role::ClaimTimeout::claim_held`) so the two
      cannot disagree about what is free (k198's shape), and passing it
      with `--claimed-by` is a usage error. `--sort title` compares
      case-insensitively as kanban-md does, ties break on `id`, and the
      comparison is on characters and not collated -- a non-ASCII title
      sorts behind every ASCII one, a documented non-goal rather than a gap
      (a collating comparator would need a locale two agents need not
      share).

    - `karr pick` ranks two `fixed-date` cards by the sooner due date
      before priority, matching kanban-md's `compareDue`: the one
      class of service that exists because a date rather than an urgency
      decides was the one place the date was ignored. The exception is
      narrow and the reference's own -- only where both candidates carry
      `fixed-date`, and only between them.

    - Invalid and no-op invocations are refused or short-circuited instead
      of resolved in silence (k231, k235). Five contradicting invocations
      -- `edit --block X --unblock`, `edit --body X --append-body Y`,
      `create TITLE --title OTHER`, `move ID STATUS --next` and `move ID
      --next --prev` -- exited 0 with one half quietly discarded; all five
      now exit 2 through `usage_error` (`move ID STATUS --next` is the one
      step away from kanban-md, which resolves it silently the other way),
      and the refused `create` is checked before an id is allocated.
      A `move` to the status a card already has, and an `edit` naming no
      field, no longer write at all: both went through the mutation path,
      stamping `updated` and logging a move that never happened -- the field
      karr-foundation's drain reads to tell a stuck card from a worked one.
      The no-op move exits 0 with `Task N is already at X` and `--json`
      gains a `changed` key; an `edit` naming no field is an empty request
      and exits 2 (ADR 0002). `move ID SAME --claim NAME` still writes,
      because the claim is a change.

    - `karr board` and `karr context` leave archived cards out entirely and
      ask the board which status is terminal instead of looking for `done`
      (k229, k234). `board` counted archived cards in the footer total and
      in `blocked` and rendered them as a column -- so this board read `241
      tasks` over ten rendered cards -- and on a board whose last column is
      `shipped`, `--done` did nothing; the hidden-count hint now names the
      real column (`2 tasks (1 shipped hidden)`) and archived cards are gone
      from all three output modes. `context` had been filtering every
      terminal status where kanban-md filters only `archived`, so finished
      work went missing from the header total and a card blocked before
      `done` fell out of the `blocked` count -- the two tools maintain one
      context block in a shared host file and have to put the same numbers
      in it. Filed cards are read with `karr list --archived` and
      `karr show ID`; default boards print the same bytes as before.

    - An appended note is a paragraph of its own, and `karr edit` takes the
      `-t`/`--timestamp` flag `karr handoff` already had. `karr edit
      ID -a` and `karr handoff --note` joined the new text with a single
      newline, which Markdown folds away; they join with a blank line now,
      through one `Task/append_body`, trimming trailing newlines first, as
      kanban-md's `AppendBody` does. An empty body still gets no leading
      blank line, existing cards are not rewritten, and `--timestamp` alone
      is refused as an empty request.

    - The board config no longer carries per-class WIP limits.
      `default_config` gave every `expedite` class `wip_limit: 1` and
      `bypass_column_wip: 1` and `validate` checked them, but no command
      ever read either -- karr has no WIP limits, per-status or per-class.

Changes  view on Meta::CPAN

      spelled out in the POD, along with the fact that one card can be in
      both. The command also stops implying an hour precision its data does
      not have: boards written before ticket k68 (karr 0.403) carry
      day-granular `started`/`completed` stamps, bare `YYYY-MM-DD` read as
      midnight, which is where every negative duration comes from, and on such
      a board an average printed to the hour is finer than what it rests on.
      On karr's own board 51 of 138 lead samples are negative, averaging
      -10.1 hours; discarding them would raise the printed average from 6h 39m
      to 16h 28m, which is why the figure is reported with the 51 beside it
      rather than tidied up with the evidence removed. No data migration:
      `karr repair` is untouched and the stored stamps stay exactly as they
      are.

    - `karr skill install` and `karr skill update` write the target SKILL.md in
      place instead of replacing it (ticket k142). Path::Tiny's `spew_utf8`
      writes a temp file and renames it over the destination, so the path it
      wrote came back on a new inode -- right for an ordinary file, wrong for a
      skill file: skills are shared between projects as a hardlink chain, one
      inode behind the same relative path in dozens of checkouts, and the
      rename broke the updated path out of its chain. That one project got the
      new skill, all the others kept the old inode with the old text, and the
      link count dropped with nothing said. Found during agent setup in an
      unrelated repository, where the workaround was piping `karr skill show`
      into a shell redirect. The write now truncates the existing file and
      writes through it, so every link sees the update; it is still
      Path::Tiny's character-level UTF-8, so the encoding boundary is unmoved.
      A target that does not exist yet is still created and a symlinked target
      is still written through. A target that cannot be opened for writing at
      all -- a read-only SKILL.md, which the rename handled because it only
      needed a writable directory -- is still updated by replacement rather
      than turned into a failure, but that is the one case left where a chain
      cannot survive, so it is now reported instead of done silently.

    - `karr init --claude-skill` writes .claude/skills/karr/SKILL.md in place
      too (ticket k145). It installs the very file `karr skill install --agent
      claude-code` installs, and it still did so with the `spew_utf8` that
      ticket k142 had just removed one command over: a temp file renamed over
      the target, so the path came back on a new inode. Where a skill file is
      one link of a hardlink chain shared between projects, that broke this
      project out of the chain and left every other one on the old inode with
      the old text. Both commands now go through one shared implementation —
      App::karr::Role::SkillFile — rather than a copy each, because the copy
      is how the rule came to be right in one place and wrong in the other; it
      carries the in-place write, the fallback for a target that cannot be
      opened for writing at all, and the warning that says so when a chain was
      there. `karr init` still reports a directory it cannot create as "Could
      not create .claude/...", which is what actually failed, and `karr skill`
      behaves exactly as before.

    - `App::karr::Role::TaskMutation` now declares the five methods it calls
      on its consumer (ticket k141): `git` and `store` from
      `Role::BoardDiscovery`, `save_task` and `log_task_write` from
      `Role::BoardAccess`, `json` from `Role::Output`. It declared nothing at
      all, and composed cleanly into anything, which is the state k128 found
      `Role::DependencyCheck` in — so a future command reaching for
      `update_task_guarded` would have inherited a compare-and-swap loop whose
      collaborators nobody had checked for, and learned about it from inside
      the callback as a "Can't locate object method". No command changes: all
      five on the mutation path already compose both supplying roles, which is
      what hid the gap. The ticket proposed six names; `check_claim` is not
      one, because it comes from `Role::ClaimTimeout`, which this role
      composes, exactly as `check_dependencies` comes from
      `Role::DependencyCheck`. Requiring either would never fail — Role::Tiny
      installs a role's methods into the consumer before checking its
      requires, so the check finds what the composition just put there. `json`
      is now declared twice, here and on `Role::DependencyCheck`; that is
      deliberate, since a role that lets one it happens to compose declare a
      collaborator on its behalf is how this gap opened.

    - `App::karr::Role::ClaimTimeout` now declares the one method it calls on
      its consumer (ticket k144): `store`, from `Role::BoardDiscovery`. It
      declared nothing at all and composed cleanly into anything, which is the
      state k128 found `Role::DependencyCheck` in and k141 fixed for
      `Role::TaskMutation` — so a consumer without `store` was still handed
      `check_claim`, karr's one claim-ownership rule, and would have found out
      from inside a mutation as a "Can't locate object method", on the one run
      where a task was actually claimed by somebody else. No command changes:
      all seven that compose the role, directly or through
      `Role::TaskMutation`, already bring `store` along via
      `Role::BoardAccess`, which is what hid the gap. One name is the whole
      list — the role's four other calls reach subs defined in the role itself,
      and unlike `Role::TaskMutation` this one composes no role at all, so
      nothing arrives by composition either. Requiring any of them could never
      fail: Role::Tiny installs a role's methods into the consumer before
      checking its requires, so the check finds what the composition just put
      there. With this, the three roles on the mutation path all say what they
      call.

    - `karr repair` gained a second migration (ticket k138): it raises a
      `started` stamp that precedes its own card's `created` up to that
      `created`. karr wrote `started` as a bare date until k68, which reads as
      midnight and therefore lands before any card filed and begun on the same
      day — 75 of 138 cards on karr's own board. Both migrations are reported
      and applied together but kept apart in the output, since a board can
      need either without the other, and neither bumps `updated`: a migration
      is not an edit, and stamping every card it rewrites would destroy the
      history it is repairing. Only the pre-k68 bare-date shape is clamped; a
      `started` that precedes `created` while carrying a time of day is a
      different and unknown fault, so it is reported and left alone rather
      than having its evidence erased. What the clamp costs is stated by the
      command rather than left to be discovered: a clamped card asserts zero
      queue time, which is false for one filed in the morning and picked up at
      night, and nothing on it marks the stamp as having been day-granular any
      more — the rewrite cannot be undone from the data. The dry run says how
      many cards that is before `--yes` applies it. `repair` also no longer
      returns early when the encoding is already current, since otherwise the
      clamp could never run on a board written by a current karr; `up_to_date`
      in the JSON keeps answering for the encoding migration alone, and says
      so.

    - `App::karr::Role::DependencyCheck` is split in two (ticket k137). It
      carried both halves of dependency handling under one name, with two
      different contracts: parsing and validating `--depends-on` arguments at
      set time, and warning at move time that a card's dependencies are
      unfinished. `Cmd::Create` composed the whole thing for the first half
      alone, which is why k128 could not put `json` in the role's `requires` —
      `create` has no `--json`, and requiring it would have refused a consumer
      that never reaches the reporting half. The set-time half is now
      `App::karr::Role::DependencyArgs`, named beside the existing
      `Role::CliArgs` for the same reason: it turns command-line values into
      validated ids. Both halves now declare every method they call on their
      consumer, `json` included. Only `Cmd::Create` and `Cmd::Edit` change
      what they compose; `Cmd::Pick` and `Role::TaskMutation` keep the
      reporting half under its old name, so external `L<...DependencyCheck>`
      references stay correct. `Cmd::Edit` turned out to be getting
      `parse_dependency_ids` by accident of the two halves sharing a role, and
      now names `DependencyArgs` itself.

    - New command `karr metrics` (ticket k126), the last kanban-md feature
      karr did not have: throughput over fixed 7- and 30-day windows, average

Changes  view on Meta::CPAN

      and C<karr repair>, which it had never mentioned, spells
      C<karr agent-name> the way the command table does, and no longer
      describes C<karr handoff> as moving to a literal C<review> — since
      ticket k102 the target is the board's review column, or its last
      non-terminal column on a board that has none. This applies to
      F<share/claude-skill.md>, the copy C<karr skill install> writes into
      other projects, so an agent set up by karr gets the corrected text
      (ticket k117 tracks that this copy and the one in this repository are
      kept in step by hand).
    - App::karr::Git now resolves every path it hands git from the work tree
      root, on both routes into is_tracked_under (tickets k113 and k114). The
      string comes out of _relative_to_root, which measures from the root, and
      libgit2 resolves it that way by itself — but the `git ls-files` fallback
      ran as `git -C ->dir`, and a pathspec is resolved against the process
      cwd. Build the class on a subdirectory, as its own SYNOPSIS shows with
      `dir => '.'`, and it asked about `subdir/tasks` while the caller asked
      about `tasks`; a pathspec that matches nothing exits 0 with no output,
      which reads back as "not tracked", so a project that owns `tasks/` would
      be told it does not — the symptom ticket k89 removed, through a different
      door. The CLI is now pinned to the root, which the transport verbs cannot
      tell apart. The root itself is `.`, a pathspec git understands but not a
      path the index can hold — entries are stored as `tasks/a.md`, never
      `./tasks/a.md` — so the native route answered 0 for a repository full of
      tracked files; at the root the question is now whether the index holds
      anything at all, which is what `ls-files -- .` answers there too. No
      karr command changes behaviour: every is_tracked_under call goes through
      the store's Git, which App::karr::Role::BoardDiscovery builds at the
      repository root, and none of them passes the root as the path. Both were
      latent, and each was a wrong answer rather than a failure — the kind that
      would have surfaced as the answer depending on whether libgit2 was
      available to ask.
    - App::karr::Git::is_tracked_under now reads the index natively, through
      Git::Native::Index, and only falls back to `git ls-files` when libgit2
      declines to answer (ticket k107). 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 k89 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
      k108). 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 k105,
      finishing k38). 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 k77). `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 k27), 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 k38). 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 k105 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



( run in 1.524 second using v1.01-cache-2.11-cpan-85d3896f969 )