diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3dc8388..55185a2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/CONTEXT.md b/CONTEXT.md index 0f4eb93..60b411f 100644 --- a/CONTEXT.md +++ b/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//`, consisting of that plugin's `apm.yml` plus the -hand-authored `plugins//.apm/` tree it deploys from (ADR-0015). -_Avoid_: plugin, package, bundle, module, plugin directory, source tree +servers under a single directory `plugins//`, consisting of that directory's `apm.yml` plus +the hand-authored `plugins//.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//` 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 diff --git a/SIMPLIFICATION-AUDIT.md b/SIMPLIFICATION-AUDIT.md index 7ec05ca..1893365 100644 --- a/SIMPLIFICATION-AUDIT.md +++ b/SIMPLIFICATION-AUDIT.md @@ -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//` 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 `@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 `@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: `. `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. diff --git a/docs/adr/0001-skills-in-agents-dir.md b/docs/adr/0001-skills-in-agents-dir.md index 528d0f6..e70f503 100644 --- a/docs/adr/0001-skills-in-agents-dir.md +++ b/docs/adr/0001-skills-in-agents-dir.md @@ -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//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. --- diff --git a/docs/adr/0006-plugin-version-parity.md b/docs/adr/0006-plugin-version-parity.md index e677929..aeb0127 100644 --- a/docs/adr/0006-plugin-version-parity.md +++ b/docs/adr/0006-plugin-version-parity.md @@ -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. diff --git a/docs/adr/0013-vale-harness-scope-and-rule-sources.md b/docs/adr/0013-vale-harness-scope-and-rule-sources.md index f35932d..1171785 100644 --- a/docs/adr/0013-vale-harness-scope-and-rule-sources.md +++ b/docs/adr/0013-vale-harness-scope-and-rule-sources.md @@ -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` diff --git a/docs/adr/0014-vale-prefilter-ships-from-the-plugin.md b/docs/adr/0014-vale-prefilter-ships-from-the-plugin.md index b93ddcc..bda5050 100644 --- a/docs/adr/0014-vale-prefilter-ships-from-the-plugin.md +++ b/docs/adr/0014-vale-prefilter-ships-from-the-plugin.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 diff --git a/docs/adr/0015-apm-replaces-plugin-marketplace-authoring.md b/docs/adr/0015-apm-replaces-plugin-marketplace-authoring.md index e8e5c52..48e6d42 100644 --- a/docs/adr/0015-apm-replaces-plugin-marketplace-authoring.md +++ b/docs/adr/0015-apm-replaces-plugin-marketplace-authoring.md @@ -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. diff --git a/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md b/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md index f3f42a1..8c869b3 100644 --- a/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md +++ b/docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md @@ -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: diff --git a/docs/adr/0024-apm-is-the-only-supported-install-path.md b/docs/adr/0024-apm-is-the-only-supported-install-path.md index 1f22f5b..708e6a1 100644 --- a/docs/adr/0024-apm-is-the-only-supported-install-path.md +++ b/docs/adr/0024-apm-is-the-only-supported-install-path.md @@ -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//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 `@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 `@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 -`//tests` exclusion). 17 `.bats` files across 6 skills therefore now deploy into +`//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#` 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 diff --git a/docs/spec/gates.md b/docs/spec/gates.md index a4db3d9..fdeb5a8 100644 --- a/docs/spec/gates.md +++ b/docs/spec/gates.md @@ -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 diff --git a/plugins/bin/README.md b/plugins/bin/README.md index ec49fea..036e9a0 100644 --- a/plugins/bin/README.md +++ b/plugins/bin/README.md @@ -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 / -claude plugin install bin@ +```yaml +dependencies: + apm: + - git: git@git.dev.rkdr.net:Defame1297/holocron.git + path: plugins/bin ``` -**GitHub Copilot CLI:** +Then: ```bash -copilot plugin marketplace add / -copilot plugin install bin +apm install ``` -**Local (development):** +The entry above is unpinned and tracks the remote's default branch — add `ref: ` 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 diff --git a/plugins/core/README.md b/plugins/core/README.md index 8f59aa8..b4d09be 100644 --- a/plugins/core/README.md +++ b/plugins/core/README.md @@ -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 / -claude plugin install core@ +```yaml +dependencies: + apm: + - git: git@git.dev.rkdr.net:Defame1297/holocron.git + path: plugins/core ``` -**GitHub Copilot CLI:** +Then: ```bash -copilot plugin marketplace add / -copilot plugin install core +apm install ``` -**Local (development):** +The entry above is unpinned and tracks the remote's default branch — add `ref: ` 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 diff --git a/plugins/git/README.md b/plugins/git/README.md index 8f7a2a3..f56f7ba 100644 --- a/plugins/git/README.md +++ b/plugins/git/README.md @@ -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 / -claude plugin install git@ +```yaml +dependencies: + apm: + - git: git@git.dev.rkdr.net:Defame1297/holocron.git + path: plugins/git ``` -**GitHub Copilot CLI:** +Then: ```bash -copilot plugin marketplace add / -copilot plugin install git +apm install ``` -**Local (development):** +The entry above is unpinned and tracks the remote's default branch — add `ref: ` 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 diff --git a/plugins/kyberforge/README.md b/plugins/kyberforge/README.md index d6c4265..55858a0 100644 --- a/plugins/kyberforge/README.md +++ b/plugins/kyberforge/README.md @@ -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 / -claude plugin install kyberforge@ +```yaml +dependencies: + apm: + - git: git@git.dev.rkdr.net:Defame1297/holocron.git + path: plugins/kyberforge ``` -**GitHub Copilot CLI:** +Then: ```bash -copilot plugin marketplace add / -copilot plugin install kyberforge +apm install ``` -**Local (development):** +The entry above is unpinned and tracks the remote's default branch — add `ref: ` 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 diff --git a/plugins/kyberforge/docs/README.md b/plugins/kyberforge/docs/README.md index 5d83311..87da340 100644 --- a/plugins/kyberforge/docs/README.md +++ b/plugins/kyberforge/docs/README.md @@ -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. diff --git a/plugins/kyberforge/docs/hooks.md b/plugins/kyberforge/docs/hooks.md index 66ddd9a..915ae74 100644 --- a/plugins/kyberforge/docs/hooks.md +++ b/plugins/kyberforge/docs/hooks.md @@ -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#`), 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///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///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. diff --git a/tests/run-tests.sh b/tests/run-tests.sh index 91b7721..9e560c6 100755 --- a/tests/run-tests.sh +++ b/tests/run-tests.sh @@ -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 `/tests/test-*.sh` file under plugins/ would also land +# at .claude/skills//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 )