refactor!: carry out the simplification audit across gates, tests, plugins and docs #135

Merged
Defame1297 merged 85 commits from docs/simplification-audit into main 2026-09-20 19:14:03 +00:00
18 changed files with 290 additions and 238 deletions
Showing only changes of commit 061bb3d5b4 - Show all commits

View File

@@ -34,15 +34,19 @@ repos:
# rationale, under which a fixture or vendored tree containing # rationale, under which a fixture or vendored tree containing
# `.../.claude-plugin/marketplace.json` would have been silently excluded # `.../.claude-plugin/marketplace.json` would have been silently excluded
# from formatting while an equivalent # from formatting while an equivalent
# `.../.agents/plugins/marketplace.json` would not. The three root # `.../.agents/plugins/marketplace.json` would not. Only the one root
# marketplace manifests match; anything else is hand-authored and gets # marketplace manifest matches now; anything else is hand-authored and
# formatted. The twelve per-plugin `plugin.json` alternatives were # gets formatted. The twelve per-plugin `plugin.json` alternatives were
# dropped with the plugin manifests themselves when native # dropped with the plugin manifests themselves when native
# `claude plugin install` support was removed -- apm probes `apm.yml` # `claude plugin install` support was removed (ADR-0024) -- apm probes
# and never reached them. # `apm.yml` and never reached them. The `.agents/plugins/` and
# `.github/plugin/` marketplace mirrors went the same way, and their
# alternations went with them: `check-useless-excludes` fails on a
# pattern that matches no file.
# #
# `.claude/settings.json` is the sixteenth, and it is excluded for a # `.claude/settings.json` is the second and last alternation, and it is
# different reason: apm OWNS that file (ADR-0018, ADR-0019), and # the only one here for a reason other than "generated manifest":
# apm OWNS that file (ADR-0018, ADR-0019), and
# `apm audit --ci` replays the install into a scratch tree and diffs # `apm audit --ci` replays the install into a scratch tree and diffs
# the result byte-for-byte. `pretty-format-json` sorts object keys # the result byte-for-byte. `pretty-format-json` sorts object keys
# unless `--no-sort-keys` is passed, while apm's hook integrator emits # unless `--no-sort-keys` is passed, while apm's hook integrator emits
@@ -53,7 +57,7 @@ repos:
# happened when the SessionStart hook first landed in 2e395a4. # happened when the SessionStart hook first landed in 2e395a4.
# Re-running `apm install` fixes the file; leaving it in scope here # Re-running `apm install` fixes the file; leaving it in scope here
# would re-break it on the very commit that carries the fix. # would re-break it on the very commit that carries the fix.
exclude: '^(\.claude-plugin/marketplace\.json|\.agents/plugins/marketplace\.json|\.github/plugin/marketplace\.json|\.claude/settings\.json)$' exclude: '^(\.claude-plugin/marketplace\.json|\.claude/settings\.json)$'
- id: check-yaml - id: check-yaml
stages: ['pre-commit'] stages: ['pre-commit']
- id: trailing-whitespace - id: trailing-whitespace

View File

@@ -52,15 +52,16 @@ _Avoid_: command, prompt, macro
**apm package**: **apm package**:
The deployable unit apm builds and installs — one or more skills, agents, hooks, commands, and MCP The deployable unit apm builds and installs — one or more skills, agents, hooks, commands, and MCP
servers under a single directory `plugins/<name>/`, consisting of that plugin's `apm.yml` plus the servers under a single directory `plugins/<name>/`, consisting of that directory's `apm.yml` plus
hand-authored `plugins/<name>/.apm/` tree it deploys from (ADR-0015). the hand-authored `plugins/<name>/.apm/` tree it deploys from (ADR-0015).
_Avoid_: plugin, package, bundle, module, plugin directory, source tree _Avoid_: bundle, module, source tree; and bare "plugin" for the *installable artifact*, which since
ADR-0024 is an apm package and not a Claude Code plugin. "Plugin" stays correct as a modifier in the
repo's settled compounds — **Plugin marketplace**, "plugin units", `plugins/`.
**Output profile**: **Output profile**:
An `apm pack` target format for the generated *marketplace* catalogue compiled from root A named ecosystem format `apm pack` can compile the marketplace manifest into, declared per profile
`apm.yml`'s `marketplace:` block. Only `claude` is declared under root `apm.yml`'s `marketplace.outputs:`. apm defines `claude` and `codex`; each writes to its
(`.claude-plugin/marketplace.json`); apm's own marketplace consumers read that same file, so one own default path unless overridden. Mechanics: `docs/spec/architecture.md`.
catalogue serves both. Mechanics: `docs/spec/architecture.md`.
_Avoid_: build target, export format _Avoid_: build target, export format
**Plugin marketplace**: **Plugin marketplace**:
@@ -167,6 +168,11 @@ _Avoid_: ticket, card, task
- Skills can answer to two names, bare (`gitea-prs`) and namespaced (`gitea:gitea-prs`), depending on - Skills can answer to two names, bare (`gitea-prs`) and namespaced (`gitea:gitea-prs`), depending on
whether a native install exists at user scope alongside the apm one (ADR-0018) — resolved: write whether a native install exists at user scope alongside the apm one (ADR-0018) — resolved: write
the bare name, which is the only form `apm install` produces. the bare name, which is the only form `apm install` produces.
- "plugin" was used both for the installable artifact under `plugins/<name>/` and as a modifier in
settled compounds (**Plugin marketplace**, "plugin units", the `plugins/` directory itself) —
resolved: the installable artifact is an **apm package**, because ADR-0024 ended native
`claude plugin install` support and it is no longer a Claude Code plugin in any operative sense;
the compounds keep the word and are not being renamed.
- "context" means both the model's live token window and the bounded domain this file describes — - "context" means both the model's live token window and the bounded domain this file describes —
resolved: unqualified "context" in this repo means the token window. resolved: unqualified "context" in this repo means the token window.
- "audit" was used for both an author skill's inline closeout and `forge`'s independent - "audit" was used for both an author skill's inline closeout and `forge`'s independent

View File

@@ -6,7 +6,20 @@ Assumptions agreed before analysis: anything is on the table, Claude Code and Co
Counting convention: line counts are hand-edited `.apm/` source unless marked "incl. mirror". Every `.apm/` file has a byte-identical generated copy at the plugin root, so plugin cuts count double in the repo total. Counting convention: line counts are hand-edited `.apm/` source unless marked "incl. mirror". Every `.apm/` file has a byte-identical generated copy at the plugin root, so plugin cuts count double in the repo total.
> **Superseded (2026-09-14):** the mirror is gone (commit `718c79a`, ADR-0024). "incl. mirror" totals below are historical; roughly 70% of each remains live (kyberforge 44,568 → ~33,000). > **Superseded (2026-09-14):** the mirror is gone (commit `718c79a`, ADR-0024). "incl. mirror" totals below are historical. Measured against the 2026-09-10 baseline (`9eb8bc7`), what remains live varies by plugin — 44% to 71%, not a uniform ~70%:
>
> | Plugin | Baseline (incl. mirror) | Today | Live |
> |---|---|---|---|
> | kyberforge | 44,568 | 31,473 | 70.6% |
> | git | 9,889 | 6,050 | 61.2% |
> | gitea | 6,047 | 3,471 | 57.4% |
> | core | 3,873 | 2,360 | 60.9% |
> | lint | 1,558 | 923 | 59.2% |
> | bin | 4,704 | 2,083 | 44.3% |
>
> Across all six the baseline was 70,639 lines and 46,360 remain (65.6%). The mirror was 20,061 of those lines, so mirror deletion alone would have left ~71.6%; everything below that line is source the other findings cut, which is why bin — where findings 10, 12, and 13 landed hardest — is the outlier. (Counted as tracked lines under `plugins/<name>/` at `9eb8bc7` and in the current working tree.)
> **Reviewed (2026-09-14):** commits `718c79a` and `d2480b8` were put through a five-agent review. Result: **zero skill, agent or hook regressions** — 39 skills before and after, all gates passing, and both hook removals (`validate-plugins`, `check-plugin-content-sync`) genuinely moot rather than merely unenforced. One real functional regression was found — MCP propagation to consumers, broken by the same commit's manifest deletion; see finding 37 — along with the numeric and bookkeeping drift in this document's own 2026-09-14 notes, corrected in place above and below.
## 1. The shape of the problem ## 1. The shape of the problem
@@ -24,7 +37,7 @@ Counting convention: line counts are hand-edited `.apm/` source unless marked "i
| Preload tax (39 skill names + descriptions) | 10,987 chars, ~2,750 tokens per session | | Preload tax (39 skill names + descriptions) | 10,987 chars, ~2,750 tokens per session |
| Commits since 2026-05-10 / share touching hook, test, gate, vale, or sync | 447 / ~25% | | Commits since 2026-05-10 / share touching hook, test, gate, vale, or sync | 447 / ~25% |
> **Corrected then done (2026-09-14):** the mirror row's figure was wrong. The true mirror was **213 files / 20,061 lines**, not 263 / ~22,000 — the original count swept in files that were never mirror output. All 213 were deleted in commit `718c79a` on `docs/simplification-audit` (245 files changed, 298 insertions, 22,602 deletions across the whole change), so the row is now zero. The enforcement row's pre-push figure of 14 is also stale: with `validate-plugins` and `check-plugin-content-sync` gone the stage now reports 11 (9 repo-authored plus 2 pre-commit `meta` hooks); see the corrected §3 target. > **Corrected then done (2026-09-14):** the mirror row's figure was wrong. The true mirror was **213 files / 20,061 lines**, not 263 / ~22,000 — the original count swept in files that were never mirror output. All 213 were deleted in commit `718c79a` on `docs/simplification-audit` (245 files changed, 298 insertions, 22,602 deletions across the whole change), so the row is now zero. The enforcement row is stale on **both** halves — it was correct at the 2026-09-10 baseline (`9eb8bc7`: 33 `- id:` entries, 14 repo-authored pre-push hooks), but `.pre-commit-config.yaml` today has **27 entries and 9 `stages: [pre-push]`**. Like for like that is 14 → 9 repo-authored pre-push hooks. The stage *reports* 11, because the 2 pre-commit `meta` hooks also run there — a different counting basis; see the corrected §3 target, which states it the same way.
The pattern across every area is the same: the payload (skill bodies, rules, decisions) is small and the scaffolding around it (mirrors, research dumps, sync gates, tests of tests, justification prose) is 10 to 30 times larger. A quarter of all commits have gone into maintaining the scaffolding. The pattern across every area is the same: the payload (skill bodies, rules, decisions) is small and the scaffolding around it (mirrors, research dumps, sync gates, tests of tests, justification prose) is 10 to 30 times larger. A quarter of all commits have gone into maintaining the scaffolding.
@@ -59,7 +72,7 @@ Where the 276 s goes (each suite run alone, sequential):
Five suites account for 215 s of 276 s. Three of those five (sync-plugin-content, vale-style-sync, adr0020-differential) test tooling that findings 2, 7, and 14 propose to delete or shrink, so the fastest path to a quick pre-push is removing the duplication those tests guard rather than optimising the tests. Five suites account for 215 s of 276 s. Three of those five (sync-plugin-content, vale-style-sync, adr0020-differential) test tooling that findings 2, 7, and 14 propose to delete or shrink, so the fastest path to a quick pre-push is removing the duplication those tests guard rather than optimising the tests.
> **Done (2026-09-14):** see commit `718c79a` on `docs/simplification-audit`. The three struck-through rows are gone: `test-sync-plugin-content.sh` (83 s, 1,289 lines, 92 cases), `check-plugin-content-sync` (4.5 s) and `validate-plugins` (4.9 s). Expected, not re-measured: roughly 88 s comes off every push (~83 s of it out of `run-tests`, which loses its single slowest suite), on the arithmetic of the 2026-09-10 figures alone. The remaining rows have not been re-timed since, so treat every number in this section as the 2026-09-10 baseline minus those three, not as a fresh measurement. > **Done (2026-09-14):** see commit `718c79a` on `docs/simplification-audit`. The three struck-through rows are gone: `test-sync-plugin-content.sh` (83 s, 1,289 lines, 92 cases), `check-plugin-content-sync` (4.5 s) and `validate-plugins` (4.9 s). Expected, not re-measured: roughly 92 s comes off every push (83 + 4.5 + 4.9 = 92.4 s) (~83 s of it out of `run-tests`, which loses its single slowest suite), on the arithmetic of the 2026-09-10 figures alone. The remaining rows have not been re-timed since, so treat every number in this section as the 2026-09-10 baseline minus those three, not as a fresh measurement.
## 3. Enforcement layer: hooks, tests, scripts ## 3. Enforcement layer: hooks, tests, scripts
@@ -94,11 +107,11 @@ This is the area you named as hardest to understand and slowest. Root cause: mos
> **Superseded then done (2026-09-14):** see commit `718c79a` on `docs/simplification-audit`. The recommendation ("target a third") is void, not met — the §8 question it depended on was settled the other way. Answering "apm-only" (ADR-0024) removed the mirror's reason to exist, and with the mirror gone the script guarded nothing, so the whole thing was deleted rather than shrunk: `scripts/sync-plugin-content.sh` (813 lines), `tests/test-sync-plugin-content.sh` (1,289 lines — the finding said 1,291), the `check-plugin-content-sync` pre-push hook, and `scripts/lib/marketplace-plugins.sh` (86 lines, whose only consumer was the sync script, and which finding 1 had explicitly kept alive for it). `validate-plugins` went with them, and the twelve per-plugin `plugin.json` manifests the script regenerated. The finding's own premise — "the mirror itself must stay" — is what turned out to be wrong. > **Superseded then done (2026-09-14):** see commit `718c79a` on `docs/simplification-audit`. The recommendation ("target a third") is void, not met — the §8 question it depended on was settled the other way. Answering "apm-only" (ADR-0024) removed the mirror's reason to exist, and with the mirror gone the script guarded nothing, so the whole thing was deleted rather than shrunk: `scripts/sync-plugin-content.sh` (813 lines), `tests/test-sync-plugin-content.sh` (1,289 lines — the finding said 1,291), the `check-plugin-content-sync` pre-push hook, and `scripts/lib/marketplace-plugins.sh` (86 lines, whose only consumer was the sync script, and which finding 1 had explicitly kept alive for it). `validate-plugins` went with them, and the twelve per-plugin `plugin.json` manifests the script regenerated. The finding's own premise — "the mirror itself must stay" — is what turned out to be wrong.
8. **`docs/spec/gates.md` (1,048 lines) is roughly 15% "what is enforced" and 85% post-mortems** of defects already fixed and pinned by tests. The 60-line hook table is the useful part. Target 200 lines. The same applies to the 106 comment lines in `.pre-commit-config.yaml` and to `scripts/`, where 8 of 15 files are 40 to 60% comments. Effort M. 8. **`docs/spec/gates.md` (1,048 lines) is roughly 15% "what is enforced" and 85% post-mortems** of defects already fixed and pinned by tests. The 60-line hook table is the useful part. Target 200 lines. The same applies to the 106 comment lines in `.pre-commit-config.yaml` and to `scripts/`, where 8 of 15 files are 40 to 60% comments. Effort M.
> **Partially done (2026-09-13):** see commit `a35f5e8` on `docs/simplification-audit`. The 85%-post-mortem characterization was stale — the file had already shrunk to 966 lines by other findings, and most of what remained is load-bearing "why this design" rationale cited by ADRs and tests, not dead incident narration. Cut only the two genuinely stale passages: a reproduction paragraph carrying explicitly outdated numbers, and a retrofit-process narrative superseded by current state — 966 → 930 lines. `.pre-commit-config.yaml`'s comments were left untouched; on inspection they're compact constraint notes, not filler. Target of 200 lines not reached and not recommended — would require deleting content the file itself flags as load-bearing. > **Partially done (2026-09-13):** see commit `a35f5e8` on `docs/simplification-audit`. The 85%-post-mortem characterization was stale — the file had already shrunk to 966 lines by other findings, and most of what remained is load-bearing "why this design" rationale cited by ADRs and tests, not dead incident narration. Cut only the two genuinely stale passages: a reproduction paragraph carrying explicitly outdated numbers, and a retrofit-process narrative superseded by current state — a 36-line cut, 966 → 930 as measured at commit `a35f5e8`. Those two figures describe that commit only, not the file: `718c79a` and later findings have edited `gates.md` again, so read its current length from the file rather than quoting a number here. `.pre-commit-config.yaml`'s comments were left untouched; on inspection they're compact constraint notes, not filler. Target of 200 lines not reached and not recommended — would require deleting content the file itself flags as load-bearing.
**Proposed target.** ~~Pre-push 14 hooks to 6: `run-tests`, `validate-plugins`, `validate-marketplace`, `apm-pack-check-clean`, `check-plugin-content-sync`, `check-release-needed`.~~ **Proposed target.** ~~Pre-push 14 hooks to 6: `run-tests`, `validate-plugins`, `validate-marketplace`, `apm-pack-check-clean`, `check-plugin-content-sync`, `check-release-needed`.~~
> **Corrected (2026-09-14):** two of the six named targets no longer exist — `validate-plugins` and `check-plugin-content-sync` were deleted in commit `718c79a` (finding 7, ADR-0024). Actual state today: **9 repo-authored pre-push hooks** — `run-tests`, `check-executables-allow-sync`, `apm-audit-ci`, `check-apm-agents-valid`, `apm-pack-check-clean`, `check-vale-style-sync`, `check-scope-walkup-sync`, `check-release-needed`, `validate-marketplace` — plus the 2 pre-commit `meta` hooks that also run at this stage, so 11 are reported at pre-push. `validate-marketplace` was kept: the root `marketplace:` block in `apm.yml` and the root `.claude-plugin/marketplace.json` + `plugin.json` stay, because apm's own marketplace consumers read that same catalogue and `<name>@holocron` short names depend on it. > **Corrected (2026-09-14):** two of the six named targets no longer exist — `validate-plugins` and `check-plugin-content-sync` were deleted in commit `718c79a` (finding 7, ADR-0024). Actual state today: **9 repo-authored pre-push hooks** — `run-tests`, `check-executables-allow-sync`, `apm-audit-ci`, `check-apm-agents-valid`, `apm-pack-check-clean`, `check-vale-style-sync`, `check-scope-walkup-sync`, `check-release-needed`, `validate-marketplace` — plus the 2 pre-commit `meta` hooks that also run at this stage, so 11 are reported at pre-push. `validate-marketplace` was kept: the root `marketplace:` block in `apm.yml` and the root `.claude-plugin/marketplace.json` stay, because apm's own marketplace consumers read that same catalogue and `<name>@holocron` short names depend on it. (That manifest is the only tracked file under `.claude-plugin/` — `git ls-files .claude-plugin` returns it alone. The sibling `.claude-plugin/plugin.json` is a local `apm pack` byproduct, has never been tracked on any branch, and is ignored at `.gitignore:59`; it was not "kept", because it was never there.)
Pre-commit stays roughly as is minus `skill-frontmatter`, and minus `check-ast` once finding 9 removes the only `.py` files. Tests 26 files to about 10 (12,400 to about 5,000 lines). Keep bats and its three submodules; the 351 bats tests ship inside plugins and are the right tool there. Do not port the bash suites to bats; delete them instead. Pre-commit stays roughly as is minus `skill-frontmatter`, and minus `check-ast` once finding 9 removes the only `.py` files. Tests 26 files to about 10 (12,400 to about 5,000 lines). Keep bats and its three submodules; the 351 bats tests ship inside plugins and are the right tool there. Do not port the bash suites to bats; delete them instead.
@@ -188,8 +201,11 @@ Not covered by the area audits above; found on a final sweep of the root config
36. **The release-tag mechanism guards an external contract with no known consumer.** `.pre-commit-hooks.yaml` exports three hooks for other repos to pin by `rev: <tag>`. `check-release-needed` (242 lines + 442 test), `test-vale-hooks-consumer` (270 lines), ADR-0014, and three tags exist to serve that. If no other repo pins these hooks today, the whole mechanism can be deferred until one does. Effort S. 36. **The release-tag mechanism guards an external contract with no known consumer.** `.pre-commit-hooks.yaml` exports three hooks for other repos to pin by `rev: <tag>`. `check-release-needed` (242 lines + 442 test), `test-vale-hooks-consumer` (270 lines), ADR-0014, and three tags exist to serve that. If no other repo pins these hooks today, the whole mechanism can be deferred until one does. Effort S.
37. **Two `.mcp.json` files declare an Obsidian vault server over `docs/`** (root and `plugins/bin/`; the other five plugin `.mcp.json` files are empty stubs), while `AGENTS.md` forbids using an external memory system for this repo. If the Obsidian tools are unused, drop both and the `reinject_mcp_servers` explanation in the bin README; the bin `plugin.json` pair regenerates. Effort S. 37. [x] ~~**Two `.mcp.json` files declare an Obsidian vault server over `docs/`** (root and `plugins/bin/`; the other five plugin `.mcp.json` files are empty stubs), while `AGENTS.md` forbids using an external memory system for this repo. If the Obsidian tools are unused, drop both and the `reinject_mcp_servers` explanation in the bin README; the bin `plugin.json` pair regenerates. Effort S.~~
> **Not proceeding (2026-09-13):** premise doesn't hold. The server exposes the repo's own git-tracked `docs/` folder — not an external/off-repo store — so it isn't the "external memory system" AGENTS.md's rule targets. It was deliberately added and versioned (3 commits), is documented as current intended behavior in both READMEs, and ADR-0018 uses it as its only concrete worked example of apm's MCP-dependency propagation mechanism actually working. No skill invokes the Obsidian tools as a workflow step, but that alone doesn't make the config dead. No changes made; recommend a human confirm whether the vault tooling is still wanted before removing it. > **Not proceeding (2026-09-13):** premise doesn't hold. The server exposes the repo's own git-tracked `docs/` folder — not an external/off-repo store — so it isn't the "external memory system" AGENTS.md's rule targets. It was deliberately added and versioned (3 commits), is documented as current intended behavior in both READMEs, and ADR-0018 uses it as its only concrete worked example of apm's MCP-dependency propagation mechanism actually working. No skill invokes the Obsidian tools as a workflow step, but that alone doesn't make the config dead. No changes made; recommend a human confirm whether the vault tooling is still wanted before removing it.
> **Confirmed and done (2026-09-14):** the human confirmed the vault tooling is not wanted — remove it entirely. All seven `.mcp.json` files deleted (the six plugin-root files and the repo-root one), and the repo-root path added to `.gitignore` so a local `apm` run cannot recreate it as tracked content. The bin README's `reinject_mcp_servers` explanation goes with it; the `plugin.json` pair the finding expected to regenerate no longer exists (deleted in `718c79a`, finding 7).
>
> What made this urgent is the substantive discovery, not the tidying: **deleting the per-plugin `plugin.json` manifests in `718c79a` had already broken MCP propagation silently.** `apm_cli/deps/plugin_parser.py` maps a plugin-root `.mcp.json` → `.apm/.mcp.json`, and that code path runs only for *marketplace* plugins — with no manifest, apm never reads the file. `plugins/bin/apm.yml` declares `dependencies.mcp: []`, so the supported mechanism was never used either. Proved on ref-pinned consumer clones: at the parent commit a consumer gets an `obsidian` server, at HEAD it gets none, and on upgrade apm prints `Removed stale MCP server 'obsidian' from .mcp.json` — which would in time have stripped the server from this repo's own tracked `.mcp.json` once the lock re-resolved. Deleting the files makes the intent match the behaviour instead of leaving a config that silently does nothing.
38. [x] ~~**`pc-author` / `pc-run` (689 lines) carry generic pre-commit documentation.** `hooks-by-language.md` (128 lines) and `failure-patterns.md` (133) restate pre-commit.com. Keep the skills, trim to the house-specific rules. Effort S.~~ 38. [x] ~~**`pc-author` / `pc-run` (689 lines) carry generic pre-commit documentation.** `hooks-by-language.md` (128 lines) and `failure-patterns.md` (133) restate pre-commit.com. Keep the skills, trim to the house-specific rules. Effort S.~~
> **Corrected then done (2026-09-13):** see commit `a622200` on `docs/simplification-audit`. Independent re-verification found the 689-line figure overstated (actual combined size 598 lines) and the realistic cut smaller than a rewrite (~60-85 lines, concentrated in the two named reference files, not the SKILL.md files or the four short flow files, which are house-specific gates rather than restatement). Landed within that range: `hooks-by-language.md` 128 → 92 lines (collapsed six per-language tables repeating the same repo/rev/rationale into one shared-repo table plus a small other-repos table); `failure-patterns.md` 133 → 109 lines (removed generic SSH/proxy and shellcheck SC-code restatement, compressed generic schema-error bullets). Kept verbatim: both "Unverified — not in research corpus" flags, the rev-freshness caveat, the `rtk git add -u`/`rtk git commit` fix (ADR-0023), and the `pre-commit install -f` warning. Combined cut: 60 lines. Flat mirror regenerated and verified byte-identical. > **Corrected then done (2026-09-13):** see commit `a622200` on `docs/simplification-audit`. Independent re-verification found the 689-line figure overstated (actual combined size 598 lines) and the realistic cut smaller than a rewrite (~60-85 lines, concentrated in the two named reference files, not the SKILL.md files or the four short flow files, which are house-specific gates rather than restatement). Landed within that range: `hooks-by-language.md` 128 → 92 lines (collapsed six per-language tables repeating the same repo/rev/rationale into one shared-repo table plus a small other-repos table); `failure-patterns.md` 133 → 109 lines (removed generic SSH/proxy and shellcheck SC-code restatement, compressed generic schema-error bullets). Kept verbatim: both "Unverified — not in research corpus" flags, the rev-freshness caveat, the `rtk git add -u`/`rtk git commit` fix (ADR-0023), and the `pre-commit install -f` warning. Combined cut: 60 lines. Flat mirror regenerated and verified byte-identical.
@@ -205,14 +221,15 @@ Findings 9, 10, 11, and 12 are coupled through the provenance validator and the
## 8. Questions to settle before starting ## 8. Questions to settle before starting
- [x] ~~**Native Claude Code marketplace install vs apm-only.** The flat mirror, `check-plugin-content-sync`, and ADR-0017 exist only for native `claude plugin install`. If apm install is the only supported path, the mirror and its 2,100 lines of tooling go away. Which install paths must work for consumers?~~ - [x] ~~**Native Claude Code marketplace install vs apm-only.** The flat mirror, `check-plugin-content-sync`, and ADR-0017 exist only for native `claude plugin install`. If apm install is the only supported path, the mirror and its 2,100 lines of tooling go away. Which install paths must work for consumers?~~
> **Answered (2026-09-14):** apm-only. See ADR-0024 (`docs/adr/0024-apm-is-the-only-supported-install-path.md`) and commit `718c79a` on `docs/simplification-audit`. Native `claude plugin install` support is dropped; the flat mirror, the twelve per-plugin manifests, `sync-plugin-content.sh`, its test suite, `lib/marketplace-plugins.sh`, and the `check-plugin-content-sync` and `validate-plugins` hooks are all deleted (245 files changed, −22,602 lines). ADR-0017 carries a superseded banner. Kept deliberately: the root `marketplace:` block and root `.claude-plugin/` manifests, which apm's own consumers read. This answer is what voided finding 7's recommendation and closed §3's `check-plugin-content-sync` target. > **Answered (2026-09-14):** apm-only. See ADR-0024 (`docs/adr/0024-apm-is-the-only-supported-install-path.md`) and commit `718c79a` on `docs/simplification-audit`. Native `claude plugin install` support is dropped; the flat mirror, the twelve per-plugin manifests, `sync-plugin-content.sh`, its test suite, `lib/marketplace-plugins.sh`, and the `check-plugin-content-sync` and `validate-plugins` hooks are all deleted (245 files changed, −22,602 lines). ADR-0017 carries a superseded banner. Kept deliberately: the root `marketplace:` block and the root `.claude-plugin/marketplace.json`, which apm's own consumers read. (`marketplace.json` is the only tracked file under `.claude-plugin/`; the root `plugin.json` beside it is untracked local `apm pack` output, ignored at `.gitignore:59`.) This answer is what voided finding 7's recommendation and closed §3's `check-plugin-content-sync` target.
- **Copilot CLI legacy path.** Is `.github/plugin/marketplace.json` still read by any Copilot version you target? If not, finding 2c is a pure delete. - **Copilot CLI legacy path.** Is `.github/plugin/marketplace.json` still read by any Copilot version you target? If not, finding 2c is a pure delete.
- **Provenance chain.** Is "which upstream informed this file" a requirement you still want, or was it a governance experiment? Finding 11 hinges on this. - **Provenance chain.** Is "which upstream informed this file" a requirement you still want, or was it a governance experiment? Finding 11 hinges on this.
- **ADR-0012 (three core skills) and the one-script-per-skill install constraint.** The merges in 14, 15, and 24 need the first revisited and are the only way around the second. Are you open to superseding ADR-0012? - **ADR-0012 (three core skills) and the one-script-per-skill install constraint.** The merges in 14, 15, and 24 need the first revisited and are the only way around the second. Are you open to superseding ADR-0012?
- **Granularity of git/gitea skills.** One `git` skill vs seven trades routing precision for size. Is one broad description acceptable? - **Granularity of git/gitea skills.** One `git` skill vs seven trades routing precision for size. Is one broad description acceptable?
- **Auto-update at session start.** Do you want the install refreshed from the remote every time a session opens (finding 34), or is a manual `apm update` acceptable? - **Auto-update at session start.** Do you want the install refreshed from the remote every time a session opens (finding 34), or is a manual `apm update` acceptable?
- **External hook consumers.** Does any other repo pin this repo's `.pre-commit-hooks.yaml` by tag today? If not, finding 36 defers the release mechanism entirely. - **External hook consumers.** Does any other repo pin this repo's `.pre-commit-hooks.yaml` by tag today? If not, finding 36 defers the release mechanism entirely.
- **Obsidian MCP.** Are the Obsidian tools over `docs/` used by anyone? If not, finding 37 is a pure delete. - [x] ~~**Obsidian MCP.** Are the Obsidian tools over `docs/` used by anyone? If not, finding 37 is a pure delete.~~
> **Answered (2026-09-14):** not used — remove entirely. All seven `.mcp.json` files are deleted and the repo-root path is gitignored; see finding 37, which also records the functional regression this uncovered (since `718c79a` deleted the per-plugin manifests, apm no longer propagated the server to consumers at all).
## 9. Carried forward from the apm-only decision (2026-09-14) ## 9. Carried forward from the apm-only decision (2026-09-14)
@@ -221,8 +238,10 @@ Recorded here so they are not rediscovered as defects. All follow from commit `7
**Two accepted residuals.** **Two accepted residuals.**
- **Native install still half-works, and cannot be prevented.** apm reuses Claude's catalogue format by design, so a Claude Code user can still register holocron natively and will install six plugins containing zero skills. Accepted, not overlooked: no schema change closes this, because the format that makes it possible is the format apm's own consumers need. - **Native install still half-works, and cannot be prevented.** apm reuses Claude's catalogue format by design, so a Claude Code user can still register holocron natively and will install six plugins containing zero skills. Accepted, not overlooked: no schema change closes this, because the format that makes it possible is the format apm's own consumers need.
- **Consumers now receive test fixtures.** apm installs from `.apm/`, which carries the `tests/` directories the mirror used to strip, so 17 `.bats` fixture files across 6 skills now ship. Suppressing them means switching all six `apm.yml` files from `includes: auto` to explicit lists, where a wrong list silently drops content — worse failure mode than the noise. Deferred deliberately. - **Consumers now receive test fixtures.** apm installs from `.apm/`, which carries the `tests/` directories the mirror used to strip, so a consumer installing from this branch receives **10 `.bats` files across 6 skills**, plus those skills' 6 `tests/README.md` files — 16 files. (Repo-wide, 17 tracked paths contain `/tests/`: the 10 `.bats` and 7 `README.md`, one of which is a template asset under `skill-author/assets/templates/tests/` and is not a test fixture.) This is what consumers *receive*, not what this checkout shows: `.claude/skills/` here currently holds zero `.bats` files, because that deployed tree is stale and predates this branch. The mechanism was confirmed empirically on a ref-pinned consumer clone — the 16 files are absent at the parent commit and present at HEAD. Suppressing them means switching all six `apm.yml` files from `includes: auto` to explicit lists, where a wrong list silently drops content — worse failure mode than the noise. Deferred deliberately.
**Negative result — do not re-litigate.** Deleting native install does *not* relax the self-containment constraint. `plugins/kyberforge/.apm/skills/skill-author/references/deployment-modes.md`, sourced from the agentskills.io spec, states it independently for APM package mode: the spec defines no cross-skill sharing. So findings 14 and 15 still require *merging* skills; sharing one file between two skills remains impossible, and §8's "one-script-per-skill install constraint" bullet is unchanged by this decision. **Negative result — do not re-litigate.** Deleting native install does *not* relax the self-containment constraint. `plugins/kyberforge/.apm/skills/skill-author/references/deployment-modes.md`, sourced from the agentskills.io spec, states it independently for APM package mode: the spec defines no cross-skill sharing. So findings 14 and 15 still require *merging* skills; sharing one file between two skills remains impossible, and §8's "one-script-per-skill install constraint" bullet is unchanged by this decision.
**Latent regression.** ADR-0017's `check_apm_symlinks()` was the only thing reporting that apm's bundle exporter silently drops symlinks under `.apm/`. It is gone. No symlinks exist under any `.apm/` today, so this is latent rather than live — but the next symlink added there will be dropped from the install with no error. **Accepted gap — symlinks under `.apm/`.** ADR-0017's `check_apm_symlinks()` was the only thing reporting that symlinks under `.apm/` do not survive to a consumer. It is gone, and no replacement guard is being added — the human decided to accept the gap.
The mechanism is not the bundle exporter, as ADR-0017 assumed; it is the **install** path, and it has since been verified. `apm_cli/security/gate.py`'s `ignore_non_content()` is a `shutil.copytree` ignore callback whose docstring says "Excludes symlinks (security)"; it is used at `apm_cli/integration/skill_integrator.py:424`, `:791` and `:1152`. Materialization into `apm_modules/` dereferences first, so symlinked content survives *there* and is dropped when skills are deployed out of it. ADR-0024 flagged the prediction as unverified; it holds, with that corrected attribution. No symlinks exist under any `.apm/` today, so nothing is broken now — but the next one added there will silently not reach consumers, and nothing will say so.

View File

@@ -5,9 +5,10 @@ as this repo's authoring source of truth) and, for plugin-scope agent files spec
ADR-0016 (plugin-scope `.apm/agents/*.agent.md` drops provider-specific fields). Since issue ADR-0016 (plugin-scope `.apm/agents/*.agent.md` drops provider-specific fields). Since issue
#90's conversion executed, plugin content is authored under `plugins/<name>/apm.yml` + #90's conversion executed, plugin content is authored under `plugins/<name>/apm.yml` +
`.apm/{skills,agents,hooks}/` — not the flat `skills/`/`agents/` layout this ADR describes — `.apm/{skills,agents,hooks}/` — not the flat `skills/`/`agents/` layout this ADR describes —
and `.claude-plugin/plugin.json`/`.github/plugin/plugin.json` are compiled output of `apm pack`, and `.claude-plugin/plugin.json`/`.github/plugin/plugin.json` were compiled output of `apm pack`,
not hand-authored. This ADR's content is kept below as the historical record of the not hand-authored — and as of ADR-0024 (2026-09-14) both are deleted, along with native
pre-APM decision; it is no longer the current model. `claude plugin install` support; `apm install` is the only route. This ADR's content is kept
below as the historical record of the pre-APM decision; it is no longer the current model.
--- ---

View File

@@ -11,6 +11,11 @@ divergence is no longer possible by construction. `plugin-author`, the skill tha
invariant, is deleted per ADR-0015 rather than adapted. Kept below as the historical record of invariant, is deleted per ADR-0015 rather than adapted. Kept below as the historical record of
the pre-APM decision. the pre-APM decision.
**Fully void as of ADR-0024 (2026-09-14).** Both manifests are now deleted outright, so the two
files this ADR was about no longer exist in any form, compiled or hand-authored. `apm.yml`'s
`version:` is the only version field a plugin has. This ADR states no patch-bump rule and never
did — ADR-0015 retired that rule explicitly; do not cite this ADR as the source of one.
--- ---
Each plugin has two manifests: `plugin.json` (Copilot CLI) and `.claude-plugin/plugin.json` (Claude Code). Both tools support a `version` field. Prior to this decision, only the CC manifest carried `version`; the Copilot manifest omitted it. Each plugin has two manifests: `plugin.json` (Copilot CLI) and `.claude-plugin/plugin.json` (Claude Code). Both tools support a `version` field. Prior to this decision, only the CC manifest carried `version`; the Copilot manifest omitted it.

View File

@@ -22,8 +22,10 @@ spec-governed content, and `plugin.json` is JSON, not prose Vale can meaningfull
**Rule categories are prose-pattern-matchable only.** Structural, schema, and security concerns **Rule categories are prose-pattern-matchable only.** Structural, schema, and security concerns
stay out of this Vale-based harness because this repo already has dedicated tools for them: stay out of this Vale-based harness because this repo already has dedicated tools for them:
`skill-frontmatter` (required frontmatter fields), `validate-plugins`/`validate-marketplace` `skill-frontmatter` (required frontmatter fields), `validate-marketplace`
(`claude plugin validate --strict`, schema), and `gitleaks`/`detect-private-key` (secrets). (`claude plugin validate --strict`, schema), and `gitleaks`/`detect-private-key` (secrets).
(ADR-0024 removed the companion `validate-plugins` gate along with the per-plugin manifests it
checked; the argument here is unaffected.)
Duplicating those concerns as Vale rules would fight tools that already own them better. Duplicating those concerns as Vale rules would fight tools that already own them better.
**Governance docs are excluded as a rule source.** `docs/research/governance_principles/CONTROLS.md` **Governance docs are excluded as a rule source.** `docs/research/governance_principles/CONTROLS.md`

View File

@@ -115,6 +115,9 @@ doesn't wonder if it was overlooked.
- `plugins/kyberforge`'s `plugin.json` and `.claude-plugin/plugin.json` both patch-bump for every - `plugins/kyberforge`'s `plugin.json` and `.claude-plugin/plugin.json` both patch-bump for every
shipped content change (per ADR-0006's version-parity invariant): `1.2.5` for the relocation shipped content change (per ADR-0006's version-parity invariant): `1.2.5` for the relocation
itself, `1.2.6` for the self-locating `vale-wrap.sh` that followed. itself, `1.2.6` for the self-locating `vale-wrap.sh` that followed.
**Amended 2026-09-14 (ADR-0024):** a record of what was done then, not current practice. Both
manifests are deleted and `apm.yml`'s `version:` is a plugin's only version field; ADR-0015
retired the parity/patch-bump rule this bullet invokes.
- **`.pre-commit-hooks.yaml` entries are a bare script path and nothing else — a constraint, not a - **`.pre-commit-hooks.yaml` entries are a bare script path and nothing else — a constraint, not a
house style, and it binds every future hook here, not just the Vale two.** Since pre-commit house style, and it binds every future hook here, not just the Vale two.** Since pre-commit
rewrites only `entry[0]` into the hook-repo clone, no argument token in any entry can reference rewrites only `entry[0]` into the hook-repo clone, no argument token in any entry can reference

View File

@@ -180,4 +180,7 @@ correction) sorted what they document into three buckets:
marketplace. The manifest-compilation deliverable this ADR describes was genuinely complete; marketplace. The manifest-compilation deliverable this ADR describes was genuinely complete;
runtime discoverability was not. Fixed in ADR-0017 (a second, compiled flat-directory content runtime discoverability was not. Fixed in ADR-0017 (a second, compiled flat-directory content
mirror at each plugin root, generated by `scripts/sync-plugin-content.sh`) — see that ADR for mirror at each plugin root, generated by `scripts/sync-plugin-content.sh`) — see that ADR for
the root cause and the fix. the root cause and the fix. **Superseded 2026-09-14:** ADR-0024 deleted that mirror and its
generator along with native `claude plugin install` support. The discoverability gap this
paragraph describes is therefore no longer bridged — it is no longer a gap this repo has, because
apm is now the only supported install path and apm reads `.apm/` directly.

View File

@@ -105,6 +105,15 @@ resolves to the mirror, where the script does not exist — verified, apm report
`plugins/kyberforge/hooks/` either: that directory is `rm -rf`'d by every content sync (ADR-0017). `plugins/kyberforge/hooks/` either: that directory is `rm -rf`'d by every content sync (ADR-0017).
A test pins the reference. A test pins the reference.
> **Amendment (2026-09-14) — the mirror half of that reasoning is gone; the conclusion is not.**
> ADR-0024 deleted the flat mirror and `scripts/sync-plugin-content.sh`, so neither the `apm pack`
> mirror-filtering behaviour nor the `rm -rf` content sync described above still happens. The
> reference must stay exactly as written, for the reason that survives independently: `.apm/` is the
> sole hand-edited authoring source (ADR-0015), it is what ships in the installed package root, and
> `plugins/kyberforge/hooks/` no longer exists at all — so `${CLAUDE_PLUGIN_ROOT}/hooks/...` still
> names a path with nothing at it, now because the directory is gone rather than because a sync
> emptied it. `tests/test-apm-current-hook.sh` still pins the literal string.
**Session startup gets slower when the install is stale.** Measured: ~0.7 s for the `apm outdated` **Session startup gets slower when the install is stale.** Measured: ~0.7 s for the `apm outdated`
check when everything is current, ~10.4 s when six packages are behind and the refresh runs. The check when everything is current, ~10.4 s when six packages are behind and the refresh runs. The
hook declares `timeout: 380` to cover a cold multi-package fetch. That number is not free-standing: hook declares `timeout: 380` to cover a cold multi-package fetch. That number is not free-standing:

View File

@@ -28,12 +28,15 @@ What that audience costs is measurable:
|---|---| |---|---|
| Tracked mirror files under `plugins/*/{skills,agents,hooks}/` | 213 files, ~20,000 lines | | Tracked mirror files under `plugins/*/{skills,agents,hooks}/` | 213 files, ~20,000 lines |
| `scripts/sync-plugin-content.sh` | 813 lines | | `scripts/sync-plugin-content.sh` | 813 lines |
| `tests/test-sync-plugin-content.sh` | 1,291 lines, 92 cases, ~83 seconds | | `tests/test-sync-plugin-content.sh` | 1,289 lines, 92 cases, ~83 s |
| `scripts/lib/marketplace-plugins.sh` | helper, used only by the above | | `scripts/lib/marketplace-plugins.sh` | helper, used only by the above |
| `check-plugin-content-sync` pre-push hook | ~5 seconds per push | | `check-plugin-content-sync` pre-push hook | ~4.5 s per push |
| `validate-plugins` pre-push hook | ~4.9 s per push (6 × `claude plugin validate --strict`) |
Roughly 22,000 lines of tracked content and tooling, and about 88 seconds on every push. The test Roughly 22,000 lines of tracked content and tooling, and about 92 seconds on every push (83 + 4.5 +
alone is close to 30% of `run-tests`' wall time — the single largest item in it. 4.9; the two hook timings are the 2026-09-10 baseline measurements recorded in
`SIMPLIFICATION-AUDIT.md`, not re-measured here). The test alone is close to 30% of `run-tests`'
wall time — the single largest item in it.
**The native path's automated gate does not gate anything.** ADR-0017 cites **The native path's automated gate does not gate anything.** ADR-0017 cites
`claude plugin validate --strict` passing on all six plugins as one of two verifications. That `claude plugin validate --strict` passing on all six plugins as one of two verifications. That
@@ -50,8 +53,13 @@ not that the mirror works.
cheap rather than a trade. apm's skills convergence deploys skills to `.agents/skills/<name>/SKILL.md`, cheap rather than a trade. apm's skills convergence deploys skills to `.agents/skills/<name>/SKILL.md`,
the shared path read by Copilot, Cursor, Codex, Gemini, OpenCode and Windsurf, with Claude Code as the shared path read by Copilot, Cursor, Codex, Gemini, OpenCode and Windsurf, with Claude Code as
the special case at `.claude/skills/`. The mirror served two hosts (Claude Code and Copilot, the the special case at `.claude/skills/`. The mirror served two hosts (Claude Code and Copilot, the
latter only ever partially — see ADR-0017's own `hooks` amendment). apm serves seven. A consumer who latter only ever partially — see ADR-0017's own `hooks` amendment). apm serves eleven through those
installs holocron through apm gets strictly more than one who installed it natively. two roots: ten targets resolve to `.agents/skills/` — the six named above plus `agent-skills`,
`antigravity`, `hermes` and `openclaw`, which are rooted there natively rather than by an explicit
`deploy_root` — and `claude` is the eleventh at `.claude/skills/`. Four further targets (`kiro`,
`grok-build`, `grok-cloud`, `copilot-cowork`) deploy skills under roots of their own, for fifteen in
total; counted from `apm_cli/integration/targets.py` this session. A consumer who installs holocron
through apm gets strictly more than one who installed it natively.
**Verified empirically, not reasoned about.** In a scratch clone with the mirror and the six **Verified empirically, not reasoned about.** In a scratch clone with the mirror and the six
per-plugin manifest pairs deleted: per-plugin manifest pairs deleted:
@@ -81,6 +89,11 @@ load-bearing for apm resolution; they were load-bearing only for the native inst
the six per-plugin manifest pairs (`.claude-plugin/plugin.json`, `.github/plugin/plugin.json`). the six per-plugin manifest pairs (`.claude-plugin/plugin.json`, `.github/plugin/plugin.json`).
- Delete `scripts/sync-plugin-content.sh`, `tests/test-sync-plugin-content.sh`, - Delete `scripts/sync-plugin-content.sh`, `tests/test-sync-plugin-content.sh`,
`scripts/lib/marketplace-plugins.sh`, and the `check-plugin-content-sync` pre-push hook. `scripts/lib/marketplace-plugins.sh`, and the `check-plugin-content-sync` pre-push hook.
- Delete the `validate-plugins` pre-push hook (`claude plugin validate --strict` over every plugin
directory). It is removed on the evidence above — it reads manifests only, so with the per-plugin
manifest pairs gone it has nothing left to read, and even while they existed it could not detect
the empty-content defect. `validate-marketplace`, which validates the one manifest this repo still
ships, is **kept**.
- **Keep** the root `marketplace:` block in `apm.yml` and the compiled - **Keep** the root `marketplace:` block in `apm.yml` and the compiled
`.claude-plugin/marketplace.json`. apm's own marketplace consumers read that same file; removing `.claude-plugin/marketplace.json`. apm's own marketplace consumers read that same file; removing
it would stop holocron being an apm marketplace at all. it would stop holocron being an apm marketplace at all.
@@ -92,7 +105,7 @@ content is what ends native support; keeping the catalogue is what preserves apm
## Considered options ## Considered options
**Status quo — keep mirroring on every branch (rejected).** Pays ~22,000 tracked lines and ~88 **Status quo — keep mirroring on every branch (rejected).** Pays ~22,000 tracked lines and ~92
seconds per push for a path with no users and no working gate. It is not free in author attention seconds per push for a path with no users and no working gate. It is not free in author attention
either: ADR-0017 accrued four amendments in two days, every one of them about a detail of the either: ADR-0017 accrued four amendments in two days, every one of them about a detail of the
mirroring mechanism rather than about the content being mirrored. mirroring mechanism rather than about the content being mirrored.
@@ -106,7 +119,7 @@ exact shape: the `mattpocock-skills` entry removed from root `apm.yml` on 2026-0
Rejected on three grounds, stacking: Rejected on three grounds, stacking:
1. It keeps the 813-line script and the 1,291-line test alive in full. It reduces how often they 1. It keeps the 813-line script and the 1,289-line test alive in full. It reduces how often they
run, not how much there is to maintain — and the maintenance, not the runtime, is what ADR-0017's run, not how much there is to maintain — and the maintenance, not the runtime, is what ADR-0017's
amendment history shows to be the real cost. amendment history shows to be the real cost.
2. It requires per-package tagging discipline this repo does not practise. `git tag` lists 2. It requires per-package tagging discipline this repo does not practise. `git tag` lists
@@ -121,11 +134,20 @@ Rejected on three grounds, stacking:
**Also delete the marketplace catalogue (proposed, then rejected on evidence).** The initial shape **Also delete the marketplace catalogue (proposed, then rejected on evidence).** The initial shape
of this decision deleted `.claude-plugin/marketplace.json` along with everything else, on the of this decision deleted `.claude-plugin/marketplace.json` along with everything else, on the
reasoning that it is a Claude Code artifact. That is wrong. `apm marketplace add` reads reasoning that it is a Claude Code artifact. That is wrong. `apm marketplace add` probes
`.claude-plugin/marketplace.json` — falling back to `.github/plugin/marketplace.json`, which this `_MARKETPLACE_PATHS` in `apm_cli/marketplace/client.py` — `marketplace.json`, then
repo already deleted — and that read is what makes holocron an apm marketplace and what gives `.github/plugin/marketplace.json`, then `.claude-plugin/marketplace.json`, first hit wins — and that
consumers the `<name>@holocron` short-name form. Deleting it would have broken apm consumers in read is what makes holocron an apm marketplace and what gives consumers the `<name>@holocron`
order to remove a file whose format Claude Code merely happens to share. short-name form. Deleting it would have broken apm consumers in order to remove a file whose format
Claude Code merely happens to share.
Note the probe order: `.claude-plugin/marketplace.json` is the **last** resort, not the first, and
the `.github/plugin/marketplace.json` this repo deleted earlier outranked it. That deletion was
still inconsequential, but for a reason that has to be established rather than assumed — dropping a
higher-priority candidate only demotes resolution to the next one, and a reviewer reproduced
`apm marketplace add` against the post-deletion tree: it registered, found all 6 plugins, and
resolved via `.claude-plugin/marketplace.json`. What would break is deleting the last candidate,
which is exactly what this option proposed.
**Declare holocron an apm marketplace as a new step (moot).** Considered as a follow-on to the **Declare holocron an apm marketplace as a new step (moot).** Considered as a follow-on to the
above, and found to be already done: the `marketplace:` block in root `apm.yml` *is* the above, and found to be already done: the `marketplace:` block in root `apm.yml` *is* the
@@ -145,7 +167,7 @@ README note is the only available mitigation, and a note is not a gate.
**2. Consumers now receive dev-fixture files.** apm installs from `.apm/`, and `.apm/` contains the **2. Consumers now receive dev-fixture files.** apm installs from `.apm/`, and `.apm/` contains the
per-skill `tests/` directories the mirror explicitly stripped (ADR-0017's depth-scoped per-skill `tests/` directories the mirror explicitly stripped (ADR-0017's depth-scoped
`<category>/<name>/tests` exclusion). 17 `.bats` files across 6 skills therefore now deploy into `<category>/<name>/tests` exclusion). 10 `.bats` files across 6 skills therefore now deploy into
every consumer's skill directories. Suppressing them would mean switching all six `apm.yml` files every consumer's skill directories. Suppressing them would mean switching all six `apm.yml` files
from `includes: auto` to explicit include lists — and an explicit list that is wrong silently drops from `includes: auto` to explicit include lists — and an explicit list that is wrong silently drops
content, which is the same failure class ADR-0017 was written to fix. Trading a cosmetic problem for content, which is the same failure class ADR-0017 was written to fix. Trading a cosmetic problem for
@@ -157,9 +179,15 @@ now discoverable in the install output and would otherwise be found and double-r
they do not belong to — exactly the `apm_modules/` problem ADR-0018 recorded, arriving by a second they do not belong to — exactly the `apm_modules/` problem ADR-0018 recorded, arriving by a second
route. Any future script that walks this repo's tree needs both exclusions. route. Any future script that walks this repo's tree needs both exclusions.
**4. No version bumps.** ADR-0006 patch-bumps a plugin when its shipped content changes. Nothing **4. No version bumps.** There is no standing rule that would require one. The patch-bump-on-content-
under `.apm/` is touched here; only compiled artifacts are removed. The shipped content is change convention this repo once followed was ADR-0006's, and ADR-0015 explicitly retired it as a
byte-identical, so no bump is owed. This also avoids triggering the `executables.allow` dual-manifest artifact: "Conventions that existed only because of hand-authored dual manifests
(ADR-0006's version-parity/patch-bump rule …) are obsolete under `apm.yml`'s single-manifest model
and were deliberately dropped." ADR-0015 also records that apm "has no native version-bump
automation at all", so nothing mechanical demands one either. What remains is the substantive test,
and it is satisfied independently: nothing under `.apm/` is touched here, only compiled artifacts are
removed, so the content every apm consumer receives is byte-identical before and after. This also
avoids triggering the `executables.allow`
`kyberforge#<version>` pin cascade ADR-0019 describes, which would otherwise turn a cleanup into a `kyberforge#<version>` pin cascade ADR-0019 describes, which would otherwise turn a cleanup into a
multi-file coordinated edit for no functional gain. multi-file coordinated edit for no functional gain.
@@ -195,14 +223,32 @@ because each was a decision someone spent real effort on:
- The `hooks/hooks.json` path-correction amendment (2026-08-14) is moot: there is no mirrored hooks - The `hooks/hooks.json` path-correction amendment (2026-08-14) is moot: there is no mirrored hooks
file to place. file to place.
- The symlink amendment (2026-08-14) is **not** moot, and this is the one real regression. - The symlink amendment (2026-08-14) is **not** moot, and this is the one real regression.
`check_apm_symlinks()` read the `.apm/` source tree directly to report symlinks, because apm's `check_apm_symlinks()` read the `.apm/` source tree directly to report symlinks, because apm
bundle exporter filters them out silently and the resulting content loss is invisible to any filters them out silently and the resulting content loss is invisible to any mirror-versus-mirror
mirror-versus-mirror diff. That check dies with the script. Whether `apm install`'s own copy path diff. That check dies with the script.
drops symlinks the same way the bundle exporter does was **not verified this session** — the
exporter's `is_file() and not is_symlink()` test is in `apm_cli/bundle/plugin_exporter.py`, a A previous revision of this ADR left open whether `apm install`'s own copy path drops symlinks the
different code path from dependency installation. If it does, symlinked content under `.apm/` is way the bundle exporter does. It does, and the mechanism is now confirmed by reading the installed
now silently lost with nothing reporting it. No symlink exists under any `.apm/` today, so this is apm source. Deployment filters them: `ignore_non_content()` in `apm_cli/security/gate.py` is a
a latent gap rather than an active one, but it should be re-checked before anyone adds one. `shutil.copytree` ignore callback whose docstring states "Excludes symlinks (security)", and
`apm_cli/integration/skill_integrator.py` passes it — or the equivalent `_build_copy_ignore()` —
to every `copytree` that materialises a skill (`:424`, `:791`, `:1152`), with further per-file
`is_symlink()` drops for `bin/` entries and the plugin manifest at `:1671` and `:1696`. None of
these log the skip. Materialization into `apm_modules/`, by contrast, **dereferences**: for git
sources `deps/github_downloader.py` copies the checkout out with `robust_copytree`/`robust_copy2`
and no symlink filter, and for local sources `install/phases/local_content.py`'s
`_copy_tree_dereferencing_validated()` resolves each in-package symlink and copies its content as
a real file (erroring on dangling, escaping or circular ones). So a symlink under `.apm/` survives
into `apm_modules/` as real content and is then silently dropped on deploy — the failure is at the
deploy step, not the fetch, which is why it would not show up in a cache inspection.
**This is accepted, and no replacement guard is added.** No symlink exists under any `.apm/`
today, and the mitigations available are worse than the exposure: a standalone repo-side checker
would be a new script to maintain for a condition that has never occurred, and it could only warn,
since the drop happens inside apm. The operative rule is therefore a convention rather than a
gate: do not introduce a symlink under any `plugins/*/.apm/` tree. If one is ever needed, the
filtering above is the reason it will not reach consumers, and closing the gap properly means an
upstream change in apm, not a local one.
**8. ADR-0017's own exit condition was different from this one, and that is worth noting.** Its **8. ADR-0017's own exit condition was different from this one, and that is worth noting.** Its
final consequence anticipated deletion, but conditioned it on an upstream fix: "a future apm release final consequence anticipated deletion, but conditioned it on an upstream fix: "a future apm release

View File

@@ -38,7 +38,7 @@ a PR merged through Gitea's merge button, where no local push happens at all.
## The pre-push gate ## The pre-push gate
Ten hooks, grouped below by what they guard rather than by the order `.pre-commit-config.yaml` declares them in. Nine hooks, grouped below by what they guard rather than by the order `.pre-commit-config.yaml` declares them in.
**Core checks** **Core checks**
@@ -70,7 +70,7 @@ drift in generated text.
| `apm-audit-ci` | `apm audit --ci` once per manifest — root plus each of the six plugin packages | | `apm-audit-ci` | `apm audit --ci` once per manifest — root plus each of the six plugin packages |
| `apm-pack-check-clean` | `apm pack --check-versions --check-clean --dry-run` — the compiled marketplace still matches what `apm.yml` + `.apm/` would generate, and per-package versions agree with the `per_package` strategy | | `apm-pack-check-clean` | `apm pack --check-versions --check-clean --dry-run` — the compiled marketplace still matches what `apm.yml` + `.apm/` would generate, and per-package versions agree with the `per_package` strategy |
**Host validators** (both need the `claude` CLI on PATH) **Host validators** (needs the `claude` CLI on PATH)
| Hook | Guards | | Hook | Guards |
|---|---| |---|---|
@@ -116,9 +116,9 @@ git ls-files | grep -E '^plugins/[^/]+/\.apm/skills/.*SKILL\.md$' \
``` ```
The first count equals the number of skill directories (`ls -d plugins/*/.apm/skills/*/ | wc -l`); The first count equals the number of skill directories (`ls -d plugins/*/.apm/skills/*/ | wc -l`);
the second returns exactly the template. The remaining unmatched `SKILL.md` files in the tree are the the second returns exactly the template. Every other tracked `SKILL.md` in the tree is one of the
generated flat mirror, which is excluded by the `.apm/` segment on purpose — a mirror edit is drift, four vendored `plugins/kyberforge/docs/research/examples/skill-write/` corpus files, excluded by the
not an authoring change. `.apm/skills/` segment — the first of the two deliberate exclusions above.
### Two independent gate families, neither replaced the other ### Two independent gate families, neither replaced the other
@@ -613,9 +613,11 @@ Two prefilter hooks, with `.apm/`-scoped `files:` patterns:
| `vale-audit-prefilter-skill` | `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` | | `vale-audit-prefilter-skill` | `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` |
| `vale-audit-prefilter-agent` | `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$` | | `vale-audit-prefilter-agent` | `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$` |
Only the **authoring source** triggers them. A `SKILL.md` in the generated flat mirror matches Only the **authoring source** triggers them. The `skill-author` scaffold template and the four
neither pattern, so prose findings surface only when you edit the file you are supposed to be vendored research-corpus `SKILL.md` files match neither pattern (see
editing. Without the binary the hooks fail with a bare "command not found" and no install pointer. [Skill and agent context gates](#skill-and-agent-context-gates-adr-0020), which uses the same scope
for `skill-size-check`), so prose findings surface only when you edit a file this repo actually
authors. Without the binary the hooks fail with a bare "command not found" and no install pointer.
**Two hooks, not one combined hook.** Both manifests split the prefilter in two precisely because a **Two hooks, not one combined hook.** Both manifests split the prefilter in two precisely because a
single hook can point at only one copy, and that copy would silently 0-file-skip the other file single hook can point at only one copy, and that copy would silently 0-file-skip the other file
@@ -864,10 +866,9 @@ does not deploy and the replay does not compare; shared enforcement belongs in
### Why it is excluded from `pretty-format-json` ### Why it is excluded from `pretty-format-json`
It is the **sixth and last alternation** in that hook's `exclude:` pattern, and the only one there It is the **second and last alternation** in that hook's `exclude:` pattern, and the only one there
for a reason other than "generated manifest". Mind which number you are quoting: **six alternations, for a reason other than "generated manifest". Mind which number you are quoting: **two alternations,
expanding to sixteen real files** — 3 root marketplace manifests, 2 per plugin × 6 plugins, plus this expanding to two real files** — `.claude-plugin/marketplace.json`, plus this one.
one.
`pretty-format-json --autofix` sorts object keys unless `--no-sort-keys` is passed, while apm's hook `pretty-format-json --autofix` sorts object keys unless `--no-sort-keys` is passed, while apm's hook
integrator emits insertion order (`matcher` before `hooks`, `type` before `command`). Leaving the integrator emits insertion order (`matcher` before `hooks`, `type` before `command`). Leaving the
@@ -895,10 +896,13 @@ this remote before any network call.
enforcement table (deterministic vs. auditor judgment), and every rejected alternative enforcement table (deterministic vs. auditor judgment), and every rejected alternative
- `docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md` — the `SessionStart` hook, the - `docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md` — the `SessionStart` hook, the
executable-trust gate, and the version-pinned allow key executable-trust gate, and the version-pinned allow key
- `docs/adr/0017-plugin-content-mirror-bridges-apm-to-host-discovery.md`, - `docs/adr/0024-apm-is-the-only-supported-install-path.md` — apm as the sole install path, and the
`docs/adr/0015-apm-replaces-plugin-marketplace-authoring.md`, deletion of the flat content mirror and its `check-plugin-content-sync` gate. It supersedes
`docs/adr/0014-vale-prefilter-ships-from-the-plugin.md` — plugin content sync, apm-generated `docs/adr/0017-plugin-content-mirror-bridges-apm-to-host-discovery.md` (**superseded** — plugin
manifests, committed Vale styles content sync, kept as the historical record)
- `docs/adr/0015-apm-replaces-plugin-marketplace-authoring.md`,
`docs/adr/0014-vale-prefilter-ships-from-the-plugin.md` — apm-generated manifests, committed Vale
styles
- `docs/spec/architecture.md` — directory structure, install pipeline, what is generated and what is - `docs/spec/architecture.md` — directory structure, install pipeline, what is generated and what is
hand-authored hand-authored
- `.pre-commit-config.yaml` — the hooks themselves, with inline rationale comments - `.pre-commit-config.yaml` — the hooks themselves, with inline rationale comments

View File

@@ -4,40 +4,32 @@ A place for things to be binned
## Install ## Install
**Claude Code:** apm is the only supported install path (ADR-0024). Declare this package in the consuming project's `apm.yml`:
```bash ```yaml
claude plugin marketplace add <owner>/<repo> dependencies:
claude plugin install bin@<marketplace-name> apm:
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
path: plugins/bin
``` ```
**GitHub Copilot CLI:** Then:
```bash ```bash
copilot plugin marketplace add <owner>/<repo> apm install
copilot plugin install bin
``` ```
**Local (development):** The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `bin@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
```bash **Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills — and Claude Code raises no error while doing it (ADR-0024).
# Claude Code
claude --plugin-dir ./plugins/bin
# GitHub Copilot CLI
copilot plugin install ./plugins/bin
```
## Contents ## Contents
| Component | Path | Description | | Component | Path | Description |
|---|---|---| |---|---|---|
| Skills | `.apm/skills/` → `skills/` | Slash commands available after install | | Skills | `.apm/skills/` | Slash commands available after install |
| MCP servers | `.mcp.json` | The `obsidian` server (`npx @bitbonsai/mcpvault@0.15.0 docs/`), hand-authored at the plugin root |
`.apm/` is the authoring source; `skills/` is the generated mirror plugin hosts scan (ADR-0017). This plugin ships no agents. It is the only plugin here with a non-empty `.mcp.json`, which is why its compiled manifests are the only ones carrying an `mcpServers` block. `.apm/` is the authoring source and the only thing apm deploys (ADR-0024). This plugin ships no agents.
The two compiled manifests get that block by different routes. `.claude-plugin/plugin.json` gets it from apm itself: `build_plugin_manifest`'s Claude branch calls `collect_mcp_servers`, which reads `.mcp.json`, sanitizes it, and inlines the resulting server objects. `.github/plugin/plugin.json` gets nothing from apm — the Copilot branch drops the field — so `scripts/sync-plugin-content.sh`'s `reinject_mcp_servers()` puts it back, as the **string `".mcp.json"`** rather than the resolved objects. Copilot's manifest schema types the field as "string or object — MCP server config path or inline definitions", and a path reference cannot carry a credential into a committed manifest. See ADR-0017's `mcpServers` amendment.
## Author ## Author

View File

@@ -4,37 +4,32 @@ Skills for authoring and auditing a repo's AGENTS.md and the provider adapter fi
## Install ## Install
**Claude Code:** apm is the only supported install path (ADR-0024). Declare this package in the consuming project's `apm.yml`:
```bash ```yaml
claude plugin marketplace add <owner>/<repo> dependencies:
claude plugin install core@<marketplace-name> apm:
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
path: plugins/core
``` ```
**GitHub Copilot CLI:** Then:
```bash ```bash
copilot plugin marketplace add <owner>/<repo> apm install
copilot plugin install core
``` ```
**Local (development):** The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `core@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
```bash **Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills — and Claude Code raises no error while doing it (ADR-0024).
# Claude Code
claude --plugin-dir ./plugins/core
# GitHub Copilot CLI
copilot plugin install ./plugins/core
```
## Contents ## Contents
| Component | Path | Description | | Component | Path | Description |
|---|---|---| |---|---|---|
| Skills | `.apm/skills/` → `skills/` | Slash commands available after install | | Skills | `.apm/skills/` | Slash commands available after install |
`.apm/` is the authoring source; `skills/` is the generated mirror plugin hosts scan (ADR-0017). `.apm/` is the authoring source and the only thing apm deploys (ADR-0024).
## Skills ## Skills

View File

@@ -4,29 +4,24 @@ Skills and agents for working with a local Git clone over the git wire protocol,
## Install ## Install
**Claude Code:** apm is the only supported install path (ADR-0024). Declare this package in the consuming project's `apm.yml`:
```bash ```yaml
claude plugin marketplace add <owner>/<repo> dependencies:
claude plugin install git@<marketplace-name> apm:
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
path: plugins/git
``` ```
**GitHub Copilot CLI:** Then:
```bash ```bash
copilot plugin marketplace add <owner>/<repo> apm install
copilot plugin install git
``` ```
**Local (development):** The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `git@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
```bash **Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills and zero agents — and Claude Code raises no error while doing it (ADR-0024).
# Claude Code
claude --plugin-dir ./plugins/git
# GitHub Copilot CLI
copilot plugin install ./plugins/git
```
## Conventions ## Conventions
@@ -38,10 +33,10 @@ Installing this plugin without `rtk`? Every prefixed command is a plain `git` in
| Component | Path | Description | | Component | Path | Description |
|---|---|---| |---|---|---|
| Skills | `.apm/skills/` → `skills/` | `git-commits`, `git-branches`, `git-history`, `git-remotes`, `git-submodules`, `git-workflow`, `git-worktrees`, `pc-author`, `pc-run` | | Skills | `.apm/skills/` | `git-commits`, `git-branches`, `git-history`, `git-remotes`, `git-submodules`, `git-workflow`, `git-worktrees`, `pc-author`, `pc-run` |
| Agents | `.apm/agents/` → `agents/` | `git-orchestrate` | | Agents | `.apm/agents/` | `git-orchestrate` |
`.apm/` is the authoring source; `skills/`/`agents/` are the generated mirrors plugin hosts scan (ADR-0017) — edit only under `.apm/`. `.apm/` is the authoring source and the only thing apm deploys (ADR-0024).
## Author ## Author

View File

@@ -4,42 +4,36 @@ Skills and agents for creating, maintaining, and managing a Claude Code / Copilo
## Install ## Install
**Claude Code:** apm is the only supported install path (ADR-0024). Declare this package in the consuming project's `apm.yml`:
```bash ```yaml
claude plugin marketplace add <owner>/<repo> dependencies:
claude plugin install kyberforge@<marketplace-name> apm:
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
path: plugins/kyberforge
``` ```
**GitHub Copilot CLI:** Then:
```bash ```bash
copilot plugin marketplace add <owner>/<repo> apm install
copilot plugin install kyberforge
``` ```
**Local (development):** The entry above is unpinned and tracks the remote's default branch — add `ref: <tag>` to pin a release. Registering the catalogue instead (`apm marketplace add git@git.dev.rkdr.net:Defame1297/holocron.git --name holocron`) gets you the `kyberforge@holocron` short name, but writes to `~/.apm/marketplaces.json` at user scope; the git+path object needs nothing beyond the manifest.
```bash **Native plugin installs do not work.** This package ships no per-plugin manifest and no flat content directories, so a host that installs it natively gets zero skills, agents and hooks — and Claude Code raises no error while doing it (ADR-0024).
# Claude Code
claude --plugin-dir ./plugins/kyberforge
# GitHub Copilot CLI
copilot plugin install ./plugins/kyberforge
```
## Contents ## Contents
Authoring source lives in `.apm/`. The `skills/`, `agents/`, and `hooks/hooks.json` paths below are the generated mirror that plugin hosts actually scan — produced by `scripts/sync-plugin-content.sh`, never hand-edited (ADR-0017). Authoring source lives in `.apm/`; it is the only content source and the only thing `apm install` deploys (ADR-0024).
| Component | Path | Description | | Component | Path | Description |
|---|---|---| |---|---|---|
| Skills | `.apm/skills/` → `skills/` | Slash commands available after install | | Skills | `.apm/skills/` | Slash commands available after install |
| Agents | `.apm/agents/*.agent.md` → `agents/` | Role-based agents; one vendor-neutral `.agent.md` per agent, copied verbatim to both targets (ADR-0016) | | Agents | `.apm/agents/*.agent.md` | Role-based agents; one vendor-neutral `.agent.md` per agent (ADR-0016) |
| Hooks | `.apm/hooks/` → `hooks/hooks.json` | Event-triggered automation — read by Claude Code only, see below | | Hooks | `.apm/hooks/` | Event-triggered automation — Claude Code only, see below |
| MCP servers | `.mcp.json` | Model Context Protocol server definitions (hand-authored at the plugin root) |
**Hooks are Claude Code-only in practice.** Claude Code convention-scans `hooks/hooks.json` at the plugin root, so that file is live. Copilot CLI has no default hooks path — `agents` and `skills` default to `agents/` and `skills/`, but `hooks` defaults to nothing (`docs/research/docs/github-copilot-plugins/configuration.md:47`), so Copilot reads hooks only via an explicit `"hooks": "hooks.json"` pointer in `plugin.json` (`docs/research/docs/github-copilot-plugins/examples.md:49`). apm's manifest compiler strips pointer fields from every manifest it generates, so neither `.claude-plugin/plugin.json` nor `.github/plugin/plugin.json` carries one, and re-injecting it is the option ADR-0017 explicitly rejected. Copilot therefore loads no hooks from this plugin — and did not load them from the old root-level `hooks.json` either. The Copilot half of this row is aspirational, not current behaviour. **Hooks are Claude Code-only.** apm merges `.apm/hooks/*.json` into the consuming project's `.claude/settings.json` at install. Copilot CLI has no default hooks path — `agents` and `skills` default to `agents/` and `skills/`, but `hooks` defaults to nothing (`docs/research/docs/github-copilot-plugins/configuration.md:47`), so Copilot reads hooks only via an explicit pointer in a plugin manifest — and since ADR-0024 there is no per-plugin manifest to carry one. Copilot therefore loads no hooks from this plugin. Details, including why a pointer was the wrong fix even when a manifest existed, are in `docs/hooks.md`.
## Skills ## Skills

View File

@@ -4,7 +4,7 @@ Plugin documentation. Not read automatically by Claude Code or GitHub Copilot CL
| Path | Purpose | | Path | Purpose |
|------|---------| |------|---------|
| `hooks.md` | Where this plugin's hooks are authored (`.apm/hooks/`), where they are generated to (`hooks/hooks.json`), and the Claude Code and Copilot CLI schemas | | `hooks.md` | Where this plugin's hooks are authored (`.apm/hooks/`), what apm deploys them into (`.claude/settings.json`), and the Claude Code and Copilot CLI schemas |
Everything else under this directory is research material. Everything else under this directory is research material.

View File

@@ -1,23 +1,21 @@
# Hooks # Hooks
Reference for this plugin's hook definitions: where to edit them, where they end up, and what the Reference for this plugin's hook definitions: where to edit them, what apm does with them, and what
host reads. the host ends up reading.
This document lives in `docs/` rather than next to the hooks it describes. `plugins/kyberforge/hooks/`
is a **generated mirror** — `scripts/sync-plugin-content.sh` runs `rm -rf` on it before every
rebuild, so any hand-written file placed there is deleted on the next sync with no drift warning
(a prior copy of this document was lost exactly that way). See ADR-0017.
## Where to edit ## Where to edit
Author hooks in `plugins/kyberforge/.apm/hooks/*.json`. `apm pack --format plugin` merges every Author hooks in `plugins/kyberforge/.apm/hooks/*.json`. `.apm/` is the only content source and
file in that directory into a single `hooks.json`, which `sync-plugin-content.sh` copies to `apm install` is the only supported install path (ADR-0024) — there is no generated mirror at the
`plugins/kyberforge/hooks/hooks.json` — the path Claude Code convention-scans. Never edit the plugin root and no per-plugin `plugin.json`, so `.apm/hooks/` is both where you edit and what ships.
mirrored file; the `check-plugin-content-sync` pre-push hook reports it as drift.
## Claude Code structure apm merges every `*.json` in that directory into a single hook definition and writes the event
bindings into the consuming project's `.claude/settings.json`; see "Deployed shape" below. Scripts a
hook invokes live in the same directory, alongside the JSON that references them.
`hooks/hooks.json` is read by Claude Code. Structure: ## Hook file structure
The shape Claude Code reads, and therefore the shape to author under `.apm/hooks/`:
```json ```json
{ {
@@ -43,7 +41,7 @@ only the per-agent `hooks` field, not the plugin-level set. Treat the five names
this repo has verified, not as the schema. Check Claude Code's own hooks documentation before wiring this repo has verified, not as the schema. Check Claude Code's own hooks documentation before wiring
an event not listed here. an event not listed here.
## Referencing a script — use the `.apm/` path, not the mirror ## Referencing a script — use the `.apm/` path
Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin; the plugin runs from a cache or Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin; the plugin runs from a cache or
`apm_modules/` path after install, not its original repo location. **Address the script at its `apm_modules/` path after install, not its original repo location. **Address the script at its
@@ -55,15 +53,9 @@ Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin; the plugin
The obvious-looking `${CLAUDE_PLUGIN_ROOT}/hooks/check-apm-current.sh` does not work, and fails The obvious-looking `${CLAUDE_PLUGIN_ROOT}/hooks/check-apm-current.sh` does not work, and fails
quietly enough to be worth spelling out. apm resolves the placeholder against the installed package quietly enough to be worth spelling out. apm resolves the placeholder against the installed package
root, where `hooks/` is the **generated mirror** — and `apm pack` merges only `*.json` out of root, and there is no `hooks/` directory there at all — the package's content is `.apm/`. apm prints
`.apm/hooks/`, dropping every non-JSON file. So the mirror contains `hooks.json` and nothing else. `Hook script not found: .../hooks/check-apm-current.sh` and then deploys the hook anyway, pointing at
apm prints `Hook script not found: .../hooks/check-apm-current.sh` and then deploys the hook anyway, a path with no file behind it.
pointing at a path with no file behind it.
Nor can the script be hand-placed in `plugins/kyberforge/hooks/` to satisfy that path:
`sync-plugin-content.sh` runs `rm -rf` on the directory before every rebuild (ADR-0017), so it would
be deleted on the next sync with no drift warning — the same trap that ate this document's
predecessor.
`tests/test-apm-current-hook.sh` pins the reference so a well-meaning "simplification" back to `tests/test-apm-current-hook.sh` pins the reference so a well-meaning "simplification" back to
`hooks/` fails the suite rather than silently disabling the hook. `hooks/` fails the suite rather than silently disabling the hook.
@@ -77,7 +69,9 @@ tracked in a `.claude/apm-hooks.json` sidecar, so an uninstall removes them with
hand-authored hooks. Both `.claude/hooks/` and the sidecar are gitignored install output. hand-authored hooks. Both `.claude/hooks/` and the sidecar are gitignored install output.
Note that apm's **executable-trust gate is off** unless the consuming project's `apm.yml` has an Note that apm's **executable-trust gate is off** unless the consuming project's `apm.yml` has an
`executables:` block — without one, package hooks deploy with no prompt. See ADR-0019. `executables:` block — without one, package hooks deploy with no prompt. The allow key is
version-pinned (`kyberforge#<version>`), so a version bump on one side alone stops the hook
deploying; `check-executables-allow-sync` is the pre-push gate that catches it. See ADR-0019.
## The SessionStart hook ## The SessionStart hook
@@ -89,11 +83,11 @@ ADR-0019.
**Where it looks for the lockfile.** The hook resolves a project directory as `${CLAUDE_PROJECT_DIR}` **Where it looks for the lockfile.** The hook resolves a project directory as `${CLAUDE_PROJECT_DIR}`
when the host exports it (Claude Code does, for SessionStart hooks) and the current directory when the host exports it (Claude Code does, for SessionStart hooks) and the current directory
otherwise, then exits silently unless that directory holds an `apm.lock.yaml` — which is what makes otherwise, then exits silently unless that directory holds an `apm.lock.yaml` — which is what makes
it inert for any host that installed this plugin natively rather than through apm. Both `apm` it inert in any project that does not consume packages through apm. Both `apm` invocations run
invocations run against the same resolved directory. The earlier spelling checked a bare against the same resolved directory. The earlier spelling checked a bare `apm.lock.yaml` against the
`apm.lock.yaml` against the session's cwd, so a session opened in a subdirectory of an session's cwd, so a session opened in a subdirectory of an apm-consuming repo no-opped silently.
apm-consuming repo no-opped silently. Keep the cwd fallback: a host that sets no Keep the cwd fallback: a host that sets no `CLAUDE_PROJECT_DIR` must still get inert-but-harmless
`CLAUDE_PROJECT_DIR` must still get inert-but-harmless behaviour, not an unset-variable error. behaviour, not an unset-variable error.
**The `timeout` in `hooks.json` must exceed the script's own budget.** The script spends at most **The `timeout` in `hooks.json` must exceed the script's own budget.** The script spends at most
`timeout 60 apm outdated` plus `timeout 300 apm update`; the hook entry declares `timeout: 380`, the `timeout 60 apm outdated` plus `timeout 300 apm update`; the hook entry declares `timeout: 380`, the
@@ -113,74 +107,45 @@ hook.
## GitHub Copilot CLI ## GitHub Copilot CLI
Copilot reads a differently-shaped `hooks.json`: `version: 1` is required, each entry is **Copilot loads no hooks from this plugin.** Two independent reasons, either one sufficient:
`type: "command"` with separate `bash` and `powershell` scripts, and the lifecycle points are
lowercase and differently named (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
`postToolUse`, `errorOccurred`, `agentStop`). See
`docs/research/docs/github-copilot-plugins/configuration.md`.
There is no separate Copilot hooks file at this plugin root, and — as things stand — **Copilot - **Nothing can point Copilot at a hooks file.** Copilot types `hooks` as a `plugin.json` field of
resolves to no hooks file at all.** Two corrections to an earlier revision of this document, which type "string or object" with **no default**
got both halves of this wrong: (`docs/research/docs/github-copilot-plugins/configuration.md:47`), so there is no convention path
for it to scan — it reads hooks only via an explicit pointer. Since ADR-0024 there is no
per-plugin Copilot manifest at all, so there is nothing to carry that pointer.
- **The two ecosystems do not share a hooks format.** Copilot reads a differently-shaped
`hooks.json`: `version: 1` is required, each entry is `type: "command"` with separate `bash` and
`powershell` scripts, and the lifecycle points are lowercase and differently named (`sessionStart`,
`sessionEnd`, `userPromptSubmitted`, `preToolUse`, `postToolUse`, `errorOccurred`, `agentStop`).
See `docs/research/docs/github-copilot-plugins/configuration.md`. apm merges `.apm/hooks/*.json`
into one definition with no per-target shaping, and that definition is Claude-shaped.
**The deleted root `hooks.json` was not a stale sync artifact.** `plugins/kyberforge/hooks.json` was The second reason is why "just add a pointer" was rejected even while a Copilot manifest existed: a
added in `2287ddc` (2026-06-20), the commit that created the plugin, well before pointer would tell Copilot that a Claude-shaped file is Copilot-shaped, trading an incomplete
`scripts/sync-plugin-content.sh` existed; `plugins/lint/hooks.json` arrived the same way in manifest for a wrong one. ADR-0024 consequence 7 records that the question is now moot — the
`f326df4`. Main's Copilot manifest `plugins/kyberforge/plugin.json` declared `"hooks": "hooks.json"`, manifest it argued about is gone — but the schema mismatch it turned on is not, and it is what any
and `plugins/lint/plugin.json` did the same — these were deliberately pointed-at Copilot hooks files, future Copilot hooks support has to solve.
not leftovers. The sync (`38f1ba4`) later took ownership of that path, and ADR-0017's 2026-08-14
amendment moved the generated file to `hooks/hooks.json` because that, not the plugin root, is the
path Claude Code convention-scans.
**Only Claude Code resolves to `hooks/hooks.json`.** Claude Code finds it by auto-discovery.
Copilot does not: `docs/research/docs/github-copilot-plugins/configuration.md:47` types `hooks` as a
`plugin.json` field of type "string or object" with **no default**, so there is no convention path to
scan, and `jq 'has("hooks")'` returns `false` for all six `.github/plugin/plugin.json` files that
`apm pack` emits. With the pointer gone and no auto-discovery to fall back on, the Copilot ecosystem
sees zero hooks.
The effect looks like the twin of the `mcpServers` gap that ADR-0017's 2026-08-13 amendment
re-injects for: same "string or object" type, same absence of a default, same outcome of a Copilot
manifest with no pointer. The *mechanism* differs, and ADR-0017 is explicit about it — `mcpServers`
is actively stripped by `build_plugin_manifest`, whereas `hooks` "was never in
`build_plugin_manifest`'s strip list at all"; it is simply never emitted, because `apm.yml` has no
key that produces one. So this is an absence apm never fills, not a removal to reverse.
## Why no `hooks` pointer is injected
**Decided (2026-08-14, PR #95): the gap stays documented rather than patched.** `sync-plugin-content.sh`
does *not* re-inject a `hooks` pointer into `.github/plugin/plugin.json`, and a test pins that
absence. Full reasoning is in ADR-0017's "no `hooks` pointer" amendment; the short version, because
the one-line fix looks obvious and someone will propose it again:
The `mcpServers` re-injection is safe because `.mcp.json` is **one format both ecosystems read**, so
the pointer is a true statement about the file whatever it contains. Hooks have no shared format.
Compare the two structures above: Claude Code wants `PreToolUse` with `matcher` objects; Copilot
requires `version: 1`, lowercase event names, and per-shell `bash`/`powershell` keys. And apm merges
`.apm/hooks/*.json` into **exactly one** `hooks.json` with no per-target shaping — the same file
Claude Code convention-scans. One file, two incompatible readers.
So a pointer would tell Copilot that a Claude-shaped file is Copilot-shaped: an incomplete manifest
traded for a wrong one. It is not inert even today — `{"hooks": {}}` has no `version: 1`, so the
pointer would name a file invalid against the very schema it is pointed at from. And it does not
become correct later: whoever writes the first real hook writes it in one shape, and it is the
Claude shape in practice, since Claude Code auto-discovers the same file and is what hooks here are
authored against.
**What this costs you:** a hook authored under `.apm/hooks/` reaches Claude Code and not Copilot. **What this costs you:** a hook authored under `.apm/hooks/` reaches Claude Code and not Copilot.
That is a real limitation, and it is the accepted one until apm emits a per-target hooks file or the That is a real limitation, and it is the accepted one until apm emits a per-target hooks file or the
two schemas converge. If you need a Copilot hook today, raise it — it needs an upstream change or a two schemas converge. If you need a Copilot hook today, raise it — it needs an upstream change or a
second authoring path, not a pointer. second authoring path, not a pointer.
## Symlinks under `.apm/` do not survive ## Symlinks under `.apm/` do not survive, and nothing reports it
Do not author any file under `plugins/kyberforge/.apm/` as a symlink. apm's bundle exporter filters Do not author any file under `plugins/kyberforge/.apm/` as a symlink. apm's copy path filters
symlinks out of the bundle entirely and says nothing, so the file never reaches the mirror. Since symlinks out silently: `ignore_non_content()` in `apm_cli/security/gate.py` is a
`sync-plugin-content.sh` builds both sides of its drift comparison from that same bundle, the loss `shutil.copytree` ignore callback that drops every entry answering `is_symlink()`, commented
used to be invisible to `--check` as well. `check_apm_symlinks()` now reads the `.apm/` source tree "Excludes symlinks (security)". The file never reaches the consumer's install, and no warning is
directly and fails the sync with the offending path — replace the symlink with a regular file. printed at any point.
It stays quiet about one place: `.apm/<category>/<name>/tests/`, the dev-fixture directory the **No gate catches this.** The pre-push check that used to read the `.apm/` tree and fail on the
mirror excludes anyway (a symlink there loses nothing, because nothing under it is mirrored). A offending path was deleted along with the content mirror, and the decision was taken not to replace
`tests/` deeper than that — `assets/templates/tests/`, a scaffolding asset the mirror does carry — it (ADR-0024 consequence 7). This document is the only thing standing between a symlink and silent
is reported like anywhere else. See ADR-0017's symlink amendment. content loss. No symlink exists under any `.apm/` today; add one and it is dropped on deploy with
nothing reporting it. Replace it with a regular file.
The old exemption for `.apm/<category>/<name>/tests/` no longer applies either. That directory was
exempt only because the mirror excluded it; apm deploys it like any other content (ADR-0024
consequence 2), so a symlink there loses content the same as anywhere else.

View File

@@ -141,6 +141,14 @@ run_bats
# apm_modules/ is excluded for the same reason tests/run-bats.sh excludes it: # apm_modules/ is excluded for the same reason tests/run-bats.sh excludes it:
# `apm install` materializes dependency copies of this repo's own plugins there, # `apm install` materializes dependency copies of this repo's own plugins there,
# and re-running a dependency's tests re-runs what plugins/ already covers. # and re-running a dependency's tests re-runs what plugins/ already covers.
#
# .claude/skills/ is excluded for the same reason, one install step later. apm
# deploys skills there straight from each plugin's `.apm/` tree, `tests/` dirs
# and all, so any `<skill>/tests/test-*.sh` file under plugins/ would also land
# at .claude/skills/<name>/tests/ and be discovered and run a second time. No
# such file exists today -- the skill suites are all .bats, where run-bats.sh
# already hit this -- so the exclusion is symmetry with its sibling, kept here
# so the first shell suite added under a skill does not reintroduce it.
SCRIPTS=() SCRIPTS=()
while IFS= read -r script; do while IFS= read -r script; do
SCRIPTS+=("$script") SCRIPTS+=("$script")
@@ -149,6 +157,7 @@ done < <(
-not -path "*/.git/*" \ -not -path "*/.git/*" \
-not -path "*/.claude/worktrees/*" \ -not -path "*/.claude/worktrees/*" \
-not -path "*/apm_modules/*" \ -not -path "*/apm_modules/*" \
-not -path "*/.claude/skills/*" \
| sort | sort
) )