diff --git a/.changelog/bundled-consumer-skill.md b/.changelog/bundled-consumer-skill.md new file mode 100644 index 0000000..5b62d63 --- /dev/null +++ b/.changelog/bundled-consumer-skill.md @@ -0,0 +1,6 @@ +--- +category: added +type: minor +--- + +Bundle the fast-forward-http-client consumer skill with executable examples, optional installation guidance and Composer discovery metadata. diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..de4b3df --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# fast-forward-http-client maintenance + +This repository owns `fast-forward/http-client`. Read Composer metadata, source and +tests before changing public integration. Preserve unrelated work. Run +`composer install`, `composer validate --strict` and `composer dev-tools`. +Keep merge, release and deployment decisions with the maintainer. Ordinary +source/resources share this root contract; consumer procedures live in SKILL.md. + +## Bundled consumer skill + +Read [the consumer skill](resources/skills/fast-forward-http-client/SKILL.md) when changing application integration +examples, consumer guides or the skill. Keep its version contract, references +and executable examples aligned with the supported public API. The canonical +distributable source is `resources/skills`; it needs no installed copy or host +alias. Repository maintenance continues to follow this AGENTS.md. diff --git a/README.md b/README.md index 4968422..33fcbbc 100644 --- a/README.md +++ b/README.md @@ -118,3 +118,12 @@ This package is open-source software licensed under the Contributions, issues, and pull requests are welcome at [php-fast-forward/http-client](https://github.com/php-fast-forward/http-client). + +## Consumer agent skill + +The package includes [fast-forward-http-client](resources/skills/fast-forward-http-client/SKILL.md) for application integration. +It supports `fast-forward/http-client:^1.2`; inspect the installed version. +See [skill installation and lifecycle](resources/skills/fast-forward-http-client/references/installation.md) for +optional project synchronization with `llm/skills ^1.15`, guarded manual +project/global copies and a pinned Skills CLI alternative. Shipping files does +not install tooling or activate the skill in an agent session. diff --git a/composer.json b/composer.json index 4610599..07ded6b 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,10 @@ }, "require-dev": { "fast-forward/dev-tools": "dev-main", - "llm/skills": "^1.15" + "nyholm/psr7": "^1.8" + }, + "suggest": { + "llm/skills": "Optionally synchronize the bundled consumer skill in your project." }, "minimum-stability": "stable", "autoload": { @@ -56,6 +59,9 @@ }, "grumphp": { "config-default-path": "vendor/fast-forward/dev-tools/grumphp.yml" + }, + "skills": { + "source": "resources/skills" } }, "scripts": { diff --git a/resources/skills/fast-forward-http-client/LICENSE b/resources/skills/fast-forward-http-client/LICENSE new file mode 100644 index 0000000..aa2c238 --- /dev/null +++ b/resources/skills/fast-forward-http-client/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2025 Felipe Sayão Lobato Abreu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/resources/skills/fast-forward-http-client/SKILL.md b/resources/skills/fast-forward-http-client/SKILL.md new file mode 100644 index 0000000..d237bd1 --- /dev/null +++ b/resources/skills/fast-forward-http-client/SKILL.md @@ -0,0 +1,110 @@ +--- +name: fast-forward-http-client +description: Provide Fast Forward's Symfony-backed PSR-18 client, supply its required PSR-17 factories, and test success and transport failures without a network. +license: MIT; see LICENSE +compatibility: Supports fast-forward/http-client ^1.2 and PHP ^8.3. Requires Composer and declared extensions; inspect the installed version. Earlier releases are outside this contract. +metadata: + author: Fast Forward + api-baseline: fast-forward/http-client ^1.2 +--- + +# Wire a PSR-18 HTTP client + +Use this skill for an application consuming `fast-forward/http-client:^1.2`. +Read its instructions, manifest/lockfile and installed source before editing. +Run `composer show fast-forward/http-client`. For versions outside this contract, +inspect matching documentation; do not silently upgrade the application. + +## First working integration + +Requires PHP ^8.3, Composer's application autoloader and this package's runtime +dependencies. The direct example also needs `nyholm/psr7:^1.8`. +With authority to install the dependency: + +```sh +composer require 'fast-forward/http-client:^1.2' 'nyholm/psr7:^1.8' +``` + +Create `app.php` in the application root alongside composer.json/vendor: + +```php + [HttpClientServiceProvider::class]]); +$factory = new Psr17Factory(); +$makeContainer = static function (MockHttpClient $transport) use ($config, $factory): ContainerInterface { + return container( + new ArrayServiceProvider([ + HttpClient::class => static fn(): MockHttpClient => $transport, + ResponseFactoryInterface::class => static fn(): Psr17Factory => $factory, + StreamFactoryInterface::class => static fn(): Psr17Factory => $factory, + ]), + $config, + ); +}; +$services = $makeContainer(new MockHttpClient(new MockResponse('{"ready":true}', ['http_code' => 200]))); +$client = $services->get(ClientInterface::class); +$response = $client->sendRequest($factory->createRequest('GET', 'https://example.invalid/health')); +if ($response->getStatusCode() !== 200 || (string) $response->getBody() !== '{"ready":true}') { + throw new RuntimeException('Client response differs.'); +} +$failedClient = $makeContainer(new MockHttpClient(static function (): never { + throw new TransportException('offline fixture'); +}))->get(ClientInterface::class); + +echo "integration ready\n"; +``` + +Run `php app.php`. It throws on a wrong result; success prints +`integration ready`. It uses no production data and starts no server. + +## Wiring and failure boundaries + +The primary product is `FastForward\Http\Client\ServiceProvider\HttpClientServiceProvider`. Place that class in ArrayConfig under `FastForward\Container\ContainerInterface::class`, pass the config to `FastForward\Container\container($config)`, and request `Psr\Http\Client\ClientInterface`. It uses Symfony HttpClient::create and Psr18Client. A provider-only application may pass the provider class directly. + +This package requires PSR-17 interfaces but does not ship a concrete message implementation or factory provider. In the application install `fast-forward/http-factory:^1.2` and register `FastForward\Http\Message\Factory\ServiceProvider\HttpMessageFactoryServiceProvider::class` before the client provider. Alternatively supply ResponseFactoryInterface and StreamFactoryInterface through your own ArrayServiceProvider. The direct example needs `nyholm/psr7:^1.8` explicitly; MockHttpClient/MockResponse come from the required Symfony component, not a test-only package. + +Put custom factories/transport in an earlier ArrayServiceProvider; first successfully resolving source wins. Provider services are cached. A missing PSR-17 factory can make get(ClientInterface::class) fail even if has() is true. The helper constructs the composed container; your application passes the resolved client to services that send requests. + +Read and validate application configuration before constructing a custom Symfony transport; this provider has no invented timeout/environment config key. For custom timeout/base URI/auth choose HttpClient::create($options) in an explicit factory. PSR-18 HTTP 4xx/5xx responses are returned normally; catch ClientExceptionInterface for client/transport failures. Keep credentials in the application's secret mechanism and avoid real network calls in tests. + +The helper tries aggregate sources in registration order. A PSR container or +not-found exception from one source permits fallback to another source; other +throwables propagate. Source priority is therefore the first successful resolution, +not a guarantee that an earlier broken factory terminates lookup. Nested provider +list errors during helper composition are caught; if services are absent, inspect +the raw configuration and resolve the provider directly to diagnose the failure. + +## Verify integration + +Run the [complete example](examples/integration.php) against the application's +autoloader in a disposable process. It checks success and negative cases: + +```sh +php /path/to/fast-forward-http-client/examples/integration.php ./vendor/autoload.php +``` + +Expected final marker: `fast-forward-http-client: ok`. Keep application-specific regression +tests in its existing PHPUnit suite, with each fixture class in its same-named +file. File copying, host discovery and completed agent tasks are different checks. +For optional copying/synchronization load +[installation and lifecycle](references/installation.md). diff --git a/resources/skills/fast-forward-http-client/examples/integration.php b/resources/skills/fast-forward-http-client/examples/integration.php new file mode 100644 index 0000000..b69ac62 --- /dev/null +++ b/resources/skills/fast-forward-http-client/examples/integration.php @@ -0,0 +1,49 @@ + [HttpClientServiceProvider::class]]); +$factory = new Psr17Factory(); +$makeContainer = static function (MockHttpClient $transport) use ($config, $factory): ContainerInterface { + return container( + new ArrayServiceProvider([ + HttpClient::class => static fn(): MockHttpClient => $transport, + ResponseFactoryInterface::class => static fn(): Psr17Factory => $factory, + StreamFactoryInterface::class => static fn(): Psr17Factory => $factory, + ]), + $config, + ); +}; +$services = $makeContainer(new MockHttpClient(new MockResponse('{"ready":true}', ['http_code' => 200]))); +$client = $services->get(ClientInterface::class); +$response = $client->sendRequest($factory->createRequest('GET', 'https://example.invalid/health')); +if ($response->getStatusCode() !== 200 || (string) $response->getBody() !== '{"ready":true}') { + throw new RuntimeException('Client response differs.'); +} +$failedClient = $makeContainer(new MockHttpClient(static function (): never { + throw new TransportException('offline fixture'); +}))->get(ClientInterface::class); +try { + $failedClient->sendRequest($factory->createRequest('GET', 'https://example.invalid/health')); + throw new RuntimeException('Transport failure unexpectedly succeeded.'); +} catch (ClientExceptionInterface) { +} + +echo "fast-forward-http-client: ok\n"; diff --git a/resources/skills/fast-forward-http-client/references/installation.md b/resources/skills/fast-forward-http-client/references/installation.md new file mode 100644 index 0000000..4ce6624 --- /dev/null +++ b/resources/skills/fast-forward-http-client/references/installation.md @@ -0,0 +1,167 @@ +# Optional installation of fast-forward-http-client + +`fast-forward/http-client:^1.2` includes this complete package under +`resources/skills/fast-forward-http-client`. Run `composer show fast-forward/http-client` +and inspect that path before copying. Earlier releases are outside this skill +contract. Composer suggestions do not install anything automatically. Library +installation, discovery-root copies and session activation are separate steps. + +## Team project with llm/skills + +Use Composer 2 and optionally add `llm/skills:^1.15` as development tooling. +Start with a project target for reviewable team configuration. Preserve existing +settings/aliases/approvals when merging these keys into `skills.json` **before** +installing or enabling the plugin: + +```json +{ + "target": ".agents/skills", + "aliases": [], + "discovery": false, + "auto-sync": true, + "vendor-sources": false, + "dependencies": { + "composer": { + "trusted": ["fast-forward/http-client"], + "trusted-replace": true + } + } +} +``` + +This replaces implicit dependency trust with a narrow donor allowlist, disables +remote vendor sources and requests no host aliases. A deliberate +`fast-forward/*` allowlist grants broader organizational trust. Use the actual +host's project discovery root; Claude Code can choose `.claude/skills`. +Do not erase intentional aliases or other donors in an existing manifest. + +If the application owner has authorized this plugin's execution, merge its +root approval, install it and inspect the report: + +```sh +composer config allow-plugins.llm/skills true +composer require --dev 'llm/skills:^1.15' +composer skills:show +``` + +The donor only suggests the tool; it neither requires it at runtime nor grants +this approval on behalf of consumers. Commit the chosen manifest, skills.json, +application lockfile and intended project copies according to project policy. +Approved Composer install/update operations then perform automatic sync. + +For manual timing, choose `auto-sync:false` before installation and use: + +```sh +composer skills:show +composer skills:update fast-forward/http-client --dry-run +composer skills:update fast-forward/http-client +``` + +Explicitly naming a donor selects/trusts it for that run. Test automatic trust +without a package argument when that policy is what matters. With Composer +2.10.3 and llm/skills 1.15.0, `--no-scripts` alone still permits automatic copy; +`auto-sync:false` disables automatic sync and `--no-plugins` stops plugin loading. +Verify these behaviors again after changing tool versions. + +## Update and removal boundaries + +A 1.15 sync overwrites incoming same-path files. It retains extra local files +and files removed from the donor's next version. Removing a donor or uninstalling +the plugin does not delete its existing instruction copies. Regard the copy as +donor-owned; keep deliberate local instructions in a separate skill name. +Record the installed donor/tool versions and review diffs before each refresh. + +Inspect cleanup without applying it: + +```sh +composer skills:update --clean --dry-run +``` + +An unscoped clean can remove all target skills, including handwritten ones. +Scoped clean reinstalls the current selection; it cannot locate an absent donor +by historical ownership. Inventory affected directories, back up outside +discovery, and obtain authority before any destructive cleanup. Do not add a +root-wide clean hook. + +## Guarded manual copying + +Choose the existing project root, then copy all files together: + +```sh +skill_source="vendor/fast-forward/http-client/resources/skills/fast-forward-http-client" +skill_target=".agents/skills/fast-forward-http-client" +test -f "$skill_source/SKILL.md" && +test ! -e "$skill_target" && +test ! -L "$skill_target" && +mkdir -p "$(dirname "$skill_target")" && +cp -R "$skill_source" "$skill_target" +``` + +Both existing directories and dangling links are refused. Global use is an +explicit alternative: set skill_target to +`$HOME/.agents/skills/fast-forward-http-client` for Codex or the configured +`$HOME/.claude/skills/fast-forward-http-client` for Claude Code. +Do not create a symlink into replaceable vendor content. A globally installed +Composer plugin does not constitute a global instruction copy. + +For a selected manual update/removal, first move only that package into a unique +backup outside discovery: + +```sh +skill_backup="$HOME/.agent-skill-backups/fast-forward-http-client-$(date +%Y%m%d%H%M%S)-$$" +test -d "$skill_target" && +test ! -L "$skill_target" && +test ! -e "$skill_backup" && +test ! -L "$skill_backup" && +mkdir -p "$(dirname "$skill_backup")" && +mv "$skill_target" "$skill_backup" +``` + +For an update review the new source, then repeat the guarded copy. To roll back, +archive any replacement and restore the saved whole directory to the same +target. A manager-owned copy may return on its next sync until the consumer +changes that donor's trust/dependency configuration. Keep backups out of every +discovery directory. + +## Optional Skills CLI + +Requires npm, Node.js 22.20.0+ and Git for repository sources. The reviewed pin +is skills@1.7.2; evaluate version changes separately. Choose only the actual host: + +```sh +npx --yes skills@1.7.2 add ./vendor/fast-forward/http-client/resources/skills/fast-forward-http-client --skill fast-forward-http-client --agent codex --copy --yes +npx --yes skills@1.7.2 list --agent codex +``` + +Use `--agent claude-code` for that host. Add `--global` to both commands only +when global scope was selected. Preserve/commit intended project copies and the +CLI's project lockfile. For a remote source first set SKILL_REF to a reviewed +immutable commit: + +```sh +npx --yes skills@1.7.2 add "https://github.com/php-fast-forward/http-client/tree/$SKILL_REF/resources/skills/fast-forward-http-client" --skill fast-forward-http-client --agent codex --copy --yes +``` + +Review the new revision and back up local edits before repeating for an update. +Remove the chosen package/scope with: + +```sh +npx --yes skills@1.7.2 remove fast-forward-http-client --agent codex --yes +# Only for a selected global copy: +npx --yes skills@1.7.2 remove fast-forward-http-client --agent codex --global --yes +``` + +After every route inspect the complete files, refresh the agent session, +confirm the public name is discovered and perform a bounded consumer task. +A copy, listing and successful task are separate observations. + +Tool contracts: [llm/skills 1.15.0](https://github.com/roxblnfk/skills/tree/1.15.0), +[Composer plugin approval](https://getcomposer.org/doc/06-config.md#allow-plugins) +and [Skills CLI](https://github.com/vercel-labs/skills). + +The Skills CLI 1.7.2 removal report is not proof that the canonical project +`.agents/skills` directory disappeared. A Codex project exercise reported one +removal while that copied package remained. Inspect the actual target and CLI +lockfiles afterwards. If an obsolete copy remains, verify that no other host +uses it, then archive only that selected directory outside discovery using the +manual recovery procedure above. Do not run a blanket clean to compensate. diff --git a/tests/ConsumerSkillTest.php b/tests/ConsumerSkillTest.php new file mode 100644 index 0000000..eb5542f --- /dev/null +++ b/tests/ConsumerSkillTest.php @@ -0,0 +1,47 @@ + + * @license https://opensource.org/licenses/MIT MIT License + * + * @see https://php-fast-forward.github.io/http-client/ + * @see https://github.com/php-fast-forward/http-client + * @see https://github.com/php-fast-forward/http-client/issues + * @see https://datatracker.ietf.org/doc/html/rfc2119 + */ + +namespace FastForward\Http\Client\Tests; + +use PHPUnit\Framework\Attributes\CoversNothing; +use PHPUnit\Framework\TestCase; +use Symfony\Component\Process\Process; + +#[CoversNothing] +final class ConsumerSkillTest extends TestCase +{ + /** + * @return void + */ + public function testBundledConsumerIntegration(): void + { + $root = \dirname(__DIR__); + $process = new Process([ + \PHP_BINARY, + $root . '/resources/skills/fast-forward-http-client/examples/integration.php', + $root . '/vendor/autoload.php', + ], $root, [ + 'XDEBUG_MODE' => 'off', + ]); + $process->setTimeout(20); + $process->run(); + + self::assertTrue($process->isSuccessful(), $process->getErrorOutput() . $process->getOutput()); + self::assertStringContainsString('fast-forward-http-client: ok', $process->getOutput()); + } +}