Compare commits
3 Commits
c3ec5f2d3d
...
3bfdf58960
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bfdf58960 | |||
| dee56c506a | |||
| 2e8732a8e5 |
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "holocron",
|
"name": "holocron",
|
||||||
"description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.",
|
"description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.",
|
||||||
"version": "0.3.4",
|
"version": "0.4.0",
|
||||||
"owner": {
|
"owner": {
|
||||||
"name": "Defame1297",
|
"name": "Defame1297",
|
||||||
"email": "defame1297@rkdr.net",
|
"email": "defame1297@rkdr.net",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kyberforge",
|
"name": "kyberforge",
|
||||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||||
"version": "1.4.1",
|
"version": "1.5.0",
|
||||||
"category": "Developer Tools",
|
"category": "Developer Tools",
|
||||||
"source": "./plugins/kyberforge"
|
"source": "./plugins/kyberforge"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,13 +1,3 @@
|
|||||||
{
|
{
|
||||||
"enabledPlugins": {
|
"hooks": {}
|
||||||
"bin@holocron": true,
|
|
||||||
"core@holocron": true,
|
|
||||||
"git@holocron": true,
|
|
||||||
"gitea@holocron": true,
|
|
||||||
"kyberforge@holocron": true,
|
|
||||||
"lint@holocron": true
|
|
||||||
},
|
|
||||||
"hooks": {
|
|
||||||
"PreToolUse": []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
4
.github/plugin/marketplace.json
vendored
4
.github/plugin/marketplace.json
vendored
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "holocron",
|
"name": "holocron",
|
||||||
"description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.",
|
"description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.",
|
||||||
"version": "0.3.4",
|
"version": "0.4.0",
|
||||||
"owner": {
|
"owner": {
|
||||||
"name": "Defame1297",
|
"name": "Defame1297",
|
||||||
"email": "defame1297@rkdr.net",
|
"email": "defame1297@rkdr.net",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
{
|
{
|
||||||
"name": "kyberforge",
|
"name": "kyberforge",
|
||||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||||
"version": "1.4.1",
|
"version": "1.5.0",
|
||||||
"category": "Developer Tools",
|
"category": "Developer Tools",
|
||||||
"source": "./plugins/kyberforge"
|
"source": "./plugins/kyberforge"
|
||||||
},
|
},
|
||||||
|
|||||||
20
.gitignore
vendored
20
.gitignore
vendored
@@ -24,3 +24,23 @@ node_modules/
|
|||||||
|
|
||||||
# Claude Code local settings (machine-specific)
|
# Claude Code local settings (machine-specific)
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
|
||||||
|
# APM dependencies
|
||||||
|
apm_modules/
|
||||||
|
|
||||||
|
# APM install output — deployed copies of released plugin content, regenerated
|
||||||
|
# by `apm install`. The authoring source is plugins/<name>/.apm/; committing a
|
||||||
|
# deployed copy would add a third mirror of the same skills to drift against.
|
||||||
|
.claude/skills/
|
||||||
|
.claude/agents/
|
||||||
|
|
||||||
|
# APM hook deployment output — `apm install` copies each package's referenced
|
||||||
|
# hook scripts here and tracks its own settings.json entries in the sidecar.
|
||||||
|
# Regenerated on every install; the authoring source is
|
||||||
|
# plugins/<name>/.apm/hooks/ (ADR-0019).
|
||||||
|
.claude/hooks/
|
||||||
|
.claude/apm-hooks.json
|
||||||
|
|
||||||
|
# `apm pack` bundle output. The pre-push gate runs pack with --dry-run, so this
|
||||||
|
# only appears after a bare `apm pack` during a release; it is not repo content.
|
||||||
|
build/
|
||||||
|
|||||||
12
.mcp.json
Normal file
12
.mcp.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"obsidian": {
|
||||||
|
"args": [
|
||||||
|
"@bitbonsai/mcpvault@latest",
|
||||||
|
"docs/"
|
||||||
|
],
|
||||||
|
"command": "npx",
|
||||||
|
"type": "stdio"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
AGENTS.md
25
AGENTS.md
@@ -4,7 +4,7 @@ This repo is the global AI development configuration repository — the authorit
|
|||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- `plugins/` — installable plugin units; each is an apm package (`apm.yml` + `.apm/`) carrying skills, agents, hooks, MCP servers, and bundled assets; install separately via `claude plugin install <name>@holocron`
|
- `plugins/` — installable plugin units; each is an apm package (`apm.yml` + `.apm/`) carrying skills, agents, hooks, MCP servers, and bundled assets. This repo consumes them through **apm**, not Claude Code's native plugin install: root `apm.yml` declares all six as `dependencies.apm` git+path entries against the holocron remote, and `apm install` deploys them into `.claude/skills/` and `.claude/agents/` (both gitignored). External consumers can still install natively via `claude plugin install <name>@holocron` — the marketplace manifests are unchanged
|
||||||
- `providers/claude-code/` — Claude Code adapter (deployed to `~/.claude/` via `install.sh`)
|
- `providers/claude-code/` — Claude Code adapter (deployed to `~/.claude/` via `install.sh`)
|
||||||
|
|
||||||
## Edit `.apm/`, never the flat mirror
|
## Edit `.apm/`, never the flat mirror
|
||||||
@@ -25,29 +25,34 @@ Nothing labels a generated file as generated — `plugins/kyberforge/skills/forg
|
|||||||
|
|
||||||
This repo dogfoods its own plugins. Before shelling out to git, gitea, or lint tooling directly, check whether an installed skill already owns the operation — it usually does:
|
This repo dogfoods its own plugins. Before shelling out to git, gitea, or lint tooling directly, check whether an installed skill already owns the operation — it usually does:
|
||||||
|
|
||||||
- Commits, branches, history, worktrees, remotes → `git:git-commits`, `git:git-branches`, `git:git-history`, `git:git-worktrees`, `git:git-remotes`
|
- Commits, branches, history, worktrees, remotes → `git-commits`, `git-branches`, `git-history`, `git-worktrees`, `git-remotes`
|
||||||
- Pre-commit hook install/config/troubleshooting → `git:pc-run` / `git:pc-author`
|
- Pre-commit hook install/config/troubleshooting → `pc-run` / `pc-author`
|
||||||
- Issues, PRs, labels, milestones → `gitea:gitea-issues`, `gitea:gitea-prs`, `gitea:gitea-labels-milestones`; also `gitea:gitea-branches`, `gitea:gitea-files`, `gitea:gitea-releases`, or `gitea:gitea-workflow` when the domain is ambiguous
|
- Issues, PRs, labels, milestones → `gitea-issues`, `gitea-prs`, `gitea-labels-milestones`; also `gitea-branches`, `gitea-files`, `gitea-releases`, or `gitea-workflow` when the domain is ambiguous
|
||||||
- Vale prose linting → `lint:vale-config` / `lint:vale-run`
|
- Vale prose linting → `vale-config` / `vale-run`
|
||||||
- This repo's own AGENTS.md → `core:agentsmd-author` / `core:agentsmd-audit`
|
- This repo's own AGENTS.md → `agentsmd-author` / `agentsmd-audit`
|
||||||
|
|
||||||
|
Names are **unnamespaced**. Under the old `claude plugin install` these were `git:git-commits`, `kyberforge:skill-audit`, and so on; `apm install` deploys each skill to `.claude/skills/<name>/` as a plain project skill, which has no plugin prefix to carry. The `<plugin>:` form no longer resolves here — it still does in any project that installs holocron natively, so a skill body written for both audiences should name the bare skill. Same for agents: `git-orchestrate`, not `git:git-orchestrate`.
|
||||||
|
|
||||||
Fall back to raw shell only when no skill covers it.
|
Fall back to raw shell only when no skill covers it.
|
||||||
|
|
||||||
## Setup and testing
|
## Setup and testing
|
||||||
|
|
||||||
- Install git hooks via `git:pc-run`, wiring all three stages — this repo's `.pre-commit-config.yaml` has no `default_install_hook_types`, so a plain install silently skips `commit-msg` (Conventional Commits) and `pre-push` (the 13-hook gate described below).
|
- Run `apm install` to deploy this repo's own skills and agents into `.claude/skills/` and `.claude/agents/`. Both are gitignored install output, not authoring source — `plugins/<name>/.apm/` remains the only place to edit. The six dependencies in root `apm.yml` resolve from the holocron **remote**, unpinned against the default branch, so a `.apm/` edit is not visible to the running session until it is pushed and `apm update` re-runs (`apm install` deploys from `apm.lock.yaml` and does not re-resolve refs). Needs the network, and needs `apm_modules/` (which it materializes) left gitignored. `apm install` also configures the `obsidian` MCP server into the repo's `.mcp.json`, carried over from `plugins/bin/.mcp.json`.
|
||||||
- Install the `apm` CLI — four pre-push hooks shell out to it: `apm-marketplace-check`, `apm-audit-ci`, `apm-pack-check-clean`, and `check-plugin-content-sync` (via `scripts/sync-plugin-content.sh`, which wraps `apm pack`). `apm-marketplace-check` and `apm-pack-check-clean` are bare `apm …` hook entries and `apm-audit-ci` is a `bash -c` loop calling `apm` once per package, so without it the push dies with an unhelpful "command not found". Use `kyberforge:apm-install`, or `curl -sSL https://aka.ms/apm-unix | sh`; verify with `apm --version`.
|
- Do not add repo-owned keys to `.claude/settings.json`. apm treats that file as its own deployed artifact: `apm audit --ci` replays the install into a scratch tree and diffs, so anything apm would not have written there — an `enabledPlugins` block, a real `hooks` entry — is permanent drift that fails the `apm-audit-ci` pre-push hook. Its committed content is whatever apm last wrote — `{"hooks": {}}` until kyberforge's `SessionStart` hook lands there, after which the merged hook entry is apm's output and belongs in the commit (ADR-0019). What does not change is that nothing repo-authored goes in the file. A hook you want in this repo is authored in `plugins/<name>/.apm/hooks/` and deployed by apm, never hand-written here. Machine-specific settings go in the gitignored `.claude/settings.local.json`, which apm does not deploy and the replay does not compare; shared enforcement belongs in `.pre-commit-config.yaml`.
|
||||||
|
- Keeping the install current is automatic but not free. Because the six dependencies are unpinned, deployed skills go stale whenever anyone merges. kyberforge ships a `SessionStart` hook that runs `apm outdated` at startup (~0.7s) and, when something is behind, runs `apm update --yes` and asks the host to re-scan skills (~10.4s). That rewrites `apm.lock.yaml`, so an unexplained modification to it after opening a session is expected, not a bug — commit or discard it deliberately. Note `apm install` alone will **not** pick up remote changes; it deploys from the lock. `apm update` is the command that re-resolves refs.
|
||||||
|
- Install git hooks via `pc-run`, wiring all three stages — this repo's `.pre-commit-config.yaml` has no `default_install_hook_types`, so a plain install silently skips `commit-msg` (Conventional Commits) and `pre-push` (the 13-hook gate described below).
|
||||||
|
- Install the `apm` CLI — four pre-push hooks shell out to it: `apm-marketplace-check`, `apm-audit-ci`, `apm-pack-check-clean`, and `check-plugin-content-sync` (via `scripts/sync-plugin-content.sh`, which wraps `apm pack`). `apm-marketplace-check` and `apm-pack-check-clean` are bare `apm …` hook entries and `apm-audit-ci` is a `bash -c` loop calling `apm` once per package, so without it the push dies with an unhelpful "command not found". Use `apm-install`, or `curl -sSL https://aka.ms/apm-unix | sh`; verify with `apm --version`.
|
||||||
- Install `jq` — required by `scripts/check-manifests.sh` and `scripts/sync-plugin-content.sh`, both pre-push. These at least fail loudly (`Error: jq is required but not installed`).
|
- Install `jq` — required by `scripts/check-manifests.sh` and `scripts/sync-plugin-content.sh`, both pre-push. These at least fail loudly (`Error: jq is required but not installed`).
|
||||||
- Install the `vale` binary — required by the `vale-audit-prefilter-skill`/`-agent` pre-commit hooks. Their `files:` patterns are `.apm/`-scoped: `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` and `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$`. Only the authoring source triggers them — a `SKILL.md` in the generated 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. `brew install vale` (macOS), `snap install vale` (Linux), `choco install vale` (Windows), or see https://vale.sh/docs/vale-cli/installation/. No `vale sync` needed — the `Kyberforge` styles are committed under `plugins/kyberforge/.apm/skills/{skill-audit,agent-audit}/assets/vale/styles/`, not downloaded packages (see ADR-0014).
|
- Install the `vale` binary — required by the `vale-audit-prefilter-skill`/`-agent` pre-commit hooks. Their `files:` patterns are `.apm/`-scoped: `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$` and `^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$`. Only the authoring source triggers them — a `SKILL.md` in the generated 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. `brew install vale` (macOS), `snap install vale` (Linux), `choco install vale` (Windows), or see https://vale.sh/docs/vale-cli/installation/. No `vale sync` needed — the `Kyberforge` styles are committed under `plugins/kyberforge/.apm/skills/{skill-audit,agent-audit}/assets/vale/styles/`, not downloaded packages (see ADR-0014).
|
||||||
- `vale` is also a **pre-push** dependency, not only pre-commit. `check-vale-style-sync` runs six glob-coverage probes by invoking `vale --config` — they are the only assertions in it that catch a `.vale.ini` glob typo, the failure mode where every text-level check stays clean while vale lints zero files. Missing `vale` is therefore a hard failure there. The opt-out is `CHECK_VALE_STYLE_SYNC_ALLOW_MISSING_VALE=1`, and it is **not** `SKIP=`: the hook still runs and still asserts everything verifiable from file text, but the six probes do not, and its summary says so explicitly — `Vale style sync check passed (text-level only, vale unavailable): … 0 glob probe(s) verified`. Use it only on a machine that genuinely cannot install `vale`, and read that summary line as "the glob axis was not checked", not as a pass.
|
- `vale` is also a **pre-push** dependency, not only pre-commit. `check-vale-style-sync` runs six glob-coverage probes by invoking `vale --config` — they are the only assertions in it that catch a `.vale.ini` glob typo, the failure mode where every text-level check stays clean while vale lints zero files. Missing `vale` is therefore a hard failure there. The opt-out is `CHECK_VALE_STYLE_SYNC_ALLOW_MISSING_VALE=1`, and it is **not** `SKIP=`: the hook still runs and still asserts everything verifiable from file text, but the six probes do not, and its summary says so explicitly — `Vale style sync check passed (text-level only, vale unavailable): … 0 glob probe(s) verified`. Use it only on a machine that genuinely cannot install `vale`, and read that summary line as "the glob axis was not checked", not as a pass.
|
||||||
- Run `bash tests/run-tests.sh` before considering any change done — it runs every `test-*.sh` script in the repo plus the bats suite (`--bats-only` for just bats). First run auto-initializes the bats submodules; no manual `git submodule update` needed.
|
- Run `bash tests/run-tests.sh` before considering any change done — it runs every `test-*.sh` script in the repo plus the bats suite (`--bats-only` for just bats). First run auto-initializes the bats submodules; no manual `git submodule update` needed.
|
||||||
- A suite that exits 77 because a dependency is missing is reported as SKIPPED, and does **not** fail an ad-hoc run. The pre-push hook invokes the same script as `--strict` (`RUN_TESTS_STRICT=1` is equivalent), where a skip **does** fail the push: at pre-push a skip means one of the dependencies above is absent on this machine, so the gate would otherwise report success having run fewer suites than it appears to. Without vale, for instance, three suites skip (`test-check-vale-style-sync.sh`, `test-vale-hooks-consumer.sh`, `test-vale-wrap.sh`) and the strict failure names each one and what to install.
|
- A suite that exits 77 because a dependency is missing is reported as SKIPPED, and does **not** fail an ad-hoc run. The pre-push hook invokes the same script as `--strict` (`RUN_TESTS_STRICT=1` is equivalent), where a skip **does** fail the push: at pre-push a skip means one of the dependencies above is absent on this machine, so the gate would otherwise report success having run fewer suites than it appears to. Without vale, for instance, three suites skip (`test-check-vale-style-sync.sh`, `test-vale-hooks-consumer.sh`, `test-vale-wrap.sh`) and the strict failure names each one and what to install.
|
||||||
- `tests/run-bats.sh` derives the set of `.bats` files it expects from `git ls-files`, so a `.bats` file deleted from the worktree but still tracked in the index fails the run rather than silently shrinking the suite. Remove one with `git rm` (or stage the deletion) when the removal is intentional; an untracked new `.bats` file is picked up and needs no ceremony.
|
- `tests/run-bats.sh` derives the set of `.bats` files it expects from `git ls-files`, so a `.bats` file deleted from the worktree but still tracked in the index fails the run rather than silently shrinking the suite. Remove one with `git rm` (or stage the deletion) when the removal is intentional; an untracked new `.bats` file is picked up and needs no ceremony. Both discovery walks (`tests/run-bats.sh` and `tests/run-tests.sh`) exclude `apm_modules/`: `apm install` materializes a full copy of every plugin there, and running a dependency's copy of a `.bats` file breaks its relative path to the bats helpers — 167 spurious failures before the exclusion landed.
|
||||||
- Pushing runs 13 repo-defined pre-push hooks, not just the test suite — `run-tests` and `check-manifests`, plus generated-content drift gates (`check-plugin-content-sync`, `check-marketplace-mirror-sync`, `check-vale-style-sync`, `check-scope-walkup-sync`), artifact validators (`check-apm-agents-valid`, which runs agent-audit's `validate.sh` over every real `plugins/*/.apm/agents/*.agent.md`), apm's own gates (`apm-marketplace-check`, `apm-audit-ci`, `apm-pack-check-clean`), host validators (`validate-plugins`, `validate-marketplace`, both needing the `claude` CLI), and `check-release-needed`. Run `pre-commit run --hook-stage pre-push --all-files` locally — one command, the whole gate. That command reports **15**, not 13: pre-commit's own `meta` hooks, `check-hooks-apply` and `check-useless-excludes`, declare no `stages:` and so run at every stage including this one.
|
- Pushing runs 13 repo-defined pre-push hooks, not just the test suite — `run-tests` and `check-manifests`, plus generated-content drift gates (`check-plugin-content-sync`, `check-marketplace-mirror-sync`, `check-vale-style-sync`, `check-scope-walkup-sync`), artifact validators (`check-apm-agents-valid`, which runs agent-audit's `validate.sh` over every real `plugins/*/.apm/agents/*.agent.md`), apm's own gates (`apm-marketplace-check`, `apm-audit-ci`, `apm-pack-check-clean`), host validators (`validate-plugins`, `validate-marketplace`, both needing the `claude` CLI), and `check-release-needed`. Run `pre-commit run --hook-stage pre-push --all-files` locally — one command, the whole gate. That command reports **15**, not 13: pre-commit's own `meta` hooks, `check-hooks-apply` and `check-useless-excludes`, declare no `stages:` and so run at every stage including this one.
|
||||||
- `apm-audit-ci` runs `apm audit --ci` once per manifest — the root one and each of the six plugin packages — because the root-only invocation audits the marketplace manifest and **nothing else**, and `apm-pack-check-clean` does not parse plugin `dependencies:` blocks either (verified: a malformed one passes `apm pack --check-versions --check-clean --dry-run` and fails `apm audit --ci` in that package's directory). It verifies two things and claims no more: each `apm.yml` parses as a valid APM manifest, and any package declaring dependencies has a consistent `apm.lock.yaml`. It does **not** enforce an org policy — apm discovers one from the git remote and only understands github.com and Azure DevOps, so against this repo's self-hosted Gitea remote it prints `No org policy found at unknown; enforcement skipped`. Do **not** "fix" that with `policy.fetch_failure_default: block` in `apm.yml`: it was tested and rejected, because with no reachable policy source it makes the hook exit 1 on every push forever.
|
- `apm-audit-ci` runs `apm audit --ci` once per manifest — the root one and each of the six plugin packages — because the root-only invocation audits the marketplace manifest and **nothing else**, and `apm-pack-check-clean` does not parse plugin `dependencies:` blocks either (verified: a malformed one passes `apm pack --check-versions --check-clean --dry-run` and fails `apm audit --ci` in that package's directory). It verifies two things and claims no more: each `apm.yml` parses as a valid APM manifest, and any package declaring dependencies has a consistent `apm.lock.yaml`. It does **not** enforce an org policy — apm discovers one from the git remote and only understands github.com and Azure DevOps, so against this repo's self-hosted Gitea remote it prints `No org policy found at unknown; enforcement skipped`. Do **not** "fix" that with `policy.fetch_failure_default: block` in `apm.yml`: it was tested and rejected, because with no reachable policy source it makes the hook exit 1 on every push forever.
|
||||||
- `check-apm-agents-valid` derives its expected agent-file set from `git ls-files` (same pattern as `tests/run-bats.sh`), so an agent file deleted from the worktree but still tracked fails the run, and discovering zero agent files is an error rather than a pass. An untracked new agent file is still validated — the derivation is one-directional on purpose, so uncommitted work is not blocked but also cannot bypass the gate.
|
- `check-apm-agents-valid` derives its expected agent-file set from `git ls-files` (same pattern as `tests/run-bats.sh`), so an agent file deleted from the worktree but still tracked fails the run, and discovering zero agent files is an error rather than a pass. An untracked new agent file is still validated — the derivation is one-directional on purpose, so uncommitted work is not blocked but also cannot bypass the gate.
|
||||||
- **Two** pre-push hooks need the network, for one shared reason: root `apm.yml`'s `marketplace.packages[]` contains exactly one remote entry (`mattpocock-skills`, `source: mattpocock/skills`), and resolving it needs a `git ls-remote`. `apm-marketplace-check` resolves every entry and is `always_run`, so it fails with `No cached refs (offline)`. `apm-pack-check-clean` (`apm pack --check-versions --check-clean --dry-run`) re-resolves the same entry and fails with `Error: Git network timeout during ls-remote`. Pinning the entry to an exact version does **not** remove the call — an exact pin still ls-remotes. `--offline` rescues neither. To push without a network, skip both using pre-commit's own mechanism: `SKIP=apm-marketplace-check,apm-pack-check-clean git push`. Skip those two alone — verified under `unshare -rn`, the other eleven pre-push hooks pass offline because they are real local checks, and adding one of them to `SKIP` disarms it silently. `apm-audit-ci` calls `apm` too but stays local: its org-policy discovery resolves nothing on this remote before any network call, so it does not join the pair above.
|
- **Two** pre-push hooks need the network, for one shared reason: root `apm.yml`'s `marketplace.packages[]` contains exactly one remote entry (`mattpocock-skills`, `source: mattpocock/skills`), and resolving it needs a `git ls-remote`. `apm-marketplace-check` resolves every entry and is `always_run`, so it fails with `No cached refs (offline)`. `apm-pack-check-clean` (`apm pack --check-versions --check-clean --dry-run`) re-resolves the same entry and fails with `Error: Git network timeout during ls-remote`. Pinning the entry to an exact version does **not** remove the call — an exact pin still ls-remotes. `--offline` rescues neither. To push without a network, skip both using pre-commit's own mechanism: `SKIP=apm-marketplace-check,apm-pack-check-clean git push`. Skip those two alone — verified under `unshare -rn`, the other eleven pre-push hooks pass offline because they are real local checks, and adding one of them to `SKIP` disarms it silently. `apm-audit-ci` calls `apm` too but stays local: its org-policy discovery resolves nothing on this remote before any network call, so it does not join the pair above.
|
||||||
- Author commits with `git:git-commits` — it validates Conventional Commits (enforced at `commit-msg`) for you.
|
- Author commits with `git-commits` — it validates Conventional Commits (enforced at `commit-msg`) for you.
|
||||||
|
|
||||||
## Key documents
|
## Key documents
|
||||||
|
|
||||||
|
|||||||
15
CONTEXT.md
15
CONTEXT.md
@@ -24,14 +24,25 @@ Before answering any design or architecture question, check for existing decisio
|
|||||||
A separate product (separate repo) for browsing, editing, and configuring AI development configs through a proper product UI. Git is the persistence layer, invisible to the user. The app is repo-agnostic — it works with any git repo that follows these conventions. This repo is the canonical default content (the official starter). See `docs/VISION.md` for the phased roadmap.
|
A separate product (separate repo) for browsing, editing, and configuring AI development configs through a proper product UI. Git is the persistence layer, invisible to the user. The app is repo-agnostic — it works with any git repo that follows these conventions. This repo is the canonical default content (the official starter). See `docs/VISION.md` for the phased roadmap.
|
||||||
|
|
||||||
### Skills
|
### Skills
|
||||||
Reusable slash commands for AI coding tools, defined as `SKILL.md` files following the [Agent Skills open standard](https://agentskills.io). Deployed via plugin — `plugins/<plugin-name>/.apm/skills/<skill-name>/SKILL.md`, available after the plugin is installed (`claude plugin install <name>@<marketplace>`). Skills are self-contained — they cannot reference files outside the plugin directory after install-time caching.
|
Reusable slash commands for AI coding tools, defined as `SKILL.md` files following the [Agent Skills open standard](https://agentskills.io). Authored at `plugins/<plugin-name>/.apm/skills/<skill-name>/SKILL.md` and reaching a host by one of two install paths: `apm install`, which deploys the skill directory to `.claude/skills/<skill-name>/` (this repo's own path — see "apm-consumed install"), or `claude plugin install <name>@<marketplace>`, which caches the whole plugin (still supported for external consumers). Skills are self-contained — they cannot reference files outside the plugin directory after install-time caching. The two paths name skills differently: apm deploys a plain project skill (`skill-audit`), a plugin install namespaces it (`kyberforge:skill-audit`).
|
||||||
|
|
||||||
### Plugin
|
### Plugin
|
||||||
The deployable unit in the plugin marketplace. A plugin bundles one or more skills, agents, hooks, prompts, MCP servers, and optionally a `bin/` directory into a single installable directory. In this repo, plugins live under `plugins/<name>/`, each with its own `apm.yml` + `.apm/{skills,agents,hooks,...}` — this is the authoring source of truth for the plugin's content (ADR-0015). Two categories of tracked output are compiled from that source, never hand-edited: `.claude-plugin/plugin.json` (Claude Code) and `.github/plugin/plugin.json` (Copilot CLI) via `apm pack`/`apm compile`; and, alongside them, a flat `agents/`, `skills/`, `commands/`, `instructions/`, `extensions/` directory mirror at the plugin root plus a merged hooks file at `hooks/hooks.json`, generated by `scripts/sync-plugin-content.sh` — Claude Code's and Copilot's installers convention-scan only these flat paths (`hooks/hooks.json` is the convention path for hooks specifically; a root-level `hooks.json` is scanned by nothing and is deleted as stale by a sync — see ADR-0017's 2026-08-14 amendment) and have no awareness of `.apm/` nesting at all, so this mirror is what actually makes `.apm/` content discoverable at install time (ADR-0017). Plugins are copied to a cache on install — they cannot reference files outside their own directory. Install a plugin with `claude plugin install <name>@<marketplace>`.
|
The deployable unit in the plugin marketplace. A plugin bundles one or more skills, agents, hooks, prompts, MCP servers, and optionally a `bin/` directory into a single installable directory. In this repo, plugins live under `plugins/<name>/`, each with its own `apm.yml` + `.apm/{skills,agents,hooks,...}` — this is the authoring source of truth for the plugin's content (ADR-0015). Two categories of tracked output are compiled from that source, never hand-edited: `.claude-plugin/plugin.json` (Claude Code) and `.github/plugin/plugin.json` (Copilot CLI) via `apm pack`/`apm compile`; and, alongside them, a flat `agents/`, `skills/`, `commands/`, `instructions/`, `extensions/` directory mirror at the plugin root plus a merged hooks file at `hooks/hooks.json`, generated by `scripts/sync-plugin-content.sh` — Claude Code's and Copilot's installers convention-scan only these flat paths (`hooks/hooks.json` is the convention path for hooks specifically; a root-level `hooks.json` is scanned by nothing and is deleted as stale by a sync — see ADR-0017's 2026-08-14 amendment) and have no awareness of `.apm/` nesting at all, so this mirror is what actually makes `.apm/` content discoverable at install time (ADR-0017). Plugins are copied to a cache on install — they cannot reference files outside their own directory. Install a plugin with `claude plugin install <name>@<marketplace>`, or consume it as an apm dependency (see "apm-consumed install").
|
||||||
|
|
||||||
### Plugin marketplace
|
### Plugin marketplace
|
||||||
A Git repository with a `marketplace.json` manifest listing installable plugins. No backend, registry, or SaaS required — the Git repo is the marketplace. This repo is the `holocron` marketplace. The manifest at `.claude-plugin/marketplace.json` (read by both Claude Code and Copilot CLI) is **compiled output** of `apm pack`, generated from the root `apm.yml`'s `marketplace:` block (owner, build/output config, versioning strategy, and the `packages:` list of installable plugins) — it is not hand-edited. See ADR-0015. `.github/plugin/marketplace.json` is Copilot CLI's legacy manifest path; apm has no output profile for it (only `claude` and `codex`, and `codex`'s is a differently-shaped file at `.agents/plugins/marketplace.json`), so `scripts/sync-marketplace-mirror.sh` keeps it byte-identical to `.claude-plugin/marketplace.json`, checked at pre-push. Each listed package's `source:` still points at that plugin's own `plugins/<name>/` root, not at an `apm pack` build artifact — which is why that root also carries the flat `agents/`/`skills/`/`commands/`/`hooks/hooks.json` content mirror described under "Plugin" (ADR-0017): without it, an install from this marketplace finds a valid manifest but no discoverable content.
|
A Git repository with a `marketplace.json` manifest listing installable plugins. No backend, registry, or SaaS required — the Git repo is the marketplace. This repo is the `holocron` marketplace. The manifest at `.claude-plugin/marketplace.json` (read by both Claude Code and Copilot CLI) is **compiled output** of `apm pack`, generated from the root `apm.yml`'s `marketplace:` block (owner, build/output config, versioning strategy, and the `packages:` list of installable plugins) — it is not hand-edited. See ADR-0015. `.github/plugin/marketplace.json` is Copilot CLI's legacy manifest path; apm has no output profile for it (only `claude` and `codex`, and `codex`'s is a differently-shaped file at `.agents/plugins/marketplace.json`), so `scripts/sync-marketplace-mirror.sh` keeps it byte-identical to `.claude-plugin/marketplace.json`, checked at pre-push. Each listed package's `source:` still points at that plugin's own `plugins/<name>/` root, not at an `apm pack` build artifact — which is why that root also carries the flat `agents/`/`skills/`/`commands/`/`hooks/hooks.json` content mirror described under "Plugin" (ADR-0017): without it, an install from this marketplace finds a valid manifest but no discoverable content.
|
||||||
|
|
||||||
|
### apm-consumed install
|
||||||
|
How this repo installs its own plugins, as of 2026-08-14: not `claude plugin install <name>@holocron`, but six `dependencies.apm` entries in the root `apm.yml`, each a `git:`/`path:` object against the holocron remote, deployed by `apm install` into `.claude/skills/` and `.claude/agents/`. Project scope only — nothing is installed at user scope, so the switch is contained to this repo and any other repo opts in by declaring its own dependencies. The git+path object form is deliberate over the shorter `<name>@holocron` marketplace alias: an alias must first be registered with `apm marketplace add`, which writes to `~/.apm/marketplaces.json` (user scope, outside the repo), whereas the object form needs nothing beyond the committed manifest and so survives a fresh clone.
|
||||||
|
|
||||||
|
Four consequences, each load-bearing:
|
||||||
|
- **Skills lose their namespace.** apm deploys plain project skills, so `git:git-commits` is now `git-commits`. The `<plugin>:` form does not resolve in this repo. It still does wherever holocron is installed natively, so cross-audience skill bodies should use the bare name.
|
||||||
|
- **apm owns `.claude/settings.json`.** `apm audit --ci` (an `apm-audit-ci` pre-push hook) replays the install into a scratch tree and diffs it against the worktree, so any key apm would not have written is permanent drift. Committed content is exactly `{"hooks": {}}`; repo-owned settings have nowhere to live in that file.
|
||||||
|
- **Install output is gitignored.** `.claude/skills/`, `.claude/agents/`, and `apm_modules/` are all regenerated by `apm install`. `apm.lock.yaml` and the generated `.mcp.json` are committed. Committing the deployed skills would add a third mirror of the same content to the two ADR-0017 already governs.
|
||||||
|
- **Test discovery must skip `apm_modules/`.** It holds a full copy of every plugin, `.bats` files included; both `tests/run-bats.sh` and `tests/run-tests.sh` exclude it.
|
||||||
|
|
||||||
|
Dependencies are unpinned against the default branch, matching the `autoUpdate: true` the native marketplace install had. The practical cost is a round trip: an edit to `plugins/<name>/.apm/` is invisible locally until it is pushed and `apm install` re-runs, because the dependency resolves from the remote rather than from the working tree beside it.
|
||||||
|
|
||||||
### HITL (human-in-the-loop)
|
### HITL (human-in-the-loop)
|
||||||
Agent pauses before a consequential action; human approves before execution. Required for irreversible or high-stakes actions (architecture changes, production deployments, security configuration). The agent drafts the change plan and waits — it does not proceed autonomously. Contrast with HOTL.
|
Agent pauses before a consequential action; human approves before execution. Required for irreversible or high-stakes actions (architecture changes, production deployments, security configuration). The agent drafts the change plan and waits — it does not proceed autonomously. Contrast with HOTL.
|
||||||
|
|
||||||
|
|||||||
2702
apm.lock.yaml
Normal file
2702
apm.lock.yaml
Normal file
File diff suppressed because it is too large
Load Diff
50
apm.yml
50
apm.yml
@@ -1,14 +1,58 @@
|
|||||||
name: holocron
|
name: holocron
|
||||||
version: 0.3.4
|
version: 0.4.0
|
||||||
description: AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.
|
description: AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.
|
||||||
license: MIT
|
license: MIT
|
||||||
|
|
||||||
|
# Consumer side: this repo installs its own published plugins from the holocron
|
||||||
|
# remote, so the working copy runs the same released content every other
|
||||||
|
# consumer gets. Addressed as git+path objects rather than <name>@holocron
|
||||||
|
# marketplace aliases — an alias needs a `apm marketplace add` registration in
|
||||||
|
# ~/.apm/marketplaces.json (user scope, outside this repo), the object form
|
||||||
|
# needs nothing beyond this manifest.
|
||||||
|
# Unpinned (default branch) on purpose: parity with the Claude Code plugin
|
||||||
|
# install this replaced, which ran autoUpdate against main. Add `ref: <tag>`
|
||||||
|
# per entry to pin.
|
||||||
|
targets:
|
||||||
|
- claude
|
||||||
|
dependencies:
|
||||||
|
apm:
|
||||||
|
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||||
|
path: plugins/bin
|
||||||
|
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||||
|
path: plugins/core
|
||||||
|
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||||
|
path: plugins/git
|
||||||
|
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||||
|
path: plugins/gitea
|
||||||
|
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||||
|
path: plugins/kyberforge
|
||||||
|
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||||
|
path: plugins/lint
|
||||||
|
mcp: []
|
||||||
|
|
||||||
|
# Turns apm's executable-trust gate ON. Without this block the gate is disabled
|
||||||
|
# and every hook, bin and MCP primitive a dependency ships deploys silently —
|
||||||
|
# verified: `apm approve --list` reports "Executable-trust gate disabled -- all
|
||||||
|
# executables deploy" until an `executables:` block exists.
|
||||||
|
#
|
||||||
|
# kyberforge ships the SessionStart hook that keeps this install level with the
|
||||||
|
# remote (ADR-0019). The key is version-pinned by apm's own design, so a
|
||||||
|
# kyberforge version bump makes this entry stop matching and the hook stops
|
||||||
|
# deploying until the version here is bumped too. If skills silently go stale
|
||||||
|
# after a kyberforge release, check this first.
|
||||||
|
executables:
|
||||||
|
allow:
|
||||||
|
kyberforge#1.5.0:
|
||||||
|
hooks: true
|
||||||
|
bin: true
|
||||||
|
|
||||||
marketplace:
|
marketplace:
|
||||||
# apm's Claude marketplace mapper only emits description:/version: into the
|
# apm's Claude marketplace mapper only emits description:/version: into the
|
||||||
# compiled marketplace.json when set explicitly here (an override) — the
|
# compiled marketplace.json when set explicitly here (an override) — the
|
||||||
# top-level apm.yml description:/version: above are NOT inherited into the
|
# top-level apm.yml description:/version: above are NOT inherited into the
|
||||||
# compiled output despite being used elsewhere (e.g. by `apm audit`).
|
# compiled output despite being used elsewhere (e.g. by `apm audit`).
|
||||||
description: AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.
|
description: AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.
|
||||||
version: 0.3.4
|
version: 0.4.0
|
||||||
owner:
|
owner:
|
||||||
name: Defame1297
|
name: Defame1297
|
||||||
email: defame1297@rkdr.net
|
email: defame1297@rkdr.net
|
||||||
@@ -35,7 +79,7 @@ marketplace:
|
|||||||
- name: kyberforge
|
- name: kyberforge
|
||||||
description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.
|
description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.
|
||||||
source: ./plugins/kyberforge
|
source: ./plugins/kyberforge
|
||||||
version: 1.4.1
|
version: 1.5.0
|
||||||
category: Developer Tools
|
category: Developer Tools
|
||||||
|
|
||||||
- name: bin
|
- name: bin
|
||||||
|
|||||||
121
docs/adr/0018-repo-consumes-its-own-plugins-through-apm.md
Normal file
121
docs/adr/0018-repo-consumes-its-own-plugins-through-apm.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# This repo installs its own plugins through apm, not Claude Code's native plugin install
|
||||||
|
|
||||||
|
ADR-0015 moved plugin **authoring** to apm; ADR-0017 added the flat content mirror that keeps the
|
||||||
|
authored `.apm/` tree discoverable by hosts that install natively. Both are about producing the
|
||||||
|
marketplace. This ADR is about consuming it: how the plugins get onto the machine this repo is
|
||||||
|
worked on.
|
||||||
|
|
||||||
|
**Status: executed (2026-08-14).** All six packages are installed into `/root/ai-development` by
|
||||||
|
`apm install`; the six native project-scope installs (`claude plugin uninstall <name>@holocron
|
||||||
|
--scope project`) are gone and `.claude/settings.json`'s `enabledPlugins` block is empty.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Until now the repo consumed its own output the same way any user would: `claude plugin install
|
||||||
|
<name>@holocron`, six plugins enabled per-project in `.claude/settings.json`, the `holocron`
|
||||||
|
marketplace registered in `~/.claude/plugins/known_marketplaces.json` with `autoUpdate: true`.
|
||||||
|
That worked. It also meant the repo's dogfooding stopped one layer short of the tooling it
|
||||||
|
publishes: `kyberforge` ships `apm-workflow` and `apm-install` skills describing an install path
|
||||||
|
the repo itself did not take.
|
||||||
|
|
||||||
|
apm supports both scopes. `apm install --global` deploys to `~/.claude/`; plain `apm install`
|
||||||
|
deploys to the project. Global was rejected deliberately — the switch should be provable in one
|
||||||
|
repo before it changes how every other project on the machine resolves its skills.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Root `apm.yml` declares all six packages under `dependencies.apm`, each as a `git:`/`path:` object
|
||||||
|
against the holocron remote:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
apm:
|
||||||
|
- git: git@git.dev.rkdr.net:Defame1297/holocron.git
|
||||||
|
path: plugins/core
|
||||||
|
```
|
||||||
|
|
||||||
|
`apm install` deploys them to `.claude/skills/<name>/` and `.claude/agents/<name>.md`.
|
||||||
|
|
||||||
|
Three sub-decisions inside that:
|
||||||
|
|
||||||
|
- **Object form over the `<name>@holocron` marketplace alias.** The alias is shorter and apm
|
||||||
|
resolves it correctly (verified end-to-end against this remote), but it first requires
|
||||||
|
`apm marketplace add`, which writes to `~/.apm/marketplaces.json` — user scope, outside the
|
||||||
|
repo, and absent on a fresh clone. The object form needs nothing beyond the committed manifest.
|
||||||
|
- **Remote source over local path.** apm accepts `path: /root/ai-development/plugins/<name>` as a
|
||||||
|
local dependency, which would make the working tree live instantly. Rejected: it erases the
|
||||||
|
distinction between editing a skill and shipping one, which is the entire point of having a
|
||||||
|
marketplace. The remote form keeps the repo running the same released content every other
|
||||||
|
consumer gets.
|
||||||
|
- **Unpinned against the default branch.** Parity with the `autoUpdate: true` the native install
|
||||||
|
had. apm warns on every install (`6 dependencies unpinned`); accepted knowingly. Pinning is a
|
||||||
|
per-entry `ref:` away once the repo tags releases per package — today `git tag` lists one tag
|
||||||
|
total, so there is nothing meaningful to pin to.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Skills lose their namespace.** apm deploys plain project skills, so `git:git-commits` is now
|
||||||
|
`git-commits` and `kyberforge:skill-audit` is `skill-audit`. This is not configurable — a project
|
||||||
|
skill has no plugin to prefix. Every `<plugin>:<skill>` reference in the repo's own instructions
|
||||||
|
was stale the moment the switch landed; `AGENTS.md` and `CONTEXT.md` are updated. The namespaced
|
||||||
|
form still resolves for anyone installing holocron natively, so skill bodies written for both
|
||||||
|
audiences should name the bare skill.
|
||||||
|
|
||||||
|
**apm owns `.claude/settings.json`.** (ADR-0019 supersedes the "exactly `{"hooks": {}}`" claim
|
||||||
|
below — once a package ships a hook, apm merges it into that file and the merged entry is apm's own
|
||||||
|
output. The rule that nothing repo-authored goes in the file is unchanged.) `apm audit --ci` replays the install into a scratch tree and
|
||||||
|
diffs it against the worktree. apm's hook integrator writes that file, so the replay expects
|
||||||
|
exactly what apm would have written — `{"hooks": {}}` — and any repo-owned key in it is permanent
|
||||||
|
drift that fails the `apm-audit-ci` pre-push hook. Verified both directions: with the pre-existing
|
||||||
|
`enabledPlugins` block present, `1 of 10 check(s) failed`; reduced to `{"hooks": {}}`,
|
||||||
|
`All 10 check(s) passed`. Nothing was lost in that reduction — `enabledPlugins` was empty after the
|
||||||
|
native uninstall and the only `hooks` entry was an empty `PreToolUse: []` — but it does mean the
|
||||||
|
file is no longer available for repo-owned settings. Machine-specific settings go in the gitignored
|
||||||
|
`.claude/settings.local.json`, which apm does not deploy; shared enforcement belongs in
|
||||||
|
`.pre-commit-config.yaml`, where this repo already keeps it.
|
||||||
|
|
||||||
|
**`apm_modules/` breaks naive tree walks.** apm materializes a full copy of every dependency there
|
||||||
|
— including this repo's own plugins, `.bats` files and all. The dependency copies resolve their
|
||||||
|
bats helpers relative to their own root, not this repo's, so `tests/run-tests.sh` went from 167
|
||||||
|
tests passing to `334 tests, 167 failures` on the first install. Both discovery walks
|
||||||
|
(`tests/run-bats.sh`, `tests/run-tests.sh`) now exclude `apm_modules/`, on the find side and on the
|
||||||
|
`git ls-files` side that derives the expected set. Any future script that walks the repo tree needs
|
||||||
|
the same exclusion.
|
||||||
|
|
||||||
|
**Install output is gitignored; the lockfile is not.** `.claude/skills/`, `.claude/agents/`, and
|
||||||
|
`apm_modules/` are regenerated by `apm install`. Committing the deployed skills would add a third
|
||||||
|
mirror of content ADR-0017 already governs two copies of. `apm.lock.yaml` is committed — it is what
|
||||||
|
makes the install reproducible, and `apm audit --ci` checks it.
|
||||||
|
|
||||||
|
**MCP survived the switch; hooks were never at risk.** apm read `plugins/bin/.mcp.json` as a
|
||||||
|
self-defined direct-dependency MCP server and configured `obsidian` into the repo's `.mcp.json`
|
||||||
|
unprompted. The `gitea` and `context7` servers were never plugin-provided — they live in
|
||||||
|
`~/.claude.json` and are untouched. Every plugin's `.apm/hooks/hooks.json` is `{"hooks": {}}`, so
|
||||||
|
apm's "contributed no entries to claude settings; skipped" warning on `kyberforge` and `lint` is
|
||||||
|
accurate and harmless.
|
||||||
|
|
||||||
|
**A `.apm/` edit now needs a round trip.** The dependency resolves from the remote, so an edit is
|
||||||
|
invisible to the running session until it is pushed and the install is refreshed. Under the native
|
||||||
|
install with `autoUpdate` the shape was the same; it was more noticeable here at first because the
|
||||||
|
refresh is a manual step where marketplace auto-update was not — ADR-0019 automates it at
|
||||||
|
`SessionStart`.
|
||||||
|
|
||||||
|
**Correction (2026-08-14): the refresh command is `apm update`, not `apm install`.** An earlier
|
||||||
|
revision of this paragraph named `apm install`, which is wrong: `apm install` deploys from the
|
||||||
|
pinned `resolved_commit` in `apm.lock.yaml` and does not re-resolve refs (`apm install --force`
|
||||||
|
documents this explicitly — "does NOT refresh refs; use 'apm update' for that"). Running it after a
|
||||||
|
merge redeploys the same content and reports success.
|
||||||
|
|
||||||
|
**User scope is untouched, deliberately.** `bin@holocron`, `gitea@holocron`, and a stale
|
||||||
|
`hello-world@holocron` remain natively installed at user scope, and every project other than this
|
||||||
|
one still resolves its skills that way. Converting them is a separate decision with a blast radius
|
||||||
|
beyond this repo.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **`apm install --global`.** Verified working in an isolated `HOME`: user-scope deploys land in
|
||||||
|
`~/.claude/skills/` and `~/.claude/agents/`, and it is the only scope where a plugin's `bin/`
|
||||||
|
executables deploy (moot here — every `bin/` in this repo is empty but for a README). Deferred,
|
||||||
|
not rejected: it changes skill resolution for every project on the machine at once.
|
||||||
|
- **Keeping both install paths.** Rejected: the same skill would be present twice under two names,
|
||||||
|
and `.claude/settings.json` cannot hold `enabledPlugins` without failing `apm audit --ci`.
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
# A SessionStart hook keeps the apm install current, replacing a git hook that never ran
|
||||||
|
|
||||||
|
ADR-0018 switched this repo to consuming its own plugins through `apm install`, with the six
|
||||||
|
packages declared as unpinned git refs against the holocron remote's default branch. That decision
|
||||||
|
left a hole it named but did not fill: the deployed content goes stale the moment anyone merges,
|
||||||
|
and nothing detects it.
|
||||||
|
|
||||||
|
**Status: accepted (2026-08-14).**
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The pre-existing answer was `scripts/git-hooks/post-push`, which pulled the marketplace clone and
|
||||||
|
ran `claude plugin update kyberforge`. Issue #78 filed it as a bug — the hook updated `kyberforge`
|
||||||
|
but not `gitea`, so gitea skills stayed pinned at a pre-refactor version after #67 merged.
|
||||||
|
|
||||||
|
The issue's premise was wrong in a way nobody had noticed for six weeks. **Git has no client-side
|
||||||
|
`post-push` hook.** `githooks(5)` does not list one, and git 2.39.5 does not invoke one.
|
||||||
|
`scripts/install.sh` copies every file in `scripts/git-hooks/` into `.git/hooks/`, so
|
||||||
|
`.git/hooks/post-push` existed on disk and looked installed. It had never fired. The hook did not
|
||||||
|
skip `gitea`; it skipped everything. Both tests that appeared to cover it — `test-post-push.sh` and
|
||||||
|
`test-git-hooks-install.sh` — asserted only that the script behaved correctly when invoked directly
|
||||||
|
and that install.sh copied the file. Neither asserted that git ever runs it.
|
||||||
|
|
||||||
|
That also makes the original framing wrong. Refreshing on push assumes the person who pushes is the
|
||||||
|
person who goes stale, which is backwards: your install goes stale when *someone else* merges, and a
|
||||||
|
push of your own is neither necessary nor sufficient for it to have happened.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
A `SessionStart` hook, shipped in `plugins/kyberforge/.apm/hooks/`, checks whether the install is
|
||||||
|
behind and refreshes it in place.
|
||||||
|
|
||||||
|
`SessionStart` is the correct trigger because the thing that goes stale is the skill content a
|
||||||
|
*session* loads, and that is the moment the staleness does damage. It also enables two things a git
|
||||||
|
hook structurally cannot do: `additionalContext` puts the notice into the agent's context rather
|
||||||
|
than terminal scrollback nobody reads, and `reloadSkills: true` makes the host re-scan the skill
|
||||||
|
directories after the hook returns, so a refresh lands in the running session without a restart.
|
||||||
|
|
||||||
|
apm's own lifecycle events (`pre-/post-install`, `pre-/post-update`, `pre-/post-uninstall`) were
|
||||||
|
rejected: they fire around apm operations already chosen, so they can announce a refresh but never
|
||||||
|
detect that one is needed.
|
||||||
|
|
||||||
|
Three sub-decisions:
|
||||||
|
|
||||||
|
- **Refresh automatically rather than report.** The hook runs `apm update --yes` and asks for a skill
|
||||||
|
reload. The rejected alternative was to report and let a human run it. Auto-refresh costs a
|
||||||
|
rewritten `apm.lock.yaml` — a committed file — appearing as an unexplained modification in the
|
||||||
|
working tree, on any branch, at any time. The emitted notice says so explicitly for that reason.
|
||||||
|
- **`plugins/kyberforge/.apm/hooks/`, not `.claude/settings.json`.** ADR-0018 established that apm
|
||||||
|
owns `.claude/settings.json` and that any repo-authored key in it is permanent `apm audit --ci`
|
||||||
|
drift. A hook shipped in a package is written into that file by apm itself, so it is apm's output
|
||||||
|
and does not drift. `.claude/settings.local.json` also works but is gitignored and machine-local,
|
||||||
|
which fails the requirement that this travel with the repo.
|
||||||
|
- **`startup` matcher only.** `resume`, `clear`, `compact` and `fork` would re-run the check on every
|
||||||
|
compaction, and a compaction is not an event after which the remote can have moved.
|
||||||
|
|
||||||
|
The executable-trust gate is switched on at the same time. Root `apm.yml` gains an `executables:`
|
||||||
|
block allowing kyberforge's hooks and bin.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**The gate is off until something turns it on, and this repo had it off.** `apm approve --list`
|
||||||
|
reports `Executable-trust gate disabled -- all executables deploy` until an `executables:` block
|
||||||
|
exists in `apm.yml`. Any hook, bin, or MCP primitive a dependency shipped would have deployed with
|
||||||
|
no prompt and no record. The block added here closes that for this repo; every other apm project on
|
||||||
|
this machine still has it open.
|
||||||
|
|
||||||
|
**The allow key is version-pinned, and that is a live failure mode.** apm writes
|
||||||
|
`kyberforge#1.5.0`, not `kyberforge` — and the release that ships this hook proved the point
|
||||||
|
immediately, since bumping kyberforge to 1.5.0 required editing the key in the same commit. A
|
||||||
|
kyberforge version bump makes the entry stop matching, the
|
||||||
|
gate blocks the hook, and the install silently stops refreshing — the exact failure this ADR exists
|
||||||
|
to end, reintroduced through the mechanism meant to secure it. The `executables:` block carries a
|
||||||
|
comment saying to check it first when skills go stale after a release.
|
||||||
|
|
||||||
|
**A referenced hook script must be addressed at its `.apm/` path.** apm resolves
|
||||||
|
`${CLAUDE_PLUGIN_ROOT}/...` against the installed package root, and `apm pack` keeps only `*.json`
|
||||||
|
from `.apm/hooks/` when it builds the flat mirror. So `${CLAUDE_PLUGIN_ROOT}/hooks/check-apm-current.sh`
|
||||||
|
resolves to the mirror, where the script does not exist — verified, apm reports
|
||||||
|
`Hook script not found` and deploys a hook pointing at nothing. The working reference is
|
||||||
|
`${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh`. The script cannot simply be placed in
|
||||||
|
`plugins/kyberforge/hooks/` either: that directory is `rm -rf`'d by every content sync (ADR-0017).
|
||||||
|
A test pins the reference.
|
||||||
|
|
||||||
|
**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: 320` to cover a cold multi-package fetch.
|
||||||
|
|
||||||
|
**The hook cannot install itself.** Dependencies resolve from the remote, so the hook does not
|
||||||
|
deploy until this change is merged and `apm update` has run once against the new default branch.
|
||||||
|
Until then the repo has the mechanism in source and not in effect.
|
||||||
|
|
||||||
|
**`.claude/settings.json` stops being `{"hooks": {}}`.** apm merges the hook into it and tracks
|
||||||
|
ownership in a `.claude/apm-hooks.json` sidecar, with the script copied to
|
||||||
|
`.claude/hooks/<pkg>/`. The sidecar and the script directory are gitignored install output; the
|
||||||
|
settings file remains committed, now with apm-generated content in it. ADR-0018's statement that the
|
||||||
|
committed content is exactly `{"hooks": {}}` is superseded on that point only — the rule it was
|
||||||
|
protecting, that nothing repo-authored goes in that file, is unchanged.
|
||||||
|
|
||||||
|
**Native consumers are protected by a guard, not by the gate.** A host installing holocron through
|
||||||
|
`claude plugin install` auto-discovers `hooks/hooks.json` and does not consult apm's trust gate at
|
||||||
|
all. The script therefore exits silently when there is no `apm.lock.yaml` in the working directory,
|
||||||
|
which is what makes it inert in a repo that does not consume packages through apm. Copilot CLI sees
|
||||||
|
no hook at all, for the reasons already documented in `plugins/kyberforge/docs/hooks.md`.
|
||||||
|
|
||||||
|
**`scripts/git-hooks/` is now empty.** `post-push` and `test-post-push.sh` are deleted.
|
||||||
|
`install.sh`'s copy block is generic and is kept; `test-git-hooks-install.sh` now synthesizes its
|
||||||
|
own fixture hook instead of depending on a real one existing, so the mechanism stays tested and can
|
||||||
|
be used again if a hook git actually invokes is ever wanted.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **A `post-merge` git hook.** Real, unlike `post-push`, and verified to fire on both a
|
||||||
|
fast-forward `git pull` and a `git pull --rebase`. Rejected as the primary mechanism because a
|
||||||
|
pull is the wrong signal, and because it cannot reload skills in a running session. It remains
|
||||||
|
the only option for a project that consumes apm packages without a Claude-family host.
|
||||||
|
- **Reporting instead of refreshing.** See the sub-decision above.
|
||||||
|
- **A seventh plugin holding only this hook**, to avoid shipping it to external kyberforge
|
||||||
|
consumers. Rejected as disproportionate: the `apm.lock.yaml` guard already makes the hook inert
|
||||||
|
for anyone not consuming through apm, and a package exists to be maintained, versioned, and
|
||||||
|
registered in the marketplace.
|
||||||
@@ -19,13 +19,13 @@ project repo (local overrides)
|
|||||||
- **Executables** (`DEPLOY_EXECUTABLES`): `providers/claude-code/statusline-command.sh` → `~/.claude/statusline-command.sh` (with `+x`)
|
- **Executables** (`DEPLOY_EXECUTABLES`): `providers/claude-code/statusline-command.sh` → `~/.claude/statusline-command.sh` (with `+x`)
|
||||||
- **Directories** (`DEPLOY_DIRS`): `core/` → `~/.claude/core/` (destination fully replaced on each deploy)
|
- **Directories** (`DEPLOY_DIRS`): `core/` → `~/.claude/core/` (destination fully replaced on each deploy)
|
||||||
|
|
||||||
Skills are **not** deployed by `install.sh`. They are distributed as plugins and installed separately via `claude plugin install <name>@holocron`.
|
Skills are **not** deployed by `install.sh`. They are distributed as plugins and installed separately — in this repo by `apm install` against the `dependencies.apm` entries in the root `apm.yml`, which lands them in `.claude/skills/` and `.claude/agents/` (ADR-0018); elsewhere by `claude plugin install <name>@holocron`.
|
||||||
|
|
||||||
`~/.claude/CLAUDE.md` is a thin adapter, not a content source. It imports `~/.agents/AGENTS.md` (always-on rules) and `governance.md` (always-on governance), then lists the content index. All always-on content lives in `AGENTS.md` files so other providers can import the same source without duplication.
|
`~/.claude/CLAUDE.md` is a thin adapter, not a content source. It imports `~/.agents/AGENTS.md` (always-on rules) and `governance.md` (always-on governance), then lists the content index. All always-on content lives in `AGENTS.md` files so other providers can import the same source without duplication.
|
||||||
|
|
||||||
## Plugin model
|
## Plugin model
|
||||||
|
|
||||||
Skills, agents, MCP servers, and hooks are distributed as self-contained plugin units under `plugins/`, installed independently via `claude plugin install <name>@holocron`. Each plugin is an **apm package**: `plugins/<name>/apm.yml` plus a hand-authored `plugins/<name>/.apm/{skills,agents,hooks,commands,instructions,extensions}/` tree (ADR-0015). There is no hand-maintained `plugin.json` — every manifest and every host-visible content directory is compiled from that source.
|
Skills, agents, MCP servers, and hooks are distributed as self-contained plugin units under `plugins/`, installed independently — via `apm install` here, or `claude plugin install <name>@holocron` for a host consuming the marketplace natively (ADR-0018). Each plugin is an **apm package**: `plugins/<name>/apm.yml` plus a hand-authored `plugins/<name>/.apm/{skills,agents,hooks,commands,instructions,extensions}/` tree (ADR-0015). There is no hand-maintained `plugin.json` — every manifest and every host-visible content directory is compiled from that source.
|
||||||
|
|
||||||
Two compilers produce the plugin roots you see in the tree:
|
Two compilers produce the plugin roots you see in the tree:
|
||||||
|
|
||||||
@@ -63,4 +63,4 @@ This repo also has a `CLAUDE.md` at its root — the Claude Code entry point for
|
|||||||
|
|
||||||
## Architectural decisions
|
## Architectural decisions
|
||||||
|
|
||||||
Key hard-to-reverse decisions are recorded as ADRs in `docs/adr/`. There is no index file — the directory holds 17 numbered ADRs whose filenames state their decision, so `ls docs/adr/` is the index. Read a superseding ADR before the one it supersedes: ADR-0015 (apm as the authoring source of truth) supersedes ADR-0001 and moots ADR-0006, and ADR-0017 corrects ADR-0015's host-discovery gap. Entry points for the structure described on this page: ADR-0002 (two-tier CLAUDE.md), ADR-0003 (AGENTS.md as the provider-agnostic entry point), ADR-0015 and ADR-0017 (the two compilers behind the plugin roots).
|
Key hard-to-reverse decisions are recorded as ADRs in `docs/adr/`. There is no index file — the directory holds 19 numbered ADRs whose filenames state their decision, so `ls docs/adr/` is the index. Read a superseding ADR before the one it supersedes: ADR-0015 (apm as the authoring source of truth) supersedes ADR-0001 and moots ADR-0006, ADR-0017 corrects ADR-0015's host-discovery gap, and ADR-0019 supersedes one claim in ADR-0018 (that `.claude/settings.json`'s committed content is exactly `{"hooks": {}}`) while keeping the rule behind it. Entry points for the structure described on this page: ADR-0002 (two-tier CLAUDE.md), ADR-0003 (AGENTS.md as the provider-agnostic entry point), ADR-0015 and ADR-0017 (the two compilers behind the plugin roots).
|
||||||
|
|||||||
42
plugins/kyberforge/.apm/hooks/check-apm-current.sh
Executable file
42
plugins/kyberforge/.apm/hooks/check-apm-current.sh
Executable file
@@ -0,0 +1,42 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# SessionStart: keep an apm-consumed install level with its remote.
|
||||||
|
#
|
||||||
|
# Packages declared as unpinned git refs resolve against the remote default
|
||||||
|
# branch, so the deployed .claude/skills/ and .claude/agents/ go stale the
|
||||||
|
# moment anyone merges. The staleness bites when a session loads skills, which
|
||||||
|
# is why this runs at SessionStart rather than off a git hook — a pull is
|
||||||
|
# neither necessary nor sufficient for the install to have drifted.
|
||||||
|
#
|
||||||
|
# Refreshes in place and asks the host to re-scan, so the running session picks
|
||||||
|
# the new content up without a restart.
|
||||||
|
#
|
||||||
|
# Inert in any project that does not consume packages through apm.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
# No lockfile means nothing was installed through apm here — e.g. a host that
|
||||||
|
# installed this plugin natively. Say nothing and cost nothing.
|
||||||
|
[[ -f apm.lock.yaml ]] || exit 0
|
||||||
|
command -v apm > /dev/null 2>&1 || exit 0
|
||||||
|
|
||||||
|
# `apm outdated` exits 0 whether or not anything is stale, so the answer has to
|
||||||
|
# come from its output. ~0.7s against six remote refs; a hung remote must not
|
||||||
|
# hold the session open.
|
||||||
|
outdated_output="$(timeout 60 apm outdated 2>&1)" || exit 0
|
||||||
|
grep -q "outdated dependencies found" <<< "$outdated_output" || exit 0
|
||||||
|
|
||||||
|
stale_count="$(grep -oE '[0-9]+ outdated dependencies found' <<< "$outdated_output" | grep -oE '^[0-9]+' || true)"
|
||||||
|
[[ "$stale_count" =~ ^[0-9]+$ ]] || stale_count="some"
|
||||||
|
|
||||||
|
# Only ever emit fixed text plus a digit-checked count — never interpolate
|
||||||
|
# command output into the JSON, which would need escaping this cannot do safely.
|
||||||
|
emit() {
|
||||||
|
printf '{"hookSpecificOutput":{"hookEventName":"SessionStart","reloadSkills":%s,"additionalContext":"%s"}}\n' "$1" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
if timeout 300 apm update --yes > /dev/null 2>&1; then
|
||||||
|
emit true "apm install was ${stale_count} package(s) behind the remote default branch and has been refreshed automatically; skills and agents were redeployed and re-scanned. apm.lock.yaml has been rewritten and is now a modified file in the working tree - commit it or discard it deliberately."
|
||||||
|
else
|
||||||
|
emit false "apm install is ${stale_count} package(s) behind the remote default branch and the automatic refresh failed. Deployed skills and agents may be stale. Run: apm update --yes"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -1,3 +1,16 @@
|
|||||||
{
|
{
|
||||||
"hooks": {}
|
"hooks": {
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh",
|
||||||
|
"timeout": 320,
|
||||||
|
"type": "command"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"matcher": "startup"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kyberforge",
|
"name": "kyberforge",
|
||||||
"version": "1.4.1",
|
"version": "1.5.0",
|
||||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Defame1297",
|
"name": "Defame1297",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "kyberforge",
|
"name": "kyberforge",
|
||||||
"version": "1.4.1",
|
"version": "1.5.0",
|
||||||
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
"description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "Defame1297",
|
"name": "Defame1297",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: kyberforge
|
name: kyberforge
|
||||||
version: 1.4.1
|
version: 1.5.0
|
||||||
description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.
|
description: Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.
|
||||||
author:
|
author:
|
||||||
name: Defame1297
|
name: Defame1297
|
||||||
|
|||||||
@@ -34,16 +34,58 @@ mirrored file; the `check-plugin-content-sync` pre-push hook reports it as drift
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Events (**partial list**): `PreToolUse`, `PostToolUse`, `Notification`, `Stop`. Claude Code's plugin
|
Events (**partial list**): `PreToolUse`, `PostToolUse`, `Notification`, `Stop`, and `SessionStart`
|
||||||
hook set is larger — `SessionStart`, `SessionEnd`, `UserPromptSubmit`, `PreCompact` and
|
(verified end-to-end by the hook below). Claude Code's plugin hook set is larger — `SessionEnd`,
|
||||||
`SubagentStop` also exist — and this repo's vendored corpus does not enumerate it anywhere:
|
`UserPromptSubmit`, `PreCompact` and `SubagentStop` also exist — and this repo's vendored corpus does
|
||||||
`docs/research/docs/claude-code-plugins/configuration.md:100` describes the file as "Event handlers
|
not enumerate it anywhere: `docs/research/docs/claude-code-plugins/configuration.md:100` describes
|
||||||
(PreToolUse, PostToolUse, etc.)", and `agent-definition.md:53` covers only the per-agent `hooks`
|
the file as "Event handlers (PreToolUse, PostToolUse, etc.)", and `agent-definition.md:53` covers
|
||||||
field, not the plugin-level set. Treat the four names above as the ones this repo has verified, not
|
only the per-agent `hooks` field, not the plugin-level set. Treat the five names above as the ones
|
||||||
as the schema. Check Claude Code's own hooks documentation before wiring an event not listed here.
|
this repo has verified, not as the schema. Check Claude Code's own hooks documentation before wiring
|
||||||
|
an event not listed here.
|
||||||
|
|
||||||
Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin — the plugin runs from a cache
|
## Referencing a script — use the `.apm/` path, not the mirror
|
||||||
path after install, not its original repo location.
|
|
||||||
|
Use `${CLAUDE_PLUGIN_ROOT}` to reference scripts inside this plugin; the plugin runs from a cache or
|
||||||
|
`apm_modules/` path after install, not its original repo location. **Address the script at its
|
||||||
|
`.apm/` path:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh"
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
`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.
|
||||||
|
|
||||||
|
## Deployed shape
|
||||||
|
|
||||||
|
At install, apm merges the event bindings into `.claude/settings.json`, copies the referenced script
|
||||||
|
to `.claude/hooks/<pkg>/` (preserving its executable bit, preserving the `.apm/hooks/` subpath), and
|
||||||
|
rewrites `command` to a `${CLAUDE_PROJECT_DIR}`-relative path. Ownership of its own entries is
|
||||||
|
tracked in a `.claude/apm-hooks.json` sidecar, so an uninstall removes them without touching
|
||||||
|
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.
|
||||||
|
|
||||||
|
## The SessionStart hook
|
||||||
|
|
||||||
|
`check-apm-current.sh` keeps an apm-consumed install level with its remote: it runs `apm outdated`,
|
||||||
|
and if anything is behind, runs `apm update --yes` and returns `reloadSkills: true` so the running
|
||||||
|
session picks up the redeployed content. It exits silently when there is no `apm.lock.yaml` in the
|
||||||
|
working directory, which makes it inert for any host that installed this plugin natively rather than
|
||||||
|
through apm. Rationale, measurements, and the failure modes are in ADR-0019.
|
||||||
|
|
||||||
## GitHub Copilot CLI
|
## GitHub Copilot CLI
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
{
|
{
|
||||||
"hooks": {}
|
"hooks": {
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"command": "${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh",
|
||||||
|
"timeout": 320,
|
||||||
|
"type": "command"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"matcher": "startup"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Refresh the kyberforge plugin cache after every push
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
echo "→ Refreshing holocron marketplace clone..."
|
|
||||||
if git -C ~/.claude/plugins/marketplaces/holocron pull --quiet; then
|
|
||||||
echo "→ Updating kyberforge plugin cache..."
|
|
||||||
if claude plugin update kyberforge; then
|
|
||||||
echo "✓ kyberforge cache updated"
|
|
||||||
else
|
|
||||||
echo "⚠ claude plugin update kyberforge failed — run it manually" >&2
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "⚠ Failed to pull holocron marketplace clone — run 'git -C ~/.claude/plugins/marketplaces/holocron pull' manually" >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
@@ -21,6 +21,13 @@ fi
|
|||||||
# keeps the loop in this shell so the appends survive. `sort` is still fed
|
# keeps the loop in this shell so the appends survive. `sort` is still fed
|
||||||
# newline-delimited output, exactly as before. Same convention as
|
# newline-delimited output, exactly as before. Same convention as
|
||||||
# tests/run-tests.sh.
|
# tests/run-tests.sh.
|
||||||
|
#
|
||||||
|
# apm_modules/ is excluded because `apm install` materializes a full copy of
|
||||||
|
# every dependency there — including this repo's own plugins, which it consumes
|
||||||
|
# from the holocron remote. Those copies carry their own .bats files whose
|
||||||
|
# relative paths (`$BATS_TEST_DIRNAME/../../../../../../`) resolve to the
|
||||||
|
# dependency's root, not this repo's, so they fail on a missing bats-support
|
||||||
|
# helper. They are the same tests already discovered under plugins/.
|
||||||
TEST_FILES=()
|
TEST_FILES=()
|
||||||
while IFS= read -r f; do
|
while IFS= read -r f; do
|
||||||
TEST_FILES+=("$f")
|
TEST_FILES+=("$f")
|
||||||
@@ -29,6 +36,7 @@ done < <(
|
|||||||
-not -path "*/tests/bats/*" \
|
-not -path "*/tests/bats/*" \
|
||||||
-not -path "*/test_helper/*" \
|
-not -path "*/test_helper/*" \
|
||||||
-not -path "*/.claude/worktrees/*" \
|
-not -path "*/.claude/worktrees/*" \
|
||||||
|
-not -path "*/apm_modules/*" \
|
||||||
| sort
|
| sort
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,7 +75,7 @@ if [[ -n "$GIT_TOPLEVEL" && "$GIT_TOPLEVEL" == "$REPO_ROOT" ]]; then
|
|||||||
[[ -n "$f" ]] && EXPECTED_FILES+=("$REPO_ROOT/$f")
|
[[ -n "$f" ]] && EXPECTED_FILES+=("$REPO_ROOT/$f")
|
||||||
done < <(
|
done < <(
|
||||||
git -C "$REPO_ROOT" ls-files -- '*.bats' \
|
git -C "$REPO_ROOT" ls-files -- '*.bats' \
|
||||||
| grep -Ev '(^|/)tests/bats/|(^|/)test_helper/|(^|/)\.claude/worktrees/' \
|
| grep -Ev '(^|/)tests/bats/|(^|/)test_helper/|(^|/)\.claude/worktrees/|(^|/)apm_modules/' \
|
||||||
| sort || true
|
| sort || true
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -119,6 +119,10 @@ run_bats
|
|||||||
# /bin/bash 3.2, which has no `mapfile`. Process substitution (not a pipe)
|
# /bin/bash 3.2, which has no `mapfile`. Process substitution (not a pipe)
|
||||||
# keeps the loop in this shell so the appends survive. `sort` is still fed
|
# keeps the loop in this shell so the appends survive. `sort` is still fed
|
||||||
# newline-delimited output, exactly as before.
|
# newline-delimited output, exactly as before.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
SCRIPTS=()
|
SCRIPTS=()
|
||||||
while IFS= read -r script; do
|
while IFS= read -r script; do
|
||||||
SCRIPTS+=("$script")
|
SCRIPTS+=("$script")
|
||||||
@@ -126,6 +130,7 @@ done < <(
|
|||||||
find "$SEARCH_ROOT" -name "test-*.sh" \
|
find "$SEARCH_ROOT" -name "test-*.sh" \
|
||||||
-not -path "*/.git/*" \
|
-not -path "*/.git/*" \
|
||||||
-not -path "*/.claude/worktrees/*" \
|
-not -path "*/.claude/worktrees/*" \
|
||||||
|
-not -path "*/apm_modules/*" \
|
||||||
| sort
|
| sort
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
159
tests/test-apm-current-hook.sh
Executable file
159
tests/test-apm-current-hook.sh
Executable file
@@ -0,0 +1,159 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Tests for plugins/kyberforge/.apm/hooks/check-apm-current.sh — the SessionStart
|
||||||
|
# hook that keeps an apm-consumed install level with its remote.
|
||||||
|
#
|
||||||
|
# `apm` is mocked throughout: the hook's contract is "read `apm outdated`, decide,
|
||||||
|
# emit SessionStart JSON", and that is testable without a network or a real
|
||||||
|
# install.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
HOOK="$REPO_ROOT/plugins/kyberforge/.apm/hooks/check-apm-current.sh"
|
||||||
|
HOOKS_JSON="$REPO_ROOT/plugins/kyberforge/.apm/hooks/hooks.json"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
||||||
|
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
||||||
|
|
||||||
|
command -v python3 > /dev/null 2>&1 || { echo "python3 required"; exit 77; }
|
||||||
|
|
||||||
|
FAKE_BIN="$(mktemp -d)"
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$FAKE_BIN" "$WORK"' EXIT
|
||||||
|
|
||||||
|
# Mock `apm`. $1 chooses what `apm outdated` reports; $2 the exit code of
|
||||||
|
# `apm update`. A sentinel file records whether update was actually invoked.
|
||||||
|
make_apm() {
|
||||||
|
local outdated_line="$1" update_exit="$2"
|
||||||
|
cat > "$FAKE_BIN/apm" << EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
case "\$1" in
|
||||||
|
outdated) echo "$outdated_line"; exit 0 ;;
|
||||||
|
update) touch "$WORK/update-was-called"; exit $update_exit ;;
|
||||||
|
esac
|
||||||
|
exit 0
|
||||||
|
EOF
|
||||||
|
chmod +x "$FAKE_BIN/apm"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_hook() { (cd "$WORK" && PATH="$FAKE_BIN:$PATH" bash "$HOOK" 2>/dev/null); }
|
||||||
|
|
||||||
|
json_field() { python3 -c 'import json,sys; print(json.load(sys.stdin)["hookSpecificOutput"][sys.argv[1]])' "$1"; }
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "--- inert without an apm-consumed install ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
make_apm "[!] 6 outdated dependencies found" 0
|
||||||
|
rm -f "$WORK/apm.lock.yaml" "$WORK/update-was-called"
|
||||||
|
out="$(run_hook)"; rc=$?
|
||||||
|
[[ $rc -eq 0 ]] && pass "exits 0 with no apm.lock.yaml" || fail "should exit 0 with no apm.lock.yaml"
|
||||||
|
[[ -z "$out" ]] && pass "emits nothing with no apm.lock.yaml" || fail "should stay silent with no apm.lock.yaml"
|
||||||
|
[[ ! -f "$WORK/update-was-called" ]] && pass "does not run apm update with no apm.lock.yaml" \
|
||||||
|
|| fail "must not touch a project that does not use apm"
|
||||||
|
|
||||||
|
# A native (non-apm) install of this plugin hits exactly this path, so it is the
|
||||||
|
# guard that keeps the hook from acting on someone else's repo.
|
||||||
|
touch "$WORK/apm.lock.yaml"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- inert when apm is absent ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
rm -f "$WORK/update-was-called"
|
||||||
|
out="$( (cd "$WORK" && PATH="$(dirname "$(command -v bash)")" bash "$HOOK" 2>/dev/null) )"; rc=$?
|
||||||
|
[[ $rc -eq 0 ]] && pass "exits 0 when apm is not on PATH" || fail "should exit 0 when apm is missing"
|
||||||
|
[[ -z "$out" ]] && pass "emits nothing when apm is not on PATH" || fail "should stay silent when apm is missing"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- install already current ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
make_apm "[*] All dependencies are up-to-date" 0
|
||||||
|
rm -f "$WORK/update-was-called"
|
||||||
|
out="$(run_hook)"; rc=$?
|
||||||
|
[[ $rc -eq 0 ]] && pass "exits 0 when current" || fail "should exit 0 when current"
|
||||||
|
[[ -z "$out" ]] && pass "emits nothing when current" || fail "should stay silent when current"
|
||||||
|
[[ ! -f "$WORK/update-was-called" ]] && pass "does not run apm update when current" \
|
||||||
|
|| fail "must not update when nothing is stale"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- stale, refresh succeeds ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
make_apm "[!] 6 outdated dependencies found" 0
|
||||||
|
rm -f "$WORK/update-was-called"
|
||||||
|
out="$(run_hook)"; rc=$?
|
||||||
|
[[ $rc -eq 0 ]] && pass "exits 0 when stale" || fail "should exit 0 when stale"
|
||||||
|
[[ -f "$WORK/update-was-called" ]] && pass "runs apm update when stale" || fail "should run apm update when stale"
|
||||||
|
if echo "$out" | python3 -m json.tool > /dev/null 2>&1; then
|
||||||
|
pass "emits valid JSON"
|
||||||
|
[[ "$(echo "$out" | json_field hookEventName)" == "SessionStart" ]] \
|
||||||
|
&& pass "declares hookEventName SessionStart" || fail "wrong hookEventName"
|
||||||
|
[[ "$(echo "$out" | json_field reloadSkills)" == "True" ]] \
|
||||||
|
&& pass "asks the host to reload skills after a successful refresh" || fail "reloadSkills should be true"
|
||||||
|
echo "$out" | json_field additionalContext | grep -q "6 package" \
|
||||||
|
&& pass "reports the stale package count" || fail "should report the count"
|
||||||
|
# The lockfile rewrite is the surprising part of auto-updating; the notice has
|
||||||
|
# to say so or a dirty worktree looks like something else went wrong.
|
||||||
|
echo "$out" | json_field additionalContext | grep -q "apm.lock.yaml" \
|
||||||
|
&& pass "warns that apm.lock.yaml was rewritten" || fail "should warn about the lockfile rewrite"
|
||||||
|
else
|
||||||
|
fail "emits valid JSON"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- stale, refresh fails ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
make_apm "[!] 3 outdated dependencies found" 1
|
||||||
|
out="$(run_hook)"; rc=$?
|
||||||
|
[[ $rc -eq 0 ]] && pass "exits 0 when the refresh fails" || fail "must never fail the session start"
|
||||||
|
if echo "$out" | python3 -m json.tool > /dev/null 2>&1; then
|
||||||
|
pass "emits valid JSON on failure"
|
||||||
|
[[ "$(echo "$out" | json_field reloadSkills)" == "False" ]] \
|
||||||
|
&& pass "does not ask for a skill reload when nothing was deployed" || fail "reloadSkills should be false"
|
||||||
|
echo "$out" | json_field additionalContext | grep -q "apm update" \
|
||||||
|
&& pass "tells the reader how to refresh by hand" || fail "should name the manual command"
|
||||||
|
else
|
||||||
|
fail "emits valid JSON on failure"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- unparseable count degrades instead of breaking the JSON ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
make_apm "[!] lots of outdated dependencies found" 0
|
||||||
|
out="$(run_hook)"
|
||||||
|
echo "$out" | python3 -m json.tool > /dev/null 2>&1 \
|
||||||
|
&& pass "still emits valid JSON when the count cannot be parsed" || fail "JSON broke on an unparseable count"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "--- hooks.json wiring ---"
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# apm resolves script paths relative to the package root, and `apm pack` keeps
|
||||||
|
# only *.json from .apm/hooks/ — so a ${CLAUDE_PLUGIN_ROOT}/hooks/... reference
|
||||||
|
# points at a directory the script never reaches. It must be .apm/-relative.
|
||||||
|
referenced="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d["hooks"]["SessionStart"][0]["hooks"][0]["command"])' "$HOOKS_JSON")"
|
||||||
|
[[ "$referenced" == '${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh' ]] \
|
||||||
|
&& pass "hooks.json references the script at its .apm/ path" \
|
||||||
|
|| fail "hooks.json references '$referenced' — must be \${CLAUDE_PLUGIN_ROOT}/.apm/hooks/check-apm-current.sh"
|
||||||
|
|
||||||
|
[[ -x "$HOOK" ]] && pass "hook script is executable" || fail "hook script must be executable"
|
||||||
|
|
||||||
|
matcher="$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d["hooks"]["SessionStart"][0]["matcher"])' "$HOOKS_JSON")"
|
||||||
|
[[ "$matcher" == "startup" ]] && pass "fires on startup only" \
|
||||||
|
|| fail "matcher is '$matcher' — resume/clear/compact would re-run this every compaction"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo ""
|
||||||
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
|
[[ $FAIL -eq 0 ]]
|
||||||
@@ -40,11 +40,25 @@ DEPLOY_EXECUTABLES=()
|
|||||||
DEPLOY_DIRS=()
|
DEPLOY_DIRS=()
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Copy the real install.sh and git-hooks into the temp repo
|
# Copy the real install.sh into the temp repo.
|
||||||
cp "$REPO_ROOT/scripts/install.sh" "$TEMP_REPO/scripts/install.sh"
|
cp "$REPO_ROOT/scripts/install.sh" "$TEMP_REPO/scripts/install.sh"
|
||||||
cp -r "$REPO_ROOT/scripts/git-hooks" "$TEMP_REPO/scripts/git-hooks"
|
|
||||||
|
|
||||||
HOOKS_SRC="$TEMP_REPO/scripts/git-hooks"
|
HOOKS_SRC="$TEMP_REPO/scripts/git-hooks"
|
||||||
|
mkdir -p "$HOOKS_SRC"
|
||||||
|
|
||||||
|
# Copy whatever real hooks exist, then add a synthetic fixture. The repo
|
||||||
|
# currently ships none — the only entry was `post-push`, removed once it was
|
||||||
|
# found that git has no such client-side hook, so it had never fired (see
|
||||||
|
# ADR-0019). install.sh's copy block is generic and stays worth testing, so the
|
||||||
|
# fixture keeps that coverage alive independently of whether any real hook
|
||||||
|
# happens to exist. Real hooks are still picked up by the loops below.
|
||||||
|
if [[ -d "$REPO_ROOT/scripts/git-hooks" ]]; then
|
||||||
|
find "$REPO_ROOT/scripts/git-hooks" -maxdepth 1 -type f -exec cp {} "$HOOKS_SRC/" \;
|
||||||
|
fi
|
||||||
|
|
||||||
|
FIXTURE_HOOK="fixture-hook"
|
||||||
|
printf '#!/usr/bin/env bash\nexit 0\n' > "$HOOKS_SRC/$FIXTURE_HOOK"
|
||||||
|
chmod +x "$HOOKS_SRC/$FIXTURE_HOOK"
|
||||||
|
|
||||||
run_install() {
|
run_install() {
|
||||||
HOME="$TEMP_HOME" bash "$TEMP_REPO/scripts/install.sh" > /dev/null 2>&1
|
HOME="$TEMP_HOME" bash "$TEMP_REPO/scripts/install.sh" > /dev/null 2>&1
|
||||||
@@ -108,7 +122,7 @@ OTHER_REPO="$(mktemp -d)"
|
|||||||
git -C "$OTHER_REPO" init -q
|
git -C "$OTHER_REPO" init -q
|
||||||
if HOME="$TEMP_HOME" GIT_DIR="$OTHER_REPO/.git" GIT_WORK_TREE="$OTHER_REPO" \
|
if HOME="$TEMP_HOME" GIT_DIR="$OTHER_REPO/.git" GIT_WORK_TREE="$OTHER_REPO" \
|
||||||
bash "$TEMP_REPO/scripts/install.sh" > /dev/null 2>&1; then
|
bash "$TEMP_REPO/scripts/install.sh" > /dev/null 2>&1; then
|
||||||
if [[ -f "$TEMP_REPO/.git/hooks/post-push" ]]; then
|
if [[ -f "$TEMP_REPO/.git/hooks/$FIXTURE_HOOK" ]]; then
|
||||||
pass "resolves \$TEMP_REPO/.git/hooks/ even with inherited GIT_DIR"
|
pass "resolves \$TEMP_REPO/.git/hooks/ even with inherited GIT_DIR"
|
||||||
else
|
else
|
||||||
fail "installed into inherited GIT_DIR instead of \$TEMP_REPO"
|
fail "installed into inherited GIT_DIR instead of \$TEMP_REPO"
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
||||||
PASS=0
|
|
||||||
FAIL=0
|
|
||||||
|
|
||||||
pass() { echo " PASS: $1"; PASS=$((PASS + 1)); }
|
|
||||||
fail() { echo " FAIL: $1"; FAIL=$((FAIL + 1)); }
|
|
||||||
|
|
||||||
HOOK="$REPO_ROOT/scripts/git-hooks/post-push"
|
|
||||||
FAKE_BIN="$(mktemp -d)"
|
|
||||||
FAKE_HOME="$(mktemp -d)"
|
|
||||||
trap 'rm -rf "$FAKE_BIN" "$FAKE_HOME"' EXIT
|
|
||||||
|
|
||||||
# Helper: write a fake git stub that exits with the given code
|
|
||||||
make_git() {
|
|
||||||
local exit_code="$1"
|
|
||||||
printf '#!/usr/bin/env bash\nexit %s\n' "$exit_code" > "$FAKE_BIN/git"
|
|
||||||
chmod +x "$FAKE_BIN/git"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Helper: write a fake claude stub; optionally touches a sentinel file on invocation
|
|
||||||
make_claude() {
|
|
||||||
local exit_code="$1"
|
|
||||||
local log="${2:-}"
|
|
||||||
if [[ -n "$log" ]]; then
|
|
||||||
printf '#!/usr/bin/env bash\ntouch "%s"\nexit %s\n' "$log" "$exit_code" > "$FAKE_BIN/claude"
|
|
||||||
else
|
|
||||||
printf '#!/usr/bin/env bash\nexit %s\n' "$exit_code" > "$FAKE_BIN/claude"
|
|
||||||
fi
|
|
||||||
chmod +x "$FAKE_BIN/claude"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run the hook with mocked PATH and HOME; suppress all output
|
|
||||||
run_hook() {
|
|
||||||
PATH="$FAKE_BIN:$PATH" HOME="$FAKE_HOME" bash "$HOOK" > /dev/null 2>&1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run the hook and capture combined stdout+stderr
|
|
||||||
capture_hook() {
|
|
||||||
PATH="$FAKE_BIN:$PATH" HOME="$FAKE_HOME" bash "$HOOK" 2>&1 || true
|
|
||||||
}
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
echo "--- post-push: always exits 0 ---"
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
make_git 1; make_claude 0
|
|
||||||
if run_hook; then
|
|
||||||
pass "exits 0 when git pull fails"
|
|
||||||
else
|
|
||||||
fail "should exit 0 when git pull fails"
|
|
||||||
fi
|
|
||||||
|
|
||||||
make_git 0; make_claude 1
|
|
||||||
if run_hook; then
|
|
||||||
pass "exits 0 when claude plugin update fails"
|
|
||||||
else
|
|
||||||
fail "should exit 0 when claude plugin update fails"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
echo ""
|
|
||||||
echo "--- post-push: success output ---"
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
make_git 0; make_claude 0
|
|
||||||
output=$(capture_hook)
|
|
||||||
echo "$output" | grep -q "kyberforge cache updated" \
|
|
||||||
&& pass "prints success message when both git pull and claude succeed" \
|
|
||||||
|| fail "should print success message when both commands succeed"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
echo ""
|
|
||||||
echo "--- post-push: warnings on failure ---"
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
make_git 1; make_claude 0
|
|
||||||
output=$(capture_hook)
|
|
||||||
echo "$output" | grep -qi "failed to pull" \
|
|
||||||
&& pass "warns when git pull fails" \
|
|
||||||
|| fail "should warn when git pull fails"
|
|
||||||
|
|
||||||
make_git 0; make_claude 1
|
|
||||||
output=$(capture_hook)
|
|
||||||
echo "$output" | grep -qi "failed" \
|
|
||||||
&& pass "warns when claude plugin update fails" \
|
|
||||||
|| fail "should warn when claude plugin update fails"
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
echo ""
|
|
||||||
echo "--- post-push: claude not called when git pull fails ---"
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
CLAUDE_LOG="$FAKE_HOME/claude-was-called"
|
|
||||||
make_git 1; make_claude 0 "$CLAUDE_LOG"
|
|
||||||
run_hook
|
|
||||||
if [[ ! -f "$CLAUDE_LOG" ]]; then
|
|
||||||
pass "claude not called when git pull fails"
|
|
||||||
else
|
|
||||||
fail "claude should not be called when git pull fails"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
echo ""
|
|
||||||
echo "Results: $PASS passed, $FAIL failed"
|
|
||||||
[[ $FAIL -eq 0 ]]
|
|
||||||
Reference in New Issue
Block a user