fix(kyberforge): bridge apm content to Claude Code's flat plugin discovery

Claude Code's (and Copilot's) native plugin installer has zero awareness of
.apm/ nesting -- it convention-scans only flat skills/, agents/, commands/,
hooks.json at each plugin's root. Confirmed via strings on the installed
claude binary and live installs of git@holocron/gitea@holocron/kyberforge@
holocron, all reporting Skills(0) Agents(0) Hooks(0) post ADR-0015's apm
conversion. Root cause (apm_cli/core/plugin_manifest.py): apm's plugin.json
compiler deliberately strips skills/agents/commands keys, assuming the host
already auto-discovers those convention directories -- it has no model of
.apm/ being host-visible at all. Separately, apm's own bundle exporter
(apm_cli/bundle/plugin_exporter.py, behind `apm pack --format plugin`)
implements the correct .apm/ -> flat mapping, but only ever targeted
build/<name>-<version>/, a path nothing in marketplace.json's source: points
at.

scripts/sync-plugin-content.sh wraps that bundle exporter and copies its
agents/, skills/, commands/, instructions/, extensions/, and merged
hooks.json back into each plugin's own root as a second tracked
compiled-output category -- same governance status as
.claude-plugin/plugin.json: generated from .apm/, never hand-edited. tests/
subdirectories are excluded from the mirror (dev fixtures, not host-visible
runtime content; several hardcode a relative repo-root walk-up sized for the
.apm/-nested depth, which breaks when duplicated one level shallower).
Applied for real across all 6 plugins and verified two ways: `claude plugin
validate --strict` passes on every real plugin directory, and a live
`claude --plugin-dir <path> -p "list skills/agents"` behavioral test
confirms content is now actually discovered.

Also, from the same issue #90 review round:
- scripts/check-manifests.sh pointed at each plugin's root-level plugin.json
  (checking skills/hooks/mcpServers/agents pointer fields) -- that file was a
  stale near-duplicate of .claude-plugin/plugin.json nothing else read or
  wrote, now deleted across all 6 plugins. check-manifests.sh is rewritten to
  validate .claude-plugin/plugin.json instead, and drops the pointer-field
  checks entirely (nothing to check -- those fields are correctly absent by
  design). Content-presence drift is now check-plugin-content-sync's job, a
  new pre-push hook wired in .pre-commit-config.yaml.

docs/adr/0017 records the root cause and decision in full, including two
rejected alternatives (patching plugin.json's path fields directly -- apm's
compiler strips them on every run; pointing marketplace.json at apm pack's
build/ output -- a version-suffixed non-source directory nothing can install
from without an extra build step). ADR-0015 and CONTEXT.md are updated to
point at it.

Refs: #90
This commit is contained in:
2026-08-13 16:59:03 +00:00
parent 7910b8b12c
commit 38f1ba4e03
217 changed files with 14455 additions and 175 deletions

View File

@@ -0,0 +1,81 @@
---
topic: branches
source_keys:
- gitea-mcp-repo
- gitea-mcp-slim-go
---
# Branch operations
Call signatures below were verified live against the deployed `gitea-mcp` server via `ToolSearch`
at authoring time, not copied from research docs — this is deliberate: research docs are generated
from source code at a point in time and can drift from the server actually deployed. Re-verify
against the live schema if these tools appear to behave differently than documented here.
## `list_branches`
**Parameters:**
- `owner` (string, required)
- `repo` (string, required)
- `page` (number, optional, default: `1`)
- `per_page` (number, optional, default: `30`)
**Call:**
```
list_branches owner: <owner> repo: <repo>
```
**Response:** one object per branch: `name`, `protected` (bool), `commit_sha` (present when the
underlying commit data is available).
Paginate if you need the full list (see Gotchas in SKILL.md) — iterate `page` until the returned
count is less than `per_page`.
## `create_branch`
**Parameters:**
- `owner` (string, required)
- `repo` (string, required)
- `branch` (string, required) — new branch name
- `old_branch` (string, optional) — source branch; if omitted, defaults to the repo's default
branch server-side (not necessarily your current local checkout)
**Call:**
```
create_branch owner: <owner> repo: <repo> branch: <new-name> old_branch: <source-branch>
```
Default dispatch: if the user gives a base ("branch off of X", "from X"), pass it as `old_branch`.
If they don't specify a base and you're mid-task on a local branch, pass your current branch
(`git branch --show-current`) as `old_branch` so the new branch forks from where you're actually
working, rather than silently falling back to the repo default. If neither applies (e.g. a fresh
top-level request with no working branch context), omit `old_branch` and let it default server-side.
A branch name collision returns `409 Conflict`.
## `delete_branch`
**Parameters:**
- `owner` (string, required)
- `repo` (string, required)
- `branch` (string, required)
**Call:**
```
delete_branch owner: <owner> repo: <repo> branch: <name>
```
Before calling this, see the hard-refusal Gotcha in SKILL.md. If the target branch's name isn't
obviously a scratch/feature branch, call `list_branches` first and check `protected` on the
matching entry — name-matching `main`/`master` alone isn't authoritative, since a repo can protect
a differently-named default branch. Confirm explicitly with the user before deleting anything
protected, every time, regardless of how the request is phrased.
## Token scope
All three — `list_branches`, `create_branch`, `delete_branch` — require `write:repository`. Gitea
gates reads behind write scope for repo-scoped operations, so `list_branches` needs the same scope
as the write operations, not `write:issue` alone. An earlier version of this doc claimed
`write:issue` alone was sufficient for `list_branches`, based on empirical testing under a token
that held both `write:issue` and `write:repository` simultaneously — that test didn't isolate the
variable, so it couldn't actually establish `write:issue` alone as sufficient.

View File

@@ -0,0 +1,73 @@
---
topic: commits
source_keys:
- gitea-mcp-repo
- gitea-mcp-slim-go
---
# Commit operations
Read-only commit history, scoped to a repo (optionally to one branch or one path). Call signatures
below were verified live against the deployed `gitea-mcp` server via `ToolSearch` at authoring time,
not copied from research docs, for the same drift-avoidance reason noted in `references/branches.md`.
This domain has no prior skill precedent — it's new coverage added alongside branches because commit
history is naturally scoped to a branch (a "what happened on this branch" question), not because it
shares any tool family with branch create/delete.
## `list_commits`
**Parameters:**
- `owner` (string, required)
- `repo` (string, required)
- `sha` (string, optional) — starting SHA or branch name; if omitted, gitea-mcp uses the repo's
default branch
- `path` (string, optional) — restrict results to commits that touched this file/path
- `page` (number, optional, default: `1`, minimum: `1`)
- `per_page` (number, optional, default: `30`, minimum: `1`)
**Call:**
```
list_commits owner: <owner> repo: <repo> sha: <branch-or-sha> path: <optional-path>
```
Dispatch defaults:
- "commits on `<branch>`" → pass `<branch>` as `sha`.
- "commits touching `<path>`" (no branch mentioned) → pass `path` alone, `sha` omitted (defaults to
the repo's default branch).
- Both given → pass both; the result is history for that path, walked from that branch/SHA.
- Neither given → omit both; this returns default-branch history, which is a reasonable default for
an open-ended "what's the recent history here" question.
**Response:** one object per commit: `sha`, `html_url`, `created`, `message` (when available),
`author` (`{name, email, date}`, when available).
Paginate per the manual-pagination Gotcha in SKILL.md if you need more than one page of history.
## `get_commit`
**Parameters:**
- `owner` (string, required)
- `repo` (string, required)
- `sha` (string, required)
**Call:**
```
get_commit owner: <owner> repo: <repo> sha: <commit-sha>
```
**Response:** same shape as a `list_commits` entry, but always fully populated (`message` and
`author` are guaranteed present, not conditional). Use this when the user asks about one specific
commit by SHA rather than browsing history — `list_commits` entries may omit `message`/`author` in
edge cases, `get_commit` will not.
## Token scope
Both tools are believed to require `write:repository`, even though they're read-only — inferred by
analogy with the scope-gating principle in `overview.md` (Gitea gates reads behind write scope for
repo-scoped operations), not a claim `overview.md` makes for commits by name: its explicit
`write:repository` enumeration lists PR, branch, file, release, and tag operations, but doesn't
mention commits. An earlier version of this doc claimed `write:issue` alone worked, based on
empirical testing under a token that held both `write:issue` and `write:repository`
simultaneously — that test didn't isolate the variable either. Treat this as unverified until
tested under a token scoped to `write:issue` only (no `write:repository`).

View File

@@ -0,0 +1,41 @@
# Sources
**Note on call signatures:** per `docs/adr/0011-gitea-skill-deep-modules.md`, the tool parameter
signatures in `references/branches.md` and `references/commits.md` were re-verified live via
`ToolSearch` against the deployed `gitea-mcp` server at authoring time — they are not copied
verbatim from `api-reference.md` below. This resolves issue #6 comment #849's root-cause finding
that a prior skill was authored from API docs that had drifted from the actual MCP tool schema.
The research docs cited here informed gotchas, response shapes, and workflow context, not the
parameter lists themselves.
## gitea-mcp-repo
- **URL:** https://gitea.com/gitea/gitea-mcp
- **Description:** Official gitea-mcp repository (v1.3.0); operation/*.go source files documenting all 55 MCP tools, their parameters, and CLI flags. Informed the dispatch table and pagination / 404-may-mean-403 gotchas in SKILL.md, and the list/create/delete branch and list/get commit mechanics (including 409 conflict and default-branch fallback behavior) in references/branches.md and references/commits.md.
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
- **Contributing files:** SKILL.md, references/branches.md, references/commits.md
- **Status:** `extracted`
## gitea-mcp-slim-go
- **URL:** https://gitea.com/gitea/gitea-mcp/raw/branch/main/operation/repo/slim.go
- **Description:** Slim response shape structs from gitea-mcp source; defines exactly which fields the MCP server returns for branches (name, protected, commit_sha) and commits (sha, html_url, created, message, author), and informed get_commit's always-populated guarantee vs. list_commits' conditional fields.
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
- **Contributing files:** references/branches.md, references/commits.md
- **Status:** `extracted`
## context7-websites-gitea
- **URL:** context7:/websites/gitea
- **Description:** Official Gitea docs mirror on Context7 — informed the protected-branch gotcha in SKILL.md (protected branches can block server-side operations regardless of client-side checks; admins aren't exempt by default).
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
- **Contributing files:** SKILL.md
- **Status:** `extracted`
## context7-gitea-tea-cli
- **URL:** context7:/git_gitea_com/gitea_tea
- **Description:** Official `tea` CLI docs on Context7 — practitioner conventions for issues, PRs, and releases (semver tags, draft/prerelease flags). Consulted as part of the shared research pass but its content is scoped to releases/tags, out of scope for branches/commits — no content from it was used in this skill.
- **Research doc:** plugins/gitea/docs/research/docs/gitea/sources.md
- **Contributing files:** (none)
- **Status:** `extracted`