- **core** — authoring and auditing a repo's `AGENTS.md` and the provider adapter files that defer to it
- **core** — authoring and auditing a repo's `AGENTS.md` and the provider adapter files that defer to it
- **lint** — configuring and running linters
- **lint** — configuring and running linters
- **bin** — cross-cutting workflow skills not yet split into a focused plugin: research, documentation, TDD, prototyping, triage, diagnosis, architecture review, requirement grilling
- **bin** — cross-cutting workflow skills not yet split into a focused plugin: research, documentation, TDD, prototyping, triage, diagnosis, architecture review, requirement grilling, compressed output (`caveman`), and re-orienting mid-task (`zoom-out`)
## Prerequisites
## Prerequisites
@@ -81,12 +81,17 @@ A suite that exits 77 because a dependency is missing is reported as SKIPPED and
## Before pushing
## Before pushing
Run the whole pre-push gate locally in one command:
Run the pre-push gate locally in one command:
```bash
```bash
pre-commit run --hook-stage pre-push --all-files
pre-commit run --hook-stage pre-push --all-files
```
```
One caveat: `check-release-needed` is a silent no-op under this invocation. It exits 0 unless
`PRE_COMMIT_REMOTE_BRANCH` is `refs/heads/main`, and pre-commit exports that only from the real
pre-push git hook during an actual `git push` — so the hook reports `Passed` having checked nothing.
Every other pre-push hook does run.
See [`docs/spec/gates.md`](docs/spec/gates.md) for what each hook enforces and why.
See [`docs/spec/gates.md`](docs/spec/gates.md) for what each hook enforces and why.
**Offline?** Exactly two pre-push hooks need the network, because root `apm.yml`'s marketplace contains one remote package entry that must be resolved with `git ls-remote`:
**Offline?** Exactly two pre-push hooks need the network, because root `apm.yml`'s marketplace contains one remote package entry that must be resolved with `git ls-remote`:
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`.
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) and carries nothing else — the content index of on-demand instruction files sits in `core/AGENTS.md`, deployed beside it. 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) and carries nothing else — the content index of on-demand instruction files sits in `core/AGENTS.md`, deployed to `~/.agents/AGENTS.md` and imported by it. All always-on content lives in `AGENTS.md` files so other providers can import the same source without duplication.
## Plugin model
## Plugin model
@@ -31,7 +31,7 @@ Which plugin a new skill belongs in follows from what each one is scoped to. The
The second boundary worth stating is `git` vs `gitea`, because both own things called branches and both touch pull requests: `git` is whatever works over the git wire protocol against a local clone, `gitea` is whatever goes through the forge's HTTP API. That is why `git-branches` and `gitea-branches` both exist and are not duplicates.
The second boundary worth stating is `git` vs `gitea`, because both own things called branches and both touch pull requests: `git` is whatever works over the git wire protocol against a local clone, `gitea` is whatever goes through the forge's HTTP API. That is why `git-branches` and `gitea-branches` both exist and are not duplicates.
These are routing boundaries, not inventories — they answer "where does a new skill go", so they deliberately do not enumerate what each plugin ships today. For what a consumer actually gets, read the plugin's published `description` in its `apm.yml`.
These are routing boundaries, not inventories — they answer "where does a new skill go", so they deliberately do not enumerate what each plugin ships today. The plugin's published `description` in its `apm.yml` states the same boundary for a consumer deciding whether to install (ADR-0021); neither carries an inventory. For what a plugin ships today, read `plugins/<name>/.apm/skills/` or the plugin list in `README.md`.
| Plugin | Scope |
| Plugin | Scope |
|---|---|
|---|---|
@@ -76,7 +76,7 @@ This repo also has a `CLAUDE.md` at its root — the Claude Code entry point for
## Reference conventions
## Reference conventions
The stated convention is that files referencing other files declare those references explicitly: the referencing file carries the forward reference (the content index in `core/AGENTS.md`, `references:` in frontmatter), the referenced file carries a `when:` field describing when it is loaded, and divergence between the two signals staleness. It is aspirational, not a description of the repo today — no file under `core/instructions/` carries frontmatter at all, `when:` appears in two of the 39 skill sources under `plugins/*/.apm/skills/`, and the reference scanner script meant to derive the reverse map ("what files reference this file?") does not exist; `docs/notes/skill-implementation-workflow.md` still lists it as unbuilt work. Treat it as intent for instruction files, skills, and workflow documents, not as a rule the repo enforces.
The stated convention is that files referencing other files declare those references explicitly: the referencing file carries the forward reference (the content index in `core/AGENTS.md`, `references:` in frontmatter), the referenced file carries a `when:` field describing when it is loaded, and divergence between the two signals staleness. It is aspirational, not a description of the repo today — no file under `core/instructions/` carries frontmatter at all, `when:` appears in exactly one of the 39 `SKILL.md` sources under `plugins/*/.apm/skills/`, and the reference scanner script meant to derive the reverse map ("what files reference this file?") does not exist; `docs/notes/skill-implementation-workflow.md` still lists it as unbuilt work. Treat it as intent for instruction files, skills, and workflow documents, not as a rule the repo enforces.
`refs/heads/main`, and pre-commit exports that variable only from the real pre-push git hook during
an actual `git push`. Running the stage by hand — or from a CI runner — therefore reports it
`Passed` having checked nothing. That is by design for feature branches — pushing WIP must not be
blocked on cutting a premature tag — but it means `--hook-stage pre-push --all-files` is a full
rehearsal of 13 hooks and a skip of the fourteenth. The script's own header records the same gap for
a PR merged through Gitea's merge button, where no local push happens at all.
## The pre-push gate
## The pre-push gate
@@ -74,7 +85,7 @@ drift in generated text.
| Hook | Guards |
| Hook | Guards |
|---|---|
|---|---|
| `check-release-needed` | on push to `main` only — fails if files exposed via `.pre-commit-hooks.yaml` changed since the last tag |
| `check-release-needed` | on a real `git push` to `main` only — fails if files exposed via `.pre-commit-hooks.yaml` changed since the last tag. A no-op everywhere else, including under `pre-commit run --hook-stage pre-push` (see [the caveat above](#running-the-gates)) |
Four of these shell out to `apm`: `apm-marketplace-check`, `apm-audit-ci`, `apm-pack-check-clean`,
Four of these shell out to `apm`: `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`). The
and `check-plugin-content-sync` (via `scripts/sync-plugin-content.sh`, which wraps `apm pack`). The
@@ -87,8 +98,9 @@ loudly (`Error: jq is required but not installed`).
## Skill and agent context gates (ADR-0020)
## Skill and agent context gates (ADR-0020)
The `skill-size-check` pre-commit hook, scoped to `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$`,
The `skill-size-check` pre-commit hook, scoped to `^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$`,
runs `scripts/skill-size-check.sh`. That scope means it never lints `docs/research/examples/`
runs `scripts/skill-size-check.sh`. That scope means it never lints the
reference skills. It is also shipped to external repos as `kyberforge-skill-size-check` (see
`plugins/kyberforge/docs/research/examples/`reference skills. It is also shipped to external repos
### Two independent gate families, neither replaced the other
### Two independent gate families, neither replaced the other
@@ -107,14 +119,13 @@ reference skills. It is also shipped to external repos as `kyberforge-skill-size
| `description` characters | 250 | 400 | the YAML-**folded** value |
| `description` characters | 250 | 400 | the YAML-**folded** value |
| body words | 600 | 900 | **body only** — everything after the frontmatter's closing `---` |
| body words | 600 | 900 | **body only** — everything after the frontmatter's closing `---` |
Plus three hard FAILs with no suggestion tier:
Plus two hard FAILs with no suggestion tier:
- **A missing, valueless or `null``description:`.** Not a skip. The description is the one field
- **A missing, valueless or `null``description:`.** Not a skip. The description is the one field
preloaded into every session, so a gate that declines to measure it reports green. (This is not
preloaded into every session, so a gate that declines to measure it reports green. (This is not
hypothetical: `description:` with no value followed by `model: sonnet` let a line regex capture the
hypothetical: `description:` with no value followed by `model: sonnet` let a line regex capture the
*next* key, which looked non-empty, so the "missing or empty" branch never fired and every gate
*next* key, which looked non-empty, so the "missing or empty" branch never fired and every gate
below early-returned on the genuinely empty folded value — exit 0, zero output, on a blocking gate.)
below early-returned on the genuinely empty folded value — exit 0, zero output, on a blocking gate.)
- **Every boundary-clause routing target must resolve** to a real skill or agent.
- **Every `references/<file>.md` a body names must exist** on disk. A dispatch table pointing at a
- **Every `references/<file>.md` a body names must exist** on disk. A dispatch table pointing at a
file that was never written is a silently dead branch, and nothing else in the gate/audit/vale
file that was never written is a silently dead branch, and nothing else in the gate/audit/vale
stack notices it.
stack notices it.
@@ -122,6 +133,26 @@ Plus three hard FAILs with no suggestion tier:
A file can sit well inside one family and fail the other. 2,770 whole-file words is a conformance
A file can sit well inside one family and fail the other. 2,770 whole-file words is a conformance
backstop; 900 body-only words is a quality gate. Conflating them is what produced the current state.
backstop; 900 body-only words is a quality gate. Conflating them is what produced the current state.
### An unresolved routing target is not automatically a FAIL
A boundary-clause target that resolves to no skill or agent has **three** possible verdicts, not one
(`unresolved_targets()` in `scripts/skill-size-check.sh`):
| Verdict | When |
|---|---|
| **SUGGESTION** — the default | the target does not resolve and neither promotion condition below holds |
| **blocking ERROR** | the target is **terminal** (not a compound modifier) **and** either written in route notation (`/name`, `-> name`) **or** corroborated by another target in the same sentence that *does* resolve |
| **INFO, "DID NOT RUN"** | no skill universe could be determined for the path at all — the targets are named and left unchecked, exit 0 |
The default is deliberately soft because a hyphenated word in a boundary clause is as likely to be a
tool, a file format or an English compound as a route: "pre-commit hooks" is prose about a tool and
never reaches the check at all, being a compound modifier rather than a terminal name. The
SUGGESTION text says how to opt in — write it as `/name` or `-> name` and it gets checked properly.
Corroboration is what makes the soft default safe: a sentence whose *other* target resolves is
demonstrably a routing sentence, so a sibling that does not resolve is a typo rather than a noun, and
gets promoted.
### Target resolution walk
### Target resolution walk
Resolution walks up **from the file being checked** — never from the script's own location. Deriving
Resolution walks up **from the file being checked** — never from the script's own location. Deriving
@@ -176,10 +207,13 @@ Three more, deterministic to measure but judgment to act on:
The hook is declared `verbose: true` so the SUGGESTION tier is audible. pre-commit prints nothing at
The hook is declared `verbose: true` so the SUGGESTION tier is audible. pre-commit prints nothing at
all for a passing hook, and a SUGGESTION deliberately does not fail — without verbose every
all for a passing hook, and a SUGGESTION deliberately does not fail — without verbose every
suggestion is swallowed, which is exactly the invisibility ADR-0013 records for Vale warnings.
suggestion is swallowed, which is exactly the invisibility ADR-0013 records for Vale warnings.
ADR-0020's arithmetic depends on it: writing to the 400-char FAIL lands the preload at 39 × 400 =
ADR-0020's preload arithmetic depends on it: writing to the 400-char FAIL delivers roughly half the
15,600 chars (a 33% cut off 23,427); writing to the 250-char SUGGESTION lands at 9,750 (58%). The
cut that writing to the 250-char SUGGESTION does, so the intended saving depends entirely on that
halving depends entirely on that tier being visible. It costs nothing on a clean file — the script
tier being visible. The numbers, and the measurement method behind them, are not restated here —
prints only findings.
they live in ADR-0020's Consequences section, under "A ceiling does not produce an average", whose
figures are pinned to the base commit the decision was taken on (`f9b919d`). Quoting them here would
just create a second copy to go stale. It costs nothing on a clean file — the script prints only
findings.
### Duplicated constants
### Duplicated constants
@@ -637,8 +671,10 @@ remote *before* any network call, so it does not join the pair above.
enforcement table (deterministic vs. auditor judgment), and every rejected alternative
enforcement table (deterministic vs. auditor judgment), and every rejected alternative
-`docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md` — the `SessionStart` hook, the
-`docs/adr/0019-session-start-hook-keeps-the-apm-install-current.md` — the `SessionStart` hook, the
executable-trust gate, and the version-pinned allow key
executable-trust gate, and the version-pinned allow key
-`docs/spec/architecture.md` — directory structure, install pipeline, what is generated and what is
-`docs/spec/architecture.md` — directory structure, install pipeline, what is generated and what is
hand-authored
hand-authored
-`.pre-commit-config.yaml` — the hooks themselves, with inline rationale comments
-`.pre-commit-config.yaml` — the hooks themselves, with inline rationale comments
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.