Skip to content
Open
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
5 changes: 5 additions & 0 deletions demo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,9 @@ phpstan.neon
.phpunit.cache
###< phpunit/phpunit ###

###> symfony/ai-mate ###
/.agents/skills/
/.claude/skills/
###< symfony/ai-mate ###

pgdata
31 changes: 31 additions & 0 deletions docs/components/mate.rst
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,31 @@ The core package provides basic system information tools:

* ``server-info`` - Get PHP runtime environment details: version, OS, OS family, and loaded extensions

Skills
------

`Agent Skills <https://agentskills.io>`_ are ``SKILL.md`` files that give your coding agent
structured, multi-step "how-to" knowledge for a task. Extensions can ship skills alongside their MCP tools, and Mate
installs them onto the filesystem where coding agents read them — a polyfill until skills can be
served over MCP directly.

You usually do not run anything: ``mate discover`` (which also runs automatically after
``composer require``) installs the skills of every enabled extension. To sync them manually, use
``mate skills:install``.

Each skill is installed under a ``mate-`` prefixed directory name (e.g. ``mate-demo-skill``) to
avoid clashing with skills you maintain from other sources, in two locations:

* ``.agents/skills/`` is the source of truth, read directly by Codex, OpenCode and GitHub Copilot.
* ``.claude/skills/`` is symlinked to ``.agents/skills/`` for Claude Code, which only reads its own
directory.

Skills are symlinked into ``.agents/skills/`` so they auto-update on ``composer update`` (the link
points into the gitignored ``vendor/`` directory, so this requires symlink privileges on Windows).

The core package itself ships a ``system-information`` skill describing how to inspect the PHP
runtime and installed package versions via the ``server-info`` tool.

Commands
--------

Expand All @@ -426,6 +451,12 @@ Commands
- Generate or update ``mate/extensions.php`` with discovered extensions
- Preserve existing enabled/disabled states for known extensions
- Default new extensions to enabled
- Install Agent Skills shipped by enabled extensions (see `Skills`_)

``mate skills:install``
Install the Agent Skills shipped by your enabled extensions so your coding agent can use
them. This runs automatically as part of ``mate discover``; use it for an explicit re-sync.
See `Skills`_.

``mate serve``
Start the MCP server with stdio transport.
Expand Down
34 changes: 34 additions & 0 deletions docs/components/mate/creating-extensions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,40 @@ Example configuration:
}
}

Skills
~~~~~~

``extra.ai-mate.skills`` (optional)

- List of directories holding `Agent Skills <https://agentskills.io>`_, relative to package root
(a single string is also accepted)
- Each immediate subdirectory is one skill and must contain a ``SKILL.md`` file
- Conventionally a single ``skills`` directory
- When a project runs ``mate discover`` (or ``mate skills:install``), each skill directory is
installed into the project's ``.agents/skills/`` and symlinked into ``.claude/skills/`` so coding
agents can use it

Example configuration:

.. code-block:: json

{
"extra": {
"ai-mate": {
"scan-dirs": ["src"],
"skills": ["skills"]
}
}
}

The directory layout for the example above::

skills/
└── my-skill/
├── SKILL.md
└── references/
└── details.md

Extension Discovery Opt-Out
~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
10 changes: 9 additions & 1 deletion src/mate/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ cd ../../.. && vendor/bin/php-cs-fixer fix src/mate/
### Running the Server
```bash
bin/mate init # Initialize configuration
bin/mate discover # Discover extensions
bin/mate discover # Discover extensions (also installs extension skills)
bin/mate skills:install # Install extension skills
bin/mate serve # Start MCP server
bin/mate clear-cache # Clear cache

Expand All @@ -67,6 +68,13 @@ Running `bin/mate discover` generates `mate/AGENT_INSTRUCTIONS.md` with extensio
- `mate/.env`: Environment variables for mate configuration
- `mate/src/`: Directory for user-defined MCP tools

## Skills

Extensions ship Agent Skills (`SKILL.md`) via the `extra.ai-mate.skills` directory key. `skills:install`
(run automatically by `discover`) symlinks each skill under a `mate-` prefixed directory into `.agents/skills/`
(read by Codex/OpenCode/Copilot) and into `.claude/skills/` for Claude Code, so they auto-update with the
package. The core package ships a built-in `system-information` skill.

### Extension Exclusion

By default, `mate init` sets `extension: false` in `composer.json` so vendor packages using Mate for internal tooling are not detected as user-facing extensions. To make a package a discoverable Mate extension, set `"extension": true` or remove the field entirely.
Expand Down
5 changes: 5 additions & 0 deletions src/mate/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
CHANGELOG
=========

0.11
----

* Add `skills:install` command and `extra.ai-mate.skills` config key to install Agent Skills shipped by extensions into `.agents/skills` and `.claude/skills` for coding agents

0.9
---

Expand Down
6 changes: 5 additions & 1 deletion src/mate/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"symfony/console": "^5.4|^6.4|^7.3|^8.0",
"symfony/dependency-injection": "^5.4|^6.4|^7.3|^8.0",
"symfony/ai-mate-composer-plugin": "^0.10",
"symfony/filesystem": "^5.4|^6.4|^7.3|^8.0",
"symfony/finder": "^5.4|^6.4|^7.3|^8.0"
},
"require-dev": {
Expand Down Expand Up @@ -75,7 +76,10 @@
"scan-dirs": [
"src/Capability"
],
"instructions": "INSTRUCTIONS.md"
"instructions": "INSTRUCTIONS.md",
"skills": [
"skills"
]
}
}
}
72 changes: 72 additions & 0 deletions src/mate/skills/system-information/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: system-information
description: >-
Inspect the runtime and dependency context of the PHP application Mate is attached to —
PHP version, OS, and loaded extensions via the server-info tool, plus installed package
versions from composer.lock — when diagnosing environment- or version-specific behavior
such as a missing extension or a version mismatch (an API that does not exist in the
release that is actually installed).
---

# System Information

Use this skill when a problem might depend on the **environment or the installed
dependency versions** rather than the code: a missing or misconfigured extension,
behavior that only reproduces on a specific PHP/OS version, or a bug that looks like a
version mismatch — a method, argument, or class that does not exist in the release that
is actually installed.

## Capabilities

| Need | Use |
|---|---|
| PHP version, OS, OS family, loaded extensions | `server-info` MCP tool (one call) |
| Installed version of a package | `composer show <vendor/package>` |
| Installed versions of a family of packages | `composer show '<vendor>/<prefix>*'` |
| Authoritative record of every installed version | `composer.lock` (`packages` / `packages-dev`) |

`server-info` reports the runtime only — it does **not** return package versions. Use
Composer for those.

## Runtime checks

1. **Start with the `server-info` tool** for PHP version, OS, OS family, and loaded
extensions — prefer it over `php -v`, `php -m`, or `uname`.
2. **Confirm a required extension is loaded** before blaming the code. If it is absent
from `server-info`, the fix is an environment/configuration change, not a code change.

## Resolving package versions

`composer.json` declares *constraints* (what is allowed); `composer.lock` records what is
*actually installed*. For "what is really running", trust the lock, not the constraint.

- **One package:** `composer show symfony/console` → installed version plus details.
- **A family:** `composer show 'symfony/ai-*'` → every matching package and its version.
- **Machine-readable:** `composer show --format=json symfony/console`.
- **No Composer binary, or you want the raw record:** look the package up by `name` under
`packages` in `composer.lock` and read its `version` field — do not load the whole lock
file into context.

Monorepo / path-repo note: locally linked packages (for example symlinked `symfony/ai-*`
in this repository) report `dev-main` or a path reference instead of a semver tag — that
is the working copy, not a released version.

## Diagnosing a version mismatch

1. Identify the package behind the failing API — the namespace usually maps to it
(`Symfony\Component\Console\…` → `symfony/console`).
2. Read the **installed** version with `composer show <package>` (or from `composer.lock`).
3. Compare it against the version the code expects: a method or argument added in a newer
minor, or removed/renamed in a newer major, is the classic signature.
4. Report the gap precisely — name the package, the installed version, and the required
one: *"symfony/console is 6.4 (installed), but the code calls a 7.1 API."*

## Rules

- Reach for `server-info` and `composer show` first; they are cheaper and more structured
than reading `composer.lock` or `php -m` wholesale.
- Report only the *one or two* facts that change the diagnosis (e.g. "ext-intl is not
loaded", "symfony/console is 6.4, the code uses a 7.1 API") — never the full extension
dump or the entire lock file.
- Never run state-changing commands. `composer show` is read-only; `composer require`,
`composer update`, and `composer install` are not. This skill is read-only inspection.
2 changes: 2 additions & 0 deletions src/mate/src/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Symfony\AI\Mate\Command\InitCommand;
use Symfony\AI\Mate\Command\ResourcesReadCommand;
use Symfony\AI\Mate\Command\ServeCommand;
use Symfony\AI\Mate\Command\SkillsInstallCommand;
use Symfony\AI\Mate\Command\StopCommand;
use Symfony\AI\Mate\Command\ToolsCallCommand;
use Symfony\AI\Mate\Command\ToolsInspectCommand;
Expand Down Expand Up @@ -54,6 +55,7 @@ public static function build(ContainerInterface $container): Application
ToolsInspectCommand::class,
ToolsCallCommand::class,
ResourcesReadCommand::class,
SkillsInstallCommand::class,
];

foreach ($commands as $commandClass) {
Expand Down
42 changes: 39 additions & 3 deletions src/mate/src/Command/DiscoverCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Symfony\AI\Mate\Agent\AgentInstructionsMaterializer;
use Symfony\AI\Mate\Discovery\ComposerExtensionDiscovery;
use Symfony\AI\Mate\Service\ExtensionConfigSynchronizer;
use Symfony\AI\Mate\Service\SkillsInstaller;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
Expand All @@ -28,6 +29,9 @@
* and generates/updates mate/extensions.php with discovered extensions.
* Also refreshes AGENT instruction artifacts for coding agents.
*
* @phpstan-import-type ExtensionData from ComposerExtensionDiscovery
* @phpstan-import-type InstallResult from SkillsInstaller
*
* @author Johannes Wachter <johannes@sulu.io>
* @author Tobias Nyholm <tobias.nyholm@gmail.com>
*/
Expand All @@ -38,6 +42,7 @@ public function __construct(
private ComposerExtensionDiscovery $extensionDiscovery,
private ExtensionConfigSynchronizer $extensionConfigSynchronizer,
private AgentInstructionsMaterializer $instructionsMaterializer,
private SkillsInstaller $skillsInstaller,
) {
parent::__construct(self::getDefaultName());
}
Expand Down Expand Up @@ -78,9 +83,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$count = \count($extensions);
if (0 === $count) {
$materializationResult = $this->instructionsMaterializer->materializeForExtensions([
'_custom' => $rootProjectExtension,
]);
$rootOnlyExtensions = ['_custom' => $rootProjectExtension];
$materializationResult = $this->instructionsMaterializer->materializeForExtensions($rootOnlyExtensions);
$skillResult = $this->syncSkills($rootOnlyExtensions);

if ($composerMode) {
$io->write('<info>AI Mate:</info> No extensions found.');
Expand All @@ -90,6 +95,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
'Packages must have "extra.ai-mate" configuration in their composer.json.',
]);
$this->displayInstructionsStatus($io, $materializationResult);
$this->displaySkillStatus($io, $skillResult);
$io->note('Run "composer require vendor/package" to install MCP extensions.');
}

Expand Down Expand Up @@ -118,6 +124,8 @@ protected function execute(InputInterface $input, OutputInterface $output): int

$materializationResult = $this->instructionsMaterializer->materializeForExtensions($enabledExtensionsForInstructions);

$skillResult = $this->syncSkills($enabledExtensionsForInstructions);

if ($composerMode) {
$this->displayComposerSummary($io, $count, $newPackages, $removedPackages);

Expand Down Expand Up @@ -152,6 +160,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

$this->displayInstructionsStatus($io, $materializationResult);
$this->displaySkillStatus($io, $skillResult);

$io->comment([
'Next steps:',
Expand All @@ -161,6 +170,33 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return Command::SUCCESS;
}

/**
* Install skills shipped by enabled extensions into the agent skill directories.
*
* Only new/missing skills are installed here; existing entries are left untouched.
* Extensions without skills are filtered out by the installer.
*
* @param array<string, ExtensionData> $enabledExtensions enabled extensions plus the root project ("_custom")
*
* @return InstallResult
*/
private function syncSkills(array $enabledExtensions): array
{
return $this->skillsInstaller->install($enabledExtensions);
}

/**
* @param InstallResult $skillResult
*/
private function displaySkillStatus(SymfonyStyle $io, array $skillResult): void
{
if ([] === $skillResult['installed']) {
return;
}

$io->text(\sprintf('Installed %d skill(s): <info>%s</info>.', \count($skillResult['installed']), implode(', ', $skillResult['installed'])));
}

/**
* @param string[] $newPackages
* @param string[] $removedPackages
Expand Down
Loading
Loading