0.400 2026-07-07 00:03:01Z
- `karr init` and `karr materialize` now ensure the board-root .gitignore
covers the materialized file view (tasks/ + config.yml), appending the
entries idempotently and preserving any existing content. Previously karr
never managed .gitignore, so running `karr materialize` in a repo that had
not ignored the view produced an untracked, accidentally-committable
config.yml + tasks/. refs/karr/* stays the canonical state; the file view
is disposable and never committed.
- Fix a bootstrap id collision after `karr import`: importing a kanban-md
tasks/ view into a fresh repo (no board) left refs/karr/meta/next-id
unset, so the next `karr create` re-allocated from id 1 and collided with
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
whenever an existing task ref is saved — matching kanban-md, which
stamps `Updated` in every mutating command. Creating a task keeps
`updated` equal to `created`, restore/import paths preserve the
original timestamps verbatim, and materializing the on-disk view no
longer rewrites `updated` to the current time (it copies the ref values
unchanged).
- Fix `karr archive` dying with an opaque Path::Tiny error ("paths require
defined, positive-length parts") on ref-backed tasks — the normal case
since boards moved to `refs/karr/*`. Archive was the only mutating
command still calling `$task->save` (which needs an on-disk `file_path`)
instead of persisting through the board store like move/edit/pick/
handoff do. `Task::save` without a directory argument now croaks with a
clear message when the task has no `file_path`, instead of the
Path::Tiny error.
0.303 2026-06-28 02:07:23Z
- Docker: build Alien::FFI against the system libffi (apt libffi-dev) instead
of fetching a libffi tarball from a GitHub release page, which broke the
image build intermittently in CI (Alien::Build itself warns the
release-page download negotiator "will typically not work"). The runtime
image now ships libffi8 for the dynamically linked FFI::Platypus. The
vendored libgit2 (share) build is unchanged, so the runtime stays
self-contained.
0.302 2026-06-21 23:04:42Z
- `karr board` now renders a compact, Markdown-flavoured plaintext board
(board name as `#`, each status as `## Section`, one
`- id | title | meta...` line per task) instead of the coloured column
dashboard. The output stays clean when piped or redirected — colour is
added only when stdout is a terminal and `NO_COLOR` is unset. Default
(`medium`) priority is suppressed, and a new `--tags` flag prints each
task's tags on an extra indented line.
- Fix releasing a claim or unblocking a task leaving a null `claimed_by`,
`claimed_at`, or `blocked` field behind. Clearing now uses real Moo
clearers so the predicate drops and the field is omitted from the task
file, instead of being written as an explicit null that reloaded as
"still set" — which made `handoff` reject released tasks and `pick`
treat them as claimed. Explicit nulls in already-written or external
task files are normalized to "unset" on load.
0.301 2026-06-04 22:35:33Z
- karr-foundation: stream agent output to the terminal when interactive
(TTY detected) or --verbose is set. The parent process now reads the
child's output through a native pipe and fans it to the log, the
terminal, and an in-memory buffer — no external `tee` and no re-reading
the log by byte offset. The per-run timeout is `select`-based (robust
against Perl's deferred signals) and only fires when max_runtime > 0
(max_runtime: 0 disables it entirely). Output is always appended to
.karr.log regardless of TTY.
- karr-foundation is now a multi-board coordinator, not just an agent
runner. Agent execution is opt-in: with no agent configured on any
board, the default action is a read-only overview of every board
(status counts, in-progress/blocked, lock/cooldown state). `--status`
forces that overview regardless of configuration.
- karr-foundation: `claude: true` synthesizes the canonical claude
invocation so you needn't retype it; `claude_bin`, `claude_max_turns`
and `claude_permission_mode` override the parts. The agent instruction
is exposed as the `$PROMPT` substitution variable (settable via `prompt`
in .karr or `default_prompt` in config), usable in any command template.
- Activity log entries are now keyed by a role-qualified identity
(`refs/karr/log/<role>/<email>`, role `user` or `agent`) so a human and
an AI sharing one Git config are told apart. The role propagates to
nested karr calls via the KARR_ROLE env var (foundation sets `agent`);
pre-existing bare-email logs are still read for the `user` role.
- karr show: with no ID shows the single most recently updated task;
`--last N` widens that, `--me` shows the task(s) the current identity
most recently acted on (via the activity log), and `--agent NAME` shows
the task(s) most recently claimed by that agent name.
- karr board: hide the `@claimed_by` badge and claimed-count for tasks in
a terminal status (done/archived) — a claim is an active lease, and the
history remains in the activity log.
- sync: surface the real libgit2 error on a failed pull/push instead of a
meaningless "(exit code $?)" (native libgit2 operations have no shell
exit code). New Git `last_error` accessor records the last remote-op
exception.
0.300 2026-05-27 20:43:23Z
- Docker: bundle libgit2 (Alien::Libgit2 share build) so the runtime
image is self-contained. Builder installs cmake/pkg-config/zlib/
libssh2 dev headers and sets ALIEN_INSTALL_TYPE=share; runtime-base
installs libssl3/libssh2-1/zlib1g (the shared libs the vendored
libgit2.so links against). Needed since Git::Native moved to
Git::Libgit2 (libgit2 FFI).
- Add .github/workflows/ci.yml (perl 5.36/5.38/5.40) using the
[@Author::GETTY] dzil-test composite action; installs libgit2-dev so
Alien::Libgit2 links the system libgit2 (>= 1.5) in CI.
- Git.pm: read git config (user.name/email) and validate helper ref
names through Git::Native (Config + reference_name_is_valid) instead
of poking Git::Libgit2::FFI directly. New Git.pm `ref_oids` helper.
- karr-foundation: detect board changes via Git::Native instead of
shelling out to `git for-each-ref` — no git binary needed for that
path anymore. Sync (`--pull`) and open-task detection now run
in-process via App::karr::Git/BoardStore instead of forking the
`karr` CLI.
- karr-foundation: drain each board instead of a single run — invoke
the agent command repeatedly until no actionable task (non-terminal
and unblocked) remains. A task the agent claims but never moves is
auto-blocked after `max_attempts` stalls (default 2) so the drain
always terminates; the agent's own `--block` reason still wins.
Observable common errors (non-zero/timeout exit, or a log match
against rate-limit/auth/network/5xx patterns, extensible via
`error_patterns`) never penalize a task and instead trigger an
exponential per-repo cooldown (1, 2, 4, … minutes, capped). New
`.karr` keys: `drain`, `max_attempts`, `max_iterations`,
`cooldown_base`, `cooldown_max`, `error_patterns`.
- cpanfile: require Git::Native 0.003 and Git::Libgit2 0.004.
- Fix `karr context` / `karr context --json` crashing with
"Can't locate object method 'strftime' via package 'Sun May ...'":
Cmd::Context now `use Time::Piece`, so `gmtime` returns a
Time::Piece object instead of a plain string. Added t/07-context.t
covering the plain, --json, and recently-completed cutoff paths.
- Fix `karr config show` (and get/set) crashing with
"Can't locate object method 'board_dir'": Cmd::Config now builds
its config via `$self->store->effective_config` and persists with
`$self->store->save_config`, instead of calling the non-existent
`board_dir` on itself. Added t/06-config-cmd.t.
- Drop hard-coded `tags = latest` / `tags = user` in the Docker
subsections so the new `[@Author::GETTY::Docker]` default
(`latest %V %v`) applies. `runtime-user` keeps a `-user`
suffix on each tag.
- Add `karr-foundation` binary and `App::karr::Foundation` module:
single-shot daemon for periodic agent execution across multiple karr
boards. Reads `~/.config/karr-foundation/config.yml` (dirs: / scan:),
checks each repo for board changes or open tasks, and invokes the
per-repo `.karr` command. Supports `--force`, `--dry-run`, `--verbose`.
Per-repo state in `.karr.state` / `.karr.lock` / `.karr.log` (gitignored).
0.202 2026-05-17 05:17:07Z
- Fix `karr list` crashing with "Can't locate object method 'load_tasks'":
Cmd::List was missing `with 'App::karr::Role::BoardAccess'` (the role was
`use`d but never consumed). Surfaced while writing worktree tests.
- Add t/29-worktree.t covering init/create/list inside `git worktree`
directories and verifying refs/karr/* are correctly shared between the
main work-tree and additional worktrees.
0.200 2026-05-16 17:45:23Z
- Centralize config knowledge: priority_order(), class_order(),
terminal_statuses(), is_terminal_status(), status_requires_claim()
moved to Config and BoardStore (no more duplication across commands).
- Add all_status_names(), status_requires_claim(), is_terminal_status()
to BoardStore for encapsulated status config access.
- Convert all require Time::Piece to use Time::Piece (Pick, Move, Edit).
- Extract append_log into App::karr::ActivityLog module.
- Architecture refactor: split Role::BoardAccess into Role::BoardDiscovery +
Role::SyncLifecycle. Commands now work directly on refs via BoardStore
instead of via a materialized temp directory.
- Add SyncGuard (push insurance on die/croak), effective_config() on BoardStore,
and $self->config via Role::BoardDiscovery.
- Add tasks/ to .gitignore (never commit materialized view).
- Fix CPAN smoker failures: skip git tests on old git (< 1.8.5, no -C flag)
- Fix skip() without SKIP block in t/11-git-impl.t (Test::More crash)
- Skip user.email test gracefully when not configured
0.101 2026-03-23 03:02:05Z
- Strengthen docs and GitHub landing pages
- 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