Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Claude Instructions

See [AGENTS.md](./AGENTS.md).

## Agent skills

### Issue tracker

Issues and PRDs are tracked in GitHub Issues for `datashaman/evolve`. See `docs/agents/issue-tracker.md`.

### Triage labels

Triage uses the default labels: `needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, and `wontfix`. See `docs/agents/triage-labels.md`.

### Domain docs

This is a single-context repo using root `CONTEXT.md` and root `docs/adr/` when present. See `docs/agents/domain.md`.
30 changes: 30 additions & 0 deletions app/Mcp/Tools/Concerns/WorksWithEvolveArtifacts.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,36 @@ protected function normalizedArtifact(array $payload): array
];
}

protected function hydrateOmittedContentFields(array $artifact, ?array $existing, array $payload): array
{
foreach (['php', 'blade', 'style'] as $field) {
if (array_key_exists($field, $payload)) {
continue;
}

$artifact[$field] = $existing[$field] ?? $this->defaultContentField((string) $artifact['kind'], $field);
}

return $artifact;
}

protected function defaultContentField(string $kind, string $field): string
{
if ($field === 'php' && in_array($kind, ['component', 'form', 'page'], true)) {
return "use Livewire\\Component;\n\nnew class extends Component {\n //\n};";
}

if ($field === 'blade') {
return match ($kind) {
'layout', 'snippet' => '{{ $slot }}',
'component', 'form', 'page', 'view' => '<div></div>',
default => '',
};
}

return '';
}

protected function idFromPath(string $path): string
{
$path = preg_replace('#\.(blade\.php|css)$#', '', str_replace('\\', '/', trim($path)));
Expand Down
1 change: 1 addition & 0 deletions app/Mcp/Tools/UpsertArtifact.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function handle(Request $request, EvolveLibrary $library): ResponseFactor
$artifact = $this->normalizedArtifact($data);
$existingId = (string) ($data['id'] ?? $artifact['id']);
$existing = $this->artifact($library, $data['kind'], $existingId);
$artifact = $this->hydrateOmittedContentFields($artifact, $existing, $data);
$dryRun = (bool) ($data['dry_run'] ?? true);

if (! $dryRun) {
Expand Down
18 changes: 18 additions & 0 deletions docs/agents/domain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Domain Docs

This is a single-context repo.

## Before exploring, read these when present

- `CONTEXT.md` at the repo root
- `docs/adr/` for architectural decisions relevant to the area being changed

If these files do not exist, proceed silently. Do not create them upfront; producer skills such as `grill-with-docs` create them when project language or decisions are clarified.

## Use project vocabulary

When naming domain concepts in issues, plans, hypotheses, tests, or refactors, use the terms defined in `CONTEXT.md` if present.

## Flag ADR conflicts

If proposed work contradicts an existing ADR, call that out explicitly before proceeding.
19 changes: 19 additions & 0 deletions docs/agents/issue-tracker.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Issue tracker: GitHub

Issues and PRDs for this repo live as GitHub issues in `datashaman/evolve`. Use the GitHub connector or the `gh` CLI for operations.

## Conventions

- Create issues in GitHub Issues for `datashaman/evolve`.
- Read issues with comments and labels before triage.
- Comment on issues when applying triage outcomes.
- Apply and remove labels using the repo's configured triage label vocabulary.
- Close issues only when the triage outcome calls for it.

## When a skill says "publish to the issue tracker"

Create a GitHub issue in `datashaman/evolve`.

## When a skill says "fetch the relevant ticket"

Fetch the GitHub issue, including body, comments, labels, reporter, and dates.
13 changes: 13 additions & 0 deletions docs/agents/triage-labels.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Triage Labels

The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker.

| Label in mattpocock/skills | Label in our tracker | Meaning |
| -------------------------- | -------------------- | ---------------------------------------- |
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
| `needs-info` | `needs-info` | Waiting on reporter for more information |
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
| `ready-for-human` | `ready-for-human` | Requires human implementation |
| `wontfix` | `wontfix` | Will not be actioned |

When a skill mentions a role, use the corresponding label string from this table.
181 changes: 181 additions & 0 deletions tests/Feature/EvolveMcpServerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,187 @@ public function test_mcp_artifact_tools_manage_route_backed_views(): void
$this->assertSame('<h1>Landing</h1>', trim(File::get(resource_path('views/marketing/landing.blade.php'))));
}

public function test_mcp_metadata_only_updates_preserve_existing_view_content(): void
{
EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'view',
'name' => 'Landing',
'path' => 'resources/views/marketing/landing.blade.php',
'route' => '/landing',
'route_name' => 'landing',
'blade' => '<h1>Landing</h1>',
'dry_run' => false,
])->assertOk();

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'view',
'id' => 'marketing/landing',
'name' => 'Landing',
'path' => 'resources/views/marketing/landing.blade.php',
'route' => '/renamed-landing',
'route_name' => 'renamed.landing',
])->assertOk()
->assertStructuredContent(fn ($json) => $json
->where('dry_run', true)
->where('artifact.blade', File::get(resource_path('views/marketing/landing.blade.php')))
->etc()
);

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'view',
'id' => 'marketing/landing',
'name' => 'Landing',
'path' => 'resources/views/marketing/landing.blade.php',
'route' => '/renamed-landing',
'route_name' => 'renamed.landing',
'dry_run' => false,
])->assertOk();

$this->assertSame('<h1>Landing</h1>', trim(File::get(resource_path('views/marketing/landing.blade.php'))));
}

public function test_mcp_metadata_only_updates_preserve_existing_sfc_content(): void
{
EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'page',
'name' => 'Landing',
'path' => 'resources/views/pages/landing.blade.php',
'route' => '/landing',
'route_name' => 'landing',
'php' => $this->componentPhp(),
'blade' => '<div>Landing</div>',
'style' => '.landing { color: red; }',
'dry_run' => false,
])->assertOk();

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'page',
'id' => 'landing',
'name' => 'Landing',
'path' => 'resources/views/pages/landing.blade.php',
'route' => '/home',
'route_name' => 'pages.home',
])->assertOk()
->assertStructuredContent(fn ($json) => $json
->where('dry_run', true)
->where('artifact.php', $this->componentPhp())
->where('artifact.blade', '<div>Landing</div>')
->where('artifact.style', '.landing { color: red; }')
->etc()
);

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'page',
'id' => 'landing',
'name' => 'Landing',
'path' => 'resources/views/pages/landing.blade.php',
'route' => '/home',
'route_name' => 'pages.home',
'dry_run' => false,
])->assertOk();

$source = File::get(resource_path('views/pages/landing.blade.php'));

$this->assertStringContainsString($this->componentPhp(), $source);
$this->assertStringContainsString('<div>Landing</div>', $source);
$this->assertStringContainsString('.landing { color: red; }', $source);
}

public function test_mcp_metadata_only_updates_preserve_layout_snippet_and_style_content(): void
{
EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'layout',
'name' => 'Marketing',
'path' => 'resources/views/layouts/marketing.blade.php',
'blade' => '<main>{{ $slot }}</main>',
'style' => '.marketing { display: grid; }',
'dry_run' => false,
])->assertOk();

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'snippet',
'name' => 'Badge',
'path' => 'resources/views/snippets/badge.blade.php',
'blade' => '<span>Badge</span>',
'dry_run' => false,
])->assertOk();

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'style',
'name' => 'Theme',
'path' => 'resources/css/theme.css',
'style' => ':root { --accent: teal; }',
'dry_run' => false,
])->assertOk();

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'layout',
'id' => 'marketing',
'name' => 'Marketing Layout',
'path' => 'resources/views/layouts/marketing.blade.php',
'dry_run' => false,
])->assertOk();

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'snippet',
'id' => 'badge',
'name' => 'Badge Snippet',
'path' => 'resources/views/snippets/badge.blade.php',
'dry_run' => false,
])->assertOk();

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'style',
'id' => 'theme',
'name' => 'Theme Tokens',
'path' => 'resources/css/theme.css',
'dry_run' => false,
])->assertOk();

$this->assertSame('<main>{{ $slot }}</main>', trim(File::get(resource_path('views/layouts/marketing.blade.php'))));
$this->assertSame('.marketing { display: grid; }', trim(File::get(resource_path('css/layouts/marketing.css'))));
$this->assertSame('<span>Badge</span>', trim(File::get(resource_path('views/snippets/badge.blade.php'))));
$this->assertSame(':root { --accent: teal; }', trim(File::get(resource_path('css/theme.css'))));
}

public function test_mcp_upsert_distinguishes_omitted_content_from_explicit_empty_content(): void
{
EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'view',
'name' => 'Landing',
'path' => 'resources/views/marketing/landing.blade.php',
'route' => '/landing',
'route_name' => 'landing',
'blade' => '<h1>Landing</h1>',
'dry_run' => false,
])->assertOk();

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'view',
'id' => 'marketing/landing',
'name' => 'Landing',
'path' => 'resources/views/marketing/landing.blade.php',
'route' => '/landing',
'route_name' => 'landing',
'blade' => '',
'dry_run' => false,
])->assertOk();

$this->assertSame('', trim(File::get(resource_path('views/marketing/landing.blade.php'))));

EvolveServer::tool(UpsertArtifact::class, [
'kind' => 'component',
'name' => 'Empty Defaults',
'path' => 'resources/views/components/empty-defaults.blade.php',
'dry_run' => false,
])->assertOk();

$source = File::get(resource_path('views/components/empty-defaults.blade.php'));

$this->assertStringContainsString('use Livewire\Component;', $source);
$this->assertStringContainsString('<div></div>', $source);
}

public function test_content_tools_list_rows_and_upsert_granularly(): void
{
$fixture = $this->fixtureModelClass();
Expand Down