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
7 changes: 3 additions & 4 deletions .claude/extensions/software-writer/writing-code.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
- `project.stacks` =
| Stack | Where | Toolchain |
|---|---|---|
| bash | `plugins/*/hooks/scripts/`, `plugins/*/mcp-server-*/`, `plugins/*/lsp-server-*/`, `plugins/*/shared/`, `templates/`, `.github/scripts/`, `plugin-tests/**/*.bats` | bash 4.0+ for MCP servers, 4.4+ for `test-writing` (`local -`); bash 3.2+ for `.github/scripts/`; `jq`; ShellCheck v0.11.0 in CI |
| python (root project) | `plugins/dev-tooling/shared/lsp_proxy.py`, `plugin-tests/dev-tooling/lsp_proxy/` | 3.12, `uv`; ruff line-length 100 selecting `E W F I B UP SIM RUF`; mypy `strict`, scoped by `files` to exactly these two paths and nothing else |
| python (`agent-skills-export/`) | `agent-skills-export/src/`, `agent-skills-export/tests/` | A separate `uv` project with its own lockfile, its own ruff (adds `C4 ARG PTH`, **ignores `E501`**), and its own mypy `strict`. Root ruff `extend-exclude`s it and root mypy does not see it — never apply the root project's rules here. |
| bash | `plugins/*/hooks/scripts/`, `plugins/*/mcp-server-*/`, `plugins/*/shared/`, `templates/`, `.github/scripts/`, `plugin-tests/**/*.bats` | bash 4.0+ for MCP servers, 4.4+ for `test-writing` (`local -`); bash 3.2+ for `.github/scripts/`; `jq`; ShellCheck v0.11.0 in CI |
| python (`agent-skills-export/`) | `agent-skills-export/src/`, `agent-skills-export/tests/` | A separate `uv` project with its own lockfile, its own ruff (adds `C4 ARG PTH`, **ignores `E501`**), and its own mypy `strict`. |

No other stack is in play. PHP, TypeScript, and Twig appear only as the *subjects* the plugins operate on in other repositories, never as source here.
- `code.primitives` =
Expand All @@ -26,7 +25,7 @@
| Resolve which scope a tool call runs under | reading `.scopes` out of the config at each call site | `resolve_scope`, `scope_get_tool_field`, `scope_get_bootstrap` (`plugins/dev-tooling/shared/scope.sh`, dev-tooling-owned and deliberately not templated) | Validates once — rejects the reserved name `shopware`, rejects an undeclared `default_scope` — and sets `SCOPE_NAME` and `SCOPE_CWD` for every `tool_*` function, so no tool re-derives a scope and none disagree about it. |
| Read a hook's stdin, load its config, or block a command | `cat` plus `jq`, then `echo >&2; exit 2` | `parse_hook_input`, `load_mcp_config`, `block_tool` (`templates/hooks-shared/common.sh`) | Carries the hook input contract, the `enforce_mcp_tools` opt-out, and the exit-2 block protocol with its message shape. |
- `code.di_pattern` = The outermost injection is `plugins/<plugin>/.mcp.json`, which points each server name at `${CLAUDE_PLUGIN_ROOT}/mcp-server-*/server.sh`; `${CLAUDE_PLUGIN_ROOT}` is the one value supplied from outside the repository. Below it, a bash MCP server's composition root is `plugins/<plugin>/mcp-server-*/server.sh`: it sets and exports the module inputs (`MCP_CONFIG_FILE`, `MCP_TOOLS_LIST_FILE`, `MCP_LOG_FILE`, `PROJECT_ROOT`, `CONFIG_PREFIX`), then sources the shared modules in dependency order and calls `run_mcp_server`. A sourced module reads its inputs from those exported variables and takes everything else as positional parameters; it never discovers its own configuration. Hook scripts take their input as JSON on stdin via `parse_hook_input`. Python uses parameter injection (`Path` arguments), never module-level state.
- `code.export_conventions` = A leading `_` marks a sourced module's internal function or variable (`_merge_configs`, `_discover_configs`, `_FOUND_CONFIGS`); unprefixed names are that file's public API. A sourced module that has a non-obvious public surface states it in a `# Public:` header block listing each exported function with a one-line contract, as `plugins/dev-tooling/shared/scope.sh` does — write one when adding a module other files will source, and keep it current when the surface changes. Python mirrors the underscore rule: `__init__.py` declares `__all__` and re-exports the public surface from `core.py`; `_`-prefixed names stay private, including in the standalone `lsp_proxy.py`, which has no package boundary and so no `__all__`.
- `code.export_conventions` = A leading `_` marks a sourced module's internal function or variable (`_merge_configs`, `_discover_configs`, `_FOUND_CONFIGS`); unprefixed names are that file's public API. A sourced module that has a non-obvious public surface states it in a `# Public:` header block listing each exported function with a one-line contract, as `plugins/dev-tooling/shared/scope.sh` does — write one when adding a module other files will source, and keep it current when the surface changes. Python mirrors the underscore rule: `__init__.py` declares `__all__` and re-exports the public surface from `core.py`; `_`-prefixed names stay private.

An MCP tool is exported by doing **both**: defining `tool_<name>` in a sourced lib, and adding its entry with an `inputSchema` to the server's `tools.json`. `handle_tools_call` dispatches via `type "tool_${tool_name}"` and `validate_tool_arguments` validates the call against that entry. A function without its `tools.json` entry is unreachable; an entry without an `inputSchema` is dispatched unvalidated.
- `code.footgun_additions` =
Expand Down
8 changes: 3 additions & 5 deletions .claude/extensions/software-writer/writing-tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,22 @@
- `project.stacks` =
| Stack | Where | Toolchain |
|---|---|---|
| bash | `plugins/*/hooks/scripts/`, `plugins/*/mcp-server-*/`, `plugins/*/lsp-server-*/`, `plugins/*/shared/`, `templates/`, `.github/scripts/`, `plugin-tests/**/*.bats` | bash 4.0+ for MCP servers, 4.4+ for `test-writing` (`local -`); bash 3.2+ for `.github/scripts/`; `jq`; ShellCheck v0.11.0 in CI |
| python (root project) | `plugins/dev-tooling/shared/lsp_proxy.py`, `plugin-tests/dev-tooling/lsp_proxy/` | 3.12, `uv`; ruff line-length 100 selecting `E W F I B UP SIM RUF`; mypy `strict`, scoped by `files` to exactly these two paths and nothing else |
| python (`agent-skills-export/`) | `agent-skills-export/src/`, `agent-skills-export/tests/` | A separate `uv` project with its own lockfile, its own ruff (adds `C4 ARG PTH`, **ignores `E501`**), and its own mypy `strict`. Root ruff `extend-exclude`s it and root mypy does not see it — never apply the root project's rules here. |
| bash | `plugins/*/hooks/scripts/`, `plugins/*/mcp-server-*/`, `plugins/*/shared/`, `templates/`, `.github/scripts/`, `plugin-tests/**/*.bats` | bash 4.0+ for MCP servers, 4.4+ for `test-writing` (`local -`); bash 3.2+ for `.github/scripts/`; `jq`; ShellCheck v0.11.0 in CI |
| python (`agent-skills-export/`) | `agent-skills-export/src/`, `agent-skills-export/tests/` | A separate `uv` project with its own lockfile, its own ruff (adds `C4 ARG PTH`, **ignores `E501`**), and its own mypy `strict`. |

No other stack is in play. PHP, TypeScript, and Twig appear only as the *subjects* the plugins operate on in other repositories, never as source here.
- `tests.frameworks` =
| Stack | Framework | Test files | Run |
|---|---|---|---|
| bash | BATS — bats-core 1.13.0, bats-support, bats-assert 2.2.4; every file declares `bats_require_minimum_version 1.11.0` | `plugin-tests/<plugin>/*.bats` | `./.github/scripts/setup-bats.sh` once, then `.bats/bats-core/bin/bats plugin-tests/**/*.bats`; CI runs `bats --timing -r plugin-tests/` |
| python | pytest with `asyncio_mode = "auto"` and `pytest-subprocess` | `plugin-tests/dev-tooling/lsp_proxy/test_*.py` | `uv run pytest plugin-tests/dev-tooling/lsp_proxy/` |
| python | pytest, a separate uv project with its own dependency set | `agent-skills-export/tests/test_*.py` | `make test` (`uv run --project agent-skills-export pytest agent-skills-export/tests/`) |

Select per file being edited. A `.bats` file is bash regardless of what language the script under test drives.
- `tests.fixture_sources` =
- `plugin-tests/test_helper/common_setup.bash` — `run_hook`, `assert_hook_blocks`, and `REPO_ROOT` resolution; loads bats-support and bats-assert. Every `.bats` file reaches it through its plugin helper.
- `plugin-tests/<plugin>/test_helper/common_setup.bash` — sets `SCRIPTS_DIR` for that plugin and holds its own builders, such as dev-tooling's `setup_config` and `setup_php_mcp_env`.
- `plugin-tests/dev-tooling/fixtures/coverage/*.xml` — committed Clover reports, read with `$(< file)` in `setup()`.
- `plugin-tests/dev-tooling/lsp_proxy/conftest.py` and `agent-skills-export/tests/conftest.py` — the pytest fixtures for their respective suites.
- `agent-skills-export/tests/conftest.py` — the pytest fixtures for that suite.

## Pre-Step-1

Expand Down
2 changes: 1 addition & 1 deletion .claude/hook-contexts/writing-commit-messages.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Infrastructure scopes:
Scope omission:

- Root docs only (`README.md`, `CONTRIBUTING.md`, `AGENTS.md`, `LICENSE`): omit scope.
- Root configs only (`pyproject.toml`, `uv.lock`, `.gitignore`): omit scope.
- Root configs only (`.gitignore`, `.shellcheckrc`): omit scope.
- Cross-cutting changes spanning 3 or more unrelated plugins: omit scope.
- Type is `ci` with only generic CI changes (not plugin-specific workflows): omit scope.

Expand Down
32 changes: 0 additions & 32 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ on:
- '.agents/**'
- '.claude-plugin/**'
- '.mcp-sdk.lock'
- 'pyproject.toml'
- 'uv.lock'
- '.shellcheckrc'
- '.github/workflows/ci.yml'
- '.github/scripts/**'
Expand All @@ -21,8 +19,6 @@ on:
- '.agents/**'
- '.claude-plugin/**'
- '.mcp-sdk.lock'
- 'pyproject.toml'
- 'uv.lock'
- '.shellcheckrc'
- '.github/workflows/ci.yml'
- '.github/scripts/**'
Expand Down Expand Up @@ -71,31 +67,3 @@ jobs:
run: |
echo "Running BATS tests with timing..."
bats --timing -r plugin-tests/

python:
name: Python Tests & Lint
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
enable-cache: true

- name: Install Python
run: uv python install

- name: Sync dependencies
run: uv sync --locked --group test --group lint

- name: Ruff
run: uv run ruff check .

- name: Mypy
run: uv run mypy

- name: Pytest
run: uv run pytest plugin-tests/dev-tooling/lsp_proxy/ -v
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ These plugins work best alongside a few Claude Code tweaks. Turn on `ENABLE_TOOL

| Plugin | Description | Components |
|-----------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|
| [dev-tooling](#dev-tooling) | PHPStan, ECS, PHPUnit, ESLint, Stylelint, Jest, and more via MCP servers. Optional phpactor LSP. | 🔌 MCP · 🪝 Hooks · 🎯 Skills · 🧠 LSP · 🤖 Agents |
| [dev-tooling](#dev-tooling) | PHPStan, ECS, PHPUnit, ESLint, Stylelint, Jest, and more via MCP servers. | 🔌 MCP · 🪝 Hooks · 🤖 Agents |
| [test-writing](#test-writing) | Automated PHPUnit test generation and validation for Shopware 6. | 🎯 Skills · 🤖 Agents · 🔌 MCP |
| [chunkhound-integration](#chunkhound-integration) | Semantic code research using ChunkHound. | 🔌 MCP · 🪝 Hooks · 🎯 Skills · 🤖 Agents |
| [ci-failure-interpretation](#ci-failure-interpretation) | CI failure log interpretation for GitHub Actions workflows. | 🎯 Skills |
Expand All @@ -48,7 +48,7 @@ These plugins work best alongside a few Claude Code tweaks. Turn on `ENABLE_TOOL

### dev-tooling

Three MCP servers for PHP and JavaScript operations plus an optional phpactor LSP for active PHP code discovery. Supports native, Docker, Docker Compose, Vagrant, and DDEV environments. Runs on Claude Code and on OpenAI Codex, where the servers start without a project root and bind one on demand; the LSP, the runner agent, and the worktree reminder hook are Claude Code only.
Three MCP servers for PHP and JavaScript operations. Supports native, Docker, Docker Compose, Vagrant, and DDEV environments. Runs on Claude Code and on OpenAI Codex, where the servers start without a project root and bind one on demand; the runner agent and the worktree reminder hook are Claude Code only.

```bash
/plugin install dev-tooling@shopware-ai-coding-tools
Expand All @@ -60,9 +60,8 @@ codex plugin add dev-tooling@shopware-ai-coding-tools
- **PHP:** PHPStan static analysis, ECS code style, PHPUnit test runner with coverage gap analysis, Symfony Console, Rector
- **Administration JS:** ESLint, Stylelint, Prettier, Jest, TypeScript, Vite builds
- **Storefront JS:** ESLint, Stylelint, Jest, Webpack builds
- **PHP LSP (optional):** document symbols, hover, go-to-definition, and references via [phpactor](https://github.com/phpactor/phpactor)

After installing, also install `plugin-setup@shopware-ai-coding-tools`, then ask Claude to help you set up the plugin — the `dev-tooling-setting-up` skill walks you through configuration. Prerequisites: `bash` 4.1+ (macOS's stock `/bin/bash` is 3.2), `jq`, and a restart after install. For LSP: `phpactor` binary available on the host (native) or inside the container (docker/docker-compose/vagrant/ddev). On Codex, trust the plugin hooks when Codex asks: the SessionStart directive then names the session directory, and the model binds each server by calling `set_project_root` itself. A server binds only to a project that already holds its configuration file (`.mcp-php-tooling.json` for `php-tooling`, `.mcp-js-tooling.json` for the two JS servers) unless `MCP_PHP_TOOLING_CONFIG` or `MCP_JS_TOOLING_CONFIG` names an existing file, and `plugin-setup` has no Codex manifest, so write that file by hand. Exporting `PROJECT_ROOT` before starting Codex is the alternative; see the [plugin README](./plugins/dev-tooling/README.md) for its caveat.
After installing, also install `plugin-setup@shopware-ai-coding-tools`, then ask Claude to help you set up the plugin — the `dev-tooling-setting-up` skill walks you through configuration. Prerequisites: `bash` 4.1+ (macOS's stock `/bin/bash` is 3.2), `jq`, and a restart after install. On Codex, trust the plugin hooks when Codex asks: the SessionStart directive then names the session directory, and the model binds each server by calling `set_project_root` itself. A server binds only to a project that already holds its configuration file (`.mcp-php-tooling.json` for `php-tooling`, `.mcp-js-tooling.json` for the two JS servers) unless `MCP_PHP_TOOLING_CONFIG` or `MCP_JS_TOOLING_CONFIG` names an existing file, and `plugin-setup` has no Codex manifest, so write that file by hand. Exporting `PROJECT_ROOT` before starting Codex is the alternative; see the [plugin README](./plugins/dev-tooling/README.md) for its caveat.

See [full documentation](./plugins/dev-tooling/README.md) for configuration and tool reference.

Expand Down
7 changes: 2 additions & 5 deletions plugin-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,16 +52,14 @@ plugin-tests/
├── test_helper/
│ └── common_setup.bash # Shared core fixtures
├── mcp-shared/ # Suites for templates/mcp-shared/ modules
│ ├── config.bats # Config filename and env-var prefix parameterization
│ ├── config.bats # Config filename and env-var derivation from CONFIG_PREFIX
│ ├── docker_compose.bats # Docker Compose call-time container/workdir resolution
│ ├── environment.bats # Environment wrapping
│ └── scope_wrap.bats # Scope-aware command wrapping per environment
├── dev-tooling/
│ ├── php_tools.bats # PHP hook blocking
│ ├── js_admin_tools.bats # Admin JS hook blocking
│ ├── js_storefront_tools.bats # Storefront JS hook blocking
│ ├── lsp_bootstrap.bats # LSP bootstrap: null stub, direct exec or python proxy
│ ├── lsp_null.bats # The minimal LSP stub used when an LSP is disabled
│ ├── mcp_tool_console.bats # Console tool tests
│ ├── mcp_tool_ecs.bats # ECS tool tests
│ ├── mcp_tool_js_admin.bats # Admin JS MCP tool tests
Expand All @@ -76,7 +74,6 @@ plugin-tests/
│ ├── scope_resolution.bats # Scope declaration, validation and refusal
│ ├── scope_session_start.bats # Scope reporting from the session-start hook
│ ├── session_start.bats # Session-start hook JSON output
│ ├── lsp_proxy/ # pytest suite for shared/lsp_proxy.py
│ ├── fixtures/
│ │ └── coverage/ # XML fixtures for coverage gap tests
│ │ ├── two_files.xml # Two files with partial coverage
Expand Down Expand Up @@ -167,7 +164,7 @@ The MCP protocol handler (`plugins/*/shared/mcpserver_core.sh`) has no suite her

## 🔄 CI

Tests run via GitHub Actions (`.github/workflows/ci.yml`) on changes to `plugins/**`, `plugin-tests/**`, `pyproject.toml`, `uv.lock`, `.shellcheckrc`, `.github/workflows/ci.yml`, or `.github/scripts/**`.
Tests run via GitHub Actions (`.github/workflows/ci.yml`) on changes to `plugins/**`, `plugin-tests/**`, `.agents/**`, `.claude-plugin/**`, `.mcp-sdk.lock`, `.shellcheckrc`, `.github/workflows/ci.yml`, or `.github/scripts/**`.

## 📌 Dependencies

Expand Down
16 changes: 1 addition & 15 deletions plugin-tests/dev-tooling/codex_manifest.bats
Original file line number Diff line number Diff line change
Expand Up @@ -64,27 +64,13 @@ _assert_server_forwards_config_vars() {
assert_output "$(jq -r '.version' "${CLAUDE_MANIFEST}")"
}

# The description and the keywords are the fields the two hosts differ on: the
# LSP is Claude Code only, and the Claude manifest advertises it in both.
# bats test_tags=static
@test "the codex manifest copies the claude manifest's shared metadata" {
local fields='{name, author, license, homepage, repository}'
local fields='{name, description, author, license, keywords, homepage, repository}'
run jq -cS "${fields}" "${CODEX_MANIFEST}"
assert_output "$(jq -cS "${fields}" "${CLAUDE_MANIFEST}")"
}

# bats test_tags=static
@test "the codex manifest keywords are the claude manifest keywords without the LSP ones" {
run jq -c '.keywords' "${CODEX_MANIFEST}"
assert_output "$(jq -c '.keywords - ["lsp", "language-server"]' "${CLAUDE_MANIFEST}")"
}

# bats test_tags=static
@test "the codex manifest description does not advertise the LSP" {
run jq -r '.description | test("\\blsp\\b|phpactor|language server"; "i")' "${CODEX_MANIFEST}"
assert_output "false"
}

# Without a hooks field Codex loads hooks/hooks.json itself; a hooks field
# would replace that file rather than add to it.
# bats test_tags=static
Expand Down
Loading
Loading