Dist-Zilla-Plugin-GatherAgentContext

 view release on metacpan or  search on metacpan

t/20-excludes.t  view on Meta::CPAN

  my @ini   = @{ $opt{ini}   || [] };
  my $files = $opt{files} || {};
  Dist::Zilla::Tester->from_config(
    { dist_root => 't/does-not-exist' },
    { add_files => {
        'source/dist.ini' => join("\n",
          'name = AC-Test', 'author = Test <test@example.com>',
          'license = Perl_5', 'copyright_holder = Test', 'version = 0.001',
          '', '[GatherAgentContext]', @ini, '',
        ),
        'source/lib/AC/Test.pm' =>
          "package AC::Test;\n# ABSTRACT: t\nour \$VERSION='0.001';\n1;\n",
        %$files,
      } },
  );
}
sub plugin { my $t = shift; (grep { $_->isa('Dist::Zilla::Plugin::GatherAgentContext') } @{ $t->plugins })[0] }
sub gathered { my $t = shift; sort map { $_->name } grep { index($_->name,'misc/agent-context/')==0 } @{ $t->files } }

{
  my $tzil = build_tzil( files => {
    'source/.claude/skills/foo/SKILL.md'                 => "body\n",
    'source/.claude/skills/foo/.gitignore'               => "*\n",
    'source/.claude/settings.json'                       => "{}\n",
    'source/.claude/settings.local.json'                 => "{}\n",
    'source/.claude/skilletor.local.json'                => "{}\n",
    'source/.claude/skilletor.json'                      => "{}\n",
    'source/.claude/skilletor.lock.json'                 => "{}\n",
    'source/.claude/agents/.local.karr-coordinator.md'   => "agent\n",
    'source/.claude/rules/.local.house.md'               => "rule\n",
    'source/.claude/worktrees/wt/f'                       => "wt\n",
  });
  plugin($tzil)->gather_files;
  my @got = gathered($tzil);
  my %have = map { $_ => 1 } @got;

  # KEPT
  ok $have{'misc/agent-context/.claude/skills/foo/SKILL.md'}, 'SKILL.md kept';
  ok $have{'misc/agent-context/.claude/skilletor.json'},      'skilletor.json kept';
  ok $have{'misc/agent-context/.claude/skilletor.lock.json'}, 'skilletor.lock.json kept (provenance index)';
  ok $have{'misc/agent-context/.claude/agents/.local.karr-coordinator.md'},
     '.local.<name>.md agent kept (consumed content)';
  ok $have{'misc/agent-context/.claude/rules/.local.house.md'},
     '.local.<name>.md rule kept (consumed content)';

  # DROPPED
  ok !( grep { m{/\.gitignore$} } @got ),      'skilletor .gitignore dropped';
  ok !( grep { m{/settings\.json$} } @got ),   'settings.json dropped';
  ok !( grep { m{\.local\.json$} } @got ),     '*.local.json config dropped';
  ok !( grep { m{/worktrees/} } @got ),        'worktrees/ dropped';
}

# --- Local state and secrets below a harness directory never reach the build.
# The harness and the user park session logs, todos, caches and login tokens in
# .claude/ and .codex/. Nothing of that may be gathered -- including names no
# list anticipated (history.jsonl, a stray backup, a new cache dir): below a
# harness directory only known agent content is taken.
{
  my %state = (
    # what the fleet .gitignore calls "never track"
    'source/.claude/.credentials.json'          => "{\"token\":\"s3cret\"}\n",
    'source/.claude/projects/-home-x/abc.jsonl' => "session log\n",
    'source/.claude/todos/a.json'               => "[]\n",
    'source/.claude/statsig/cache.json'         => "{}\n",
    'source/.claude/local/notes.md'             => "private\n",
    'source/.claude/settings.local.json'        => "{}\n",
    'source/.claude/settings.local.json.bak'    => "{}\n",
    'source/.claude/CLAUDE.local.md'            => "private memory\n",
    'source/.codex/auth.json'                   => "{\"token\":\"s3cret\"}\n",
    # names nobody put on a list
    'source/.claude/history.jsonl'              => "prompt history\n",
    'source/.claude/shell-snapshots/s.sh'       => "export TOKEN=s3cret\n",
    'source/.claude/notes.txt'                  => "scratch\n",
    'source/.codex/config.toml'                 => "[mcp_servers.x]\n",
    'source/.codex/sessions/2026/s.jsonl'       => "session log\n",
    # a content name of the wrong kind is not content
    'source/.codex/rules'                       => "a file, not the rules dir\n",
    # tokens and local overrides tucked into otherwise gathered directories
    'source/.claude/skills/foo/auth.json'         => "{}\n",
    'source/.claude/skills/foo/.credentials.json' => "{}\n",
    'source/.claude/agents/reviewer.local.md'     => "local override\n",
    'source/.agents/skills/bar/auth.json'         => "{}\n",
  );
  my %content = (
    'source/CLAUDE.md'                                 => "c\n",
    'source/AGENTS.md'                                 => "a\n",
    'source/.claude/CLAUDE.md'                         => "c\n",
    'source/.claude/agents/worker.md'                  => "agent\n",
    'source/.claude/agents/.local.karr-coordinator.md' => "agent\n",
    'source/.claude/skills/foo/SKILL.md'               => "body\n",
    'source/.claude/skills/foo/references/projects/p.md' => "ref\n",
    'source/.claude/rules/house.md'                    => "rule\n",
    'source/.claude/hooks/pre.sh'                      => "#!/bin/sh\n",
    'source/.claude/commands/ship.md'                  => "cmd\n",
    'source/.claude/skilletor.json'                    => "{}\n",
    'source/.claude/skilletor.lock.json'               => "{}\n",
    'source/.codex/agents/.local.x.toml'               => "x\n",
    'source/.codex/skills/baz/SKILL.md'                => "s\n",
    'source/.codex/skilletor-rules.md'                 => "rules\n",
    'source/.agents/skills/bar/SKILL.md'               => "b\n",
  );
  my $tzil = build_tzil( files => { %state, %content } );
  plugin($tzil)->gather_files;
  my @got  = gathered($tzil);
  my %have = map { $_ => 1 } @got;

  for my $src (sort keys %state) {
    (my $rel = $src) =~ s{^source/}{};
    ok !$have{"misc/agent-context/$rel"}, "$rel stays out of the build";
  }
  is_deeply \@got,
    [ sort map { (my $rel = $_) =~ s{^source/}{misc/agent-context/}; $rel } keys %content ],
    'exactly the agent content is gathered, nothing else';
}

# --- An explicitly configured harness directory is filtered the same way:
# `dir = .claude` means "the agent content of .claude", not "all of .claude".
{
  my $tzil = build_tzil(
    ini   => [ 'dir = .claude' ],
    files => {
      'source/.claude/skills/foo/SKILL.md'  => "body\n",
      'source/.claude/.credentials.json'    => "{}\n",
      'source/.claude/projects/p/abc.jsonl' => "session log\n",
    },
  );
  plugin($tzil)->gather_files;
  is_deeply [ gathered($tzil) ],
    [ 'misc/agent-context/.claude/skills/foo/SKILL.md' ],
    'explicit dir = .claude still takes agent content only';
}

# --- A harness directory met deeper in a configured tree is filtered too.
{
  my $tzil = build_tzil(
    ini   => [ 'dir = sub' ],
    files => {
      'source/sub/notes.md'                     => "n\n",
      'source/sub/.claude/skills/foo/SKILL.md'  => "body\n",
      'source/sub/.claude/todos/a.json'         => "[]\n",
      'source/sub/.codex/auth.json'             => "{}\n",
    },
  );
  plugin($tzil)->gather_files;
  is_deeply [ gathered($tzil) ],
    [ 'misc/agent-context/sub/.claude/skills/foo/SKILL.md',
      'misc/agent-context/sub/notes.md' ],
    'nested harness directories take agent content only';
}

# --- The way to take more: name the subdirectory itself. It is then walked in
# full (built-in excludes still apply).
{
  my $tzil = build_tzil(
    ini   => [ 'dir = .claude', 'dir = .claude/output-styles' ],
    files => {
      'source/.claude/skills/foo/SKILL.md'        => "body\n",
      'source/.claude/output-styles/terse.md'     => "style\n",
      'source/.claude/output-styles/x.local.json' => "{}\n",
      'source/.claude/todos/a.json'               => "[]\n",
    },
  );
  plugin($tzil)->gather_files;
  is_deeply [ gathered($tzil) ],
    [ 'misc/agent-context/.claude/output-styles/terse.md',
      'misc/agent-context/.claude/skills/foo/SKILL.md' ],
    'a subdirectory outside the content set is gathered when configured by name';
}

# --- Binär-/Nicht-UTF-8-Datei -> fataler Abbruch, der die Datei benennt ---
{
  my $tzil = build_tzil( files => {
    'source/.claude/skills/bin/SKILL.md' => "ok\n",
  });
  # add_files kodiert Perl-Strings zu UTF-8; fuer echte INVALIDE Bytes hier
  # direkt roh unter den zilla-root schreiben.
  path($tzil->root)->child('.claude/skills/bin/image.bin')
    ->spew_raw("\xff\xfe\x00\x80\xc0\xc1");
  ok !eval { plugin($tzil)->gather_files; 1 },
     'a non-UTF-8 context file makes the build die (fail loud)';
  like $@, qr/image\.bin/, 'the fatal message names the offending file';
}



( run in 0.674 second using v1.01-cache-2.11-cpan-3f962061493 )