Repository navigation
feat: bundle http-message consumer skill #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| category: added | ||
| type: minor | ||
| --- | ||
|
|
||
| Bundle the fast-forward-http-message consumer skill with executable examples, optional installation guidance and Composer discovery metadata. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| # fast-forward-http-message maintenance | ||
|
|
||
| This repository owns `fast-forward/http-message`. 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-message/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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| --- | ||
| name: fast-forward-http-message | ||
| description: Use Fast Forward PSR-7 payload responses, JSON streams, status and header helpers, preserving immutability and propagating encoding failures. | ||
| license: MIT; see LICENSE | ||
| compatibility: Supports fast-forward/http-message ^1.5 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-message ^1.5 | ||
| --- | ||
|
|
||
| # Build immutable HTTP responses | ||
|
|
||
| Use this skill for an application consuming `fast-forward/http-message:^1.5`. | ||
| Read its instructions, manifest/lockfile and installed source before editing. | ||
| Run `composer show fast-forward/http-message`. 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. | ||
| With authority to install the dependency: | ||
|
|
||
| ```sh | ||
| composer require 'fast-forward/http-message:^1.5' | ||
| ``` | ||
|
|
||
| Create `app.php` in the application root alongside composer.json/vendor: | ||
|
|
||
| ```php | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| require __DIR__ . '/vendor/autoload.php'; | ||
|
|
||
| use FastForward\Http\Message\JsonResponse; | ||
| use FastForward\Http\Message\JsonStream; | ||
| use FastForward\Http\Message\RedirectResponse; | ||
| use FastForward\Http\Message\Header\Authorization; | ||
|
|
||
| $original = new JsonResponse(['ready' => false]); | ||
| $changed = $original->withPayload(['ready' => true])->withStatus(201); | ||
| if ($original->getPayload() !== ['ready' => false] || $changed->getPayload() !== ['ready' => true] | ||
| || $changed->getStatusCode() !== 201 || !str_starts_with($changed->getHeaderLine('Content-Type'), 'application/json') | ||
| || json_decode((string) $changed->getBody(), true, flags: JSON_THROW_ON_ERROR) !== ['ready' => true] | ||
| || (new RedirectResponse('/login'))->getStatusCode() !== 302 | ||
| || Authorization::parse('unsupported fixture') !== null) { | ||
| throw new RuntimeException('Response contract differs.'); | ||
| } | ||
|
|
||
| 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 | ||
|
|
||
| This library uses `FastForward\Http\Message`; no container/provider is required. Construct JsonResponse with an application payload, use withStatus/withHeader/withPayload and retain the returned object. The application server or emitter must write the response; constructing it sends nothing. | ||
|
|
||
| JsonResponse sets an application/json Content-Type with charset and uses JsonStream. withPayload returns a new stream/response, but PSR-7 streams can themselves be mutable: do not treat a shared stream's cursor or write() as an immutable payload update. Top-level resources raise InvalidArgumentException; malformed UTF-8 and other JSON encoding failures raise JsonException. Keep the original response when testing mutation boundaries. | ||
|
|
||
| TextResponse and HtmlResponse set their corresponding content types, EmptyResponse is 204, RedirectResponse uses 302 or 301 for its permanent flag. RequestMethod/StatusCode and Header helpers are independent utilities. Authorization::parse returns null for missing/unknown or malformed supported credentials; parsing is not authentication. Never log credentials. Digest/AWS parsing uses mb_trim, which is native only from PHP 8.4: on PHP 8.3 install an appropriate polyfill before using those optional routes. The core JSON example needs no such route. | ||
|
|
||
| ## 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-message/examples/integration.php ./vendor/autoload.php | ||
| ``` | ||
|
|
||
| Expected final marker: `fast-forward-http-message: 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). |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| require $argv[1] ?? dirname(__DIR__, 4) . '/vendor/autoload.php'; | ||
|
|
||
| use FastForward\Http\Message\JsonResponse; | ||
| use FastForward\Http\Message\JsonStream; | ||
| use FastForward\Http\Message\RedirectResponse; | ||
| use FastForward\Http\Message\Header\Authorization; | ||
|
|
||
| $original = new JsonResponse(['ready' => false]); | ||
| $changed = $original->withPayload(['ready' => true])->withStatus(201); | ||
| if ($original->getPayload() !== ['ready' => false] || $changed->getPayload() !== ['ready' => true] | ||
| || $changed->getStatusCode() !== 201 || !str_starts_with($changed->getHeaderLine('Content-Type'), 'application/json') | ||
| || json_decode((string) $changed->getBody(), true, flags: JSON_THROW_ON_ERROR) !== ['ready' => true] | ||
| || (new RedirectResponse('/login'))->getStatusCode() !== 302 | ||
| || Authorization::parse('unsupported fixture') !== null) { | ||
| throw new RuntimeException('Response contract differs.'); | ||
| } | ||
| try { | ||
| new JsonStream("\xB1"); | ||
| throw new RuntimeException('Invalid UTF-8 unexpectedly encoded.'); | ||
| } catch (JsonException) { | ||
| } | ||
|
|
||
| echo "fast-forward-http-message: ok\n"; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| # Optional installation of fast-forward-http-message | ||
|
|
||
| `fast-forward/http-message:^1.5` includes this complete package under | ||
| `resources/skills/fast-forward-http-message`. Run `composer show fast-forward/http-message` | ||
| 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-message"], | ||
| "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-message --dry-run | ||
| composer skills:update fast-forward/http-message | ||
| ``` | ||
|
|
||
| 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-message/resources/skills/fast-forward-http-message" | ||
| skill_target=".agents/skills/fast-forward-http-message" | ||
| 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-message` for Codex or the configured | ||
| `$HOME/.claude/skills/fast-forward-http-message` 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-message-$(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-message/resources/skills/fast-forward-http-message --skill fast-forward-http-message --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-message/tree/$SKILL_REF/resources/skills/fast-forward-http-message" --skill fast-forward-http-message --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-message --agent codex --yes | ||
| # Only for a selected global copy: | ||
| npx --yes skills@1.7.2 remove fast-forward-http-message --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. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * Fast Forward PSR-7 HTTP Message utility classes. | ||
| * | ||
| * This file is part of fast-forward/http-message project. | ||
| * | ||
| * @author Felipe Sayão Lobato Abreu <github@mentordosnerds.com> | ||
| * @license https://opensource.org/licenses/MIT MIT License | ||
| * | ||
| * @see https://php-fast-forward.github.io/http-message/ | ||
| * @see https://github.com/php-fast-forward/http-message | ||
| * @see https://github.com/php-fast-forward/http-message/issues | ||
| * @see https://datatracker.ietf.org/doc/html/rfc2119 | ||
| */ | ||
|
|
||
| namespace FastForward\Http\Message\Tests; | ||
|
|
||
| use PHPUnit\Framework\Attributes\CoversNothing; | ||
| use PHPUnit\Framework\TestCase; | ||
| use Symfony\Component\Process\Process; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
composer show symfony/process 2>/dev/null | head -5
rg -n '"symfony/process"' composer.json composer.lock | headRepository: php-fast-forward/http-message Length of output: 871 🏁 Script executed: set -eu
printf '%s\n' '--- composer.json dependency declarations ---'
nl -ba composer.json | sed -n '1,180p'
printf '%s\n' '--- test imports and usage ---'
rg -n -C 5 -F -- 'Symfony\Component\Process\Process' tests/ConsumerSkillTest.php
printf '%s\n' '--- locked symfony/process package ---'
python3 - <<'PY'
import json
from pathlib import Path
data = json.loads(Path('composer.lock').read_text())
for section in ('packages', 'packages-dev'):
for package in data.get(section, []):
if package.get('name') == 'symfony/process':
print(section)
print(json.dumps(package, indent=2))
PY
printf '%s\n' '--- lock packages that require symfony/process ---'
python3 - <<'PY'
import json
from pathlib import Path
data = json.loads(Path('composer.lock').read_text())
for section in ('packages', 'packages-dev'):
for package in data.get(section, []):
requires = package.get('require', {})
if 'symfony/process' in requires:
print(section, package.get('name'), requires['symfony/process'])
PYRepository: php-fast-forward/http-message Length of output: 4754 🏁 Script executed: nl -ba tests/ConsumerSkillTest.php | sed -n '1,180p'Repository: php-fast-forward/http-message Length of output: 1935 Declare
🤖 Prompt for AI Agents |
||
|
|
||
| #[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-message/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-message: ok', $process->getOutput()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: php-fast-forward/http-message
Length of output: 3745
🏁 Script executed:
Repository: php-fast-forward/http-message
Length of output: 8371
Require the autoloader path for globally copied skills.
dirname(__DIR__, 4)is correct for the repository and project-local.agents/skillscopies. It is not correct for the documented global copies under$HOME/.agentsor$HOME/.claude: without$argv[1], the example can require$HOME/vendor/autoload.phpinstead of the application autoloader. The documented command already requires an explicit autoloader path.Suggested fix
📝 Committable suggestion
🧰 Tools
🪛 OpenGrep (1.30.1)
[ERROR] 5-5: Dynamic file path passed to include/require. This can lead to local or remote file inclusion. Use a fixed allowlist of paths.
(coderabbit.file-inclusion.php-dynamic-include)
🤖 Prompt for AI Agents