Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
51 changes: 51 additions & 0 deletions .claude/extensions/software-writer/writing-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
## Named-value assignments

- `project.stacks` =
- `bash` — `plugins/**/*.sh`, `.github/scripts/*.sh`, `plugin-tests/**/*.{bats,bash,sh}`
- `typescript` — `plugins/github-mcp/pi/*.ts`, `plugin-tests/github-mcp/{pi,fixtures/pi}/*.ts`; executed directly by Node ≥22.19 type stripping, no build step (`tsconfig.json`: `erasableSyntaxOnly`, `allowImportingTsExtensions`; relative imports keep the `.ts` extension)
- `code.primitives` =
| Call shape | Raw primitive | Helper | Invariant carried |
|---|---|---|---|
| Resolve the target repository from tool args | `jq` reads of `repo` / `repository` / `owner` / `url` | `_gh_resolve_owner_repo`, `_gh_resolve_owner_repo_optional` (`mcp-server-gh/lib/common.sh`) | Precedence `url` > `owner`+`repo` > `repository` > `repo` > `GH_DEFAULT_REPO`; a slash in split-form `repo` is rejected |
| Resolve an organization | `gh repo view`, raw `org` arg | `_gh_resolve_org` (`lib/common.sh`) | Result always passes `_gh_validate_org`, so it cannot steer the request to another API path |
| Validate a number, repo, SHA, or repository path argument | Ad hoc `[[ =~ ]]` checks | `_gh_validate_number`, `_gh_validate_repo`, `_gh_validate_sha`, `_gh_validate_path` (`lib/common.sh`) | `_gh_validate_path` rejects a leading `/` and any `..` segment |
| Apply caller-supplied `jq_filter` / `grep_pattern` / `head` / `tail` | Piping output through `jq` or `grep` directly | `_gh_validate_jq_filter`, `_gh_post_process` (`lib/common.sh`) | Filter compile-checked before use; steps run in a fixed jq → grep → head → tail order, each a no-op when unset |
| Emit `gh` output to the model | Raw `gh` stdout | `_gh_strip_ansi` (`lib/common.sh`) | No ESC byte reaches a terminal-rendering host; BSD-`sed`-compatible BRE under `LC_ALL=C` |
| Download file bytes to disk | `gh api … > file`, `mktemp` | `_gh_download_file` with `_gh_partial_create` / `_gh_partial_finish` / `_gh_partial_cleanup` (`lib/common.sh`) | Unique `noclobber` naming, atomic finish, partial file removed on cancellation |
| Locate the project config file | Hand-listed `.claude` / `.codex` / `.pi` candidate paths | `_load_gh_config` (`mcp-server-gh/lib/common.sh`, servers), `find_mcp_config` (`hooks/scripts/lib/common.sh`, hooks), both ordered by `github_mcp_config_dirs` (`shared/config-dirs.sh`) | The active host's directory takes priority, and servers and hooks agree on one host order |
| Read a config key | `jq` on `$GH_TOOLING_CONFIG_FILE` | `_gh_config_value` (`lib/common.sh`) | Returns the default when the file is missing or the key empty |
| Run a hook script from the pi extension | `child_process.spawn` | `runScript`, `runGate` (`plugins/github-mcp/pi/gate.ts`) | Never rejects; timeout kills the whole process group; sets `GITHUB_MCP_HOST=pi` and drops `CLAUDE_PROJECT_DIR`; blocks only on exit 2 |
| Invoke `gh` | A command string | `local -a cmd=(gh …)` then `"${cmd[@]}"` | Arguments stay array elements; never `eval` |
- `code.di_pattern` = TypeScript: the default export `githubMcp(pi: ExtensionAPI)` in `plugins/github-mcp/pi/index.ts` is the composition root; it computes script paths and timeouts and wires `pi.on(...)` handlers. Modules beside it (`gate.ts`) take script path, input, and timeout as parameters and hold no module-level state. Bash: a tool function `tool_<name>()` takes the JSON args string as `$1`; host and project context enter through environment variables set at the server or hook entry point (`GITHUB_MCP_HOST`, `CLAUDE_PROJECT_DIR`, `MCP_GH_TOOLING_CONFIG`, `GH_DEFAULT_REPO`, `PROJECT_ROOT`); `gh` resolves from PATH, so tests substitute a `gh()` function.
- `code.export_conventions` =
- `plugins/github-mcp/pi/index.ts` exports only the default extension factory named in root `package.json` `pi.extensions`; helpers live in sibling modules as named exports.
- Import `@earendil-works/*` packages only with `import type`; the extension carries no runtime dependency on pi (`@typescript-eslint/no-restricted-imports` in `eslint.config.mjs` enforces this for `plugins/github-mcp/pi/**/*.ts`).
- A new MCP tool is a `tool_<name>()` in `mcp-server-gh/lib/<group>.sh` plus its schema entry in `tools-read.json` or `tools-write.json`; a server runs only the tools its list declares. Before adding one, read `plugins/github-mcp/AGENTS.md` §When to Modify What.
- `code.footgun_additions` =
- `plugins/github-mcp/shared/mcpserver_core.sh` is vendored from `shopwareLabs/bash-mcp-sdk`: never edit it; protocol changes go upstream and arrive via `.mcp-sdk.lock` plus `.github/scripts/vendor-mcp-sdk.sh`.
- pi extension `.ts` files run under Node type stripping: no `enum`, `namespace`, parameter properties, or other non-erasable syntax.
- A new runtime file outside the root `package.json` `files` globs is missing from the npm package; only `plugin-tests/github-mcp/pi_e2e.bats`, which runs pi against the packed tarball, surfaces it.
- Host-specific behavior branches only on `GITHUB_MCP_HOST` (servers) or `HOOK_HOST` (hooks); runtime scripts stay shared across Claude Code, Codex, and pi.
- Adding or removing a plugin, MCP server, or hook requires running `.github/scripts/update-issue-templates.sh`; CI fails on stale issue-template dropdowns.
- `comments.preserve_patterns` =
- `# shellcheck disable=…` and `# shellcheck source=…` directives, together with the justification comment beside them
- The `# Tools: <names>` header line in `plugins/github-mcp/mcp-server-gh/lib/*.sh`
- `domain.terms` = `read server`, `write server`, `gh-tooling`, `gh-tooling-write` (server IDs; distinct from the plugin name `github-mcp`), `host`, `SessionStart directive`, `enforcement`, `escape hatch`, `enforce_mcp_tools`, `enable_write_server`, `block_api_commands`, `block_api_tool_read`, `block_api_tool_write`
- `docs.surfaces` =
| Surface | Owns | Shape | Single-owner |
|---|---|---|---|
| `README.md` | Marketplace pitch, per-host install commands, component summary, pointers into plugin docs | Root README | enforced |
| `AGENTS.md` | Three-host model, runtime-vs-docs file split, repository architecture, development workflow, version bumps, test commands, pre-release checklist, release and distribution | Architecture document | enforced |
| `CLAUDE.md`, `plugins/plugin-setup/CLAUDE.md` | Nothing; a one-line `@AGENTS.md` import. Gitignored and local to each checkout: never the home for a shared fact | LLM pointer file | not applicable |
| `plugins/github-mcp/README.md` | Configuration keys and lookup order, tool catalog and tool counts, write server, label semantics, enforcement layers, blocked commands, pi notes, troubleshooting, dependencies | Module README | enforced, except §Quick Start: exempt duplicate of the root README install commands because this file is the npm package homepage and must stand alone |
| `plugins/github-mcp/AGENTS.md` | Plugin file layout, config-loading internals, protocol flow, tool dispatch convention, standard execution block, task-to-file navigation, suite-to-coverage table | Module architecture and navigation document | enforced |
| `plugins/github-mcp/REFERENCE.md` | Per-tool parameters and examples | Reference | enforced |
| `plugins/github-mcp/SETUP.md` | Interactive setup procedure, permission groups | Guide | exempt: kept byte-identical to `plugins/plugin-setup/skills/github-mcp-setting-up/references/plugin-setup.md` |
| `plugins/plugin-setup/README.md`, `plugins/plugin-setup/AGENTS.md` | Setup-plugin install and usage; skill layout, setup-copy sync procedure, design decisions | Module README; module navigation document | enforced |
| `plugins/github-mcp/CHANGELOG.md`, `plugins/plugin-setup/CHANGELOG.md` | Released changes per version | Changelog | exempt: each entry restates what its release changed |

Runtime prompt text under `plugins/github-mcp/hooks/prompts/*.md` and `plugins/plugin-setup/skills/*/SKILL.md` are runtime files, not documentation surfaces.

## Post-Step-6

After editing a `.sh` file, run `shellcheck --shell=bash` on it; CI lints all of `plugins/`, `plugin-tests/`, and `.github/scripts/`. After editing a `.ts` file, run `npx tsc --noEmit -p .` and `npx eslint . --max-warnings 0`.
25 changes: 25 additions & 0 deletions .claude/extensions/software-writer/writing-docs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Named-value assignments

- `docs.surfaces` =
| Surface | Owns | Shape | Single-owner |
|---|---|---|---|
| `README.md` | Marketplace pitch, per-host install commands, component summary, pointers into plugin docs | Root README | enforced |
| `AGENTS.md` | Three-host model, runtime-vs-docs file split, repository architecture, development workflow, version bumps, test commands, pre-release checklist, release and distribution | Architecture document | enforced |
| `CLAUDE.md`, `plugins/plugin-setup/CLAUDE.md` | Nothing; a one-line `@AGENTS.md` import. Gitignored and local to each checkout: never the home for a shared fact | LLM pointer file | not applicable |
| `plugins/github-mcp/README.md` | Configuration keys and lookup order, tool catalog and tool counts, write server, label semantics, enforcement layers, blocked commands, pi notes, troubleshooting, dependencies | Module README | enforced, except §Quick Start: exempt duplicate of the root README install commands because this file is the npm package homepage and must stand alone |
| `plugins/github-mcp/AGENTS.md` | Plugin file layout, config-loading internals, protocol flow, tool dispatch convention, standard execution block, task-to-file navigation, suite-to-coverage table | Module architecture and navigation document | enforced |
| `plugins/github-mcp/REFERENCE.md` | Per-tool parameters and examples | Reference | enforced |
| `plugins/github-mcp/SETUP.md` | Interactive setup procedure, permission groups | Guide | exempt: kept byte-identical to `plugins/plugin-setup/skills/github-mcp-setting-up/references/plugin-setup.md` |
| `plugins/plugin-setup/README.md`, `plugins/plugin-setup/AGENTS.md` | Setup-plugin install and usage; skill layout, setup-copy sync procedure, design decisions | Module README; module navigation document | enforced |
| `plugins/github-mcp/CHANGELOG.md`, `plugins/plugin-setup/CHANGELOG.md` | Released changes per version | Changelog | exempt: each entry restates what its release changed |

Runtime prompt text under `plugins/github-mcp/hooks/prompts/*.md` and `plugins/plugin-setup/skills/*/SKILL.md` are runtime files, not documentation surfaces.
- `docs.pointer_file` = `CLAUDE.md` — a gitignored, per-checkout single `@AGENTS.md` line; each `AGENTS.md` in turn opens with `@README.md`
- `docs.jargon_home` = `AGENTS.md`
- `docs.changelog` = `plugins/github-mcp/CHANGELOG.md` and `plugins/plugin-setup/CHANGELOG.md`; Keep a Changelog with `## [x.y.z] - YYYY-MM-DD` entries, each version matching its plugin manifest version

## Post-Step-5

After editing `plugins/github-mcp/SETUP.md`, follow the sync procedure in `plugins/plugin-setup/AGENTS.md` §The skill, then confirm the copy with `cmp plugins/github-mcp/SETUP.md plugins/plugin-setup/skills/github-mcp-setting-up/references/plugin-setup.md`.

When the change documents altered tool behavior, confirm both `plugins/github-mcp/README.md` and `plugins/github-mcp/REFERENCE.md` were updated.
25 changes: 25 additions & 0 deletions .claude/extensions/software-writer/writing-tests.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Named-value assignments

- `project.stacks` =
- `bash` — `plugins/**/*.sh`, `.github/scripts/*.sh`, `plugin-tests/**/*.{bats,bash,sh}`
- `typescript` — `plugins/github-mcp/pi/*.ts`, `plugin-tests/github-mcp/{pi,fixtures/pi}/*.ts`; executed directly by Node ≥22.19 type stripping, no build step (`tsconfig.json`: `erasableSyntaxOnly`, `allowImportingTsExtensions`; relative imports keep the `.ts` extension)
- `tests.frameworks` =
- BATS (bats-core + bats-support + bats-assert, vendored into `.bats/` by `.github/scripts/setup-bats.sh`) for every `plugin-tests/**/*.bats`. Run with `.bats/bats-core/bin/bats -r plugin-tests/`. No BATS framework reference ships with the skill: apply the universal rules. Every suite opens with `# bats file_tags=github-mcp,<area>[,<sub-area>]` and `bats_require_minimum_version 1.11.0`; keep both on new suites.
- `node:test` for `plugin-tests/github-mcp/pi/*.test.ts`. Run with `node --test 'plugin-tests/github-mcp/pi/*.test.ts'`; type-checked by `npx tsc --noEmit -p .`.
- End-to-end layer: `plugin-tests/github-mcp/pi_e2e.bats` drives the real `node_modules/.bin/pi` with `--offline` against the scripted faux model in `plugin-tests/github-mcp/fixtures/pi/driver.ts`. Requires `npm ci` and GNU `timeout` or `gtimeout` on PATH.
- `tests.fixture_sources` =
- `plugin-tests/test_helper/common_setup.bash` — `run_hook`, `assert_hook_blocks`, `setup_config`, `setup_codex_config` for hook-script suites.
- `plugin-tests/github-mcp/test_helper/common_setup.bash` — path constants `PLUGIN_DIR`, `SCRIPTS_DIR`, `SESSION_SCRIPT`, `SHARED_DIR`, `GH_SERVER_DIR`, `GH_LIB_DIR`, and the default `setup()`; load it with `load 'test_helper/common_setup'` instead of hardcoding `plugins/github-mcp/` paths.
- Stubbing `gh` in tool-function suites: define a `gh()` function override in `setup()` that records what the tests assert on, then answers through `gh_stub_respond` (`plugin-tests/github-mcp/test_helper/common_setup.bash`: `GH_STUB_STDERR` on stderr, `GH_STUB_OUTPUT` on stdout, exit `GH_STUB_EXIT`); call `reset_gh_stub` in `setup()`.
- JSON-RPC requests to `server-read.sh` / `server-write.sh`: `jsonrpc_request <method> <id> [params-json]` from the same helper, never a hand-formatted JSON string.
- pi end-to-end only: `plugin-tests/github-mcp/fixtures/pi/gh-stub.sh` (PATH-shim `gh`, logs to `$GH_LOG`) and the `$SCENARIO`-selected scripts in `fixtures/pi/driver.ts`.
- `node:test`: per-test temp directories from `mkdtempSync(join(tmpdir(), …))` removed in `t.after`.
- `tests.parallelism` = Both runners currently execute serially: CI runs `bats --timing -r plugin-tests/` without `--jobs`, and no `plugin-tests/github-mcp/pi/*.test.ts` file sets `concurrency`. Write every test to stay correct under `bats --jobs`: per-test state lives in `BATS_TEST_TMPDIR`, `setup_file` state in `BATS_FILE_TMPDIR`, and nothing under `PLUGIN_DIR`, `GH_SERVER_DIR`, or `SCRIPTS_DIR` is mutated in place — copy it into the temp directory first, as `plugin-tests/github-mcp/server_startup_tools_list.bats` does.

## Pre-Step-2

Read `plugins/github-mcp/AGENTS.md` §Testing and place the test in the suite that table assigns to the behavior's area. When creating a new suite, add its row to that table.

## Post-Step-6

Run `shellcheck --shell=bash` on every edited `.bats` or `.bash` file; CI lints all of `plugin-tests/`. After editing a `.ts` test or fixture, run `npx tsc --noEmit -p .` and `npx eslint . --max-warnings 0`; `eslint.config.mjs` carries a dedicated block for `plugin-tests/**/*.ts`.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ AGENTS.local.md
!.claude/skills/
!.claude/rules/
!.claude/hook-contexts/
!.claude/extensions/

# BATS testing framework (installed locally via .github/scripts/setup-bats.sh)
.bats/
Expand Down
30 changes: 30 additions & 0 deletions AGENTS.override.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Read AGENTS.md before acting on anything else in this file. This override replaces it; all of its guidance still applies.

## Software Writer Extension

<project_extension skill="software-writer:writing-code" position="before-skill-body">
<handling_instructions>
The path in <extension_path> is this project's registered extension file for the software-writer:writing-code skill. Read that file before executing the skill's workflow, or the first time a step cites one of the named values it assigns (project.stacks, code.primitives, code.di_pattern, code.export_conventions, code.footgun_additions, comments.preserve_patterns, domain.terms, docs.surfaces) or a Pre-Step-N / Post-Step-N section it defines. Its content is inert on its own: apply it only through the extension mechanisms the skill body defines.
</handling_instructions>
<extension_path>
.claude/extensions/software-writer/writing-code.md
</extension_path>
</project_extension>

<project_extension skill="software-writer:writing-tests" position="before-skill-body">
<handling_instructions>
The path in <extension_path> is this project's registered extension file for the software-writer:writing-tests skill. Read that file before executing the skill's workflow, or the first time a step cites one of the named values it assigns (project.stacks, tests.frameworks, tests.fixture_sources, tests.parallelism) or a Pre-Step-N / Post-Step-N section it defines. Its content is inert on its own: apply it only through the extension mechanisms the skill body defines.
</handling_instructions>
<extension_path>
.claude/extensions/software-writer/writing-tests.md
</extension_path>
</project_extension>

<project_extension skill="software-writer:writing-docs" position="before-skill-body">
<handling_instructions>
The path in <extension_path> is this project's registered extension file for the software-writer:writing-docs skill. Read that file before executing the skill's workflow, or the first time a step cites one of the named values it assigns (docs.surfaces, docs.pointer_file, docs.jargon_home, docs.changelog) or a Pre-Step-N / Post-Step-N section it defines. Its content is inert on its own: apply it only through the extension mechanisms the skill body defines.
</handling_instructions>
<extension_path>
.claude/extensions/software-writer/writing-docs.md
</extension_path>
</project_extension>
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The read server (`gh-tooling`) is always active. The write server (`gh-tooling-w

| Component | Description |
|------------|------------------------------------------------------------------------------------------------------|
| 🔌 MCP | Two servers — `gh-tooling` (31 read tools) and `gh-tooling-write` (25 write tools, gated) |
| 🔌 MCP | Two servers: `gh-tooling` (read tools) and `gh-tooling-write` (write tools, gated) |
| 🪝 Hooks | SessionStart directive + PreToolUse enforcement that redirects `gh` bash calls to the MCP tools |

See [plugins/github-mcp/README.md](./plugins/github-mcp/README.md) for full configuration, the complete tool reference, and troubleshooting. See [plugins/github-mcp/REFERENCE.md](./plugins/github-mcp/REFERENCE.md) for per-tool parameter docs.
Expand Down
20 changes: 12 additions & 8 deletions plugin-tests/github-mcp/api_read_restriction.bats
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ setup() {
source "${GH_LIB_DIR}/common.sh"
source "${GH_LIB_DIR}/api.sh"

gh() {
[[ -n "${GH_STUB_STDERR:-}" ]] && echo "${GH_STUB_STDERR}" >&2
[[ -n "${GH_STUB_OUTPUT:-}" ]] && printf '%s\n' "${GH_STUB_OUTPUT}"
return "${GH_STUB_EXIT:-0}"
}
GH_STUB_OUTPUT=""
GH_STUB_STDERR=""
GH_STUB_EXIT=0
gh() { gh_stub_respond; }
reset_gh_stub
}

@test "tool_api_read with suppress_errors returns no error text when the call fails" {
# gh api prints an HTTP error's JSON body on stdout and its summary on stderr.
GH_STUB_OUTPUT='{"message":"Not Found","documentation_url":"https://docs.github.com/rest","status":"404"}'
GH_STUB_STDERR="gh: Not Found (HTTP 404)"
GH_STUB_EXIT=1
run tool_api_read '{"endpoint": "repos/shopware/shopware/pulls/0", "suppress_errors": true}'
assert_failure
assert_output ""
}

@test "tool_api_read allows GET method" {
Expand Down
Loading
Loading