App-karr
view release on metacpan or search on metacpan
docs/superpowers/plans/2026-03-15-karr-git-sync.md view on Meta::CPAN
- Add has_remote, fetch, push_refs methods
- Add basic test for has_remote
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
### Chunk 2: Lock Mechanism
**Files:**
- Create: `lib/App/karr/Lock.pm`
- Modify: `t/10-sync.t` (add lock tests)
- [ ] **Step 1: Write failing lock test**
```perl
# Add to t/10-sync.t
use App::karr::Lock;
my $lock = App::karr::Lock->new(
board_dir => 't/fixtures/board',
task_id => 1,
);
ok($lock->can_acquire('agent-fox'), 'can acquire lock');
ok($lock->acquire('agent-fox'), 'acquire returns true');
ok(!$lock->can_acquire('agent-owl'), 'other agent cannot acquire');
ok($lock->release('agent-fox'), 'release returns true');
```
Run: `prove -l t/10-sync.t`
Expected: FAIL - Lock module not found
- [ ] **Step 2: Create Lock.pm**
```perl
# ABSTRACT: Task lock management via Git refs
package App::karr::Lock;
use Moo;
use Git::Raw;
use Path::Tiny;
has board_dir => ( is => 'ro', required => 1 );
has task_id => ( is => 'ro', required => 1 );
sub ref_path {
my ($self) = @_;
return "refs/karr/tasks/" . $self->task_id . "/lock";
}
sub can_acquire {
my ($self, $agent) = @_;
my $repo = Git::Raw::Repository->open($self->board_dir->stringify);
my $ref = $self->ref_path;
my $current = eval { $repo->reference($ref) };
return 1 unless $current;
my $content = $current->target->content;
chomp(my $locked_by = $content);
return $locked_by eq '' || $locked_by eq $agent;
}
sub acquire {
my ($self, $agent) = @_;
return 0 unless $self->can_acquire($agent);
my $repo = Git::Raw::Repository->open($self->board_dir->stringify);
my $ref = $self->ref_path;
my $blob = $repo->blob($agent);
my $commit = eval { $repo->reference($ref) };
# Create or update ref
$repo->reference($ref, $blob, 1);
return 1;
}
sub release {
my ($self, $agent) = @_;
my $repo = Git::Raw::Repository->open($self->board_dir->stringify);
my $ref = $self->ref_path;
# Delete ref to release
eval { $repo->reference($ref)->delete };
return 1;
}
1;
```
Run: `prove -l t/10-sync.t`
Expected: FAIL - need to handle Git::Raw API
- [ ] **Step 3: Fix Lock.pm for Git::Raw API**
```perl
# Simplified: use git command for now
sub can_acquire {
my ($self, $agent) = @_;
my $ref = $self->ref_path;
my $content = `cd @{[$self->board_dir]} && git cat-file -p $ref 2>/dev/null`;
return 1 unless $content;
my ($locked_by) = $content =~ /^(\S+)/m;
return !$locked_by || $locked_by eq $agent;
}
sub acquire {
my ($self, $agent) = @_;
return 0 unless $self->can_acquire($agent);
my $ref = $self->ref_path;
system("cd @{[$self->board_dir]} && git update-ref $ref $agent");
return $? == 0;
}
sub release {
my ($self, $agent) = @_;
my $ref = $self->ref_path;
system("cd @{[$self->board_dir]} && git delete-ref $ref");
return 1;
}
```
Run: `prove -l t/10-sync.t`
Expected: PASS (with test fixtures)
- [ ] **Step 4: Commit**
```bash
git add lib/App/karr/Lock.pm t/10-sync.t
git commit -m "feat: add Lock module with acquire/release
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
### Chunk 3: Sync Command
**Files:**
- Create: `lib/App/karr/Cmd/Sync.pm`
- [ ] **Step 1: Create Sync command**
```perl
# ABSTRACT: Sync karr board with remote
package App::karr::Cmd::Sync;
use Moo::Role;
use MooX::Options;
use App::karr::Sync;
option push => ( is => 'ro', default => 0 );
option pull => ( is => 'ro', default => 0 );
option watch => ( is => 'ro', default => 0 );
option wait => ( is => 'ro', default => 0 );
option 'no-sync' => ( is => 'ro', default => 0 );
sub execute {
my ( $self, $args, $data ) = @_;
my $sync = App::karr::Sync->new( board_dir => $self->board_dir );
unless ( $sync->has_remote ) {
docs/superpowers/plans/2026-03-15-karr-git-sync.md view on Meta::CPAN
my ( $self, $task_id ) = @_;
my $ref = "refs/karr/tasks/$task_id";
my $content = `cd @{[$self->board_dir]} && git cat-file -p $ref 2>/dev/null`;
return undef unless $content;
return Load($content);
}
sub write_task_ref {
my ( $self, $task_id, $data ) = @_;
my $content = Dump($data);
my $ref = "refs/karr/tasks/$task_id";
# Write to ref (requires ODB blob + ref update)
}
```
- [ ] **Step 2: Commit**
```bash
git add lib/App/karr/Sync.pm
git commit -m "feat: add task ref read/write
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
### Chunk 5: Auto-Sync Integration
**Files:**
- Create: `lib/App/karr/Role/GitSync.pm`
- Modify: `lib/App/karr/Cmd/Move.pm`, `Cmd/Edit.pm`, etc.
- [ ] **Step 1: Create GitSync role**
```perl
# ABSTRACT: Role for commands that auto-sync
package App::karr::Role::GitSync;
use Moo::Role;
use App::karr::Sync;
use App::karr::Lock;
has sync => ( is => 'lazy' );
sub _build_sync {
my ($self) = @_;
return App::karr::Sync->new( board_dir => $self->board_dir );
}
sub auto_sync {
my ( $self, $task_id, $agent ) = @_;
return if $self->no_sync;
my $lock = App::karr::Lock->new(
board_dir => $self->board_dir,
task_id => $task_id,
);
unless ( $lock->acquire($agent) ) {
die "Task $task_id is locked by another agent";
}
$self->sync->pull;
# Do the actual operation
$self->sync->push;
$lock->release($agent);
}
1;
```
- [ ] **Step 2: Add to Move command**
```perl
with 'App::karr::Role::GitSync';
sub execute {
my ( $self, $args, $data ) = @_;
# ... existing code ...
$self->auto_sync( $task_id, $self->claim )
if $self->claim && !$self->no_sync;
}
```
- [ ] **Step 3: Commit**
```bash
git add lib/App/karr/Role/GitSync.pm lib/App/karr/Cmd/Move.pm
git commit -m "feat: add auto-sync to move command
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>"
```
---
### Chunk 6: External Repos (Future)
**Files:**
- Create: `lib/App/karr/External.pm`
This chunk is optional for v1. Skip for initial release.
---
## Test Fixtures
Create `t/fixtures/board/` with a minimal git repo:
```
t/fixtures/board/
.git/
karr/
config.yml
tasks/
```
( run in 1.227 second using v1.01-cache-2.11-cpan-2398b32b56e )