App-karr

 view release on metacpan or  search on metacpan

.claude/skills/kanban-issues-karr-cli/SKILL.md  view on Meta::CPAN

```bash
karr skill install                           # install skill for detected agents
karr skill install --agent claude-code       # install for specific agent
karr skill install --global                  # install globally (~/)
karr skill install --force                   # force reinstall
karr skill check                             # check if installed skills are current
karr skill update                            # update outdated skills
karr skill show                              # print skill content to stdout
```

Supported agents: `claude-code`, `codex`, `cursor`.

For Docker-wrapped usage, prefer the `raudssus/karr:latest` alias that mounts
the current project at `/work` and uses `/home/karr` as `HOME`, so the image
can drop privileges to the owner of the mounted workspace without breaking
access to Git config or agent skill directories.

### Sync

```bash
karr sync

Changes  view on Meta::CPAN


    - 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

docs/superpowers/specs/2026-03-22-ref-first-board-design.md  view on Meta::CPAN

3. recreate refs from the YAML snapshot
4. push the resulting ref set

Without `--yes`, restore must fail with a strong warning. The command should
make it obvious that refs missing from the backup will be removed.

## Skill installation and Docker

Skill installation must keep working when `karr` is run through Docker. The
important requirement is that the target `HOME` inside the container is the one
that actually contains mounted `.codex`, `.claude`, or `.cursor` directories,
and that the final process runs as a non-root user where appropriate.

This is separate from board storage, but it should be verified during the same
cleanup pass because the current vendor-style usage depends on it.

## Migration

Boards created under the current local-file-first model need a migration path.
The tool should support one of these:

ex/README.md  view on Meta::CPAN

globally in the home directory:

```bash
karr skill install
karr skill install --agent claude-code
karr skill install --agent codex --global --force
karr skill check --global
karr skill update
```

Targets are `claude-code`, `codex` and `cursor`.

### Characters inside, octets only at the edge

A rule that has no command line but affects every output: everything between the
CLI entry point and the Git ref blob is a Perl **character** string.
`App::karr::Encoding` owns every crossing — `@ARGV`, STDOUT/STDERR, ref reading
and writing, YAML, JSON — and nothing else may encode or decode directly. Boards
written before this rule are detected via `refs/karr/meta/encoding` and repaired
on read; `karr repair` migrates them permanently. The reason this is worth
mentioning at all: a double-encoded UTF-8 in a card title is the bug nobody

lib/App/karr/Cmd/Delete.pm  view on Meta::CPAN

      # likely to read before doing something irreversible.
      #
      # The channel is unconditional rather than a branch on --json, for three
      # reasons. A prompt is not a result -- `deleted` below is the result, and
      # the question is dialogue, which is what STDERR is for; the rule
      # App::karr::Role::DependencyCheck states one module over ("the human
      # copy goes to STDERR so STDOUT stays parseable") is likewise
      # unconditional, only its *suppression* depends on an option. Second, the
      # non-JSON path has the same defect in a quieter form: `karr delete 1 >
      # kept.txt` wrote the question into the file, so the operator at the
      # terminal was asked nothing and waited at a blank cursor. And third,
      # making the channel depend on a flag means the fix only reaches the
      # caller who remembered the flag.
      #
      # Rejecting `--json` without `--yes` outright was the other candidate. It
      # would have deleted a live answer: `deleted => false` with the two
      # warning keys beside it is exactly the shape #236 and #242 built for a
      # card the operator declined to delete, and under --yes there is no
      # prompt to decline at all, so that shape would become unreachable. It
      # also refuses `printf 'y\nn\ny\n' | karr delete 1,2,3 --json`, a
      # per-card answer that --yes cannot express because --yes is

lib/App/karr/Cmd/Skill.pm  view on Meta::CPAN

# SkillFile: _skill_content and _write_skill, shared with `karr init
# --claude-skill`, which writes the same file this command writes for the
# claude-code agent (tickets #145, #146).
with 'App::karr::Role::Output', 'App::karr::Role::CliArgs',
     'App::karr::Role::ExitCodes', 'App::karr::Role::SkillFile';


option agent => (
  is => 'ro',
  format => 's',
  doc => 'Target agent (claude-code, codex, cursor)',
);

option global => (
  is => 'ro',
  doc => 'Install/check globally (~/) instead of project-level',
);

option force => (
  is => 'ro',
  doc => 'Force reinstall even if current',
);

my %AGENTS = (
  'claude-code' => { project => '.claude/skills', global => '.claude/skills' },
  'codex'       => { project => '.agents/skills', global => '.codex/skills' },
  'cursor'      => { project => '.cursor/skills', global => '.cursor/skills' },
);

sub execute {
  my ($self, $args_ref, $chain_ref) = @_;
  $self->_reject_root_dir($chain_ref);
  my @pos    = $self->positional_args($args_ref);
  my $action = $pos[0] // 'install';
  $self->check_positional_args($args_ref, 1);   # only the action is a positional

  if ($action eq 'install') {

lib/App/karr/Cmd/Skill.pm  view on Meta::CPAN


App::karr::Cmd::Skill - Install, check, and update bundled agent skills

=head1 VERSION

version 0.600

=head1 SYNOPSIS

    karr skill install
    karr skill install --agent codex,cursor
    karr skill check --global
    karr skill update --force
    karr skill show

=head1 DESCRIPTION

Installs and maintains the bundled C<karr> skill file for supported agent
clients. The command can target project-local directories or global skill
locations in the current user's home directory, which makes it useful both for
direct Perl installs and Docker-wrapped vendor usage.

Writes go into the target file B<in place>, keeping its inode, so a
F<SKILL.md> that is one link of a hardlink chain shared across projects stays
part of that chain instead of being silently broken out of it.

C<--global> selects the home-directory location instead of the project-local
one; the two coincide for C<claude-code> and C<cursor> but differ for
C<codex> (see L</SUPPORTED AGENTS>).

=head1 TARGET DIRECTORY

The project-local target is the B<current working directory>: the skill file
is written straight underneath it, at
F<.claude/skills/kanban-issues-karr-cli/SKILL.md> for C<claude-code> and at
the equivalent path for the other agents. Nothing is discovered on the way
there and no repository is involved -- this command has no board, and
installing into a directory that is not a Git repository at all is a

lib/App/karr/Cmd/Skill.pm  view on Meta::CPAN

ticket #226 the root placement was accepted and then discarded without a
word, so the file was written into the tree the caller happened to be
standing in while the message read as if the named one had been used.

C<karr init --claude-skill> writes the very same F<SKILL.md> and does honour
C<--dir>: it installs into the root of the repository it is initializing, and
it needs a repository in the first place.

=head1 SUPPORTED AGENTS

The built-in agent targets are C<claude-code>, C<codex>, and C<cursor>. When
C<--agent> is omitted, the command auto-detects available client directories and
falls back to all known agents if nothing is detected.

=head1 ACTIONS

=over 4

=item * C<install>

Writes the current bundled skill file to the selected target locations. A

share/claude-skill.md  view on Meta::CPAN

```bash
karr skill install                           # install skill for detected agents
karr skill install --agent claude-code       # install for specific agent
karr skill install --global                  # install globally (~/)
karr skill install --force                   # force reinstall
karr skill check                             # check if installed skills are current
karr skill update                            # update outdated skills
karr skill show                              # print skill content to stdout
```

Supported agents: `claude-code`, `codex`, `cursor`.

For Docker-wrapped usage, prefer the `raudssus/karr:latest` alias that mounts
the current project at `/work` and uses `/home/karr` as `HOME`, so the image
can drop privileges to the owner of the mounted workspace without breaking
access to Git config or agent skill directories.

### Sync

```bash
karr sync

t/241-delete-prompt-flush.t  view on Meta::CPAN

use App::karr::BoardStore;
use App::karr::Task;

# Ticket #241: `karr delete` printed its confirmation with printf and no
# trailing newline, then blocked on <STDIN> without flushing. The question was
# therefore still sitting in karr's own buffer while the command was already
# waiting for the answer to it.
#
# What this test pins is exactly that: the question has reached the other end
# of the pipe *before* karr blocks on stdin. Everything a human notices --
# hanging cursor, typing blind, the answer echoing above the question -- is a
# consequence of that one property, and it is the only part of the story that
# can be observed without a terminal.
#
# Why not the pseudo-terminal the ticket was found on:
#
#   * Nothing in this suite allocates one today, IO::Pty is not a dependency,
#     and script(1) takes its arguments differently on util-linux and on BSD.
#     A test that hangs or fails on somebody else's machine would be worse than
#     no test.
#

t/241-delete-prompt-flush.t  view on Meta::CPAN


    my $old = getcwd();
    chdir $repo or die "chdir $repo: $!";
    my $err = gensym;
    my $pid = open3( my $in, my $out, $err,
        $^X, "-I$ROOT/lib", $BIN, 'delete', '1' );
    chdir $old or die "chdir $old: $!";

    # Deliberately nothing written to $in yet, and $in stays open: karr is left
    # blocking on a read that will not complete, which is the moment the
    # operator is looking at a cursor.
    #
    # Watched on stderr, not stdout: ticket #248 moved the question to the
    # channel a question belongs on, so that stdout carries only the outcome and
    # stays decodable under --json. The property this subtest pins is unchanged
    # by that, and unchanged again by #249's autoflush: the question is out on
    # the wire before karr blocks, whether Delete's own flush put it there or
    # the autoflush App::karr::Encoding sets beside the :encoding(UTF-8) layer.
    my $prompt   = '';
    my $deadline = time + $DEADLINE;
    while ( time < $deadline ) {

t/248-delete-json-prompt-channel.t  view on Meta::CPAN


    unlike( $rv->{stdout}, qr/\[y\/N\]/,
        'neither question reached STDOUT' );
};

subtest 'the plain path keeps the question off stdout too (#248)' => sub {
    my $repo = _board();

    # Without --json there is no object to corrupt, but the same rule decides
    # the channel: `karr delete 1 > kept.txt` used to write the question into
    # the file and show the operator a blank cursor.
    my $rv = _karr( $repo, ["n\n"], 'delete', '1' );
    is( $rv->{exit}, 0, 'answering no exits 0' ) or diag $rv->{stderr};

    like( $rv->{stderr}, qr/Delete task 1: Card one\? \[y\/N\] /,
        'the question is on STDERR' );
    unlike( $rv->{stdout}, qr/\[y\/N\]/,
        'and not on STDOUT' );
    like( $rv->{stdout}, qr/Skipped task 1: Card one/,
        'while the outcome stays on STDOUT, where results live' );
};

t/84-skill-usage-exit.t  view on Meta::CPAN

    like $rv->{stderr}, qr/\QUnknown action: bogusaction\E/,
        'and names the action it rejected';
    unlike $rv->{stderr}, qr/ at \S+ line \d+/, 'no file:line suffix';
};

subtest 'an unknown --agent value is a usage error' => sub {
    my $rv = run_karr( 'skill', 'check', '--agent', 'bogusagent' );
    is $rv->{exit}, 2, 'exits 2, not 1' or diag "stderr: $rv->{stderr}";
    like $rv->{stderr}, qr/^Usage: karr skill --agent /m, 'stderr opens with the usage line';
    like $rv->{stderr}, qr/\QUnknown agent: bogusagent\E/, 'and names the agent it rejected';
    like $rv->{stderr}, qr/claude-code, codex, cursor/, 'and lists the known agents';
    unlike $rv->{stderr}, qr/ at \S+ line \d+/, 'no file:line suffix';

    # One bad name in a comma list rejects the whole list.
    my $mixed = run_karr( 'skill', 'check', '--agent', 'codex,bogusagent' );
    is $mixed->{exit}, 2, 'a bad name among good ones still exits 2';
};

subtest 'valid skill usage is unaffected' => sub {
    my $show = run_karr( 'skill', 'show' );
    is $show->{exit}, 0, 'karr skill show still exits 0' or diag $show->{stderr};



( run in 0.861 second using v1.01-cache-2.11-cpan-d01c6094234 )