refactor!: carry out the simplification audit across gates, tests, plugins and docs #135
@@ -34,15 +34,19 @@ repos:
|
||||
# rationale, under which a fixture or vendored tree containing
|
||||
# `.../.claude-plugin/marketplace.json` would have been silently excluded
|
||||
# from formatting while an equivalent
|
||||
# `.../.agents/plugins/marketplace.json` would not. The three root
|
||||
# marketplace manifests match; anything else is hand-authored and gets
|
||||
# formatted. The twelve per-plugin `plugin.json` alternatives were
|
||||
# `.../.agents/plugins/marketplace.json` would not. Only the one root
|
||||
# marketplace manifest matches now; anything else is hand-authored and
|
||||
# gets formatted. The twelve per-plugin `plugin.json` alternatives were
|
||||
# dropped with the plugin manifests themselves when native
|
||||
# `claude plugin install` support was removed -- apm probes `apm.yml`
|
||||
# and never reached them.
|
||||
# `claude plugin install` support was removed (ADR-0024) -- apm probes
|
||||
# `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
|
||||
# different reason: apm OWNS that file (ADR-0018, ADR-0019), and
|
||||
# `.claude/settings.json` is the second and last alternation, and it is
|
||||
# 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
|
||||
# the result byte-for-byte. `pretty-format-json` sorts object keys
|
||||
# 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.
|
||||
# Re-running `apm install` fixes the file; leaving it in scope here
|
||||
# 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
|
||||
stages: ['pre-commit']
|
||||
- id: trailing-whitespace
|
||||
|
||||
20
CONTEXT.md
20
CONTEXT.md
@@ -52,15 +52,16 @@ _Avoid_: command, prompt, macro
|
||||
|
||||
**apm package**:
|
||||
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
|
||||
hand-authored `plugins/<name>/.apm/` tree it deploys from (ADR-0015).
|
||||
_Avoid_: plugin, package, bundle, module, plugin directory, source tree
|
||||
servers under a single directory `plugins/<name>/`, consisting of that directory's `apm.yml` plus
|
||||
the hand-authored `plugins/<name>/.apm/` tree it deploys from (ADR-0015).
|
||||
_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**:
|
||||
An `apm pack` target format for the generated *marketplace* catalogue compiled from root
|
||||
`apm.yml`'s `marketplace:` block. Only `claude` is declared
|
||||
(`.claude-plugin/marketplace.json`); apm's own marketplace consumers read that same file, so one
|
||||
catalogue serves both. Mechanics: `docs/spec/architecture.md`.
|
||||
A named ecosystem format `apm pack` can compile the marketplace manifest into, declared per profile
|
||||
under root `apm.yml`'s `marketplace.outputs:`. apm defines `claude` and `codex`; each writes to its
|
||||
own default path unless overridden. Mechanics: `docs/spec/architecture.md`.
|
||||
_Avoid_: build target, export format
|
||||
|
||||
**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
|
||||
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.
|
||||
- "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 —
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
> **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
|
||||
|
||||
@@ -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 |
|
||||
| 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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
> **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
|
||||
|
||||
@@ -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.
|
||||
|
||||
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`.~~
|
||||
|
||||
> **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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
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.
|
||||
> **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.~~
|
||||
> **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
|
||||
|
||||
- [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.
|
||||
- **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?
|
||||
- **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?
|
||||
- **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)
|
||||
|
||||
@@ -221,8 +238,10 @@ Recorded here so they are not rediscovered as defects. All follow from commit `7
|
||||
**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.
|
||||
- **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.
|
||||
|
||||
**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.
|
||||
|
||||
@@ -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
|
||||
#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 —
|
||||
and `.claude-plugin/plugin.json`/`.github/plugin/plugin.json` are compiled output of `apm pack`,
|
||||
not hand-authored. This ADR's content is kept below as the historical record of the
|
||||
pre-APM decision; it is no longer the current model.
|
||||
and `.claude-plugin/plugin.json`/`.github/plugin/plugin.json` were compiled output of `apm pack`,
|
||||
not hand-authored — and as of ADR-0024 (2026-09-14) both are deleted, along with native
|
||||
`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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
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).
|
||||
(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.
|
||||
|
||||
**Governance docs are excluded as a rule source.** `docs/research/governance_principles/CONTROLS.md`
|
||||
|
||||
@@ -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
|
||||
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.
|
||||
**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
|
||||
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
|
||||
|
||||
@@ -180,4 +180,7 @@ correction) sorted what they document into three buckets:
|
||||
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
|
||||
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.
|
||||
|
||||
@@ -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).
|
||||
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`
|
||||
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:
|
||||
|
||||
@@ -28,12 +28,15 @@ What that audience costs is measurable:
|
||||
|---|---|
|
||||
| Tracked mirror files under `plugins/*/{skills,agents,hooks}/` | 213 files, ~20,000 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 |
|
||||
| `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
|
||||
alone is close to 30% of `run-tests`' wall time — the single largest item in it.
|
||||
Roughly 22,000 lines of tracked content and tooling, and about 92 seconds on every push (83 + 4.5 +
|
||||
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
|
||||
`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`,
|
||||
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
|
||||
latter only ever partially — see ADR-0017's own `hooks` amendment). apm serves seven. A consumer who
|
||||
installs holocron through apm gets strictly more than one who installed it natively.
|
||||
latter only ever partially — see ADR-0017's own `hooks` amendment). apm serves eleven through those
|
||||
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
|
||||
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`).
|
||||
- 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.
|
||||
- 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
|
||||
`.claude-plugin/marketplace.json`. apm's own marketplace consumers read that same file; removing
|
||||
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
|
||||
|
||||
**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
|
||||
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.
|
||||
@@ -106,7 +119,7 @@ exact shape: the `mattpocock-skills` entry removed from root `apm.yml` on 2026-0
|
||||
|
||||
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
|
||||
amendment history shows to be the real cost.
|
||||
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
|
||||
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
|
||||
`.claude-plugin/marketplace.json` — falling back to `.github/plugin/marketplace.json`, which this
|
||||
repo already deleted — and that read is what makes holocron an apm marketplace and what gives
|
||||
consumers the `<name>@holocron` short-name form. Deleting it would have broken apm consumers in
|
||||
order to remove a file whose format Claude Code merely happens to share.
|
||||
reasoning that it is a Claude Code artifact. That is wrong. `apm marketplace add` probes
|
||||
`_MARKETPLACE_PATHS` in `apm_cli/marketplace/client.py` — `marketplace.json`, then
|
||||
`.github/plugin/marketplace.json`, then `.claude-plugin/marketplace.json`, first hit wins — and that
|
||||
read is what makes holocron an apm marketplace and what gives consumers the `<name>@holocron`
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
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
|
||||
under `.apm/` is touched here; only compiled artifacts are removed. The shipped content is
|
||||
byte-identical, so no bump is owed. This also avoids triggering the `executables.allow`
|
||||
**4. No version bumps.** There is no standing rule that would require one. The patch-bump-on-content-
|
||||
change convention this repo once followed was ADR-0006's, and ADR-0015 explicitly retired it as a
|
||||
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
|
||||
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
|
||||
file to place.
|
||||
- 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
|
||||
bundle exporter filters them out silently and the resulting content loss is invisible to any
|
||||
mirror-versus-mirror diff. That check dies with the script. Whether `apm install`'s own copy path
|
||||
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
|
||||
different code path from dependency installation. If it does, symlinked content under `.apm/` is
|
||||
now silently lost with nothing reporting it. No symlink exists under any `.apm/` today, so this is
|
||||
a latent gap rather than an active one, but it should be re-checked before anyone adds one.
|
||||
`check_apm_symlinks()` read the `.apm/` source tree directly to report symlinks, because apm
|
||||
filters them out silently and the resulting content loss is invisible to any mirror-versus-mirror
|
||||
diff. That check dies with the script.
|
||||
|
||||
A previous revision of this ADR left open whether `apm install`'s own copy path drops symlinks the
|
||||
way the bundle exporter does. It does, and the mechanism is now confirmed by reading the installed
|
||||
apm source. Deployment filters them: `ignore_non_content()` in `apm_cli/security/gate.py` is a
|
||||
`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
|
||||
final consequence anticipated deletion, but conditioned it on an upstream fix: "a future apm release
|
||||
|
||||
@@ -38,7 +38,7 @@ a PR merged through Gitea's merge button, where no local push happens at all.
|
||||
|
||||
## 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**
|
||||
|
||||
@@ -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-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 |
|
||||
|---|---|
|
||||
@@ -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 second returns exactly the template. The remaining unmatched `SKILL.md` files in the tree are the
|
||||
generated flat mirror, which is excluded by the `.apm/` segment on purpose — a mirror edit is drift,
|
||||
not an authoring change.
|
||||
the second returns exactly the template. Every other tracked `SKILL.md` in the tree is one of the
|
||||
four vendored `plugins/kyberforge/docs/research/examples/skill-write/` corpus files, excluded by the
|
||||
`.apm/skills/` segment — the first of the two deliberate exclusions above.
|
||||
|
||||
### 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-agent` | `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$` |
|
||||
|
||||
Only the **authoring source** triggers them. A `SKILL.md` in the generated flat mirror matches
|
||||
neither pattern, so prose findings surface only when you edit the file you are supposed to be
|
||||
editing. Without the binary the hooks fail with a bare "command not found" and no install pointer.
|
||||
Only the **authoring source** triggers them. The `skill-author` scaffold template and the four
|
||||
vendored research-corpus `SKILL.md` files match neither pattern (see
|
||||
[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
|
||||
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`
|
||||
|
||||
It is the **sixth 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,
|
||||
expanding to sixteen real files** — 3 root marketplace manifests, 2 per plugin × 6 plugins, plus this
|
||||
one.
|
||||
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: **two alternations,
|
||||
expanding to two real files** — `.claude-plugin/marketplace.json`, plus this one.
|
||||
|
||||
`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
|
||||
@@ -895,10 +896,13 @@ this remote before any network call.
|
||||
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
|
||||
executable-trust gate, and the version-pinned allow key
|
||||
- `docs/adr/0017-plugin-content-mirror-bridges-apm-to-host-discovery.md`,
|
||||
`docs/adr/0015-apm-replaces-plugin-marketplace-authoring.md`,
|
||||
`docs/adr/0014-vale-prefilter-ships-from-the-plugin.md` — plugin content sync, apm-generated
|
||||
manifests, committed Vale styles
|
||||
- `docs/adr/0024-apm-is-the-only-supported-install-path.md` — apm as the sole install path, and the
|
||||
deletion of the flat content mirror and its `check-plugin-content-sync` gate. It supersedes
|
||||
`docs/adr/0017-plugin-content-mirror-bridges-apm-to-host-discovery.md` (**superseded** — plugin
|
||||
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
|
||||
hand-authored
|
||||
- `.pre-commit-config.yaml` — the hooks themselves, with inline rationale comments
|
||||
|
||||
@@ -4,40 +4,32 @@ A place for things to be binned
|
||||
|
||||
## Install
|
||||
|
||||
**Claude Code:**
|
||||
apm is the only supported install path (ADR-0024). Declare this package in the consuming project's `apm.yml`:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add <owner>/<repo>
|
||||
claude plugin install bin@<marketplace-name>
|
||||
```yaml
|
||||
dependencies:
|
||||
apm:
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/bin
|
||||
```
|
||||
|
||||
**GitHub Copilot CLI:**
|
||||
Then:
|
||||
|
||||
```bash
|
||||
copilot plugin marketplace add <owner>/<repo>
|
||||
copilot plugin install bin
|
||||
apm install
|
||||
```
|
||||
|
||||
**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
|
||||
# Claude Code
|
||||
claude --plugin-dir ./plugins/bin
|
||||
|
||||
# GitHub Copilot CLI
|
||||
copilot plugin install ./plugins/bin
|
||||
```
|
||||
**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).
|
||||
|
||||
## Contents
|
||||
|
||||
| Component | Path | Description |
|
||||
|---|---|---|
|
||||
| Skills | `.apm/skills/` → `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 |
|
||||
| Skills | `.apm/skills/` | Slash commands available after install |
|
||||
|
||||
`.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.
|
||||
|
||||
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.
|
||||
`.apm/` is the authoring source and the only thing apm deploys (ADR-0024). This plugin ships no agents.
|
||||
|
||||
## Author
|
||||
|
||||
|
||||
@@ -4,37 +4,32 @@ Skills for authoring and auditing a repo's AGENTS.md and the provider adapter fi
|
||||
|
||||
## Install
|
||||
|
||||
**Claude Code:**
|
||||
apm is the only supported install path (ADR-0024). Declare this package in the consuming project's `apm.yml`:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add <owner>/<repo>
|
||||
claude plugin install core@<marketplace-name>
|
||||
```yaml
|
||||
dependencies:
|
||||
apm:
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/core
|
||||
```
|
||||
|
||||
**GitHub Copilot CLI:**
|
||||
Then:
|
||||
|
||||
```bash
|
||||
copilot plugin marketplace add <owner>/<repo>
|
||||
copilot plugin install core
|
||||
apm install
|
||||
```
|
||||
|
||||
**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
|
||||
# Claude Code
|
||||
claude --plugin-dir ./plugins/core
|
||||
|
||||
# GitHub Copilot CLI
|
||||
copilot plugin install ./plugins/core
|
||||
```
|
||||
**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).
|
||||
|
||||
## Contents
|
||||
|
||||
| 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
|
||||
|
||||
|
||||
@@ -4,29 +4,24 @@ Skills and agents for working with a local Git clone over the git wire protocol,
|
||||
|
||||
## Install
|
||||
|
||||
**Claude Code:**
|
||||
apm is the only supported install path (ADR-0024). Declare this package in the consuming project's `apm.yml`:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add <owner>/<repo>
|
||||
claude plugin install git@<marketplace-name>
|
||||
```yaml
|
||||
dependencies:
|
||||
apm:
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/git
|
||||
```
|
||||
|
||||
**GitHub Copilot CLI:**
|
||||
Then:
|
||||
|
||||
```bash
|
||||
copilot plugin marketplace add <owner>/<repo>
|
||||
copilot plugin install git
|
||||
apm install
|
||||
```
|
||||
|
||||
**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
|
||||
# Claude Code
|
||||
claude --plugin-dir ./plugins/git
|
||||
|
||||
# GitHub Copilot CLI
|
||||
copilot plugin install ./plugins/git
|
||||
```
|
||||
**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).
|
||||
|
||||
## Conventions
|
||||
|
||||
@@ -38,10 +33,10 @@ Installing this plugin without `rtk`? Every prefixed command is a plain `git` in
|
||||
|
||||
| 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` |
|
||||
| Agents | `.apm/agents/` → `agents/` | `git-orchestrate` |
|
||||
| Skills | `.apm/skills/` | `git-commits`, `git-branches`, `git-history`, `git-remotes`, `git-submodules`, `git-workflow`, `git-worktrees`, `pc-author`, `pc-run` |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -4,42 +4,36 @@ Skills and agents for creating, maintaining, and managing a Claude Code / Copilo
|
||||
|
||||
## Install
|
||||
|
||||
**Claude Code:**
|
||||
apm is the only supported install path (ADR-0024). Declare this package in the consuming project's `apm.yml`:
|
||||
|
||||
```bash
|
||||
claude plugin marketplace add <owner>/<repo>
|
||||
claude plugin install kyberforge@<marketplace-name>
|
||||
```yaml
|
||||
dependencies:
|
||||
apm:
|
||||
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||
path: plugins/kyberforge
|
||||
```
|
||||
|
||||
**GitHub Copilot CLI:**
|
||||
Then:
|
||||
|
||||
```bash
|
||||
copilot plugin marketplace add <owner>/<repo>
|
||||
copilot plugin install kyberforge
|
||||
apm install
|
||||
```
|
||||
|
||||
**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
|
||||
# Claude Code
|
||||
claude --plugin-dir ./plugins/kyberforge
|
||||
|
||||
# GitHub Copilot CLI
|
||||
copilot plugin install ./plugins/kyberforge
|
||||
```
|
||||
**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).
|
||||
|
||||
## 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 |
|
||||
|---|---|---|
|
||||
| Skills | `.apm/skills/` → `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) |
|
||||
| Hooks | `.apm/hooks/` → `hooks/hooks.json` | Event-triggered automation — read by Claude Code only, see below |
|
||||
| MCP servers | `.mcp.json` | Model Context Protocol server definitions (hand-authored at the plugin root) |
|
||||
| Skills | `.apm/skills/` | Slash commands available after install |
|
||||
| Agents | `.apm/agents/*.agent.md` | Role-based agents; one vendor-neutral `.agent.md` per agent (ADR-0016) |
|
||||
| Hooks | `.apm/hooks/` | Event-triggered automation — Claude Code only, see below |
|
||||
|
||||
**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
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Plugin documentation. Not read automatically by Claude Code or GitHub Copilot CL
|
||||
|
||||
| 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.
|
||||
|
||||
|
||||
@@ -1,23 +1,21 @@
|
||||
# Hooks
|
||||
|
||||
Reference for this plugin's hook definitions: where to edit them, where they end up, and what the
|
||||
host reads.
|
||||
|
||||
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.
|
||||
Reference for this plugin's hook definitions: where to edit them, what apm does with them, and what
|
||||
the host ends up reading.
|
||||
|
||||
## Where to edit
|
||||
|
||||
Author hooks in `plugins/kyberforge/.apm/hooks/*.json`. `apm pack --format plugin` merges every
|
||||
file in that directory into a single `hooks.json`, which `sync-plugin-content.sh` copies to
|
||||
`plugins/kyberforge/hooks/hooks.json` — the path Claude Code convention-scans. Never edit the
|
||||
mirrored file; the `check-plugin-content-sync` pre-push hook reports it as drift.
|
||||
Author hooks in `plugins/kyberforge/.apm/hooks/*.json`. `.apm/` is the only content source and
|
||||
`apm install` is the only supported install path (ADR-0024) — there is no generated mirror at the
|
||||
plugin root and no per-plugin `plugin.json`, so `.apm/hooks/` is both where you edit and what ships.
|
||||
|
||||
## 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
|
||||
{
|
||||
@@ -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
|
||||
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
|
||||
`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
|
||||
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
|
||||
`.apm/hooks/`, dropping every non-JSON file. So the mirror contains `hooks.json` and nothing else.
|
||||
apm prints `Hook script not found: .../hooks/check-apm-current.sh` and then deploys the hook anyway,
|
||||
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.
|
||||
root, and there is no `hooks/` directory there at all — the package's content is `.apm/`. apm prints
|
||||
`Hook script not found: .../hooks/check-apm-current.sh` and then deploys the hook anyway, pointing at
|
||||
a path with no file behind it.
|
||||
|
||||
`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.
|
||||
@@ -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.
|
||||
|
||||
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
|
||||
|
||||
@@ -89,11 +83,11 @@ ADR-0019.
|
||||
**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
|
||||
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`
|
||||
invocations run against the same resolved directory. The earlier spelling checked a bare
|
||||
`apm.lock.yaml` against the session's cwd, so a session opened in a subdirectory of an
|
||||
apm-consuming repo no-opped silently. Keep the cwd fallback: a host that sets no
|
||||
`CLAUDE_PROJECT_DIR` must still get inert-but-harmless behaviour, not an unset-variable error.
|
||||
it inert in any project that does not consume packages through apm. Both `apm` invocations run
|
||||
against the same resolved directory. The earlier spelling checked a bare `apm.lock.yaml` against the
|
||||
session's cwd, so a session opened in a subdirectory of an apm-consuming repo no-opped silently.
|
||||
Keep the cwd fallback: a host that sets no `CLAUDE_PROJECT_DIR` must still get inert-but-harmless
|
||||
behaviour, not an unset-variable error.
|
||||
|
||||
**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
|
||||
@@ -113,74 +107,45 @@ hook.
|
||||
|
||||
## GitHub Copilot CLI
|
||||
|
||||
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`.
|
||||
**Copilot loads no hooks from this plugin.** Two independent reasons, either one sufficient:
|
||||
|
||||
There is no separate Copilot hooks file at this plugin root, and — as things stand — **Copilot
|
||||
resolves to no hooks file at all.** Two corrections to an earlier revision of this document, which
|
||||
got both halves of this wrong:
|
||||
- **Nothing can point Copilot at a hooks file.** Copilot types `hooks` as a `plugin.json` field of
|
||||
type "string or object" with **no default**
|
||||
(`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
|
||||
added in `2287ddc` (2026-06-20), the commit that created the plugin, well before
|
||||
`scripts/sync-plugin-content.sh` existed; `plugins/lint/hooks.json` arrived the same way in
|
||||
`f326df4`. Main's Copilot manifest `plugins/kyberforge/plugin.json` declared `"hooks": "hooks.json"`,
|
||||
and `plugins/lint/plugin.json` did the same — these were deliberately pointed-at Copilot hooks files,
|
||||
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.
|
||||
The second reason is why "just add a pointer" was rejected even while a Copilot manifest existed: a
|
||||
pointer would tell Copilot that a Claude-shaped file is Copilot-shaped, trading an incomplete
|
||||
manifest for a wrong one. ADR-0024 consequence 7 records that the question is now moot — the
|
||||
manifest it argued about is gone — but the schema mismatch it turned on is not, and it is what any
|
||||
future Copilot hooks support has to solve.
|
||||
|
||||
**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
|
||||
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.
|
||||
|
||||
## 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
|
||||
symlinks out of the bundle entirely and says nothing, so the file never reaches the mirror. Since
|
||||
`sync-plugin-content.sh` builds both sides of its drift comparison from that same bundle, the loss
|
||||
used to be invisible to `--check` as well. `check_apm_symlinks()` now reads the `.apm/` source tree
|
||||
directly and fails the sync with the offending path — replace the symlink with a regular file.
|
||||
Do not author any file under `plugins/kyberforge/.apm/` as a symlink. apm's copy path filters
|
||||
symlinks out silently: `ignore_non_content()` in `apm_cli/security/gate.py` is a
|
||||
`shutil.copytree` ignore callback that drops every entry answering `is_symlink()`, commented
|
||||
"Excludes symlinks (security)". The file never reaches the consumer's install, and no warning is
|
||||
printed at any point.
|
||||
|
||||
It stays quiet about one place: `.apm/<category>/<name>/tests/`, the dev-fixture directory the
|
||||
mirror excludes anyway (a symlink there loses nothing, because nothing under it is mirrored). A
|
||||
`tests/` deeper than that — `assets/templates/tests/`, a scaffolding asset the mirror does carry —
|
||||
is reported like anywhere else. See ADR-0017's symlink amendment.
|
||||
**No gate catches this.** The pre-push check that used to read the `.apm/` tree and fail on the
|
||||
offending path was deleted along with the content mirror, and the decision was taken not to replace
|
||||
it (ADR-0024 consequence 7). This document is the only thing standing between a symlink and silent
|
||||
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.
|
||||
|
||||
@@ -141,6 +141,14 @@ run_bats
|
||||
# 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,
|
||||
# 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=()
|
||||
while IFS= read -r script; do
|
||||
SCRIPTS+=("$script")
|
||||
@@ -149,6 +157,7 @@ done < <(
|
||||
-not -path "*/.git/*" \
|
||||
-not -path "*/.claude/worktrees/*" \
|
||||
-not -path "*/apm_modules/*" \
|
||||
-not -path "*/.claude/skills/*" \
|
||||
| sort
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user